Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2025-04-02 17:52:10 +02:00
parent 3c1512028d
commit e89659fe71
44 changed files with 169 additions and 102 deletions

View File

@ -1,12 +1,13 @@
"""authentik API AppConfig"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikAPIConfig(AppConfig):
class AuthentikAPIConfig(ManagedAppConfig):
"""authentik API Config"""
name = "authentik.api"
label = "authentik_api"
mountpoint = "api/"
verbose_name = "authentik API"
default = True
mountpoint = "api/"

View File

@ -6,6 +6,7 @@ from inspect import ismethod
from django.apps import AppConfig
from django.db import DatabaseError, InternalError, ProgrammingError
from dramatiq.broker import get_broker
from structlog.stdlib import BoundLogger, get_logger
from authentik.lib.utils.time import fqdn_rand
@ -92,10 +93,6 @@ class ManagedAppConfig(AppConfig):
"""Get a list of schedule specs that must exist in the default tenant"""
return []
def _reconcile_schedules(self, schedules: list[ScheduleSpec]):
for schedule in schedules:
schedule.update_or_create()
def _reconcile_tenant(self) -> None:
"""reconcile ourselves for tenanted methods"""
from authentik.tenants.models import Tenant
@ -108,7 +105,6 @@ class ManagedAppConfig(AppConfig):
for tenant in tenants:
with tenant:
self._reconcile(self.RECONCILE_TENANT_CATEGORY)
self._reconcile_schedules(self.tenant_schedule_specs)
def _reconcile_global(self) -> None:
"""
@ -119,7 +115,6 @@ class ManagedAppConfig(AppConfig):
with schema_context(get_public_schema_name()):
self._reconcile(self.RECONCILE_GLOBAL_CATEGORY)
self._reconcile_schedules(self.global_schedule_specs)
class AuthentikBlueprintsConfig(ManagedAppConfig):
@ -131,9 +126,15 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
default = True
@ManagedAppConfig.reconcile_global
def load_blueprints_v1_tasks(self):
"""Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks")
def tasks_middlewares(self):
from authentik.blueprints.v1.tasks import BlueprintWatcherMiddleware
get_broker().add_middleware(BlueprintWatcherMiddleware())
# @ManagedAppConfig.reconcile_global
# def load_blueprints_v1_tasks(self):
# """Load v1 tasks"""
# self.import_module("authentik.blueprints.v1.tasks")
@ManagedAppConfig.reconcile_tenant
def blueprints_discovery(self):

View File

@ -0,0 +1,2 @@
# Import all v1 tasks for auto task discovery
from authentik.blueprints.v1.tasks import * # noqa: F403

View File

@ -65,7 +65,7 @@ class BlueprintWatcherMiddleware(Middleware):
)
observer.start()
def before_worker_boot(self, broker, worker):
def after_worker_boot(self, broker, worker):
self.start_blueprint_watcher()

View File

@ -1,14 +1,15 @@
"""authentik brands app"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikBrandsConfig(AppConfig):
class AuthentikBrandsConfig(ManagedAppConfig):
"""authentik Brand app"""
name = "authentik.brands"
label = "authentik_brands"
verbose_name = "authentik Brands"
default = True
mountpoints = {
"authentik.brands.urls_root": "",
}

View File

@ -317,7 +317,7 @@ class Outpost(ScheduledModel, SerializerModel, ManagedModel):
ScheduleSpec(
actor_name="authentik.outposts.tasks.outpost_controller",
uid=self.pk,
args=(self.pk, "up"),
args=(self.pk,),
kwargs={"action": "up", "from_cache": False},
crontab=f"{fqdn_rand('outpost_controller')} */4 * * *",
description=_(

View File

@ -134,8 +134,9 @@ def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = F
@actor
def outpost_token_ensurer():
"""Periodically ensure that all Outposts have valid Service Accounts
and Tokens"""
"""
Periodically ensure that all Outposts have valid Service Accounts and Tokens
"""
self: Task = CurrentTask.get_task()
all_outposts = Outpost.objects.all()
for outpost in all_outposts:

View File

@ -1,11 +1,12 @@
"""Authentik policy dummy app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPolicyDummyConfig(AppConfig):
class AuthentikPolicyDummyConfig(ManagedAppConfig):
"""Authentik policy_dummy app config"""
name = "authentik.policies.dummy"
label = "authentik_policies_dummy"
verbose_name = "authentik Policies.Dummy"
default = True

View File

@ -1,11 +1,12 @@
"""authentik Event Matcher policy app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPoliciesEventMatcherConfig(AppConfig):
class AuthentikPoliciesEventMatcherConfig(ManagedAppConfig):
"""authentik Event Matcher policy app config"""
name = "authentik.policies.event_matcher"
label = "authentik_policies_event_matcher"
verbose_name = "authentik Policies.Event Matcher"
default = True

View File

@ -1,11 +1,12 @@
"""Authentik policy_expiry app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPolicyExpiryConfig(AppConfig):
class AuthentikPolicyExpiryConfig(ManagedAppConfig):
"""Authentik policy_expiry app config"""
name = "authentik.policies.expiry"
label = "authentik_policies_expiry"
verbose_name = "authentik Policies.Expiry"
default = True

View File

@ -1,11 +1,12 @@
"""Authentik policy_expression app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPolicyExpressionConfig(AppConfig):
class AuthentikPolicyExpressionConfig(ManagedAppConfig):
"""Authentik policy_expression app config"""
name = "authentik.policies.expression"
label = "authentik_policies_expression"
verbose_name = "authentik Policies.Expression"
default = True

View File

@ -1,11 +1,12 @@
"""Authentik policy geoip app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPolicyGeoIPConfig(AppConfig):
class AuthentikPolicyGeoIPConfig(ManagedAppConfig):
"""Authentik policy_geoip app config"""
name = "authentik.policies.geoip"
label = "authentik_policies_geoip"
verbose_name = "authentik Policies.GeoIP"
default = True

View File

@ -1,11 +1,12 @@
"""authentik Password policy app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikPoliciesPasswordConfig(AppConfig):
class AuthentikPoliciesPasswordConfig(ManagedAppConfig):
"""authentik Password policy app config"""
name = "authentik.policies.password"
label = "authentik_policies_password"
verbose_name = "authentik Policies.Password"
default = True

View File

@ -1,11 +1,12 @@
"""authentik ldap provider app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderLDAPConfig(AppConfig):
class AuthentikProviderLDAPConfig(ManagedAppConfig):
"""authentik ldap provider app config"""
name = "authentik.providers.ldap"
label = "authentik_providers_ldap"
verbose_name = "authentik Providers.LDAP"
default = True

View File

@ -1,11 +1,12 @@
"""authentik radius provider app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderRadiusConfig(AppConfig):
class AuthentikProviderRadiusConfig(ManagedAppConfig):
"""authentik radius provider app config"""
name = "authentik.providers.radius"
label = "authentik_providers_radius"
verbose_name = "authentik Providers.Radius"
default = True

View File

@ -1,12 +1,13 @@
"""authentik SAML IdP app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikProviderSAMLConfig(AppConfig):
class AuthentikProviderSAMLConfig(ManagedAppConfig):
"""authentik SAML IdP app config"""
name = "authentik.providers.saml"
label = "authentik_providers_saml"
verbose_name = "authentik Providers.SAML"
mountpoint = "application/saml/"
default = True

View File

@ -1,12 +1,13 @@
"""authentik Recovery app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikRecoveryConfig(AppConfig):
class AuthentikRecoveryConfig(ManagedAppConfig):
"""authentik Recovery app config"""
name = "authentik.recovery"
label = "authentik_recovery"
verbose_name = "authentik Recovery"
mountpoint = "recovery/"
default = True

View File

@ -1,11 +1,12 @@
"""authentik plex config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikSourcePlexConfig(AppConfig):
class AuthentikSourcePlexConfig(ManagedAppConfig):
"""authentik source plex config"""
name = "authentik.sources.plex"
label = "authentik_sources_plex"
verbose_name = "authentik Sources.Plex"
default = True

View File

@ -1,11 +1,12 @@
"""Authenticator"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageAuthenticatorConfig(AppConfig):
class AuthentikStageAuthenticatorConfig(ManagedAppConfig):
"""Authenticator App config"""
name = "authentik.stages.authenticator"
label = "authentik_stages_authenticator"
verbose_name = "authentik Stages.Authenticator"
default = True

View File

@ -1,11 +1,12 @@
"""SMS"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageAuthenticatorSMSConfig(AppConfig):
class AuthentikStageAuthenticatorSMSConfig(ManagedAppConfig):
"""SMS App config"""
name = "authentik.stages.authenticator_sms"
label = "authentik_stages_authenticator_sms"
verbose_name = "authentik Stages.Authenticator.SMS"
default = True

View File

@ -1,11 +1,12 @@
"""TOTP"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageAuthenticatorTOTPConfig(AppConfig):
class AuthentikStageAuthenticatorTOTPConfig(ManagedAppConfig):
"""TOTP App config"""
name = "authentik.stages.authenticator_totp"
label = "authentik_stages_authenticator_totp"
verbose_name = "authentik Stages.Authenticator.TOTP"
default = True

View File

@ -1,11 +1,12 @@
"""Authenticator Validation Stage"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageAuthenticatorValidateConfig(AppConfig):
class AuthentikStageAuthenticatorValidateConfig(ManagedAppConfig):
"""Authenticator Validation Stage"""
name = "authentik.stages.authenticator_validate"
label = "authentik_stages_authenticator_validate"
verbose_name = "authentik Stages.Authenticator.Validate"
default = True

View File

@ -1,11 +1,12 @@
"""authentik captcha app"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageCaptchaConfig(AppConfig):
class AuthentikStageCaptchaConfig(ManagedAppConfig):
"""authentik captcha app"""
name = "authentik.stages.captcha"
label = "authentik_stages_captcha"
verbose_name = "authentik Stages.Captcha"
default = True

View File

@ -1,11 +1,12 @@
"""authentik consent app"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageConsentConfig(AppConfig):
class AuthentikStageConsentConfig(ManagedAppConfig):
"""authentik consent app"""
name = "authentik.stages.consent"
label = "authentik_stages_consent"
verbose_name = "authentik Stages.Consent"
default = True

View File

@ -1,11 +1,12 @@
"""authentik deny stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageDenyConfig(AppConfig):
class AuthentikStageDenyConfig(ManagedAppConfig):
"""authentik deny stage config"""
name = "authentik.stages.deny"
label = "authentik_stages_deny"
verbose_name = "authentik Stages.Deny"
default = True

View File

@ -1,11 +1,12 @@
"""authentik dummy stage config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageDummyConfig(AppConfig):
class AuthentikStageDummyConfig(ManagedAppConfig):
"""authentik dummy stage config"""
name = "authentik.stages.dummy"
label = "authentik_stages_dummy"
verbose_name = "authentik Stages.Dummy"
default = True

View File

@ -1,11 +1,12 @@
"""authentik identification stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageIdentificationConfig(AppConfig):
class AuthentikStageIdentificationConfig(ManagedAppConfig):
"""authentik identification stage config"""
name = "authentik.stages.identification"
label = "authentik_stages_identification"
verbose_name = "authentik Stages.Identification"
default = True

View File

@ -1,11 +1,12 @@
"""authentik invitation stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageInvitationConfig(AppConfig):
class AuthentikStageInvitationConfig(ManagedAppConfig):
"""authentik invitation stage config"""
name = "authentik.stages.invitation"
label = "authentik_stages_invitation"
verbose_name = "authentik Stages.Invitation"
default = True

View File

@ -1,11 +1,12 @@
"""authentik core app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStagePasswordConfig(AppConfig):
class AuthentikStagePasswordConfig(ManagedAppConfig):
"""authentik password stage config"""
name = "authentik.stages.password"
label = "authentik_stages_password"
verbose_name = "authentik Stages.Password"
default = True

View File

@ -1,11 +1,12 @@
"""authentik prompt stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStagePromptConfig(AppConfig):
class AuthentikStagePromptConfig(ManagedAppConfig):
"""authentik prompt stage config"""
name = "authentik.stages.prompt"
label = "authentik_stages_prompt"
verbose_name = "authentik Stages.Prompt"
default = True

View File

@ -1,11 +1,12 @@
"""authentik redirect app"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageRedirectConfig(AppConfig):
class AuthentikStageRedirectConfig(ManagedAppConfig):
"""authentik redirect app"""
name = "authentik.stages.redirect"
label = "authentik_stages_redirect"
verbose_name = "authentik Stages.Redirect"
default = True

View File

@ -1,11 +1,12 @@
"""authentik delete stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageUserDeleteConfig(AppConfig):
class AuthentikStageUserDeleteConfig(ManagedAppConfig):
"""authentik delete stage config"""
name = "authentik.stages.user_delete"
label = "authentik_stages_user_delete"
verbose_name = "authentik Stages.User Delete"
default = True

View File

@ -1,11 +1,12 @@
"""authentik login stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageUserLoginConfig(AppConfig):
class AuthentikStageUserLoginConfig(ManagedAppConfig):
"""authentik login stage config"""
name = "authentik.stages.user_login"
label = "authentik_stages_user_login"
verbose_name = "authentik Stages.User Login"
default = True

View File

@ -1,11 +1,12 @@
"""authentik logout stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageUserLogoutConfig(AppConfig):
class AuthentikStageUserLogoutConfig(ManagedAppConfig):
"""authentik logout stage config"""
name = "authentik.stages.user_logout"
label = "authentik_stages_user_logout"
verbose_name = "authentik Stages.User Logout"
default = True

View File

@ -1,11 +1,12 @@
"""authentik write stage app config"""
from django.apps import AppConfig
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikStageUserWriteConfig(AppConfig):
class AuthentikStageUserWriteConfig(ManagedAppConfig):
"""authentik write stage config"""
name = "authentik.stages.user_write"
label = "authentik_stages_user_write"
verbose_name = "authentik Stages.User Write"
default = True

View File

@ -35,6 +35,7 @@ class AuthentikTasksConfig(ManagedAppConfig):
broker.add_middleware(Pipelines())
broker.add_middleware(Retries(max_retries=max_retries))
broker.add_middleware(Results(backend=PostgresBackend(), store_results=True))
broker.add_middleware(FullyQualifiedActorName())
broker.add_middleware(CurrentTask())

View File

@ -319,20 +319,29 @@ class _PostgresConsumer(Consumer):
@raise_connection_error
def requeue(self, messages: Iterable[Message]):
for message in messages:
self.unlock_queue.put_nowait(message)
self.query_set.filter(
message_id__in=[message.message_id for message in messages],
).update(
state=TaskState.QUEUED,
)
# We don't care about locks, requeue occurs on worker stop
# TODO: this is not true, we need to handle them
for message in messages:
self.in_processing.remove(message.message_id)
self._purge_locks()
def _fetch_pending_notifies(self) -> list[Notify]:
self.logger.debug(f"Polling for lost messages in {self.queue_name}")
notifies = self.query_set.filter(
state__in=(TaskState.QUEUED, TaskState.CONSUMED),
queue_name=self.queue_name,
).values_list("message_id", flat=True)
notifies = (
self.query_set.filter(
state__in=(TaskState.QUEUED, TaskState.CONSUMED),
queue_name=self.queue_name,
)
.exclude(
message_id__in=self.in_processing,
)
.values_list("message_id", flat=True)
)
channel = channel_name(self.queue_name, ChannelIdentifier.ENQUEUE)
return [Notify(pid=0, channel=channel, payload=item) for item in notifies]
@ -383,7 +392,7 @@ class _PostgresConsumer(Consumer):
processing = len(self.in_processing)
if processing >= self.prefetch:
# Wait and don't consume the message, other worker will be fast
# Wait and don't consume the message, other worker will be faster
self.misses, backoff_ms = compute_backoff(self.misses, max_backoff=1000)
self.logger.debug(
f"Too many messages in processing: {processing}. Sleeping {backoff_ms} ms"

View File

@ -1,6 +1,7 @@
import os
import sys
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.module_loading import module_has_submodule
@ -48,7 +49,7 @@ class Command(BaseCommand):
):
executable_name = "dramatiq-gevent" if use_gevent else "dramatiq"
executable_path = self._resolve_executable(executable_name)
watch_args = ["--watch", "."] if use_watcher else []
watch_args = ["--watch", "."] if use_watcher or settings.DEBUG else []
if watch_args and use_polling_watcher:
watch_args.append("--watch-use-polling")

View File

@ -35,8 +35,8 @@ class ScheduleSerializer(ModelSerializer):
try:
actor: Actor = get_broker().get_actor(instance.actor_name)
except ActorNotFound:
return None
return actor.fn.__doc__
return "FIXME this shouldn't happen"
return actor.fn.__doc__.strip()
class ScheduleViewSet(

View File

@ -1,4 +1,5 @@
from authentik.blueprints.apps import ManagedAppConfig
from authentik.lib.utils.reflection import get_apps
from authentik.tasks.schedules.lib import ScheduleSpec
@ -21,3 +22,26 @@ class AuthentikTasksSchedulesConfig(ManagedAppConfig):
spec.rel_obj = obj
schedules.append(spec)
return schedules
def _reconcile_schedules(self, specs: list[ScheduleSpec]):
from django.db import transaction
from authentik.tasks.schedules.models import Schedule
with transaction.atomic():
pks_to_keep = []
for spec in specs:
schedule = spec.update_or_create()
pks_to_keep.append(schedule.pk)
Schedule.objects.exclude(pk__in=pks_to_keep).delete()
@ManagedAppConfig.reconcile_tenant
def reconcile_tenant_schedules(self):
from authentik.tenants.utils import get_current_tenant, get_public_schema_name
schedule_specs = []
for app in get_apps():
schedule_specs.extend(app.tenant_schedule_specs)
if get_current_tenant().schema_name == get_public_schema_name():
schedule_specs.extend(app.global_schedule_specs)
self._reconcile_schedules(schedule_specs)

View File

@ -1,14 +1,10 @@
import os
import sys
import warnings
from cryptography.hazmat.backends.openssl.backend import backend
from defusedxml import defuse_stdlib
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
from authentik.lib.config import CONFIG
from lifecycle.migrate import run_migrations
from lifecycle.wait_for_db import wait_for_db
warnings.filterwarnings("ignore", "SelectableGroups dict interface")
warnings.filterwarnings(
@ -25,21 +21,15 @@ defuse_stdlib()
if CONFIG.get_bool("compliance.fips.enabled", False):
backend._enable_fips()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
wait_for_db()
print(sys.argv)
if (
len(sys.argv) > 1
# Explicitly only run migrate for server and worker
# `bootstrap_tasks` is a special case as that command might be triggered by the `ak`
# script to pre-run certain tasks for an automated install
and sys.argv[1] in ["dev_server", "worker", "bootstrap_tasks"]
# and don't run if this is the child process of a dev_server
and os.environ.get(DJANGO_AUTORELOAD_ENV, None) is None
):
run_migrations()
import django # noqa: E402
django.setup()
from authentik.root.signals import post_startup, pre_startup, startup # noqa: E402
_startup_sender = type("WorkerStartup", (object,), {})
pre_startup.send(sender=_startup_sender)
startup.send(sender=_startup_sender)
post_startup.send(sender=_startup_sender)

View File

@ -108,6 +108,7 @@ func NewWebServer() *WebServer {
func (ws *WebServer) Start() {
go ws.runMetricsServer()
go ws.attemptStartBackend()
go ws.attemptStartWorker()
go ws.listenPlain()
go ws.listenTLS()
}
@ -137,6 +138,12 @@ func (ws *WebServer) attemptStartBackend() {
}
}
func (ws *WebServer) attemptStartWorker() {
if ws.worker == nil {
return
}
}
func (ws *WebServer) Core() *gounicorn.GoUnicorn {
return ws.g
}

View File

@ -30,7 +30,7 @@ type Worker struct {
}
func New(healthcheck func() bool) *Worker {
logger := log.WithField("logger", "authentik.router.unicorn")
logger := log.WithField("logger", "authentik.router.worker")
w := &Worker{
Healthcheck: healthcheck,
log: logger,

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python
"""Django manage.py"""
import os
import sys
import warnings