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
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.
@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
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
name =
'authentik.blueprints'
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