sources/ldap: bump timeout, run each sync component in its own task
closes #1411 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
9257f3c919
commit
06af306e8a
|
@ -1,5 +1,6 @@
|
||||||
"""authentik lib reflection utilities"""
|
"""authentik lib reflection utilities"""
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -19,12 +20,12 @@ def all_subclasses(cls, sort=True):
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
|
|
||||||
def class_to_path(cls):
|
def class_to_path(cls: type) -> str:
|
||||||
"""Turn Class (Class or instance) into module path"""
|
"""Turn Class (Class or instance) into module path"""
|
||||||
return f"{cls.__module__}.{cls.__name__}"
|
return f"{cls.__module__}.{cls.__name__}"
|
||||||
|
|
||||||
|
|
||||||
def path_to_class(path):
|
def path_to_class(path: Union[str, None]) -> Union[type, None]:
|
||||||
"""Import module and return class"""
|
"""Import module and return class"""
|
||||||
if not path:
|
if not path:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.http.response import Http404
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django_filters.filters import AllValuesMultipleFilter
|
from django_filters.filters import AllValuesMultipleFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
|
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -19,6 +18,9 @@ from authentik.core.api.sources import SourceSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.events.monitored_tasks import TaskInfo
|
from authentik.events.monitored_tasks import TaskInfo
|
||||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||||
|
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||||
|
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
|
||||||
|
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
|
||||||
|
|
||||||
|
|
||||||
class LDAPSourceSerializer(SourceSerializer):
|
class LDAPSourceSerializer(SourceSerializer):
|
||||||
|
@ -95,19 +97,24 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
200: TaskSerializer(many=False),
|
200: TaskSerializer(many=True),
|
||||||
404: OpenApiResponse(description="Task not found"),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(methods=["GET"], detail=True)
|
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def sync_status(self, request: Request, slug: str) -> Response:
|
def sync_status(self, request: Request, slug: str) -> Response:
|
||||||
"""Get source's sync status"""
|
"""Get source's sync status"""
|
||||||
source = self.get_object()
|
source = self.get_object()
|
||||||
task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}")
|
results = []
|
||||||
if not task:
|
for sync_class in [
|
||||||
raise Http404
|
UserLDAPSynchronizer,
|
||||||
return Response(TaskSerializer(task, many=False).data)
|
GroupLDAPSynchronizer,
|
||||||
|
MembershipLDAPSynchronizer,
|
||||||
|
]:
|
||||||
|
task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}-{sync_class.__name__}")
|
||||||
|
if task:
|
||||||
|
results.append(task)
|
||||||
|
return Response(TaskSerializer(results, many=True).data)
|
||||||
|
|
||||||
|
|
||||||
class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
|
class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from celery.schedules import crontab
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"sources_ldap_sync": {
|
"sources_ldap_sync": {
|
||||||
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
||||||
"schedule": crontab(minute="*/60"), # Run every hour
|
"schedule": crontab(minute="*/120"), # Run every other hour
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,12 @@ from authentik.core.models import User
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
from authentik.sources.ldap.password import LDAPPasswordChanger
|
from authentik.sources.ldap.password import LDAPPasswordChanger
|
||||||
|
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||||
|
from authentik.sources.ldap.sync.membership import MembershipLDAPSynchronizer
|
||||||
|
from authentik.sources.ldap.sync.users import UserLDAPSynchronizer
|
||||||
from authentik.sources.ldap.tasks import ldap_sync
|
from authentik.sources.ldap.tasks import ldap_sync
|
||||||
from authentik.stages.prompt.signals import password_validate
|
from authentik.stages.prompt.signals import password_validate
|
||||||
|
|
||||||
|
@ -22,7 +26,12 @@ from authentik.stages.prompt.signals import password_validate
|
||||||
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
|
||||||
"""Ensure that source is synced on save (if enabled)"""
|
"""Ensure that source is synced on save (if enabled)"""
|
||||||
if instance.enabled:
|
if instance.enabled:
|
||||||
ldap_sync.delay(instance.pk)
|
for sync_class in [
|
||||||
|
UserLDAPSynchronizer,
|
||||||
|
GroupLDAPSynchronizer,
|
||||||
|
MembershipLDAPSynchronizer,
|
||||||
|
]:
|
||||||
|
ldap_sync.delay(instance.pk, class_to_path(sync_class))
|
||||||
|
|
||||||
|
|
||||||
@receiver(password_validate)
|
@receiver(password_validate)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from ldap3.core.exceptions import LDAPException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
from authentik.lib.utils.reflection import class_to_path, path_to_class
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.sources.ldap.models import LDAPSource
|
from authentik.sources.ldap.models import LDAPSource
|
||||||
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||||
|
@ -17,11 +18,18 @@ LOGGER = get_logger()
|
||||||
def ldap_sync_all():
|
def ldap_sync_all():
|
||||||
"""Sync all sources"""
|
"""Sync all sources"""
|
||||||
for source in LDAPSource.objects.filter(enabled=True):
|
for source in LDAPSource.objects.filter(enabled=True):
|
||||||
ldap_sync.delay(source.pk)
|
for sync_class in [
|
||||||
|
UserLDAPSynchronizer,
|
||||||
|
GroupLDAPSynchronizer,
|
||||||
|
MembershipLDAPSynchronizer,
|
||||||
|
]:
|
||||||
|
ldap_sync.delay(source.pk, class_to_path(sync_class))
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(
|
||||||
def ldap_sync(self: MonitoredTask, source_pk: str):
|
bind=True, base=MonitoredTask, soft_time_limit=60 * 60 * 2, task_time_limit=60 * 60 * 2
|
||||||
|
)
|
||||||
|
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
|
||||||
"""Synchronization of an LDAP Source"""
|
"""Synchronization of an LDAP Source"""
|
||||||
self.result_timeout_hours = 2
|
self.result_timeout_hours = 2
|
||||||
try:
|
try:
|
||||||
|
@ -30,17 +38,13 @@ def ldap_sync(self: MonitoredTask, source_pk: str):
|
||||||
# Because the source couldn't be found, we don't have a UID
|
# Because the source couldn't be found, we don't have a UID
|
||||||
# to set the state with
|
# to set the state with
|
||||||
return
|
return
|
||||||
self.set_uid(slugify(source.name))
|
sync = path_to_class(sync_class)
|
||||||
|
self.set_uid(f"{slugify(source.name)}-{sync.__name__}")
|
||||||
try:
|
try:
|
||||||
messages = []
|
messages = []
|
||||||
for sync_class in [
|
sync_inst = sync(source)
|
||||||
UserLDAPSynchronizer,
|
|
||||||
GroupLDAPSynchronizer,
|
|
||||||
MembershipLDAPSynchronizer,
|
|
||||||
]:
|
|
||||||
sync_inst = sync_class(source)
|
|
||||||
count = sync_inst.sync()
|
count = sync_inst.sync()
|
||||||
messages.append(f"Synced {count} objects from {sync_class.__name__}")
|
messages.append(f"Synced {count} objects.")
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskResult(
|
TaskResult(
|
||||||
TaskResultStatus.SUCCESSFUL,
|
TaskResultStatus.SUCCESSFUL,
|
||||||
|
|
|
@ -12018,7 +12018,7 @@ paths:
|
||||||
$ref: '#/components/schemas/GenericError'
|
$ref: '#/components/schemas/GenericError'
|
||||||
/sources/ldap/{slug}/sync_status/:
|
/sources/ldap/{slug}/sync_status/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_ldap_sync_status_retrieve
|
operationId: sources_ldap_sync_status_list
|
||||||
description: Get source's sync status
|
description: Get source's sync status
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
|
@ -12036,10 +12036,10 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
$ref: '#/components/schemas/Task'
|
$ref: '#/components/schemas/Task'
|
||||||
description: ''
|
description: ''
|
||||||
'404':
|
|
||||||
description: Task not found
|
|
||||||
'400':
|
'400':
|
||||||
$ref: '#/components/schemas/ValidationError'
|
$ref: '#/components/schemas/ValidationError'
|
||||||
'403':
|
'403':
|
||||||
|
|
|
@ -3464,7 +3464,6 @@ msgstr "Resources"
|
||||||
msgid "Result"
|
msgid "Result"
|
||||||
msgstr "Result"
|
msgstr "Result"
|
||||||
|
|
||||||
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
|
|
||||||
#: src/pages/system-tasks/SystemTaskListPage.ts
|
#: src/pages/system-tasks/SystemTaskListPage.ts
|
||||||
msgid "Retry Task"
|
msgid "Retry Task"
|
||||||
msgstr "Retry Task"
|
msgstr "Retry Task"
|
||||||
|
@ -3491,6 +3490,10 @@ msgstr "Return to device picker"
|
||||||
msgid "Revoked?"
|
msgid "Revoked?"
|
||||||
msgstr "Revoked?"
|
msgstr "Revoked?"
|
||||||
|
|
||||||
|
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
|
||||||
|
msgid "Run sync again"
|
||||||
|
msgstr "Run sync again"
|
||||||
|
|
||||||
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
||||||
msgid "SAML Attribute Name"
|
msgid "SAML Attribute Name"
|
||||||
msgstr "SAML Attribute Name"
|
msgstr "SAML Attribute Name"
|
||||||
|
|
|
@ -3456,7 +3456,6 @@ msgstr ""
|
||||||
msgid "Result"
|
msgid "Result"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
|
|
||||||
#: src/pages/system-tasks/SystemTaskListPage.ts
|
#: src/pages/system-tasks/SystemTaskListPage.ts
|
||||||
msgid "Retry Task"
|
msgid "Retry Task"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -3483,6 +3482,10 @@ msgstr ""
|
||||||
msgid "Revoked?"
|
msgid "Revoked?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/sources/ldap/LDAPSourceViewPage.ts
|
||||||
|
msgid "Run sync again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
#: src/pages/property-mappings/PropertyMappingSAMLForm.ts
|
||||||
msgid "SAML Attribute Name"
|
msgid "SAML Attribute Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -12,6 +12,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||||
import AKGlobal from "../../../authentik.css";
|
import AKGlobal from "../../../authentik.css";
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||||
|
|
||||||
import "../../../elements/buttons/SpinnerButton";
|
import "../../../elements/buttons/SpinnerButton";
|
||||||
import "../../../elements/buttons/ActionButton";
|
import "../../../elements/buttons/ActionButton";
|
||||||
|
@ -53,6 +54,7 @@ export class LDAPSourceViewPage extends LitElement {
|
||||||
PFCard,
|
PFCard,
|
||||||
PFDescriptionList,
|
PFDescriptionList,
|
||||||
PFSizing,
|
PFSizing,
|
||||||
|
PFList,
|
||||||
AKGlobal,
|
AKGlobal,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -168,35 +170,34 @@ export class LDAPSourceViewPage extends LitElement {
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
${until(
|
${until(
|
||||||
new SourcesApi(DEFAULT_CONFIG)
|
new SourcesApi(DEFAULT_CONFIG)
|
||||||
.sourcesLdapSyncStatusRetrieve({
|
.sourcesLdapSyncStatusList({
|
||||||
slug: this.source.slug,
|
slug: this.source.slug,
|
||||||
})
|
})
|
||||||
.then((ls) => {
|
.then((tasks) => {
|
||||||
let header = html``;
|
if (tasks.length < 1) {
|
||||||
if (ls.status === StatusEnum.Warning) {
|
return html`<p>${t`Not synced yet.`}</p>`;
|
||||||
header = html`<p>
|
|
||||||
${t`Task finished with warnings`}
|
|
||||||
</p>`;
|
|
||||||
} else if (status === StatusEnum.Error) {
|
|
||||||
header = html`<p>
|
|
||||||
${t`Task finished with errors`}
|
|
||||||
</p>`;
|
|
||||||
} else {
|
|
||||||
header = html`<p>
|
|
||||||
${t`Last sync: ${ls.taskFinishTimestamp.toLocaleString()}`}
|
|
||||||
</p>`;
|
|
||||||
}
|
}
|
||||||
return html`
|
return html`<ul class="pf-c-list">
|
||||||
${header}
|
${tasks.map((task) => {
|
||||||
<ul>
|
let header = "";
|
||||||
${ls.messages.map((m) => {
|
if (task.status === StatusEnum.Warning) {
|
||||||
|
header = t`Task finished with warnings`;
|
||||||
|
} else if (task.status === StatusEnum.Error) {
|
||||||
|
header = t`Task finished with errors`;
|
||||||
|
} else {
|
||||||
|
header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
return html`<li>
|
||||||
|
<p>${task.taskName}</p>
|
||||||
|
<ul class="pf-c-list">
|
||||||
|
<li>${header}</li>
|
||||||
|
${task.messages.map((m) => {
|
||||||
return html`<li>${m}</li>`;
|
return html`<li>${m}</li>`;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
</li> `;
|
||||||
})
|
})}
|
||||||
.catch(() => {
|
</ul>`;
|
||||||
return html`<p>${t`Not synced yet.`}</p>`;
|
|
||||||
}),
|
}),
|
||||||
"loading",
|
"loading",
|
||||||
)}
|
)}
|
||||||
|
@ -212,7 +213,7 @@ export class LDAPSourceViewPage extends LitElement {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${t`Retry Task`}
|
${t`Run sync again`}
|
||||||
</ak-action-button>
|
</ak-action-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue