authentik.blueprints.apps

authentik Blueprints app

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

Basic reconciliation logic for apps

ManagedAppConfig(app_name: str, *args, **kwargs)
28    def __init__(self, app_name: str, *args, **kwargs) -> None:
29        super().__init__(app_name, *args, **kwargs)
30        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:
32    def ready(self) -> None:
33        self.import_related()
34        startup.connect(self._on_startup_callback, dispatch_uid=self.label)
35        return super().ready()

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

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

Load module

@staticmethod
def reconcile_tenant(func: Callable):
90    @staticmethod
91    def reconcile_tenant(func: Callable):
92        """Mark a function to be called on startup (for each tenant)"""
93        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_TENANT_CATEGORY
94        return func

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

@staticmethod
def reconcile_global(func: Callable):
 96    @staticmethod
 97    def reconcile_global(func: Callable):
 98        """Mark a function to be called on startup (globally)"""
 99        func._authentik_managed_reconcile = ManagedAppConfig.RECONCILE_GLOBAL_CATEGORY
100        return func

Mark a function to be called on startup (globally)

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

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

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

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

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

authentik Blueprints app

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

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