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))
CACHE_PREFIX = 'goauthentik.io/admin/files'
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
THEME_VARIABLE = '%(theme)s'
def get_content_type(name: str) -> str:
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.

def get_valid_themes() -> list[str]:
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.

def has_theme_variable(name: str) -> bool:
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.

def substitute_theme(name: str, theme: str) -> str:
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.

class Backend:
 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

Backend(usage: authentik.admin.files.usage.FileUsage)
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

usage
def supports_file(self, name: str) -> bool:
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

def list_files(self) -> Generator[str]:
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

def file_url( self, name: str, request: django.http.request.HttpRequest | None = None, use_cache: bool = True) -> str:
 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)

def themed_urls( self, name: str, request: django.http.request.HttpRequest | None = None) -> dict[str, str] | None:
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

class ManageableBackend(Backend):
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.

name: str
manageable: bool
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

def save_file(self, name: str, content: bytes) -> None:
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

def save_file_stream(self, name: str) -> Iterator:
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...")

def delete_file(self, name: str) -> None:
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

def file_exists(self, name: str) -> bool:
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