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