authentik.admin.files.manager
1from collections.abc import Generator, Iterator 2 3from django.core.exceptions import ImproperlyConfigured 4from django.http.request import HttpRequest 5from rest_framework.request import Request 6from structlog.stdlib import get_logger 7 8from authentik.admin.files.backends.base import ManageableBackend 9from authentik.admin.files.backends.file import FileBackend 10from authentik.admin.files.backends.passthrough import PassthroughBackend 11from authentik.admin.files.backends.s3 import S3Backend 12from authentik.admin.files.backends.static import StaticBackend 13from authentik.admin.files.usage import FileUsage 14from authentik.lib.config import CONFIG 15 16LOGGER = get_logger() 17 18 19_FILE_BACKENDS = [ 20 StaticBackend, 21 PassthroughBackend, 22 FileBackend, 23 S3Backend, 24] 25 26 27class FileManager: 28 def __init__(self, usage: FileUsage) -> None: 29 management_backend_name = CONFIG.get( 30 f"storage.{usage.value}.backend", 31 CONFIG.get("storage.backend", "file"), 32 ) 33 34 self.management_backend = None 35 for backend in _FILE_BACKENDS: 36 if issubclass(backend, ManageableBackend) and backend.name == management_backend_name: 37 self.management_backend = backend(usage) 38 if self.management_backend is None: 39 LOGGER.warning( 40 f"Storage backend configuration for {usage.value} is " 41 f"invalid: {management_backend_name}" 42 ) 43 44 self.backends = [] 45 for backend in _FILE_BACKENDS: 46 if usage not in backend.allowed_usages: 47 continue 48 if isinstance(self.management_backend, backend): 49 self.backends.append(self.management_backend) 50 elif not issubclass(backend, ManageableBackend): 51 self.backends.append(backend(usage)) 52 53 @property 54 def manageable(self) -> bool: 55 """ 56 Whether this file manager is able to manage files. 57 """ 58 return self.management_backend is not None and self.management_backend.manageable 59 60 def list_files(self, manageable_only: bool = False) -> Generator[str]: 61 """ 62 List available files. 63 """ 64 for backend in self.backends: 65 if manageable_only and not isinstance(backend, ManageableBackend): 66 continue 67 yield from backend.list_files() 68 69 def file_url( 70 self, 71 name: str | None, 72 request: HttpRequest | Request | None = None, 73 use_cache: bool = True, 74 ) -> str: 75 """ 76 Get URL for accessing the file. 77 78 Set ``use_cache=False`` when the caller needs a fresh signed URL instead 79 of a cached one, for example when serializing flow/login payloads that 80 may be refreshed after the previous JWT has expired. 81 """ 82 if not name: 83 return "" 84 85 if isinstance(request, Request): 86 request = request._request 87 88 for backend in self.backends: 89 if backend.supports_file(name): 90 return backend.file_url(name, request, use_cache=use_cache) 91 92 LOGGER.warning(f"Could not find file backend for file: {name}") 93 return "" 94 95 def themed_urls( 96 self, 97 name: str | None, 98 request: HttpRequest | Request | None = None, 99 use_cache: bool = True, 100 ) -> dict[str, str] | None: 101 """ 102 Get URLs for each theme variant when filename contains %(theme)s. 103 104 ``use_cache`` has the same semantics as ``file_url()`` and allows 105 callers to force regeneration of expiring signed URLs. 106 107 Returns dict mapping theme to URL if %(theme)s present, None otherwise. 108 """ 109 if not name: 110 return None 111 112 if isinstance(request, Request): 113 request = request._request 114 115 for backend in self.backends: 116 if backend.supports_file(name): 117 return backend.themed_urls(name, request, use_cache=use_cache) 118 119 return None 120 121 def _check_manageable(self) -> None: 122 if not self.manageable: 123 raise ImproperlyConfigured("No file management backend configured.") 124 125 def save_file(self, file_path: str, content: bytes) -> None: 126 """ 127 Save file contents to storage. 128 """ 129 self._check_manageable() 130 assert self.management_backend is not None # nosec 131 return self.management_backend.save_file(file_path, content) 132 133 def save_file_stream(self, file_path: str) -> Iterator: 134 """ 135 Context manager for streaming file writes. 136 137 Args: 138 file_path: Relative file path 139 140 Returns: 141 Context manager that yields a writable file-like object 142 143 Usage: 144 with manager.save_file_stream("output.csv") as f: 145 f.write(b"data...") 146 """ 147 self._check_manageable() 148 assert self.management_backend is not None # nosec 149 return self.management_backend.save_file_stream(file_path) 150 151 def delete_file(self, file_path: str) -> None: 152 """ 153 Delete file from storage. 154 """ 155 self._check_manageable() 156 assert self.management_backend is not None # nosec 157 return self.management_backend.delete_file(file_path) 158 159 def file_exists(self, file_path: str) -> bool: 160 """ 161 Check if a file exists. 162 """ 163 self._check_manageable() 164 assert self.management_backend is not None # nosec 165 return self.management_backend.file_exists(file_path) 166 167 168MANAGERS = {usage: FileManager(usage) for usage in list(FileUsage)} 169 170 171def get_file_manager(usage: FileUsage) -> FileManager: 172 return MANAGERS[usage]
28class FileManager: 29 def __init__(self, usage: FileUsage) -> None: 30 management_backend_name = CONFIG.get( 31 f"storage.{usage.value}.backend", 32 CONFIG.get("storage.backend", "file"), 33 ) 34 35 self.management_backend = None 36 for backend in _FILE_BACKENDS: 37 if issubclass(backend, ManageableBackend) and backend.name == management_backend_name: 38 self.management_backend = backend(usage) 39 if self.management_backend is None: 40 LOGGER.warning( 41 f"Storage backend configuration for {usage.value} is " 42 f"invalid: {management_backend_name}" 43 ) 44 45 self.backends = [] 46 for backend in _FILE_BACKENDS: 47 if usage not in backend.allowed_usages: 48 continue 49 if isinstance(self.management_backend, backend): 50 self.backends.append(self.management_backend) 51 elif not issubclass(backend, ManageableBackend): 52 self.backends.append(backend(usage)) 53 54 @property 55 def manageable(self) -> bool: 56 """ 57 Whether this file manager is able to manage files. 58 """ 59 return self.management_backend is not None and self.management_backend.manageable 60 61 def list_files(self, manageable_only: bool = False) -> Generator[str]: 62 """ 63 List available files. 64 """ 65 for backend in self.backends: 66 if manageable_only and not isinstance(backend, ManageableBackend): 67 continue 68 yield from backend.list_files() 69 70 def file_url( 71 self, 72 name: str | None, 73 request: HttpRequest | Request | None = None, 74 use_cache: bool = True, 75 ) -> str: 76 """ 77 Get URL for accessing the file. 78 79 Set ``use_cache=False`` when the caller needs a fresh signed URL instead 80 of a cached one, for example when serializing flow/login payloads that 81 may be refreshed after the previous JWT has expired. 82 """ 83 if not name: 84 return "" 85 86 if isinstance(request, Request): 87 request = request._request 88 89 for backend in self.backends: 90 if backend.supports_file(name): 91 return backend.file_url(name, request, use_cache=use_cache) 92 93 LOGGER.warning(f"Could not find file backend for file: {name}") 94 return "" 95 96 def themed_urls( 97 self, 98 name: str | None, 99 request: HttpRequest | Request | None = None, 100 use_cache: bool = True, 101 ) -> dict[str, str] | None: 102 """ 103 Get URLs for each theme variant when filename contains %(theme)s. 104 105 ``use_cache`` has the same semantics as ``file_url()`` and allows 106 callers to force regeneration of expiring signed URLs. 107 108 Returns dict mapping theme to URL if %(theme)s present, None otherwise. 109 """ 110 if not name: 111 return None 112 113 if isinstance(request, Request): 114 request = request._request 115 116 for backend in self.backends: 117 if backend.supports_file(name): 118 return backend.themed_urls(name, request, use_cache=use_cache) 119 120 return None 121 122 def _check_manageable(self) -> None: 123 if not self.manageable: 124 raise ImproperlyConfigured("No file management backend configured.") 125 126 def save_file(self, file_path: str, content: bytes) -> None: 127 """ 128 Save file contents to storage. 129 """ 130 self._check_manageable() 131 assert self.management_backend is not None # nosec 132 return self.management_backend.save_file(file_path, content) 133 134 def save_file_stream(self, file_path: str) -> Iterator: 135 """ 136 Context manager for streaming file writes. 137 138 Args: 139 file_path: Relative file path 140 141 Returns: 142 Context manager that yields a writable file-like object 143 144 Usage: 145 with manager.save_file_stream("output.csv") as f: 146 f.write(b"data...") 147 """ 148 self._check_manageable() 149 assert self.management_backend is not None # nosec 150 return self.management_backend.save_file_stream(file_path) 151 152 def delete_file(self, file_path: str) -> None: 153 """ 154 Delete file from storage. 155 """ 156 self._check_manageable() 157 assert self.management_backend is not None # nosec 158 return self.management_backend.delete_file(file_path) 159 160 def file_exists(self, file_path: str) -> bool: 161 """ 162 Check if a file exists. 163 """ 164 self._check_manageable() 165 assert self.management_backend is not None # nosec 166 return self.management_backend.file_exists(file_path)
29 def __init__(self, usage: FileUsage) -> None: 30 management_backend_name = CONFIG.get( 31 f"storage.{usage.value}.backend", 32 CONFIG.get("storage.backend", "file"), 33 ) 34 35 self.management_backend = None 36 for backend in _FILE_BACKENDS: 37 if issubclass(backend, ManageableBackend) and backend.name == management_backend_name: 38 self.management_backend = backend(usage) 39 if self.management_backend is None: 40 LOGGER.warning( 41 f"Storage backend configuration for {usage.value} is " 42 f"invalid: {management_backend_name}" 43 ) 44 45 self.backends = [] 46 for backend in _FILE_BACKENDS: 47 if usage not in backend.allowed_usages: 48 continue 49 if isinstance(self.management_backend, backend): 50 self.backends.append(self.management_backend) 51 elif not issubclass(backend, ManageableBackend): 52 self.backends.append(backend(usage))
54 @property 55 def manageable(self) -> bool: 56 """ 57 Whether this file manager is able to manage files. 58 """ 59 return self.management_backend is not None and self.management_backend.manageable
Whether this file manager is able to manage files.
61 def list_files(self, manageable_only: bool = False) -> Generator[str]: 62 """ 63 List available files. 64 """ 65 for backend in self.backends: 66 if manageable_only and not isinstance(backend, ManageableBackend): 67 continue 68 yield from backend.list_files()
List available files.
70 def file_url( 71 self, 72 name: str | None, 73 request: HttpRequest | Request | None = None, 74 use_cache: bool = True, 75 ) -> str: 76 """ 77 Get URL for accessing the file. 78 79 Set ``use_cache=False`` when the caller needs a fresh signed URL instead 80 of a cached one, for example when serializing flow/login payloads that 81 may be refreshed after the previous JWT has expired. 82 """ 83 if not name: 84 return "" 85 86 if isinstance(request, Request): 87 request = request._request 88 89 for backend in self.backends: 90 if backend.supports_file(name): 91 return backend.file_url(name, request, use_cache=use_cache) 92 93 LOGGER.warning(f"Could not find file backend for file: {name}") 94 return ""
Get URL for accessing the file.
Set use_cache=False when the caller needs a fresh signed URL instead
of a cached one, for example when serializing flow/login payloads that
may be refreshed after the previous JWT has expired.
96 def themed_urls( 97 self, 98 name: str | None, 99 request: HttpRequest | Request | None = None, 100 use_cache: bool = True, 101 ) -> dict[str, str] | None: 102 """ 103 Get URLs for each theme variant when filename contains %(theme)s. 104 105 ``use_cache`` has the same semantics as ``file_url()`` and allows 106 callers to force regeneration of expiring signed URLs. 107 108 Returns dict mapping theme to URL if %(theme)s present, None otherwise. 109 """ 110 if not name: 111 return None 112 113 if isinstance(request, Request): 114 request = request._request 115 116 for backend in self.backends: 117 if backend.supports_file(name): 118 return backend.themed_urls(name, request, use_cache=use_cache) 119 120 return None
Get URLs for each theme variant when filename contains %(theme)s.
use_cache has the same semantics as file_url() and allows
callers to force regeneration of expiring signed URLs.
Returns dict mapping theme to URL if %(theme)s present, None otherwise.
126 def save_file(self, file_path: str, content: bytes) -> None: 127 """ 128 Save file contents to storage. 129 """ 130 self._check_manageable() 131 assert self.management_backend is not None # nosec 132 return self.management_backend.save_file(file_path, content)
Save file contents to storage.
134 def save_file_stream(self, file_path: str) -> Iterator: 135 """ 136 Context manager for streaming file writes. 137 138 Args: 139 file_path: Relative file path 140 141 Returns: 142 Context manager that yields a writable file-like object 143 144 Usage: 145 with manager.save_file_stream("output.csv") as f: 146 f.write(b"data...") 147 """ 148 self._check_manageable() 149 assert self.management_backend is not None # nosec 150 return self.management_backend.save_file_stream(file_path)
Context manager for streaming file writes.
Args: file_path: Relative file path
Returns: Context manager that yields a writable file-like object
Usage: with manager.save_file_stream("output.csv") as f: f.write(b"data...")
152 def delete_file(self, file_path: str) -> None: 153 """ 154 Delete file from storage. 155 """ 156 self._check_manageable() 157 assert self.management_backend is not None # nosec 158 return self.management_backend.delete_file(file_path)
Delete file from storage.
160 def file_exists(self, file_path: str) -> bool: 161 """ 162 Check if a file exists. 163 """ 164 self._check_manageable() 165 assert self.management_backend is not None # nosec 166 return self.management_backend.file_exists(file_path)
Check if a file exists.