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 use_cache: bool = True, 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=use_cache) 126 for theme in get_valid_themes() 127 } 128 129 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))
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 use_cache: bool = True, 111 ) -> dict[str, str] | None: 112 """ 113 Get URLs for each theme variant when filename contains %(theme)s. 114 115 Args: 116 name: File path potentially containing %(theme)s 117 request: Optional Django HttpRequest for URL building 118 119 Returns: 120 Dict mapping theme to URL if %(theme)s present, None otherwise 121 """ 122 if not has_theme_variable(name): 123 return None 124 125 return { 126 theme: self.file_url(substitute_theme(name, theme), request, use_cache=use_cache) 127 for theme in get_valid_themes() 128 }
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 use_cache: bool = True, 111 ) -> dict[str, str] | None: 112 """ 113 Get URLs for each theme variant when filename contains %(theme)s. 114 115 Args: 116 name: File path potentially containing %(theme)s 117 request: Optional Django HttpRequest for URL building 118 119 Returns: 120 Dict mapping theme to URL if %(theme)s present, None otherwise 121 """ 122 if not has_theme_variable(name): 123 return None 124 125 return { 126 theme: self.file_url(substitute_theme(name, theme), request, use_cache=use_cache) 127 for theme in get_valid_themes() 128 }
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
131class ManageableBackend(Backend): 132 """ 133 Base class for manageable file storage backends. 134 135 Class attributes: 136 name: Canonical name of the storage backend, for use in configuration. 137 """ 138 139 name: str 140 141 @property 142 def manageable(self) -> bool: 143 """ 144 Whether this backend can actually be used for management. 145 146 Used only for management check, not for created the backend 147 """ 148 raise NotImplementedError 149 150 def save_file(self, name: str, content: bytes) -> None: 151 """ 152 Save file content to storage. 153 154 Args: 155 file_path: Relative file path 156 content: File content as bytes 157 """ 158 raise NotImplementedError 159 160 def save_file_stream(self, name: str) -> Iterator: 161 """ 162 Context manager for streaming file writes. 163 164 Args: 165 file_path: Relative file path 166 167 Returns: 168 Context manager that yields a writable file-like object 169 170 FileUsage: 171 with backend.save_file_stream("output.csv") as f: 172 f.write(b"data...") 173 """ 174 raise NotImplementedError 175 176 def delete_file(self, name: str) -> None: 177 """ 178 Delete file from storage. 179 180 Args: 181 file_path: Relative file path 182 """ 183 raise NotImplementedError 184 185 def file_exists(self, name: str) -> bool: 186 """ 187 Check if a file exists. 188 189 Args: 190 file_path: Relative file path 191 192 Returns: 193 True if file exists, False otherwise 194 """ 195 raise NotImplementedError 196 197 def _cache_get_or_set( 198 self, 199 name: str, 200 request: HttpRequest | None, 201 default: Callable[[str, HttpRequest | None], str], 202 timeout: int, 203 ) -> str: 204 timeout_ignore = 60 205 timeout = int(timeout * 0.67) 206 if timeout < timeout_ignore: 207 timeout = 0 208 209 request_key = "None" 210 if request is not None: 211 request_key = f"{request.build_absolute_uri('/')}" 212 cache_key = f"{CACHE_PREFIX}/{self.name}/{self.usage}/{request_key}/{name}" 213 214 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.
141 @property 142 def manageable(self) -> bool: 143 """ 144 Whether this backend can actually be used for management. 145 146 Used only for management check, not for created the backend 147 """ 148 raise NotImplementedError
Whether this backend can actually be used for management.
Used only for management check, not for created the backend
150 def save_file(self, name: str, content: bytes) -> None: 151 """ 152 Save file content to storage. 153 154 Args: 155 file_path: Relative file path 156 content: File content as bytes 157 """ 158 raise NotImplementedError
Save file content to storage.
Args: file_path: Relative file path content: File content as bytes
160 def save_file_stream(self, name: str) -> Iterator: 161 """ 162 Context manager for streaming file writes. 163 164 Args: 165 file_path: Relative file path 166 167 Returns: 168 Context manager that yields a writable file-like object 169 170 FileUsage: 171 with backend.save_file_stream("output.csv") as f: 172 f.write(b"data...") 173 """ 174 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...")
176 def delete_file(self, name: str) -> None: 177 """ 178 Delete file from storage. 179 180 Args: 181 file_path: Relative file path 182 """ 183 raise NotImplementedError
Delete file from storage.
Args: file_path: Relative file path
185 def file_exists(self, name: str) -> bool: 186 """ 187 Check if a file exists. 188 189 Args: 190 file_path: Relative file path 191 192 Returns: 193 True if file exists, False otherwise 194 """ 195 raise NotImplementedError
Check if a file exists.
Args: file_path: Relative file path
Returns: True if file exists, False otherwise