authentik.admin.files.backends.base
1import mimetypes 2from collections.abc import Callable, Generator, Iterator 3from typing import cast 4 5from django.core.cache import cache 6from django.http.request import HttpRequest 7from structlog.stdlib import get_logger 8 9from authentik.admin.files.usage import FileUsage 10 11CACHE_PREFIX = "goauthentik.io/admin/files" 12LOGGER = get_logger() 13 14# Theme variable placeholder for theme-specific files like logo-%(theme)s.png 15THEME_VARIABLE = "%(theme)s" 16 17 18def get_content_type(name: str) -> str: 19 """Get MIME type for a file based on its extension.""" 20 content_type, _ = mimetypes.guess_type(name) 21 return content_type or "application/octet-stream" 22 23 24def get_valid_themes() -> list[str]: 25 """Get valid themes that can be substituted for %(theme)s.""" 26 from authentik.brands.api import Themes 27 28 return [t.value for t in Themes if t != Themes.AUTOMATIC] 29 30 31def has_theme_variable(name: str) -> bool: 32 """Check if filename contains %(theme)s variable.""" 33 return THEME_VARIABLE in name 34 35 36def substitute_theme(name: str, theme: str) -> str: 37 """Replace %(theme)s with the given theme.""" 38 return name.replace(THEME_VARIABLE, theme) 39 40 41class Backend: 42 """ 43 Base class for file storage backends. 44 45 Class attributes: 46 allowed_usages: List of usages that can be used with this backend 47 """ 48 49 allowed_usages: list[FileUsage] 50 51 def __init__(self, usage: FileUsage): 52 """ 53 Initialize backend for the given usage type. 54 55 Args: 56 usage: FileUsage type enum value 57 """ 58 self.usage = usage 59 LOGGER.debug( 60 "Initializing storage backend", 61 backend=self.__class__.__name__, 62 usage=usage.value, 63 ) 64 65 def supports_file(self, name: str) -> bool: 66 """ 67 Check if this backend can handle the given file path. 68 69 Args: 70 name: File path to check 71 72 Returns: 73 True if this backend supports this file path 74 """ 75 raise NotImplementedError 76 77 def list_files(self) -> Generator[str]: 78 """ 79 List all files stored in this backend. 80 81 Yields: 82 Relative file paths 83 """ 84 raise NotImplementedError 85 86 def file_url( 87 self, 88 name: str, 89 request: HttpRequest | None = None, 90 use_cache: bool = True, 91 ) -> str: 92 """ 93 Get URL for accessing the file. 94 95 Args: 96 file_path: Relative file path 97 request: Optional Django HttpRequest for fully qualified URL building 98 use_cache: whether to retrieve the URL from cache 99 100 Returns: 101 URL to access the file (may be relative or absolute depending on backend) 102 """ 103 raise NotImplementedError 104 105 def themed_urls( 106 self, 107 name: str, 108 request: HttpRequest | None = None, 109 ) -> dict[str, str] | None: 110 """ 111 Get URLs for each theme variant when filename contains %(theme)s. 112 113 Args: 114 name: File path potentially containing %(theme)s 115 request: Optional Django HttpRequest for URL building 116 117 Returns: 118 Dict mapping theme to URL if %(theme)s present, None otherwise 119 """ 120 if not has_theme_variable(name): 121 return None 122 123 return { 124 theme: self.file_url(substitute_theme(name, theme), request, use_cache=True) 125 for theme in get_valid_themes() 126 } 127 128 129class ManageableBackend(Backend): 130 """ 131 Base class for manageable file storage backends. 132 133 Class attributes: 134 name: Canonical name of the storage backend, for use in configuration. 135 """ 136 137 name: str 138 139 @property 140 def manageable(self) -> bool: 141 """ 142 Whether this backend can actually be used for management. 143 144 Used only for management check, not for created the backend 145 """ 146 raise NotImplementedError 147 148 def save_file(self, name: str, content: bytes) -> None: 149 """ 150 Save file content to storage. 151 152 Args: 153 file_path: Relative file path 154 content: File content as bytes 155 """ 156 raise NotImplementedError 157 158 def save_file_stream(self, name: str) -> Iterator: 159 """ 160 Context manager for streaming file writes. 161 162 Args: 163 file_path: Relative file path 164 165 Returns: 166 Context manager that yields a writable file-like object 167 168 FileUsage: 169 with backend.save_file_stream("output.csv") as f: 170 f.write(b"data...") 171 """ 172 raise NotImplementedError 173 174 def delete_file(self, name: str) -> None: 175 """ 176 Delete file from storage. 177 178 Args: 179 file_path: Relative file path 180 """ 181 raise NotImplementedError 182 183 def file_exists(self, name: str) -> bool: 184 """ 185 Check if a file exists. 186 187 Args: 188 file_path: Relative file path 189 190 Returns: 191 True if file exists, False otherwise 192 """ 193 raise NotImplementedError 194 195 def _cache_get_or_set( 196 self, 197 name: str, 198 request: HttpRequest | None, 199 default: Callable[[str, HttpRequest | None], str], 200 timeout: int, 201 ) -> str: 202 timeout_ignore = 60 203 timeout = int(timeout * 0.67) 204 if timeout < timeout_ignore: 205 timeout = 0 206 207 request_key = "None" 208 if request is not None: 209 request_key = f"{request.build_absolute_uri('/')}" 210 cache_key = f"{CACHE_PREFIX}/{self.name}/{self.usage}/{request_key}/{name}" 211 212 return cast(str, cache.get_or_set(cache_key, lambda: default(name, request), timeout))
19def get_content_type(name: str) -> str: 20 """Get MIME type for a file based on its extension.""" 21 content_type, _ = mimetypes.guess_type(name) 22 return content_type or "application/octet-stream"
Get MIME type for a file based on its extension.
25def get_valid_themes() -> list[str]: 26 """Get valid themes that can be substituted for %(theme)s.""" 27 from authentik.brands.api import Themes 28 29 return [t.value for t in Themes if t != Themes.AUTOMATIC]
Get valid themes that can be substituted for %(theme)s.
32def has_theme_variable(name: str) -> bool: 33 """Check if filename contains %(theme)s variable.""" 34 return THEME_VARIABLE in name
Check if filename contains %(theme)s variable.
37def substitute_theme(name: str, theme: str) -> str: 38 """Replace %(theme)s with the given theme.""" 39 return name.replace(THEME_VARIABLE, theme)
Replace %(theme)s with the given theme.
42class Backend: 43 """ 44 Base class for file storage backends. 45 46 Class attributes: 47 allowed_usages: List of usages that can be used with this backend 48 """ 49 50 allowed_usages: list[FileUsage] 51 52 def __init__(self, usage: FileUsage): 53 """ 54 Initialize backend for the given usage type. 55 56 Args: 57 usage: FileUsage type enum value 58 """ 59 self.usage = usage 60 LOGGER.debug( 61 "Initializing storage backend", 62 backend=self.__class__.__name__, 63 usage=usage.value, 64 ) 65 66 def supports_file(self, name: str) -> bool: 67 """ 68 Check if this backend can handle the given file path. 69 70 Args: 71 name: File path to check 72 73 Returns: 74 True if this backend supports this file path 75 """ 76 raise NotImplementedError 77 78 def list_files(self) -> Generator[str]: 79 """ 80 List all files stored in this backend. 81 82 Yields: 83 Relative file paths 84 """ 85 raise NotImplementedError 86 87 def file_url( 88 self, 89 name: str, 90 request: HttpRequest | None = None, 91 use_cache: bool = True, 92 ) -> str: 93 """ 94 Get URL for accessing the file. 95 96 Args: 97 file_path: Relative file path 98 request: Optional Django HttpRequest for fully qualified URL building 99 use_cache: whether to retrieve the URL from cache 100 101 Returns: 102 URL to access the file (may be relative or absolute depending on backend) 103 """ 104 raise NotImplementedError 105 106 def themed_urls( 107 self, 108 name: str, 109 request: HttpRequest | None = None, 110 ) -> dict[str, str] | None: 111 """ 112 Get URLs for each theme variant when filename contains %(theme)s. 113 114 Args: 115 name: File path potentially containing %(theme)s 116 request: Optional Django HttpRequest for URL building 117 118 Returns: 119 Dict mapping theme to URL if %(theme)s present, None otherwise 120 """ 121 if not has_theme_variable(name): 122 return None 123 124 return { 125 theme: self.file_url(substitute_theme(name, theme), request, use_cache=True) 126 for theme in get_valid_themes() 127 }
Base class for file storage backends.
Class attributes: allowed_usages: List of usages that can be used with this backend
52 def __init__(self, usage: FileUsage): 53 """ 54 Initialize backend for the given usage type. 55 56 Args: 57 usage: FileUsage type enum value 58 """ 59 self.usage = usage 60 LOGGER.debug( 61 "Initializing storage backend", 62 backend=self.__class__.__name__, 63 usage=usage.value, 64 )
Initialize backend for the given usage type.
Args: usage: FileUsage type enum value
66 def supports_file(self, name: str) -> bool: 67 """ 68 Check if this backend can handle the given file path. 69 70 Args: 71 name: File path to check 72 73 Returns: 74 True if this backend supports this file path 75 """ 76 raise NotImplementedError
Check if this backend can handle the given file path.
Args: name: File path to check
Returns: True if this backend supports this file path
78 def list_files(self) -> Generator[str]: 79 """ 80 List all files stored in this backend. 81 82 Yields: 83 Relative file paths 84 """ 85 raise NotImplementedError
List all files stored in this backend.
Yields: Relative file paths
87 def file_url( 88 self, 89 name: str, 90 request: HttpRequest | None = None, 91 use_cache: bool = True, 92 ) -> str: 93 """ 94 Get URL for accessing the file. 95 96 Args: 97 file_path: Relative file path 98 request: Optional Django HttpRequest for fully qualified URL building 99 use_cache: whether to retrieve the URL from cache 100 101 Returns: 102 URL to access the file (may be relative or absolute depending on backend) 103 """ 104 raise NotImplementedError
Get URL for accessing the file.
Args: file_path: Relative file path request: Optional Django HttpRequest for fully qualified URL building use_cache: whether to retrieve the URL from cache
Returns: URL to access the file (may be relative or absolute depending on backend)
106 def themed_urls( 107 self, 108 name: str, 109 request: HttpRequest | None = None, 110 ) -> dict[str, str] | None: 111 """ 112 Get URLs for each theme variant when filename contains %(theme)s. 113 114 Args: 115 name: File path potentially containing %(theme)s 116 request: Optional Django HttpRequest for URL building 117 118 Returns: 119 Dict mapping theme to URL if %(theme)s present, None otherwise 120 """ 121 if not has_theme_variable(name): 122 return None 123 124 return { 125 theme: self.file_url(substitute_theme(name, theme), request, use_cache=True) 126 for theme in get_valid_themes() 127 }
Get URLs for each theme variant when filename contains %(theme)s.
Args: name: File path potentially containing %(theme)s request: Optional Django HttpRequest for URL building
Returns: Dict mapping theme to URL if %(theme)s present, None otherwise
130class ManageableBackend(Backend): 131 """ 132 Base class for manageable file storage backends. 133 134 Class attributes: 135 name: Canonical name of the storage backend, for use in configuration. 136 """ 137 138 name: str 139 140 @property 141 def manageable(self) -> bool: 142 """ 143 Whether this backend can actually be used for management. 144 145 Used only for management check, not for created the backend 146 """ 147 raise NotImplementedError 148 149 def save_file(self, name: str, content: bytes) -> None: 150 """ 151 Save file content to storage. 152 153 Args: 154 file_path: Relative file path 155 content: File content as bytes 156 """ 157 raise NotImplementedError 158 159 def save_file_stream(self, name: str) -> Iterator: 160 """ 161 Context manager for streaming file writes. 162 163 Args: 164 file_path: Relative file path 165 166 Returns: 167 Context manager that yields a writable file-like object 168 169 FileUsage: 170 with backend.save_file_stream("output.csv") as f: 171 f.write(b"data...") 172 """ 173 raise NotImplementedError 174 175 def delete_file(self, name: str) -> None: 176 """ 177 Delete file from storage. 178 179 Args: 180 file_path: Relative file path 181 """ 182 raise NotImplementedError 183 184 def file_exists(self, name: str) -> bool: 185 """ 186 Check if a file exists. 187 188 Args: 189 file_path: Relative file path 190 191 Returns: 192 True if file exists, False otherwise 193 """ 194 raise NotImplementedError 195 196 def _cache_get_or_set( 197 self, 198 name: str, 199 request: HttpRequest | None, 200 default: Callable[[str, HttpRequest | None], str], 201 timeout: int, 202 ) -> str: 203 timeout_ignore = 60 204 timeout = int(timeout * 0.67) 205 if timeout < timeout_ignore: 206 timeout = 0 207 208 request_key = "None" 209 if request is not None: 210 request_key = f"{request.build_absolute_uri('/')}" 211 cache_key = f"{CACHE_PREFIX}/{self.name}/{self.usage}/{request_key}/{name}" 212 213 return cast(str, cache.get_or_set(cache_key, lambda: default(name, request), timeout))
Base class for manageable file storage backends.
Class attributes: name: Canonical name of the storage backend, for use in configuration.
140 @property 141 def manageable(self) -> bool: 142 """ 143 Whether this backend can actually be used for management. 144 145 Used only for management check, not for created the backend 146 """ 147 raise NotImplementedError
Whether this backend can actually be used for management.
Used only for management check, not for created the backend
149 def save_file(self, name: str, content: bytes) -> None: 150 """ 151 Save file content to storage. 152 153 Args: 154 file_path: Relative file path 155 content: File content as bytes 156 """ 157 raise NotImplementedError
Save file content to storage.
Args: file_path: Relative file path content: File content as bytes
159 def save_file_stream(self, name: str) -> Iterator: 160 """ 161 Context manager for streaming file writes. 162 163 Args: 164 file_path: Relative file path 165 166 Returns: 167 Context manager that yields a writable file-like object 168 169 FileUsage: 170 with backend.save_file_stream("output.csv") as f: 171 f.write(b"data...") 172 """ 173 raise NotImplementedError
Context manager for streaming file writes.
Args: file_path: Relative file path
Returns: Context manager that yields a writable file-like object
FileUsage: with backend.save_file_stream("output.csv") as f: f.write(b"data...")
175 def delete_file(self, name: str) -> None: 176 """ 177 Delete file from storage. 178 179 Args: 180 file_path: Relative file path 181 """ 182 raise NotImplementedError
Delete file from storage.
Args: file_path: Relative file path
184 def file_exists(self, name: str) -> bool: 185 """ 186 Check if a file exists. 187 188 Args: 189 file_path: Relative file path 190 191 Returns: 192 True if file exists, False otherwise 193 """ 194 raise NotImplementedError
Check if a file exists.
Args: file_path: Relative file path
Returns: True if file exists, False otherwise