authentik.blueprints.apps

authentik Blueprints app

  1"""authentik Blueprints app"""
  2
  3import traceback
  4from collections.abc import Callable
  5from importlib import import_module
  6
  7from django.apps import AppConfig
  8from django.conf import settings
  9from django.db import DatabaseError, InternalError, ProgrammingError
 10from dramatiq.broker import get_broker
 11from structlog.stdlib import BoundLogger, get_logger
 12
 13from authentik.lib.utils.time import fqdn_rand
 14from authentik.root.signals import startup
 15from authentik.tasks.schedules.common import ScheduleSpec
 16
 17
 18class ManagedAppConfig(AppConfig):
 19    """Basic reconciliation logic for apps"""
 20
 21    logger: BoundLogger
 22
 23    RECONCILE_GLOBAL_CATEGORY: str = "global"
 24    RECONCILE_TENANT_CATEGORY: str = "tenant"
 25
 26    def __init__(self, app_name: str, *args, **kwargs) -> None:
 27        super().__init__(app_name, *args, **kwargs)
 28        self.logger = get_logger().bind(app_name=app_name)
 29
 30    def ready(self) -> None:
 31        self.import_related()
 32        startup.connect(self._on_startup_callback, dispatch_uid=self.label)
 33        return super().ready()
 34
 35    def _on_startup_callback(self, sender, **_):
 36        self._reconcile_global()
 37        self._reconcile_tenant()
 38
 39    def import_related(self):
 40        """Automatically import related modules which rely on just being imported
 41        to register themselves (mainly django signals and tasks)"""
 42
 43        def import_relative(rel_module: str):
 44            try:
 45                module_name = f"{self.name}.{rel_module}"
 46                import_module(module_name)
 47                self.logger.info("Imported related module", module=module_name)
 48            except ModuleNotFoundError as exc:
 49                if settings.DEBUG:
 50                    # This is a heuristic for determining whether the exception was caused
 51                    # "directly" by the `import_module` call or whether the initial import
 52                    # succeeded and a later import (within the existing module) failed.
 53                    # 1. <the calling function>
 54                    # 2. importlib.import_module
 55                    # 3. importlib._bootstrap._gcd_import
 56                    # 4. importlib._bootstrap._find_and_load
 57                    # 5. importlib._bootstrap._find_and_load_unlocked
 58                    STACK_LENGTH_HEURISTIC = 5
 59
 60                    stack_length = len(traceback.extract_tb(exc.__traceback__))
 61                    if stack_length > STACK_LENGTH_HEURISTIC:
 62                        raise
 63
 64        import_relative("checks")
 65        import_relative("tasks")
 66        import_relative("signals")
 67
 68    def import_module(self, path: str):
 69        """Load module"""
 70        import_module(path)
 71
 72    def _reconcile(self, prefix: str) -> None:
 73        for meth_name in dir(self):
 74            # Check the attribute on the class to avoid evaluating @property descriptors.
 75            # Using getattr(self, ...) on a @property would evaluate it, which can trigger
 76            # expensive side effects (e.g. tenant_schedule_specs iterating all providers
 77            # and running PolicyEngine queries for every user).
 78            class_attr = getattr(type(self), meth_name, None)
 79            if class_attr is None or isinstance(class_attr, property):
 80                continue
 81            if not callable(class_attr):
 82                continue
 83            category = getattr(class_attr, "_authentik_managed_reconcile", None)
 84            if category != prefix:
 85                continue
 86            meth = getattr(self, meth_name)
 87            name = meth_name.replace(prefix, "")
 88            try:
 89                self.logger.debug("Starting reconciler", name=name)
 90                meth()
 91                self.logger.debug("Successfully reconciled", name=name)
 92            except (DatabaseError, ProgrammingError, InternalError) as exc:
 93                self.logger.warning("Failed to run reconcile", name=name, exc=exc)
 94
 95    @staticmethod
 96    def reconcile_tenant(func: Callable):
 97        """Mark a function to be called on startup (for each tenant)"""
 98        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_TENANT_CATEGORY
 99        return func
100
101    @staticmethod
102    def reconcile_global(func: Callable):
103        """Mark a function to be called on startup (globally)"""
104        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_GLOBAL_CATEGORY
105        return func
106
107    @property
108    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
109        """Get a list of schedule specs that must exist in each tenant"""
110        return []
111
112    @property
113    def global_schedule_specs(self) -> list[ScheduleSpec]:
114        """Get a list of schedule specs that must exist in the default tenant"""
115        return []
116
117    def _reconcile_tenant(self) -> None:
118        """reconcile ourselves for tenanted methods"""
119        from authentik.tenants.models import Tenant
120
121        try:
122            tenants = list(Tenant.objects.filter(ready=True))
123        except (DatabaseError, ProgrammingError, InternalError) as exc:
124            self.logger.debug("Failed to get tenants to run reconcile", exc=exc)
125            return
126        for tenant in tenants:
127            with tenant:
128                self._reconcile(self.RECONCILE_TENANT_CATEGORY)
129
130    def _reconcile_global(self) -> None:
131        """
132        reconcile ourselves for global methods.
133        Used for signals, tasks, etc. Database queries should not be made in here.
134        """
135        from django_tenants.utils import get_public_schema_name, schema_context
136
137        try:
138            with schema_context(get_public_schema_name()):
139                self._reconcile(self.RECONCILE_GLOBAL_CATEGORY)
140        except (DatabaseError, ProgrammingError, InternalError) as exc:
141            self.logger.debug("Failed to access database to run reconcile", exc=exc)
142            return
143
144
145class AuthentikBlueprintsConfig(ManagedAppConfig):
146    """authentik Blueprints app"""
147
148    name = "authentik.blueprints"
149    label = "authentik_blueprints"
150    verbose_name = "authentik Blueprints"
151    default = True
152
153    def import_models(self):
154        super().import_models()
155        self.import_module("authentik.blueprints.v1.meta.apply_blueprint")
156
157    @ManagedAppConfig.reconcile_global
158    def tasks_middlewares(self):
159        from authentik.blueprints.v1.tasks import BlueprintWatcherMiddleware
160
161        get_broker().add_middleware(BlueprintWatcherMiddleware())
162
163    @property
164    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
165        from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
166
167        return [
168            ScheduleSpec(
169                actor=blueprints_discovery,
170                crontab=f"{fqdn_rand('blueprints_v1_discover')} * * * *",
171                send_on_startup=True,
172            ),
173            ScheduleSpec(
174                actor=clear_failed_blueprints,
175                crontab=f"{fqdn_rand('blueprints_v1_cleanup')} * * * *",
176                send_on_startup=True,
177            ),
178        ]
class ManagedAppConfig(django.apps.config.AppConfig):
 19class ManagedAppConfig(AppConfig):
 20    """Basic reconciliation logic for apps"""
 21
 22    logger: BoundLogger
 23
 24    RECONCILE_GLOBAL_CATEGORY: str = "global"
 25    RECONCILE_TENANT_CATEGORY: str = "tenant"
 26
 27    def __init__(self, app_name: str, *args, **kwargs) -> None:
 28        super().__init__(app_name, *args, **kwargs)
 29        self.logger = get_logger().bind(app_name=app_name)
 30
 31    def ready(self) -> None:
 32        self.import_related()
 33        startup.connect(self._on_startup_callback, dispatch_uid=self.label)
 34        return super().ready()
 35
 36    def _on_startup_callback(self, sender, **_):
 37        self._reconcile_global()
 38        self._reconcile_tenant()
 39
 40    def import_related(self):
 41        """Automatically import related modules which rely on just being imported
 42        to register themselves (mainly django signals and tasks)"""
 43
 44        def import_relative(rel_module: str):
 45            try:
 46                module_name = f"{self.name}.{rel_module}"
 47                import_module(module_name)
 48                self.logger.info("Imported related module", module=module_name)
 49            except ModuleNotFoundError as exc:
 50                if settings.DEBUG:
 51                    # This is a heuristic for determining whether the exception was caused
 52                    # "directly" by the `import_module` call or whether the initial import
 53                    # succeeded and a later import (within the existing module) failed.
 54                    # 1. <the calling function>
 55                    # 2. importlib.import_module
 56                    # 3. importlib._bootstrap._gcd_import
 57                    # 4. importlib._bootstrap._find_and_load
 58                    # 5. importlib._bootstrap._find_and_load_unlocked
 59                    STACK_LENGTH_HEURISTIC = 5
 60
 61                    stack_length = len(traceback.extract_tb(exc.__traceback__))
 62                    if stack_length > STACK_LENGTH_HEURISTIC:
 63                        raise
 64
 65        import_relative("checks")
 66        import_relative("tasks")
 67        import_relative("signals")
 68
 69    def import_module(self, path: str):
 70        """Load module"""
 71        import_module(path)
 72
 73    def _reconcile(self, prefix: str) -> None:
 74        for meth_name in dir(self):
 75            # Check the attribute on the class to avoid evaluating @property descriptors.
 76            # Using getattr(self, ...) on a @property would evaluate it, which can trigger
 77            # expensive side effects (e.g. tenant_schedule_specs iterating all providers
 78            # and running PolicyEngine queries for every user).
 79            class_attr = getattr(type(self), meth_name, None)
 80            if class_attr is None or isinstance(class_attr, property):
 81                continue
 82            if not callable(class_attr):
 83                continue
 84            category = getattr(class_attr, "_authentik_managed_reconcile", None)
 85            if category != prefix:
 86                continue
 87            meth = getattr(self, meth_name)
 88            name = meth_name.replace(prefix, "")
 89            try:
 90                self.logger.debug("Starting reconciler", name=name)
 91                meth()
 92                self.logger.debug("Successfully reconciled", name=name)
 93            except (DatabaseError, ProgrammingError, InternalError) as exc:
 94                self.logger.warning("Failed to run reconcile", name=name, exc=exc)
 95
 96    @staticmethod
 97    def reconcile_tenant(func: Callable):
 98        """Mark a function to be called on startup (for each tenant)"""
 99        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_TENANT_CATEGORY
100        return func
101
102    @staticmethod
103    def reconcile_global(func: Callable):
104        """Mark a function to be called on startup (globally)"""
105        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_GLOBAL_CATEGORY
106        return func
107
108    @property
109    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
110        """Get a list of schedule specs that must exist in each tenant"""
111        return []
112
113    @property
114    def global_schedule_specs(self) -> list[ScheduleSpec]:
115        """Get a list of schedule specs that must exist in the default tenant"""
116        return []
117
118    def _reconcile_tenant(self) -> None:
119        """reconcile ourselves for tenanted methods"""
120        from authentik.tenants.models import Tenant
121
122        try:
123            tenants = list(Tenant.objects.filter(ready=True))
124        except (DatabaseError, ProgrammingError, InternalError) as exc:
125            self.logger.debug("Failed to get tenants to run reconcile", exc=exc)
126            return
127        for tenant in tenants:
128            with tenant:
129                self._reconcile(self.RECONCILE_TENANT_CATEGORY)
130
131    def _reconcile_global(self) -> None:
132        """
133        reconcile ourselves for global methods.
134        Used for signals, tasks, etc. Database queries should not be made in here.
135        """
136        from django_tenants.utils import get_public_schema_name, schema_context
137
138        try:
139            with schema_context(get_public_schema_name()):
140                self._reconcile(self.RECONCILE_GLOBAL_CATEGORY)
141        except (DatabaseError, ProgrammingError, InternalError) as exc:
142            self.logger.debug("Failed to access database to run reconcile", exc=exc)
143            return

Basic reconciliation logic for apps

ManagedAppConfig(app_name: str, *args, **kwargs)
27    def __init__(self, app_name: str, *args, **kwargs) -> None:
28        super().__init__(app_name, *args, **kwargs)
29        self.logger = get_logger().bind(app_name=app_name)
logger: structlog.stdlib.BoundLogger
RECONCILE_GLOBAL_CATEGORY: str = 'global'
RECONCILE_TENANT_CATEGORY: str = 'tenant'
def ready(self) -> None:
31    def ready(self) -> None:
32        self.import_related()
33        startup.connect(self._on_startup_callback, dispatch_uid=self.label)
34        return super().ready()

Override this method in subclasses to run code when Django starts.

def import_module(self, path: str):
69    def import_module(self, path: str):
70        """Load module"""
71        import_module(path)

Load module

@staticmethod
def reconcile_tenant(func: Callable):
 96    @staticmethod
 97    def reconcile_tenant(func: Callable):
 98        """Mark a function to be called on startup (for each tenant)"""
 99        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_TENANT_CATEGORY
100        return func

Mark a function to be called on startup (for each tenant)

@staticmethod
def reconcile_global(func: Callable):
102    @staticmethod
103    def reconcile_global(func: Callable):
104        """Mark a function to be called on startup (globally)"""
105        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_GLOBAL_CATEGORY
106        return func

Mark a function to be called on startup (globally)

tenant_schedule_specs: list[authentik.tasks.schedules.common.ScheduleSpec]
108    @property
109    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
110        """Get a list of schedule specs that must exist in each tenant"""
111        return []

Get a list of schedule specs that must exist in each tenant

global_schedule_specs: list[authentik.tasks.schedules.common.ScheduleSpec]
113    @property
114    def global_schedule_specs(self) -> list[ScheduleSpec]:
115        """Get a list of schedule specs that must exist in the default tenant"""
116        return []

Get a list of schedule specs that must exist in the default tenant

class AuthentikBlueprintsConfig(ManagedAppConfig):
146class AuthentikBlueprintsConfig(ManagedAppConfig):
147    """authentik Blueprints app"""
148
149    name = "authentik.blueprints"
150    label = "authentik_blueprints"
151    verbose_name = "authentik Blueprints"
152    default = True
153
154    def import_models(self):
155        super().import_models()
156        self.import_module("authentik.blueprints.v1.meta.apply_blueprint")
157
158    @ManagedAppConfig.reconcile_global
159    def tasks_middlewares(self):
160        from authentik.blueprints.v1.tasks import BlueprintWatcherMiddleware
161
162        get_broker().add_middleware(BlueprintWatcherMiddleware())
163
164    @property
165    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
166        from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
167
168        return [
169            ScheduleSpec(
170                actor=blueprints_discovery,
171                crontab=f"{fqdn_rand('blueprints_v1_discover')} * * * *",
172                send_on_startup=True,
173            ),
174            ScheduleSpec(
175                actor=clear_failed_blueprints,
176                crontab=f"{fqdn_rand('blueprints_v1_cleanup')} * * * *",
177                send_on_startup=True,
178            ),
179        ]

authentik Blueprints app

label = 'authentik_blueprints'
verbose_name = 'authentik Blueprints'
default = True
def import_models(self):
154    def import_models(self):
155        super().import_models()
156        self.import_module("authentik.blueprints.v1.meta.apply_blueprint")
@ManagedAppConfig.reconcile_global
def tasks_middlewares(self):
158    @ManagedAppConfig.reconcile_global
159    def tasks_middlewares(self):
160        from authentik.blueprints.v1.tasks import BlueprintWatcherMiddleware
161
162        get_broker().add_middleware(BlueprintWatcherMiddleware())
tenant_schedule_specs: list[authentik.tasks.schedules.common.ScheduleSpec]
164    @property
165    def tenant_schedule_specs(self) -> list[ScheduleSpec]:
166        from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
167
168        return [
169            ScheduleSpec(
170                actor=blueprints_discovery,
171                crontab=f"{fqdn_rand('blueprints_v1_discover')} * * * *",
172                send_on_startup=True,
173            ),
174            ScheduleSpec(
175                actor=clear_failed_blueprints,
176                crontab=f"{fqdn_rand('blueprints_v1_cleanup')} * * * *",
177                send_on_startup=True,
178            ),
179        ]

Get a list of schedule specs that must exist in each tenant