wip: description, fix total_seconds

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2025-06-20 18:45:02 +02:00
parent b80abffafc
commit 8554a8e0c5
22 changed files with 99 additions and 66 deletions

View File

@ -33,10 +33,10 @@ def _set_prom_info():
)
@actor
@actor(description=_("Update latest version info"))
def update_latest_version():
"""Update latest version info"""
self: Task = CurrentTask.get_task()
raise RuntimeError("whatever")
if CONFIG.get_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
self.info("Version check disabled.")

View File

@ -1,5 +1,7 @@
"""v1 blueprints tasks"""
from django.utils.translation import gettext_lazy as _
from dataclasses import asdict, dataclass, field
from hashlib import sha512
from pathlib import Path
@ -106,10 +108,10 @@ class BlueprintEventHandler(FileSystemEventHandler):
@actor(
description=_("Find blueprints as `blueprints_find` does, but return a safe dict"),
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_find_dict():
"""Find blueprints as `blueprints_find` does, but return a safe dict"""
blueprints = []
for blueprint in blueprints_find():
blueprints.append(sanitize_dict(asdict(blueprint)))
@ -145,9 +147,11 @@ def blueprints_find() -> list[BlueprintFile]:
return blueprints
@actor(throws=(DatabaseError, ProgrammingError, InternalError))
@actor(
description=_("Find blueprints and check if they need to be created in the database"),
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_discovery(path: str | None = None):
"""Find blueprints and check if they need to be created in the database"""
self: Task = CurrentTask.get_task()
count = 0
for blueprint in blueprints_find():
@ -185,9 +189,8 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
apply_blueprint.send_with_options(args=(instance.pk,), rel_obj=instance)
@actor
@actor(description=_("Apply single blueprint"))
def apply_blueprint(instance_pk: UUID):
"""Apply single blueprint"""
self: Task = CurrentTask.get_task()
self.set_uid(str(instance_pk))
instance: BlueprintInstance | None = None
@ -237,9 +240,8 @@ def apply_blueprint(instance_pk: UUID):
instance.save()
@actor
@actor(description=_("Remove blueprints which couldn't be fetched"))
def clear_failed_blueprints():
"""Remove blueprints which couldn't be fetched"""
# Exclude OCI blueprints as those might be temporarily unavailable
for blueprint in BlueprintInstance.objects.exclude(path__startswith=OCI_PREFIX):
try:

View File

@ -1,5 +1,7 @@
"""authentik core tasks"""
from django.utils.translation import gettext_lazy as _
from datetime import datetime, timedelta
from django.utils.timezone import now
@ -18,9 +20,8 @@ from authentik.tasks.models import Task
LOGGER = get_logger()
@actor
@actor(description=_("Remove expired objects"))
def clean_expired_models():
"""Remove expired objects"""
self: Task = CurrentTask.get_task()
for cls in ExpiringModel.__subclasses__():
cls: ExpiringModel
@ -34,9 +35,8 @@ def clean_expired_models():
self.info(f"Expired {amount} {cls._meta.verbose_name_plural}")
@actor
@actor(description=_("Remove temporary users created by SAML Sources"))
def clean_temporary_users():
"""Remove temporary users created by SAML Sources"""
self: Task = CurrentTask.get_task()
_now = datetime.now()
deleted_users = 0

View File

@ -1,5 +1,7 @@
"""Crypto tasks"""
from django.utils.translation import gettext_lazy as _
from glob import glob
from pathlib import Path
@ -35,9 +37,8 @@ def ensure_certificate_valid(body: str):
return body
@actor
@actor(description=_("Discover, import and update certificates from the filesystem"))
def certificate_discovery():
"""Discover, import and update certificates from the filesystem"""
self: Task = CurrentTask.get_task()
certs = {}
private_keys = {}

View File

@ -2,6 +2,7 @@ from django.db.models.aggregates import Count
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog import get_logger
from django.utils.translation import gettext_lazy as _
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
@ -12,11 +13,12 @@ from authentik.tasks.models import Task
LOGGER = get_logger()
@actor
@actor(
description=_(
"Check if any UniquePasswordPolicy exists, and if not, purge the password history table."
)
)
def check_and_purge_password_history():
"""Check if any UniquePasswordPolicy exists, and if not, purge the password history table.
This is run on a schedule instead of being triggered by policy binding deletion.
"""
self: Task = CurrentTask.get_task()
if not UniquePasswordPolicy.objects.exists():
@ -28,10 +30,8 @@ def check_and_purge_password_history():
self.info("Not purging password histories, a unique password policy exists")
@actor
@actor(description=_("Remove user password history that are too old."))
def trim_password_histories():
self: Task = CurrentTask.get_task()
"""Removes rows from UserPasswordHistory older than
the `n` most recent entries.
@ -39,6 +39,8 @@ def trim_password_histories():
UniquePasswordPolicy policies.
"""
self: Task = CurrentTask.get_task()
# No policy, we'll let the cleanup above do its thing
if not UniquePasswordPolicy.objects.exists():
return

View File

@ -4,26 +4,27 @@ from dramatiq.actor import actor
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.lib.sync.outgoing.tasks import SyncTasks
from django.utils.translation import gettext_lazy as _
sync_tasks = SyncTasks(GoogleWorkspaceProvider)
@actor
@actor(description=_("Sync Google Workspace provider objects."))
def google_workspace_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@actor
@actor(description=_("Full sync for Google Workspace provider."))
def google_workspace_sync(provider_pk: int, *args, **kwargs):
"""Run full sync for Google Workspace provider"""
return sync_tasks.sync(provider_pk, google_workspace_sync_objects)
@actor
@actor(description=_("Sync a direct object (user, group) for Google Workspace provider."))
def google_workspace_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@actor
@actor(description=_("Sync a related object (memberships) for Google Workspace provider."))
def google_workspace_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -4,26 +4,27 @@ from dramatiq.actor import actor
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.lib.sync.outgoing.tasks import SyncTasks
from django.utils.translation import gettext_lazy as _
sync_tasks = SyncTasks(MicrosoftEntraProvider)
@actor
@actor(description=_("Sync Microsoft Entra provider objects."))
def microsoft_entra_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@actor
@actor(description=_("Full sync for Microsoft Entra provider."))
def microsoft_entra_sync(provider_pk: int, *args, **kwargs):
"""Run full sync for Microsoft Entra provider"""
return sync_tasks.sync(provider_pk, microsoft_entra_sync_objects)
@actor
@actor(description=_("Sync a direct object (user, group) for Microsoft Entra provider."))
def microsoft_entra_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@actor
@actor(description=_("Sync a related object (memberships) for Microsoft Entra provider."))
def microsoft_entra_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -1,4 +1,5 @@
from typing import Any
from django.utils.translation import gettext_lazy as _
from uuid import UUID
from django.http import HttpRequest
@ -61,7 +62,7 @@ def _check_app_access(stream: Stream, event_data: dict) -> bool:
return engine.passing
@actor
@actor(description=_("Send an SSF event"))
def _send_ssf_event(stream_uuid: UUID, event_data: dict[str, Any]):
self: Task = CurrentTask.get_task()

View File

@ -1,11 +1,12 @@
"""Enterprise tasks"""
from django.utils.translation import gettext_lazy as _
from dramatiq.actor import actor
from authentik.enterprise.license import LicenseKey
@actor
@actor(description=_("Update enterprise license status."))
def enterprise_update_usage():
"""Update enterprise license status"""
LicenseKey.get_total().record_usage()

View File

@ -2,6 +2,7 @@
from uuid import UUID
from django.utils.translation import gettext_lazy as _
from django.db.models.query_utils import Q
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
@ -22,7 +23,7 @@ from authentik.tasks.models import Task
LOGGER = get_logger()
@actor
@actor(description=_("Check if policies attached to NotificationRule match event"))
def event_trigger_handler(event_uuid: UUID, trigger_name: str):
"""Check if policies attached to NotificationRule match event"""
event: Event = Event.objects.filter(event_uuid=event_uuid).first()
@ -79,7 +80,7 @@ def event_trigger_handler(event_uuid: UUID, trigger_name: str):
break
@actor
@actor(description=_("Send notification"))
def notification_transport(transport_pk: int, event_pk: str, user_pk: int, trigger_pk: str):
"""Send notification over specified transport"""
event = Event.objects.filter(pk=event_pk).first()
@ -100,7 +101,7 @@ def notification_transport(transport_pk: int, event_pk: str, user_pk: int, trigg
transport.send(notification)
@actor
@actor(description=_("Cleanup events for GDPR compliance"))
def gdpr_cleanup(user_pk: int):
"""cleanup events from gdpr_compliance"""
events = Event.objects.filter(user__pk=user_pk)

View File

@ -1,5 +1,7 @@
"""outpost tasks"""
from django.utils.translation import gettext_lazy as _
from hashlib import sha256
from os import R_OK, access
from pathlib import Path
@ -82,7 +84,7 @@ def controller_for_outpost(outpost: Outpost) -> type[BaseController] | None:
return None
@actor
@actor(description=_("Update cached state of a service connection"))
def outpost_service_connection_monitor(connection_pk: Any):
"""Update cached state of a service connection"""
connection: OutpostServiceConnection = (
@ -107,7 +109,7 @@ def outpost_service_connection_monitor(connection_pk: Any):
cache.set(connection.state_key, state, timeout=None)
@actor
@actor(description=_("Create/update/monitor/delete the deployment of an Outpost"))
def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = False):
"""Create/update/monitor/delete the deployment of an Outpost"""
self: Task = CurrentTask.get_task()
@ -140,7 +142,7 @@ def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = F
self.info(log)
@actor
@actor(description=_("Ensure that all Outposts have valid Service Accounts and Tokens"))
def outpost_token_ensurer():
"""
Periodically ensure that all Outposts have valid Service Accounts and Tokens
@ -153,7 +155,7 @@ def outpost_token_ensurer():
self.info(f"Successfully checked {len(all_outposts)} Outposts.")
@actor
@actor(description=_("If an Outpost is saved, ensure that token is created/updated"))
def outpost_post_save(model_class: str, model_pk: Any):
"""If an Outpost is saved, Ensure that token is created/updated
@ -225,7 +227,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
async_to_sync(layer.group_send)(group, {"type": "event.update"})
@actor
@actor(description=_("Checks the local environment and create Service connections."))
def outpost_connection_discovery():
"""Checks the local environment and create Service connections."""
self: Task = CurrentTask.get_task()
@ -266,9 +268,8 @@ def outpost_connection_discovery():
)
@actor
@actor(description=_("Terminate session on all outposts"))
def outpost_session_end(session_id: str):
"""Update outpost instances connected to a single outpost"""
layer = get_channel_layer()
hashed_session_id = hash_session_key(session_id)
for outpost in Outpost.objects.all():

View File

@ -7,11 +7,11 @@ from dramatiq.actor import actor
from authentik.outposts.consumer import OUTPOST_GROUP
from authentik.outposts.models import Outpost, OutpostType
from authentik.providers.oauth2.id_token import hash_session_key
from django.utils.translation import gettext_lazy as _
@actor
@actor(description=_("Terminate session on Proxy outpost"))
def proxy_on_logout(session_id: str):
"""Update outpost instances connected to a single outpost"""
layer = get_channel_layer()
hashed_session_id = hash_session_key(session_id)
for outpost in Outpost.objects.filter(type=OutpostType.PROXY):

View File

@ -4,26 +4,27 @@ from dramatiq.actor import actor
from authentik.lib.sync.outgoing.tasks import SyncTasks
from authentik.providers.scim.models import SCIMProvider
from django.utils.translation import gettext_lazy as _
sync_tasks = SyncTasks(SCIMProvider)
@actor
@actor(description=_("Sync SCIM provider objects."))
def scim_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@actor
@actor(description=_("Full sync for SCIM provider."))
def scim_sync(provider_pk: int, *args, **kwargs):
"""Run full sync for SCIM provider"""
return sync_tasks.sync(provider_pk, scim_sync_objects)
@actor
@actor(description=_("Sync a direct object (user, group) for SCIM provider."))
def scim_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@actor
@actor(description=_("Sync a related object (memberships) for SCIM provider."))
def scim_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -360,8 +360,8 @@ DRAMATIQ = {
"task_model": "authentik.tasks.models.Task",
"task_purge_interval": timedelta_from_string(
CONFIG.get("worker.task_purge_interval")
).total_seconds,
"task_expiration": timedelta_from_string(CONFIG.get("worker.task_expiration")).total_seconds,
).total_seconds(),
"task_expiration": timedelta_from_string(CONFIG.get("worker.task_expiration")).total_seconds(),
"autodiscovery": {
"enabled": True,
"setup_module": "authentik.tasks.setup",
@ -372,7 +372,7 @@ DRAMATIQ = {
"threads": CONFIG.get_int("worker.threads", 1),
"consumer_listen_timeout": timedelta_from_string(
CONFIG.get("worker.consumer_listen_timeout")
),
).total_seconds(),
},
"scheduler_class": "authentik.tasks.schedules.scheduler.Scheduler",
"schedule_model": "authentik.tasks.schedules.models.Schedule",
@ -400,6 +400,7 @@ DRAMATIQ = {
("authentik.tasks.middleware.TenantMiddleware", {}),
("authentik.tasks.middleware.RelObjMiddleware", {}),
("authentik.tasks.middleware.LoggingMiddleware", {}),
("authentik.tasks.middleware.DescriptionMiddleware", {}),
),
"test": TEST,
}

View File

@ -5,6 +5,7 @@ from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from structlog.stdlib import get_logger
from django.utils.translation import gettext_lazy as _
from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
@ -16,7 +17,7 @@ LOGGER = get_logger()
CACHE_KEY_STATUS = "goauthentik.io/sources/kerberos/status/"
@actor
@actor(description=_("Check connectivity for Kerberos sources"))
def kerberos_connectivity_check(pk: str):
"""Check connectivity for Kerberos Sources"""
# 2 hour timeout, this task should run every hour
@ -28,9 +29,11 @@ def kerberos_connectivity_check(pk: str):
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout)
@actor(time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5 * 1000)
@actor(
time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5 * 1000,
description=_("Sync Kerberos source"),
)
def kerberos_sync(pk: str):
"""Sync a single source"""
self: Task = CurrentTask.get_task()
source: KerberosSource = KerberosSource.objects.filter(enabled=True, pk=pk).first()
if not source:

View File

@ -9,6 +9,7 @@ from dramatiq.composition import group
from dramatiq.message import Message
from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
from django.utils.translation import gettext_lazy as _
from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync
@ -33,7 +34,7 @@ CACHE_KEY_PREFIX = "goauthentik.io/sources/ldap/page/"
CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/"
@actor
@actor(description=_("Check connectivity for LDAP sources"))
def ldap_connectivity_check(pk: str | None = None):
"""Check connectivity for LDAP Sources"""
timeout = 60 * 60 * 2
@ -48,8 +49,8 @@ def ldap_connectivity_check(pk: str | None = None):
# We take the configured hours timeout time by 3.5 as we run user and
# group in parallel and then membership, then deletions, so 3x is to cover the serial tasks,
# and 0.5x on top of that to give some more leeway
time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000)
* 3.5,
time_limit=(60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000) * 3.5,
description=_("Sync LDAP source"),
)
def ldap_sync(source_pk: str):
"""Sync a single source"""
@ -116,7 +117,10 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) ->
return messages
@actor(time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000)
@actor(
time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours") * 1000,
description=_("Sync page for LDAP source"),
)
def ldap_sync_page(source_pk: str, sync_class: str, page_cache_key: str):
"""Synchronization of an LDAP Source"""
self: Task = CurrentTask.get_task()

View File

@ -6,6 +6,7 @@ from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from requests import RequestException
from structlog.stdlib import get_logger
from django.utils.translation import gettext_lazy as _
from authentik.lib.utils.http import get_http_session
from authentik.sources.oauth.models import OAuthSource
@ -14,9 +15,12 @@ from authentik.tasks.models import Task
LOGGER = get_logger()
@actor
@actor(
description=_(
"Update OAuth sources' config from well_known, and JWKS info from the configured URL"
)
)
def update_well_known_jwks():
"""Update OAuth sources' config from well_known, and JWKS info from the configured URL"""
self: Task = CurrentTask.get_task()
session = get_http_session()
for source in OAuthSource.objects.all().exclude(oidc_well_known_url=""):

View File

@ -9,9 +9,10 @@ from authentik.lib.utils.errors import exception_to_string
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
from authentik.tasks.models import Task
from django.utils.translation import gettext_lazy as _
@actor
@actor(description=_("Check the validity of a Plex source"))
def check_plex_token(source_pk: str):
"""Check the validity of a Plex source."""
self: Task = CurrentTask.get_task()

View File

@ -9,6 +9,7 @@ from django.db.transaction import atomic
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from fido2.mds3 import filter_revoked, parse_blob
from django.utils.translation import gettext_lazy as _
from authentik.stages.authenticator_webauthn.models import (
UNKNOWN_DEVICE_TYPE_AAGUID,
@ -29,7 +30,7 @@ def mds_ca() -> bytes:
return _raw_root.read()
@actor
@actor(description=_("Background task to import FIDO Alliance MDS blob and AAGUIDs into database"))
def webauthn_mds_import(force=False):
"""Background task to import FIDO Alliance MDS blob and AAGUIDs into database"""
self: Task = CurrentTask.get_task()

View File

@ -6,6 +6,7 @@ from typing import Any
from django.core.mail import EmailMultiAlternatives
from django.core.mail.utils import DNS_NAME
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq.actor import actor
from dramatiq.composition import group
@ -48,7 +49,7 @@ def get_email_body(email: EmailMultiAlternatives) -> str:
return email.body
@actor
@actor(description=_("Send email"))
def send_mail(
message: dict[Any, Any],
stage_class_path: str | None = None,

View File

@ -72,3 +72,9 @@ class LoggingMiddleware(Middleware):
message=f"Task {task.actor_name} encountered an error: "
"{exception_to_string(exception)}",
).save()
class DescriptionMiddleware(Middleware):
@property
def actor_options(self):
return {"description"}

View File

@ -52,9 +52,9 @@ class ScheduleSerializer(ModelSerializer):
actor: Actor = get_broker().get_actor(instance.actor_name)
except ActorNotFound:
return "FIXME this shouldn't happen"
if not actor.fn.__doc__:
if "description" not in actor.options:
return "no doc"
return actor.fn.__doc__.strip()
return actor.options["description"]
class ScheduleFilter(FilterSet):