events: migrate SystemTasks to DB (#8159)
* events: migrate system tasks to save in DB Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prefill in app startup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup api Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update web Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use string for status Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix enum Signed-off-by: Jens Langhammer <jens@goauthentik.io> * save start and end directly in timestamp from default_timer() Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve metrics Signed-off-by: Jens Langhammer <jens@goauthentik.io> * lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename globally to system task Signed-off-by: Jens Langhammer <jens@goauthentik.io> * recreate migrations, better denote anonymous user Signed-off-by: Jens Langhammer <jens@goauthentik.io> * events: lookup actual django app instead of using module path, fallback to module path Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix logger call Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -1,134 +0,0 @@ | ||||
| """Tasks API""" | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.http.response import Http404 | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import ( | ||||
|     CharField, | ||||
|     ChoiceField, | ||||
|     DateTimeField, | ||||
|     ListField, | ||||
|     SerializerMethodField, | ||||
| ) | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus | ||||
| from authentik.rbac.permissions import HasPermission | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class TaskSerializer(PassiveSerializer): | ||||
|     """Serialize TaskInfo and TaskResult""" | ||||
|  | ||||
|     task_name = CharField() | ||||
|     task_description = CharField() | ||||
|     task_finish_timestamp = DateTimeField(source="finish_time") | ||||
|     task_duration = SerializerMethodField() | ||||
|  | ||||
|     status = ChoiceField( | ||||
|         source="result.status.name", | ||||
|         choices=[(x.name, x.name) for x in TaskResultStatus], | ||||
|     ) | ||||
|     messages = ListField(source="result.messages") | ||||
|  | ||||
|     def get_task_duration(self, instance: TaskInfo) -> int: | ||||
|         """Get the duration a task took to run""" | ||||
|         return max(instance.finish_timestamp - instance.start_timestamp, 0) | ||||
|  | ||||
|     def to_representation(self, instance: TaskInfo): | ||||
|         """When a new version of authentik adds fields to TaskInfo, | ||||
|         the API will fail with an AttributeError, as the classes | ||||
|         are pickled in cache. In that case, just delete the info""" | ||||
|         try: | ||||
|             return super().to_representation(instance) | ||||
|         # pylint: disable=broad-except | ||||
|         except Exception:  # pragma: no cover | ||||
|             if isinstance(self.instance, list): | ||||
|                 for inst in self.instance: | ||||
|                     inst.delete() | ||||
|             else: | ||||
|                 self.instance.delete() | ||||
|             return {} | ||||
|  | ||||
|  | ||||
| class TaskViewSet(ViewSet): | ||||
|     """Read-only view set that returns all background tasks""" | ||||
|  | ||||
|     permission_classes = [HasPermission("authentik_rbac.view_system_tasks")] | ||||
|     serializer_class = TaskSerializer | ||||
|  | ||||
|     @extend_schema( | ||||
|         responses={ | ||||
|             200: TaskSerializer(many=False), | ||||
|             404: OpenApiResponse(description="Task not found"), | ||||
|         }, | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 "id", | ||||
|                 type=OpenApiTypes.STR, | ||||
|                 location=OpenApiParameter.PATH, | ||||
|                 required=True, | ||||
|             ), | ||||
|         ], | ||||
|     ) | ||||
|     def retrieve(self, request: Request, pk=None) -> Response: | ||||
|         """Get a single system task""" | ||||
|         task = TaskInfo.by_name(pk) | ||||
|         if not task: | ||||
|             raise Http404 | ||||
|         return Response(TaskSerializer(task, many=False).data) | ||||
|  | ||||
|     @extend_schema(responses={200: TaskSerializer(many=True)}) | ||||
|     def list(self, request: Request) -> Response: | ||||
|         """List system tasks""" | ||||
|         tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name) | ||||
|         return Response(TaskSerializer(tasks, many=True).data) | ||||
|  | ||||
|     @permission_required(None, ["authentik_rbac.run_system_tasks"]) | ||||
|     @extend_schema( | ||||
|         request=OpenApiTypes.NONE, | ||||
|         responses={ | ||||
|             204: OpenApiResponse(description="Task retried successfully"), | ||||
|             404: OpenApiResponse(description="Task not found"), | ||||
|             500: OpenApiResponse(description="Failed to retry task"), | ||||
|         }, | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 "id", | ||||
|                 type=OpenApiTypes.STR, | ||||
|                 location=OpenApiParameter.PATH, | ||||
|                 required=True, | ||||
|             ), | ||||
|         ], | ||||
|     ) | ||||
|     @action(detail=True, methods=["post"]) | ||||
|     def retry(self, request: Request, pk=None) -> Response: | ||||
|         """Retry task""" | ||||
|         task = TaskInfo.by_name(pk) | ||||
|         if not task: | ||||
|             raise Http404 | ||||
|         try: | ||||
|             task_module = import_module(task.task_call_module) | ||||
|             task_func = getattr(task_module, task.task_call_func) | ||||
|             LOGGER.debug("Running task", task=task_func) | ||||
|             task_func.delay(*task.task_call_args, **task.task_call_kwargs) | ||||
|             messages.success( | ||||
|                 self.request, | ||||
|                 _("Successfully re-scheduled Task %(name)s!" % {"name": task.task_name}), | ||||
|             ) | ||||
|             return Response(status=204) | ||||
|         except (ImportError, AttributeError):  # pragma: no cover | ||||
|             LOGGER.warning("Failed to run task, remove state", task=task) | ||||
|             # if we get an import error, the module path has probably changed | ||||
|             task.delete() | ||||
|             return Response(status=500) | ||||
| @ -1,7 +1,6 @@ | ||||
| """admin signals""" | ||||
| from django.dispatch import receiver | ||||
|  | ||||
| from authentik.admin.api.tasks import TaskInfo | ||||
| from authentik.admin.apps import GAUGE_WORKERS | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.root.monitoring import monitoring_set | ||||
| @ -12,10 +11,3 @@ def monitoring_set_workers(sender, **kwargs): | ||||
|     """Set worker gauge""" | ||||
|     count = len(CELERY_APP.control.ping(timeout=0.5)) | ||||
|     GAUGE_WORKERS.set(count) | ||||
|  | ||||
|  | ||||
| @receiver(monitoring_set) | ||||
| def monitoring_set_tasks(sender, **kwargs): | ||||
|     """Set task gauges""" | ||||
|     for task in TaskInfo.all().values(): | ||||
|         task.update_metrics() | ||||
|  | ||||
| @ -11,12 +11,7 @@ from structlog.stdlib import get_logger | ||||
| from authentik import __version__, get_build_hash | ||||
| from authentik.admin.apps import PROM_INFO | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.root.celery import CELERY_APP | ||||
| @ -54,13 +49,13 @@ def clear_update_notifications(): | ||||
|             notification.delete() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def update_latest_version(self: MonitoredTask): | ||||
| def update_latest_version(self: SystemTask): | ||||
|     """Update latest version info""" | ||||
|     if CONFIG.get_bool("disable_update_check"): | ||||
|         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."])) | ||||
|         self.set_status(TaskStatus.WARNING, "Version check disabled.") | ||||
|         return | ||||
|     try: | ||||
|         response = get_http_session().get( | ||||
| @ -70,9 +65,7 @@ def update_latest_version(self: MonitoredTask): | ||||
|         data = response.json() | ||||
|         upstream_version = data.get("stable", {}).get("version") | ||||
|         cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status( | ||||
|             TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]) | ||||
|         ) | ||||
|         self.set_status(TaskStatus.SUCCESSFUL, "Successfully updated latest Version") | ||||
|         _set_prom_info() | ||||
|         # Check if upstream version is newer than what we're running, | ||||
|         # and if no event exists yet, create one. | ||||
| @ -89,7 +82,7 @@ def update_latest_version(self: MonitoredTask): | ||||
|             Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save() | ||||
|     except (RequestException, IndexError) as exc: | ||||
|         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|  | ||||
|  | ||||
| _set_prom_info() | ||||
|  | ||||
| @ -7,8 +7,6 @@ from django.urls import reverse | ||||
| from authentik import __version__ | ||||
| from authentik.blueprints.tests import reconcile_app | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.core.tasks import clean_expired_models | ||||
| from authentik.events.monitored_tasks import TaskResultStatus | ||||
| from authentik.lib.generators import generate_id | ||||
|  | ||||
|  | ||||
| @ -23,53 +21,6 @@ class TestAdminAPI(TestCase): | ||||
|         self.group.save() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_tasks(self): | ||||
|         """Test Task API""" | ||||
|         clean_expired_models.delay() | ||||
|         response = self.client.get(reverse("authentik_api:admin_system_tasks-list")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertTrue(any(task["task_name"] == "clean_expired_models" for task in body)) | ||||
|  | ||||
|     def test_tasks_single(self): | ||||
|         """Test Task API (read single)""" | ||||
|         clean_expired_models.delay() | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
|                 "authentik_api:admin_system_tasks-detail", | ||||
|                 kwargs={"pk": "clean_expired_models"}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(body["status"], TaskResultStatus.SUCCESSFUL.name) | ||||
|         self.assertEqual(body["task_name"], "clean_expired_models") | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:admin_system_tasks-detail", kwargs={"pk": "qwerqwer"}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 404) | ||||
|  | ||||
|     def test_tasks_retry(self): | ||||
|         """Test Task API (retry)""" | ||||
|         clean_expired_models.delay() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_api:admin_system_tasks-retry", | ||||
|                 kwargs={"pk": "clean_expired_models"}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 204) | ||||
|  | ||||
|     def test_tasks_retry_404(self): | ||||
|         """Test Task API (retry, 404)""" | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_api:admin_system_tasks-retry", | ||||
|                 kwargs={"pk": "qwerqewrqrqewrqewr"}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 404) | ||||
|  | ||||
|     def test_version(self): | ||||
|         """Test Version API""" | ||||
|         response = self.client.get(reverse("authentik_api:admin_version")) | ||||
|  | ||||
| @ -4,12 +4,10 @@ from django.urls import path | ||||
| from authentik.admin.api.meta import AppsViewSet, ModelViewSet | ||||
| from authentik.admin.api.metrics import AdministrationMetricsViewSet | ||||
| from authentik.admin.api.system import SystemView | ||||
| from authentik.admin.api.tasks import TaskViewSet | ||||
| from authentik.admin.api.version import VersionView | ||||
| from authentik.admin.api.workers import WorkerView | ||||
|  | ||||
| api_urlpatterns = [ | ||||
|     ("admin/system_tasks", TaskViewSet, "admin_system_tasks"), | ||||
|     ("admin/apps", AppsViewSet, "apps"), | ||||
|     ("admin/models", ModelViewSet, "models"), | ||||
|     path( | ||||
|  | ||||
| @ -11,14 +11,14 @@ from structlog.stdlib import BoundLogger, get_logger | ||||
| class ManagedAppConfig(AppConfig): | ||||
|     """Basic reconciliation logic for apps""" | ||||
|  | ||||
|     _logger: BoundLogger | ||||
|     logger: BoundLogger | ||||
|  | ||||
|     RECONCILE_GLOBAL_PREFIX: str = "reconcile_global_" | ||||
|     RECONCILE_TENANT_PREFIX: str = "reconcile_tenant_" | ||||
|  | ||||
|     def __init__(self, app_name: str, *args, **kwargs) -> None: | ||||
|         super().__init__(app_name, *args, **kwargs) | ||||
|         self._logger = get_logger().bind(app_name=app_name) | ||||
|         self.logger = get_logger().bind(app_name=app_name) | ||||
|  | ||||
|     def ready(self) -> None: | ||||
|         self.reconcile_global() | ||||
| @ -38,11 +38,11 @@ class ManagedAppConfig(AppConfig): | ||||
|                 continue | ||||
|             name = meth_name.replace(prefix, "") | ||||
|             try: | ||||
|                 self._logger.debug("Starting reconciler", name=name) | ||||
|                 self.logger.debug("Starting reconciler", name=name) | ||||
|                 meth() | ||||
|                 self._logger.debug("Successfully reconciled", name=name) | ||||
|                 self.logger.debug("Successfully reconciled", name=name) | ||||
|             except (DatabaseError, ProgrammingError, InternalError) as exc: | ||||
|                 self._logger.warning("Failed to run reconcile", name=name, exc=exc) | ||||
|                 self.logger.warning("Failed to run reconcile", name=name, exc=exc) | ||||
|  | ||||
|     def reconcile_tenant(self) -> None: | ||||
|         """reconcile ourselves for tenanted methods""" | ||||
| @ -51,7 +51,7 @@ class ManagedAppConfig(AppConfig): | ||||
|         try: | ||||
|             tenants = list(Tenant.objects.filter(ready=True)) | ||||
|         except (DatabaseError, ProgrammingError, InternalError) as exc: | ||||
|             self._logger.debug("Failed to get tenants to run reconcile", exc=exc) | ||||
|             self.logger.debug("Failed to get tenants to run reconcile", exc=exc) | ||||
|             return | ||||
|         for tenant in tenants: | ||||
|             with tenant: | ||||
|  | ||||
| @ -14,6 +14,7 @@ from django.db.models import Model | ||||
| from django.db.models.query_utils import Q | ||||
| from django.db.transaction import atomic | ||||
| from django.db.utils import IntegrityError | ||||
| from guardian.models import UserObjectPermission | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.serializers import BaseSerializer, Serializer | ||||
| from structlog.stdlib import BoundLogger, get_logger | ||||
| @ -38,12 +39,16 @@ from authentik.core.models import ( | ||||
|     UserSourceConnection, | ||||
| ) | ||||
| from authentik.enterprise.models import LicenseKey, LicenseUsage | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken | ||||
| from authentik.events.models import SystemTask | ||||
| from authentik.events.utils import cleanse_dict | ||||
| from authentik.flows.models import FlowToken, Stage | ||||
| from authentik.lib.models import SerializerModel | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.outposts.models import OutpostServiceConnection | ||||
| from authentik.policies.models import Policy, PolicyBindingModel | ||||
| from authentik.policies.reputation.models import Reputation | ||||
| from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken | ||||
| from authentik.providers.scim.models import SCIMGroup, SCIMUser | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
| @ -65,6 +70,7 @@ def excluded_models() -> list[type[Model]]: | ||||
|         DjangoGroup, | ||||
|         ContentType, | ||||
|         Permission, | ||||
|         UserObjectPermission, | ||||
|         # Base classes | ||||
|         Provider, | ||||
|         Source, | ||||
| @ -82,6 +88,12 @@ def excluded_models() -> list[type[Model]]: | ||||
|         SCIMGroup, | ||||
|         SCIMUser, | ||||
|         Tenant, | ||||
|         SystemTask, | ||||
|         ConnectionToken, | ||||
|         AuthorizationCode, | ||||
|         AccessToken, | ||||
|         RefreshToken, | ||||
|         Reputation, | ||||
|     ) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -29,12 +29,8 @@ from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, E | ||||
| from authentik.blueprints.v1.importer import Importer | ||||
| from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE | ||||
| from authentik.blueprints.v1.oci import OCI_PREFIX | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.events.utils import sanitize_dict | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.root.celery import CELERY_APP | ||||
| @ -134,10 +130,10 @@ def blueprints_find() -> list[BlueprintFile]: | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True | ||||
|     throws=(DatabaseError, ProgrammingError, InternalError), base=SystemTask, bind=True | ||||
| ) | ||||
| @prefill_task | ||||
| def blueprints_discovery(self: MonitoredTask, path: Optional[str] = None): | ||||
| def blueprints_discovery(self: SystemTask, path: Optional[str] = None): | ||||
|     """Find blueprints and check if they need to be created in the database""" | ||||
|     count = 0 | ||||
|     for blueprint in blueprints_find(): | ||||
| @ -146,10 +142,7 @@ def blueprints_discovery(self: MonitoredTask, path: Optional[str] = None): | ||||
|         check_blueprint_v1_file(blueprint) | ||||
|         count += 1 | ||||
|     self.set_status( | ||||
|         TaskResult( | ||||
|             TaskResultStatus.SUCCESSFUL, | ||||
|             messages=[_("Successfully imported %(count)d files." % {"count": count})], | ||||
|         ) | ||||
|         TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": count}) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @ -182,9 +175,9 @@ def check_blueprint_v1_file(blueprint: BlueprintFile): | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     bind=True, | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
| ) | ||||
| def apply_blueprint(self: MonitoredTask, instance_pk: str): | ||||
| def apply_blueprint(self: SystemTask, instance_pk: str): | ||||
|     """Apply single blueprint""" | ||||
|     self.save_on_success = False | ||||
|     instance: Optional[BlueprintInstance] = None | ||||
| @ -202,18 +195,18 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str): | ||||
|         if not valid: | ||||
|             instance.status = BlueprintInstanceStatus.ERROR | ||||
|             instance.save() | ||||
|             self.set_status(TaskResult(TaskResultStatus.ERROR, [x["event"] for x in logs])) | ||||
|             self.set_status(TaskStatus.ERROR, *[x["event"] for x in logs]) | ||||
|             return | ||||
|         applied = importer.apply() | ||||
|         if not applied: | ||||
|             instance.status = BlueprintInstanceStatus.ERROR | ||||
|             instance.save() | ||||
|             self.set_status(TaskResult(TaskResultStatus.ERROR, "Failed to apply")) | ||||
|             self.set_status(TaskStatus.ERROR, "Failed to apply") | ||||
|             return | ||||
|         instance.status = BlueprintInstanceStatus.SUCCESSFUL | ||||
|         instance.last_applied_hash = file_hash | ||||
|         instance.last_applied = now() | ||||
|         self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL)) | ||||
|         self.set_status(TaskStatus.SUCCESSFUL) | ||||
|     except ( | ||||
|         DatabaseError, | ||||
|         ProgrammingError, | ||||
| @ -224,7 +217,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str): | ||||
|     ) as exc: | ||||
|         if instance: | ||||
|             instance.status = BlueprintInstanceStatus.ERROR | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|     finally: | ||||
|         if instance: | ||||
|             instance.save() | ||||
|  | ||||
| @ -31,7 +31,7 @@ class UsedBySerializer(PassiveSerializer): | ||||
|     model_name = CharField() | ||||
|     pk = CharField() | ||||
|     name = CharField() | ||||
|     action = ChoiceField(choices=[(x.name, x.name) for x in DeleteAction]) | ||||
|     action = ChoiceField(choices=[(x.value, x.name) for x in DeleteAction]) | ||||
|  | ||||
|  | ||||
| def get_delete_action(manager: Manager) -> str: | ||||
|  | ||||
| @ -13,20 +13,15 @@ from authentik.core.models import ( | ||||
|     ExpiringModel, | ||||
|     User, | ||||
| ) | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def clean_expired_models(self: MonitoredTask): | ||||
| def clean_expired_models(self: SystemTask): | ||||
|     """Remove expired objects""" | ||||
|     messages = [] | ||||
|     for cls in ExpiringModel.__subclasses__(): | ||||
| @ -54,12 +49,12 @@ def clean_expired_models(self: MonitoredTask): | ||||
|             amount += 1 | ||||
|     LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount) | ||||
|     messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}") | ||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def clean_temporary_users(self: MonitoredTask): | ||||
| def clean_temporary_users(self: SystemTask): | ||||
|     """Remove temporary users created by SAML Sources""" | ||||
|     _now = datetime.now() | ||||
|     messages = [] | ||||
| @ -75,4 +70,4 @@ def clean_temporary_users(self: MonitoredTask): | ||||
|             user.delete() | ||||
|             deleted_users += 1 | ||||
|     messages.append(f"Successfully deleted {deleted_users} users.") | ||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|  | ||||
| @ -9,12 +9,8 @@ from django.utils.translation import gettext_lazy as _ | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| @ -39,9 +35,9 @@ def ensure_certificate_valid(body: str): | ||||
|     return body | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def certificate_discovery(self: MonitoredTask): | ||||
| def certificate_discovery(self: SystemTask): | ||||
|     """Discover, import and update certificates from the filesystem""" | ||||
|     certs = {} | ||||
|     private_keys = {} | ||||
| @ -88,8 +84,5 @@ def certificate_discovery(self: MonitoredTask): | ||||
|         if dirty: | ||||
|             cert.save() | ||||
|     self.set_status( | ||||
|         TaskResult( | ||||
|             TaskResultStatus.SUCCESSFUL, | ||||
|             messages=[_("Successfully imported %(count)d files." % {"count": discovered})], | ||||
|         ) | ||||
|         TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": discovered}) | ||||
|     ) | ||||
|  | ||||
							
								
								
									
										107
									
								
								authentik/events/api/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								authentik/events/api/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,107 @@ | ||||
| """Tasks API""" | ||||
| from datetime import datetime, timezone | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.decorators import permission_required | ||||
| from authentik.events.models import SystemTask, TaskStatus | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class SystemTaskSerializer(ModelSerializer): | ||||
|     """Serialize TaskInfo and TaskResult""" | ||||
|  | ||||
|     name = CharField() | ||||
|     full_name = SerializerMethodField() | ||||
|     uid = CharField(required=False) | ||||
|     description = CharField() | ||||
|     start_timestamp = SerializerMethodField() | ||||
|     finish_timestamp = SerializerMethodField() | ||||
|     duration = SerializerMethodField() | ||||
|  | ||||
|     status = ChoiceField(choices=[(x.value, x.name) for x in TaskStatus]) | ||||
|     messages = ListField(child=CharField()) | ||||
|  | ||||
|     def get_full_name(self, instance: SystemTask) -> str: | ||||
|         """Get full name with UID""" | ||||
|         if instance.uid: | ||||
|             return f"{instance.name}:{instance.uid}" | ||||
|         return instance.name | ||||
|  | ||||
|     def get_start_timestamp(self, instance: SystemTask) -> datetime: | ||||
|         """Timestamp when the task started""" | ||||
|         return datetime.fromtimestamp(instance.start_timestamp, tz=timezone.utc) | ||||
|  | ||||
|     def get_finish_timestamp(self, instance: SystemTask) -> datetime: | ||||
|         """Timestamp when the task finished""" | ||||
|         return datetime.fromtimestamp(instance.finish_timestamp, tz=timezone.utc) | ||||
|  | ||||
|     def get_duration(self, instance: SystemTask) -> float: | ||||
|         """Get the duration a task took to run""" | ||||
|         return max(instance.finish_timestamp - instance.start_timestamp, 0) | ||||
|  | ||||
|     class Meta: | ||||
|         model = SystemTask | ||||
|         fields = [ | ||||
|             "uuid", | ||||
|             "name", | ||||
|             "full_name", | ||||
|             "uid", | ||||
|             "description", | ||||
|             "start_timestamp", | ||||
|             "finish_timestamp", | ||||
|             "duration", | ||||
|             "status", | ||||
|             "messages", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class SystemTaskViewSet(ReadOnlyModelViewSet): | ||||
|     """Read-only view set that returns all background tasks""" | ||||
|  | ||||
|     queryset = SystemTask.objects.all() | ||||
|     serializer_class = SystemTaskSerializer | ||||
|     filterset_fields = ["name", "uid", "status"] | ||||
|     ordering = ["name", "uid", "status"] | ||||
|     search_fields = ["name", "description", "uid", "status"] | ||||
|  | ||||
|     @permission_required(None, ["authentik_events.run_task"]) | ||||
|     @extend_schema( | ||||
|         request=OpenApiTypes.NONE, | ||||
|         responses={ | ||||
|             204: OpenApiResponse(description="Task retried successfully"), | ||||
|             404: OpenApiResponse(description="Task not found"), | ||||
|             500: OpenApiResponse(description="Failed to retry task"), | ||||
|         }, | ||||
|     ) | ||||
|     @action(detail=True, methods=["post"]) | ||||
|     def run(self, request: Request, pk=None) -> Response: | ||||
|         """Run task""" | ||||
|         task: SystemTask = self.get_object() | ||||
|         try: | ||||
|             task_module = import_module(task.task_call_module) | ||||
|             task_func = getattr(task_module, task.task_call_func) | ||||
|             LOGGER.info("Running task", task=task_func) | ||||
|             task_func.delay(*task.task_call_args, **task.task_call_kwargs) | ||||
|             messages.success( | ||||
|                 self.request, | ||||
|                 _("Successfully started task %(name)s." % {"name": task.name}), | ||||
|             ) | ||||
|             return Response(status=204) | ||||
|         except (ImportError, AttributeError) as exc:  # pragma: no cover | ||||
|             LOGGER.warning("Failed to run task, remove state", task=task.name, exc=exc) | ||||
|             # if we get an import error, the module path has probably changed | ||||
|             task.delete() | ||||
|             return Response(status=500) | ||||
| @ -1,15 +1,26 @@ | ||||
| """authentik events app""" | ||||
| from prometheus_client import Gauge | ||||
| from prometheus_client import Gauge, Histogram | ||||
|  | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
| from authentik.lib.config import CONFIG, ENV_PREFIX | ||||
|  | ||||
| # TODO: Deprecated metric - remove in 2024.2 or later | ||||
| GAUGE_TASKS = Gauge( | ||||
|     "authentik_system_tasks", | ||||
|     "System tasks and their status", | ||||
|     ["tenant", "task_name", "task_uid", "status"], | ||||
| ) | ||||
|  | ||||
| SYSTEM_TASK_TIME = Histogram( | ||||
|     "authentik_system_tasks_time_seconds", | ||||
|     "Runtime of system tasks", | ||||
| ) | ||||
| SYSTEM_TASK_STATUS = Gauge( | ||||
|     "authentik_system_tasks_status", | ||||
|     "System task status", | ||||
|     ["task_name", "task_uid", "status"], | ||||
| ) | ||||
|  | ||||
|  | ||||
| class AuthentikEventsConfig(ManagedAppConfig): | ||||
|     """authentik events app""" | ||||
| @ -43,3 +54,14 @@ class AuthentikEventsConfig(ManagedAppConfig): | ||||
|                 replacement_env=replace_env, | ||||
|                 message=msg, | ||||
|             ).save() | ||||
|  | ||||
|     def reconcile_prefill_tasks(self): | ||||
|         """Prefill tasks""" | ||||
|         from authentik.events.models import SystemTask | ||||
|         from authentik.events.system_tasks import _prefill_tasks | ||||
|  | ||||
|         for task in _prefill_tasks: | ||||
|             if SystemTask.objects.filter(name=task.name).exists(): | ||||
|                 continue | ||||
|             task.save() | ||||
|             self.logger.debug("prefilled task", task_name=task.name) | ||||
|  | ||||
| @ -9,19 +9,14 @@ from django.core.exceptions import SuspiciousOperation | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import m2m_changed, post_save, pre_delete | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from guardian.models import UserObjectPermission | ||||
| from structlog.stdlib import BoundLogger, get_logger | ||||
|  | ||||
| from authentik.blueprints.v1.importer import excluded_models | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.enterprise.providers.rac.models import ConnectionToken | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.utils import model_to_dict | ||||
| from authentik.lib.sentry import before_send | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.policies.reputation.models import Reputation | ||||
| from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken | ||||
| from authentik.providers.scim.models import SCIMGroup, SCIMUser | ||||
| from authentik.stages.authenticator_static.models import StaticToken | ||||
|  | ||||
| IGNORED_MODELS = tuple( | ||||
| @ -29,16 +24,8 @@ IGNORED_MODELS = tuple( | ||||
|     + ( | ||||
|         Event, | ||||
|         Notification, | ||||
|         UserObjectPermission, | ||||
|         StaticToken, | ||||
|         Session, | ||||
|         AuthorizationCode, | ||||
|         AccessToken, | ||||
|         RefreshToken, | ||||
|         SCIMUser, | ||||
|         SCIMGroup, | ||||
|         Reputation, | ||||
|         ConnectionToken, | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
							
								
								
									
										60
									
								
								authentik/events/migrations/0004_systemtask.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								authentik/events/migrations/0004_systemtask.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| # Generated by Django 5.0.1 on 2024-01-24 12:48 | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import authentik.core.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0003_rename_tenant_event_brand"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="SystemTask", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "expires", | ||||
|                     models.DateTimeField(default=authentik.core.models.default_token_duration), | ||||
|                 ), | ||||
|                 ("expiring", models.BooleanField(default=True)), | ||||
|                 ( | ||||
|                     "uuid", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, editable=False, primary_key=True, serialize=False | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.TextField()), | ||||
|                 ("uid", models.TextField(null=True)), | ||||
|                 ("start_timestamp", models.FloatField()), | ||||
|                 ("finish_timestamp", models.FloatField()), | ||||
|                 ( | ||||
|                     "status", | ||||
|                     models.TextField( | ||||
|                         choices=[ | ||||
|                             ("unknown", "Unknown"), | ||||
|                             ("successful", "Successful"), | ||||
|                             ("warning", "Warning"), | ||||
|                             ("error", "Error"), | ||||
|                         ] | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("description", models.TextField(null=True)), | ||||
|                 ("messages", models.JSONField()), | ||||
|                 ("task_call_module", models.TextField()), | ||||
|                 ("task_call_func", models.TextField()), | ||||
|                 ("task_call_args", models.JSONField(default=list)), | ||||
|                 ("task_call_kwargs", models.JSONField(default=dict)), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "System Task", | ||||
|                 "verbose_name_plural": "System Tasks", | ||||
|                 "permissions": [("run_task", "Run task")], | ||||
|                 "default_permissions": ["view"], | ||||
|                 "unique_together": {("name", "uid")}, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -2,11 +2,14 @@ | ||||
| import time | ||||
| from collections import Counter | ||||
| from datetime import timedelta | ||||
| from difflib import get_close_matches | ||||
| from functools import lru_cache | ||||
| from inspect import currentframe | ||||
| from smtplib import SMTPException | ||||
| from typing import TYPE_CHECKING, Optional | ||||
| from typing import Optional | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.apps import apps | ||||
| from django.db import models | ||||
| from django.db.models import Count, ExpressionWrapper, F | ||||
| from django.db.models.fields import DurationField | ||||
| @ -18,6 +21,7 @@ from django.http.request import QueryDict | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext as _ | ||||
| from requests import RequestException | ||||
| from rest_framework.serializers import Serializer | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import get_full_version | ||||
| @ -28,6 +32,7 @@ from authentik.core.middleware import ( | ||||
|     SESSION_KEY_IMPERSONATE_USER, | ||||
| ) | ||||
| from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | ||||
| from authentik.events.apps import GAUGE_TASKS, SYSTEM_TASK_STATUS, SYSTEM_TASK_TIME | ||||
| from authentik.events.context_processors.base import get_context_processors | ||||
| from authentik.events.utils import ( | ||||
|     cleanse_dict, | ||||
| @ -46,8 +51,6 @@ from authentik.stages.email.utils import TemplateEmailMessage | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| if TYPE_CHECKING: | ||||
|     from rest_framework.serializers import Serializer | ||||
|  | ||||
|  | ||||
| def default_event_duration(): | ||||
| @ -61,6 +64,12 @@ def default_brand(): | ||||
|     return sanitize_dict(model_to_dict(DEFAULT_BRAND)) | ||||
|  | ||||
|  | ||||
| @lru_cache() | ||||
| def django_app_names() -> list[str]: | ||||
|     """Get a cached list of all django apps' names (not labels)""" | ||||
|     return [x.name for x in apps.app_configs.values()] | ||||
|  | ||||
|  | ||||
| class NotificationTransportError(SentryIgnoredException): | ||||
|     """Error raised when a notification fails to be delivered""" | ||||
|  | ||||
| @ -198,6 +207,11 @@ class Event(SerializerModel, ExpiringModel): | ||||
|             current = currentframe() | ||||
|             parent = current.f_back | ||||
|             app = parent.f_globals["__name__"] | ||||
|             # Attempt to match the calling module to the django app it belongs to | ||||
|             # if we can't find a match, keep the module name | ||||
|             django_apps = get_close_matches(app, django_app_names(), n=1) | ||||
|             if len(django_apps) > 0: | ||||
|                 app = django_apps[0] | ||||
|         cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs)) | ||||
|         event = Event(action=action, app=app, context=cleaned_kwargs) | ||||
|         return event | ||||
| @ -270,7 +284,7 @@ class Event(SerializerModel, ExpiringModel): | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> "Serializer": | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.events.api.events import EventSerializer | ||||
|  | ||||
|         return EventSerializer | ||||
| @ -478,7 +492,7 @@ class NotificationTransport(SerializerModel): | ||||
|             raise NotificationTransportError(exc) from exc | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> "Serializer": | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.events.api.notification_transports import NotificationTransportSerializer | ||||
|  | ||||
|         return NotificationTransportSerializer | ||||
| @ -511,7 +525,7 @@ class Notification(SerializerModel): | ||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> "Serializer": | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.events.api.notifications import NotificationSerializer | ||||
|  | ||||
|         return NotificationSerializer | ||||
| @ -554,7 +568,7 @@ class NotificationRule(SerializerModel, PolicyBindingModel): | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> "Serializer": | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.events.api.notification_rules import NotificationRuleSerializer | ||||
|  | ||||
|         return NotificationRuleSerializer | ||||
| @ -575,7 +589,7 @@ class NotificationWebhookMapping(PropertyMapping): | ||||
|         return "ak-property-mapping-notification-form" | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type["Serializer"]: | ||||
|     def serializer(self) -> type[type[Serializer]]: | ||||
|         from authentik.events.api.notification_mappings import NotificationWebhookMappingSerializer | ||||
|  | ||||
|         return NotificationWebhookMappingSerializer | ||||
| @ -586,3 +600,66 @@ class NotificationWebhookMapping(PropertyMapping): | ||||
|     class Meta: | ||||
|         verbose_name = _("Webhook Mapping") | ||||
|         verbose_name_plural = _("Webhook Mappings") | ||||
|  | ||||
|  | ||||
| class TaskStatus(models.TextChoices): | ||||
|     """Possible states of tasks""" | ||||
|  | ||||
|     UNKNOWN = "unknown" | ||||
|     SUCCESSFUL = "successful" | ||||
|     WARNING = "warning" | ||||
|     ERROR = "error" | ||||
|  | ||||
|  | ||||
| class SystemTask(SerializerModel, ExpiringModel): | ||||
|     """Info about a system task running in the background along with details to restart the task""" | ||||
|  | ||||
|     uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|     name = models.TextField() | ||||
|     uid = models.TextField(null=True) | ||||
|  | ||||
|     start_timestamp = models.FloatField() | ||||
|     finish_timestamp = models.FloatField() | ||||
|  | ||||
|     status = models.TextField(choices=TaskStatus.choices) | ||||
|  | ||||
|     description = models.TextField(null=True) | ||||
|     messages = models.JSONField() | ||||
|  | ||||
|     task_call_module = models.TextField() | ||||
|     task_call_func = models.TextField() | ||||
|     task_call_args = models.JSONField(default=list) | ||||
|     task_call_kwargs = models.JSONField(default=dict) | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> type[Serializer]: | ||||
|         from authentik.events.api.tasks import SystemTaskSerializer | ||||
|  | ||||
|         return SystemTaskSerializer | ||||
|  | ||||
|     def update_metrics(self): | ||||
|         """Update prometheus metrics""" | ||||
|         duration = max(self.finish_timestamp - self.start_timestamp, 0) | ||||
|         # TODO: Deprecated metric - remove in 2024.2 or later | ||||
|         GAUGE_TASKS.labels( | ||||
|             task_name=self.name, | ||||
|             task_uid=self.uid or "", | ||||
|             status=self.status.lower(), | ||||
|         ).set(duration) | ||||
|         SYSTEM_TASK_TIME.observe(duration) | ||||
|         SYSTEM_TASK_STATUS.labels( | ||||
|             task_name=self.name, | ||||
|             task_uid=self.uid or "", | ||||
|             status=self.status.lower(), | ||||
|         ).inc() | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"System Task {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = (("name", "uid"),) | ||||
|         # Remove "add", "change" and "delete" permissions as those are not used | ||||
|         default_permissions = ["view"] | ||||
|         permissions = [("run_task", _("Run task"))] | ||||
|         verbose_name = _("System Task") | ||||
|         verbose_name_plural = _("System Tasks") | ||||
|  | ||||
| @ -1,216 +0,0 @@ | ||||
| """Monitored tasks""" | ||||
| from dataclasses import dataclass, field | ||||
| from datetime import datetime | ||||
| from enum import Enum | ||||
| from timeit import default_timer | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.db import connection | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from structlog.stdlib import get_logger | ||||
| from tenant_schemas_celery.task import TenantTask | ||||
|  | ||||
| from authentik.events.apps import GAUGE_TASKS | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| CACHE_KEY_PREFIX = "goauthentik.io/events/tasks/" | ||||
|  | ||||
|  | ||||
| class TaskResultStatus(Enum): | ||||
|     """Possible states of tasks""" | ||||
|  | ||||
|     SUCCESSFUL = 1 | ||||
|     WARNING = 2 | ||||
|     ERROR = 4 | ||||
|     UNKNOWN = 8 | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class TaskResult: | ||||
|     """Result of a task run, this class is created by the task itself | ||||
|     and used by self.set_status""" | ||||
|  | ||||
|     status: TaskResultStatus | ||||
|  | ||||
|     messages: list[str] = field(default_factory=list) | ||||
|  | ||||
|     # Optional UID used in cache for tasks that run in different instances | ||||
|     uid: Optional[str] = field(default=None) | ||||
|  | ||||
|     def with_error(self, exc: Exception) -> "TaskResult": | ||||
|         """Since errors might not always be pickle-able, set the traceback""" | ||||
|         # TODO: Mark exception somehow so that is rendered as <pre> in frontend | ||||
|         self.messages.append(exception_to_string(exc)) | ||||
|         return self | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class TaskInfo: | ||||
|     """Info about a task run""" | ||||
|  | ||||
|     task_name: str | ||||
|     start_timestamp: float | ||||
|     finish_timestamp: float | ||||
|     finish_time: datetime | ||||
|  | ||||
|     result: TaskResult | ||||
|  | ||||
|     task_call_module: str | ||||
|     task_call_func: str | ||||
|     task_call_args: list[Any] = field(default_factory=list) | ||||
|     task_call_kwargs: dict[str, Any] = field(default_factory=dict) | ||||
|  | ||||
|     task_description: Optional[str] = field(default=None) | ||||
|  | ||||
|     @staticmethod | ||||
|     def all() -> dict[str, "TaskInfo"]: | ||||
|         """Get all TaskInfo objects""" | ||||
|         return cache.get_many(cache.keys(CACHE_KEY_PREFIX + "*")) | ||||
|  | ||||
|     @staticmethod | ||||
|     def by_name(name: str) -> Optional["TaskInfo"] | Optional[list["TaskInfo"]]: | ||||
|         """Get TaskInfo Object by name""" | ||||
|         if "*" in name: | ||||
|             return cache.get_many(cache.keys(CACHE_KEY_PREFIX + name)).values() | ||||
|         return cache.get(CACHE_KEY_PREFIX + name, None) | ||||
|  | ||||
|     @property | ||||
|     def full_name(self) -> str: | ||||
|         """Get the full cache key with task name and UID""" | ||||
|         key = CACHE_KEY_PREFIX + self.task_name | ||||
|         if self.result.uid: | ||||
|             uid_suffix = f":{self.result.uid}" | ||||
|             key += uid_suffix | ||||
|             if not self.task_name.endswith(uid_suffix): | ||||
|                 self.task_name += uid_suffix | ||||
|         return key | ||||
|  | ||||
|     def delete(self): | ||||
|         """Delete task info from cache""" | ||||
|         return cache.delete(self.full_name) | ||||
|  | ||||
|     def update_metrics(self): | ||||
|         """Update prometheus metrics""" | ||||
|         start = default_timer() | ||||
|         if hasattr(self, "start_timestamp"): | ||||
|             start = self.start_timestamp | ||||
|         try: | ||||
|             duration = max(self.finish_timestamp - start, 0) | ||||
|         except TypeError: | ||||
|             duration = 0 | ||||
|         GAUGE_TASKS.labels( | ||||
|             tenant=connection.schema_name, | ||||
|             task_name=self.task_name.split(":")[0], | ||||
|             task_uid=self.result.uid or "", | ||||
|             status=self.result.status.name.lower(), | ||||
|         ).set(duration) | ||||
|  | ||||
|     def save(self, timeout_hours=6): | ||||
|         """Save task into cache""" | ||||
|         self.update_metrics() | ||||
|         cache.set(self.full_name, self, timeout=timeout_hours * 60 * 60) | ||||
|  | ||||
|  | ||||
| class MonitoredTask(TenantTask): | ||||
|     """Task which can save its state to the cache""" | ||||
|  | ||||
|     # For tasks that should only be listed if they failed, set this to False | ||||
|     save_on_success: bool | ||||
|  | ||||
|     _result: Optional[TaskResult] | ||||
|  | ||||
|     _uid: Optional[str] | ||||
|     start: Optional[float] = None | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.save_on_success = True | ||||
|         self._uid = None | ||||
|         self._result = None | ||||
|         self.result_timeout_hours = 6 | ||||
|  | ||||
|     def set_uid(self, uid: str): | ||||
|         """Set UID, so in the case of an unexpected error its saved correctly""" | ||||
|         self._uid = uid | ||||
|  | ||||
|     def set_status(self, result: TaskResult): | ||||
|         """Set result for current run, will overwrite previous result.""" | ||||
|         self._result = result | ||||
|  | ||||
|     def before_start(self, task_id, args, kwargs): | ||||
|         self.start = default_timer() | ||||
|         return super().before_start(task_id, args, kwargs) | ||||
|  | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo): | ||||
|         super().after_return(status, retval, task_id, args, kwargs, einfo=einfo) | ||||
|         if not self._result: | ||||
|             return | ||||
|         if not self._result.uid: | ||||
|             self._result.uid = self._uid | ||||
|         info = TaskInfo( | ||||
|             task_name=self.__name__, | ||||
|             task_description=self.__doc__, | ||||
|             start_timestamp=self.start or default_timer(), | ||||
|             finish_timestamp=default_timer(), | ||||
|             finish_time=datetime.now(), | ||||
|             result=self._result, | ||||
|             task_call_module=self.__module__, | ||||
|             task_call_func=self.__name__, | ||||
|             task_call_args=args, | ||||
|             task_call_kwargs=kwargs, | ||||
|         ) | ||||
|         if self._result.status == TaskResultStatus.SUCCESSFUL and not self.save_on_success: | ||||
|             info.delete() | ||||
|             return | ||||
|         info.save(self.result_timeout_hours) | ||||
|  | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def on_failure(self, exc, task_id, args, kwargs, einfo): | ||||
|         super().on_failure(exc, task_id, args, kwargs, einfo=einfo) | ||||
|         if not self._result: | ||||
|             self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[str(exc)]) | ||||
|         if not self._result.uid: | ||||
|             self._result.uid = self._uid | ||||
|         TaskInfo( | ||||
|             task_name=self.__name__, | ||||
|             task_description=self.__doc__, | ||||
|             start_timestamp=self.start or default_timer(), | ||||
|             finish_timestamp=default_timer(), | ||||
|             finish_time=datetime.now(), | ||||
|             result=self._result, | ||||
|             task_call_module=self.__module__, | ||||
|             task_call_func=self.__name__, | ||||
|             task_call_args=args, | ||||
|             task_call_kwargs=kwargs, | ||||
|         ).save(self.result_timeout_hours) | ||||
|         Event.new( | ||||
|             EventAction.SYSTEM_TASK_EXCEPTION, | ||||
|             message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}", | ||||
|         ).save() | ||||
|  | ||||
|     def run(self, *args, **kwargs): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def prefill_task(func): | ||||
|     """Ensure a task's details are always in cache, so it can always be triggered via API""" | ||||
|     status = TaskInfo.by_name(func.__name__) | ||||
|     if status: | ||||
|         return func | ||||
|     TaskInfo( | ||||
|         task_name=func.__name__, | ||||
|         task_description=func.__doc__, | ||||
|         result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]), | ||||
|         task_call_module=func.__module__, | ||||
|         task_call_func=func.__name__, | ||||
|         # We don't have real values for these attributes but they cannot be null | ||||
|         start_timestamp=0, | ||||
|         finish_timestamp=0, | ||||
|         finish_time=datetime.now(), | ||||
|     ).save(86400) | ||||
|     LOGGER.debug("prefilled task", task_name=func.__name__) | ||||
|     return func | ||||
| @ -8,11 +8,13 @@ from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.signals import login_failed, password_changed | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.apps import SYSTEM_TASK_STATUS | ||||
| from authentik.events.models import Event, EventAction, SystemTask | ||||
| from authentik.events.tasks import event_notification_handler, gdpr_cleanup | ||||
| from authentik.flows.models import Stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.root.monitoring import monitoring_set | ||||
| from authentik.stages.invitation.models import Invitation | ||||
| from authentik.stages.invitation.signals import invitation_used | ||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||
| @ -100,3 +102,11 @@ def event_user_pre_delete_cleanup(sender, instance: User, **_): | ||||
|     """If gdpr_compliance is enabled, remove all the user's events""" | ||||
|     if get_current_tenant().gdpr_compliance: | ||||
|         gdpr_cleanup.delay(instance.pk) | ||||
|  | ||||
|  | ||||
| @receiver(monitoring_set) | ||||
| def monitoring_system_task(sender, **_): | ||||
|     """Update metrics when task is saved""" | ||||
|     SYSTEM_TASK_STATUS.clear() | ||||
|     for task in SystemTask.objects.all(): | ||||
|         task.update_metrics() | ||||
|  | ||||
							
								
								
									
										135
									
								
								authentik/events/system_tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								authentik/events/system_tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | ||||
| """Monitored tasks""" | ||||
| from datetime import timedelta | ||||
| from timeit import default_timer | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from django.utils.timezone import now | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from structlog.stdlib import get_logger | ||||
| from tenant_schemas_celery.task import TenantTask | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.models import SystemTask as DBSystemTask | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.utils import sanitize_item | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class SystemTask(TenantTask): | ||||
|     """Task which can save its state to the cache""" | ||||
|  | ||||
|     # For tasks that should only be listed if they failed, set this to False | ||||
|     save_on_success: bool | ||||
|  | ||||
|     _status: Optional[TaskStatus] | ||||
|     _messages: list[str] | ||||
|  | ||||
|     _uid: Optional[str] | ||||
|     _start: Optional[float] = None | ||||
|  | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.save_on_success = True | ||||
|         self._uid = None | ||||
|         self._status = None | ||||
|         self._messages = [] | ||||
|         self.result_timeout_hours = 6 | ||||
|  | ||||
|     def set_uid(self, uid: str): | ||||
|         """Set UID, so in the case of an unexpected error its saved correctly""" | ||||
|         self._uid = uid | ||||
|  | ||||
|     def set_status(self, status: TaskStatus, *messages: str): | ||||
|         """Set result for current run, will overwrite previous result.""" | ||||
|         self._status = status | ||||
|         self._messages = messages | ||||
|  | ||||
|     def set_error(self, exception: Exception): | ||||
|         """Set result to error and save exception""" | ||||
|         self._status = TaskStatus.ERROR | ||||
|         self._messages = [exception_to_string(exception)] | ||||
|  | ||||
|     def before_start(self, task_id, args, kwargs): | ||||
|         self._start = default_timer() | ||||
|         return super().before_start(task_id, args, kwargs) | ||||
|  | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo): | ||||
|         super().after_return(status, retval, task_id, args, kwargs, einfo=einfo) | ||||
|         if not self._status: | ||||
|             return | ||||
|         if self._status == TaskStatus.SUCCESSFUL and not self.save_on_success: | ||||
|             DBSystemTask.objects.filter( | ||||
|                 name=self.__name__, | ||||
|                 uid=self._uid, | ||||
|             ).delete() | ||||
|             return | ||||
|         DBSystemTask.objects.update_or_create( | ||||
|             name=self.__name__, | ||||
|             uid=self._uid, | ||||
|             defaults={ | ||||
|                 "description": self.__doc__, | ||||
|                 "start_timestamp": self._start or default_timer(), | ||||
|                 "finish_timestamp": default_timer(), | ||||
|                 "task_call_module": self.__module__, | ||||
|                 "task_call_func": self.__name__, | ||||
|                 "task_call_args": args, | ||||
|                 "task_call_kwargs": kwargs, | ||||
|                 "status": self._status, | ||||
|                 "messages": sanitize_item(self._messages), | ||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||
|                 "expiring": True, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def on_failure(self, exc, task_id, args, kwargs, einfo): | ||||
|         super().on_failure(exc, task_id, args, kwargs, einfo=einfo) | ||||
|         if not self._status: | ||||
|             self._status = TaskStatus.ERROR | ||||
|             self._messages = exception_to_string(exc) | ||||
|         DBSystemTask.objects.update_or_create( | ||||
|             name=self.__name__, | ||||
|             uid=self._uid, | ||||
|             defaults={ | ||||
|                 "description": self.__doc__, | ||||
|                 "start_timestamp": self._start or default_timer(), | ||||
|                 "finish_timestamp": default_timer(), | ||||
|                 "task_call_module": self.__module__, | ||||
|                 "task_call_func": self.__name__, | ||||
|                 "task_call_args": args, | ||||
|                 "task_call_kwargs": kwargs, | ||||
|                 "status": self._status, | ||||
|                 "messages": sanitize_item(self._messages), | ||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), | ||||
|                 "expiring": True, | ||||
|             }, | ||||
|         ) | ||||
|         Event.new( | ||||
|             EventAction.SYSTEM_TASK_EXCEPTION, | ||||
|             message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}", | ||||
|         ).save() | ||||
|  | ||||
|     def run(self, *args, **kwargs): | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def prefill_task(func): | ||||
|     """Ensure a task's details are always in cache, so it can always be triggered via API""" | ||||
|     _prefill_tasks.append( | ||||
|         DBSystemTask( | ||||
|             name=func.__name__, | ||||
|             description=func.__doc__, | ||||
|             status=TaskStatus.UNKNOWN, | ||||
|             messages=sanitize_item([_("Task has not been run yet.")]), | ||||
|             task_call_module=func.__module__, | ||||
|             task_call_func=func.__name__, | ||||
|             expiring=False, | ||||
|         ) | ||||
|     ) | ||||
|     return func | ||||
|  | ||||
|  | ||||
| _prefill_tasks = [] | ||||
| @ -13,13 +13,9 @@ from authentik.events.models import ( | ||||
|     NotificationRule, | ||||
|     NotificationTransport, | ||||
|     NotificationTransportError, | ||||
|     TaskStatus, | ||||
| ) | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.models import PolicyBinding, PolicyEngineMode | ||||
| from authentik.root.celery import CELERY_APP | ||||
| @ -99,10 +95,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str): | ||||
|     bind=True, | ||||
|     autoretry_for=(NotificationTransportError,), | ||||
|     retry_backoff=True, | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
| ) | ||||
| def notification_transport( | ||||
|     self: MonitoredTask, transport_pk: int, event_pk: str, user_pk: int, trigger_pk: str | ||||
|     self: SystemTask, transport_pk: int, event_pk: str, user_pk: int, trigger_pk: str | ||||
| ): | ||||
|     """Send notification over specified transport""" | ||||
|     self.save_on_success = False | ||||
| @ -123,9 +119,9 @@ def notification_transport( | ||||
|         if not transport: | ||||
|             return | ||||
|         transport.send(notification) | ||||
|         self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL)) | ||||
|         self.set_status(TaskStatus.SUCCESSFUL) | ||||
|     except (NotificationTransportError, PropertyMappingExpressionException) as exc: | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|         raise exc | ||||
|  | ||||
|  | ||||
| @ -137,13 +133,13 @@ def gdpr_cleanup(user_pk: int): | ||||
|     events.delete() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def notification_cleanup(self: MonitoredTask): | ||||
| def notification_cleanup(self: SystemTask): | ||||
|     """Cleanup seen notifications and notifications whose event expired.""" | ||||
|     notifications = Notification.objects.filter(Q(event=None) | Q(seen=True)) | ||||
|     amount = notifications.count() | ||||
|     for notification in notifications: | ||||
|         notification.delete() | ||||
|     LOGGER.debug("Expired notifications", amount=amount) | ||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, [f"Expired {amount} Notifications"])) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications") | ||||
|  | ||||
| @ -1,14 +1,26 @@ | ||||
| """Test Monitored tasks""" | ||||
| from django.test import TestCase | ||||
| from json import loads | ||||
|  | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskInfo, TaskResult, TaskResultStatus | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.tasks import clean_expired_models | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.events.models import SystemTask as DBSystemTask | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
|  | ||||
| class TestMonitoredTasks(TestCase): | ||||
| class TestSystemTasks(APITestCase): | ||||
|     """Test Monitored tasks""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = create_test_admin_user() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_failed_successful_remove_state(self): | ||||
|         """Test that a task with `save_on_success` set to `False` that failed saves | ||||
|         a state, and upon successful completion will delete the state""" | ||||
| @ -17,27 +29,74 @@ class TestMonitoredTasks(TestCase): | ||||
|  | ||||
|         @CELERY_APP.task( | ||||
|             bind=True, | ||||
|             base=MonitoredTask, | ||||
|             base=SystemTask, | ||||
|         ) | ||||
|         def test_task(self: MonitoredTask): | ||||
|         def test_task(self: SystemTask): | ||||
|             self.save_on_success = False | ||||
|             self.set_uid(uid) | ||||
|             self.set_status( | ||||
|                 TaskResult(TaskResultStatus.ERROR if should_fail else TaskResultStatus.SUCCESSFUL) | ||||
|             ) | ||||
|             self.set_status(TaskStatus.ERROR if should_fail else TaskStatus.SUCCESSFUL) | ||||
|  | ||||
|         # First test successful run | ||||
|         should_fail = False | ||||
|         test_task.delay().get() | ||||
|         self.assertIsNone(TaskInfo.by_name(f"test_task:{uid}")) | ||||
|         self.assertIsNone(DBSystemTask.objects.filter(name="test_task", uid=uid).first()) | ||||
|  | ||||
|         # Then test failed | ||||
|         should_fail = True | ||||
|         test_task.delay().get() | ||||
|         info = TaskInfo.by_name(f"test_task:{uid}") | ||||
|         self.assertEqual(info.result.status, TaskResultStatus.ERROR) | ||||
|         task = DBSystemTask.objects.filter(name="test_task", uid=uid).first() | ||||
|         self.assertEqual(task.status, TaskStatus.ERROR) | ||||
|  | ||||
|         # Then after that, the state should be removed | ||||
|         should_fail = False | ||||
|         test_task.delay().get() | ||||
|         self.assertIsNone(TaskInfo.by_name(f"test_task:{uid}")) | ||||
|         self.assertIsNone(DBSystemTask.objects.filter(name="test_task", uid=uid).first()) | ||||
|  | ||||
|     def test_tasks(self): | ||||
|         """Test Task API""" | ||||
|         clean_expired_models.delay().get() | ||||
|         response = self.client.get(reverse("authentik_api:systemtask-list")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertTrue(any(task["name"] == "clean_expired_models" for task in body["results"])) | ||||
|  | ||||
|     def test_tasks_single(self): | ||||
|         """Test Task API (read single)""" | ||||
|         clean_expired_models.delay().get() | ||||
|         task = DBSystemTask.objects.filter(name="clean_expired_models").first() | ||||
|         response = self.client.get( | ||||
|             reverse( | ||||
|                 "authentik_api:systemtask-detail", | ||||
|                 kwargs={"pk": str(task.pk)}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(body["status"], TaskStatus.SUCCESSFUL.value) | ||||
|         self.assertEqual(body["name"], "clean_expired_models") | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:systemtask-detail", kwargs={"pk": "qwerqwer"}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 404) | ||||
|  | ||||
|     def test_tasks_run(self): | ||||
|         """Test Task API (run)""" | ||||
|         clean_expired_models.delay().get() | ||||
|         task = DBSystemTask.objects.filter(name="clean_expired_models").first() | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_api:systemtask-run", | ||||
|                 kwargs={"pk": str(task.pk)}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 204) | ||||
|  | ||||
|     def test_tasks_run_404(self): | ||||
|         """Test Task API (run, 404)""" | ||||
|         response = self.client.post( | ||||
|             reverse( | ||||
|                 "authentik_api:systemtask-run", | ||||
|                 kwargs={"pk": "qwerqewrqrqewrqewr"}, | ||||
|             ) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 404) | ||||
|  | ||||
| @ -4,11 +4,13 @@ from authentik.events.api.notification_mappings import NotificationWebhookMappin | ||||
| from authentik.events.api.notification_rules import NotificationRuleViewSet | ||||
| from authentik.events.api.notification_transports import NotificationTransportViewSet | ||||
| from authentik.events.api.notifications import NotificationViewSet | ||||
| from authentik.events.api.tasks import SystemTaskViewSet | ||||
|  | ||||
| api_urlpatterns = [ | ||||
|     ("events/events", EventViewSet), | ||||
|     ("events/notifications", NotificationViewSet), | ||||
|     ("events/transports", NotificationTransportViewSet), | ||||
|     ("events/rules", NotificationRuleViewSet), | ||||
|     ("events/system_tasks", SystemTaskViewSet), | ||||
|     ("propertymappings/notification", NotificationWebhookMappingViewSet), | ||||
| ] | ||||
|  | ||||
| @ -18,6 +18,7 @@ from django.http.request import HttpRequest | ||||
| from django.utils import timezone | ||||
| from django.views.debug import SafeExceptionReporterFilter | ||||
| from geoip2.models import ASN, City | ||||
| from guardian.conf import settings | ||||
| from guardian.utils import get_anonymous_user | ||||
|  | ||||
| from authentik.blueprints.v1.common import YAMLTag | ||||
| @ -84,6 +85,8 @@ def get_user(user: User | AnonymousUser, original_user: Optional[User] = None) - | ||||
|         "pk": user.pk, | ||||
|         "email": user.email, | ||||
|     } | ||||
|     if user.username == settings.ANONYMOUS_USER_NAME: | ||||
|         user_data["is_anonymous"] = True | ||||
|     if original_user: | ||||
|         original_data = get_user(original_user) | ||||
|         original_data["on_behalf_of"] = user_data | ||||
|  | ||||
| @ -19,12 +19,8 @@ from yaml import safe_load | ||||
|  | ||||
| from authentik.enterprise.providers.rac.controllers.docker import RACDockerController | ||||
| from authentik.enterprise.providers.rac.controllers.kubernetes import RACKubernetesController | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.reflection import path_to_class | ||||
| from authentik.outposts.consumer import OUTPOST_GROUP | ||||
| @ -108,20 +104,18 @@ def outpost_service_connection_state(connection_pk: Any): | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     bind=True, | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
|     throws=(DatabaseError, ProgrammingError, InternalError), | ||||
| ) | ||||
| @prefill_task | ||||
| def outpost_service_connection_monitor(self: MonitoredTask): | ||||
| def outpost_service_connection_monitor(self: SystemTask): | ||||
|     """Regularly check the state of Outpost Service Connections""" | ||||
|     connections = OutpostServiceConnection.objects.all() | ||||
|     for connection in connections.iterator(): | ||||
|         outpost_service_connection_state.delay(connection.pk) | ||||
|     self.set_status( | ||||
|         TaskResult( | ||||
|             TaskResultStatus.SUCCESSFUL, | ||||
|             [f"Successfully updated {len(connections)} connections."], | ||||
|         ) | ||||
|         TaskStatus.SUCCESSFUL, | ||||
|         f"Successfully updated {len(connections)} connections.", | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @ -134,9 +128,9 @@ def outpost_controller_all(): | ||||
|         outpost_controller.delay(outpost.pk.hex, "up", from_cache=False) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| def outpost_controller( | ||||
|     self: MonitoredTask, outpost_pk: str, action: str = "up", from_cache: bool = False | ||||
|     self: SystemTask, outpost_pk: str, action: str = "up", from_cache: bool = False | ||||
| ): | ||||
|     """Create/update/monitor/delete the deployment of an Outpost""" | ||||
|     logs = [] | ||||
| @ -161,16 +155,16 @@ def outpost_controller( | ||||
|                 LOGGER.debug(log) | ||||
|             LOGGER.debug("-----------------Outpost Controller logs end-------------------") | ||||
|     except (ControllerException, ServiceConnectionInvalid) as exc: | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|     else: | ||||
|         if from_cache: | ||||
|             cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk) | ||||
|         self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs)) | ||||
|         self.set_status(TaskStatus.SUCCESSFUL, *logs) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def outpost_token_ensurer(self: MonitoredTask): | ||||
| def outpost_token_ensurer(self: SystemTask): | ||||
|     """Periodically ensure that all Outposts have valid Service Accounts | ||||
|     and Tokens""" | ||||
|     all_outposts = Outpost.objects.all() | ||||
| @ -178,10 +172,8 @@ def outpost_token_ensurer(self: MonitoredTask): | ||||
|         _ = outpost.token | ||||
|         outpost.build_user_permissions(outpost.user) | ||||
|     self.set_status( | ||||
|         TaskResult( | ||||
|             TaskResultStatus.SUCCESSFUL, | ||||
|             [f"Successfully checked {len(all_outposts)} Outposts."], | ||||
|         ) | ||||
|         TaskStatus.SUCCESSFUL, | ||||
|         f"Successfully checked {len(all_outposts)} Outposts.", | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @ -256,32 +248,32 @@ def _outpost_single_update(outpost: Outpost, layer=None): | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
|     bind=True, | ||||
| ) | ||||
| def outpost_connection_discovery(self: MonitoredTask): | ||||
| def outpost_connection_discovery(self: SystemTask): | ||||
|     """Checks the local environment and create Service connections.""" | ||||
|     status = TaskResult(TaskResultStatus.SUCCESSFUL) | ||||
|     messages = [] | ||||
|     if not CONFIG.get_bool("outposts.discover"): | ||||
|         status.messages.append("Outpost integration discovery is disabled") | ||||
|         self.set_status(status) | ||||
|         messages.append("Outpost integration discovery is disabled") | ||||
|         self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|         return | ||||
|     # Explicitly check against token filename, as that's | ||||
|     # only present when the integration is enabled | ||||
|     if Path(SERVICE_TOKEN_FILENAME).exists(): | ||||
|         status.messages.append("Detected in-cluster Kubernetes Config") | ||||
|         messages.append("Detected in-cluster Kubernetes Config") | ||||
|         if not KubernetesServiceConnection.objects.filter(local=True).exists(): | ||||
|             status.messages.append("Created Service Connection for in-cluster") | ||||
|             messages.append("Created Service Connection for in-cluster") | ||||
|             KubernetesServiceConnection.objects.create( | ||||
|                 name="Local Kubernetes Cluster", local=True, kubeconfig={} | ||||
|             ) | ||||
|     # For development, check for the existence of a kubeconfig file | ||||
|     kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser() | ||||
|     if kubeconfig_path.exists(): | ||||
|         status.messages.append("Detected kubeconfig") | ||||
|         messages.append("Detected kubeconfig") | ||||
|         kubeconfig_local_name = f"k8s-{gethostname()}" | ||||
|         if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists(): | ||||
|             status.messages.append("Creating kubeconfig Service Connection") | ||||
|             messages.append("Creating kubeconfig Service Connection") | ||||
|             with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig: | ||||
|                 KubernetesServiceConnection.objects.create( | ||||
|                     name=kubeconfig_local_name, | ||||
| @ -290,12 +282,12 @@ def outpost_connection_discovery(self: MonitoredTask): | ||||
|     unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path | ||||
|     socket = Path(unix_socket_path) | ||||
|     if socket.exists() and access(socket, R_OK): | ||||
|         status.messages.append("Detected local docker socket") | ||||
|         messages.append("Detected local docker socket") | ||||
|         if len(DockerServiceConnection.objects.filter(local=True)) == 0: | ||||
|             status.messages.append("Created Service Connection for docker") | ||||
|             messages.append("Created Service Connection for docker") | ||||
|             DockerServiceConnection.objects.create( | ||||
|                 name="Local Docker connection", | ||||
|                 local=True, | ||||
|                 url=unix_socket_path, | ||||
|             ) | ||||
|     self.set_status(status) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|  | ||||
| @ -4,12 +4,8 @@ from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | ||||
| from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | ||||
| from authentik.events.monitored_tasks import ( | ||||
|     MonitoredTask, | ||||
|     TaskResult, | ||||
|     TaskResultStatus, | ||||
|     prefill_task, | ||||
| ) | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask, prefill_task | ||||
| from authentik.policies.reputation.models import Reputation | ||||
| from authentik.policies.reputation.signals import CACHE_KEY_PREFIX | ||||
| from authentik.root.celery import CELERY_APP | ||||
| @ -17,9 +13,9 @@ from authentik.root.celery import CELERY_APP | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| @prefill_task | ||||
| def save_reputation(self: MonitoredTask): | ||||
| def save_reputation(self: SystemTask): | ||||
|     """Save currently cached reputation to database""" | ||||
|     objects_to_update = [] | ||||
|     for _, score in cache.get_many(cache.keys(CACHE_KEY_PREFIX + "*")).items(): | ||||
| @ -32,4 +28,4 @@ def save_reputation(self: MonitoredTask): | ||||
|         rep.score = score["score"] | ||||
|         objects_to_update.append(rep) | ||||
|     Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"]) | ||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated Reputation"])) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, "Successfully updated Reputation") | ||||
|  | ||||
| @ -1,17 +1,17 @@ | ||||
| """SCIM Provider API Views""" | ||||
| from django.utils.text import slugify | ||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import BooleanField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.admin.api.tasks import TaskSerializer | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.events.monitored_tasks import TaskInfo | ||||
| from authentik.events.api.tasks import SystemTaskSerializer | ||||
| from authentik.providers.scim.models import SCIMProvider | ||||
|  | ||||
|  | ||||
| @ -43,7 +43,7 @@ class SCIMSyncStatusSerializer(PassiveSerializer): | ||||
|     """SCIM Provider sync status""" | ||||
|  | ||||
|     is_running = BooleanField(read_only=True) | ||||
|     tasks = TaskSerializer(many=True, read_only=True) | ||||
|     tasks = SystemTaskSerializer(many=True, read_only=True) | ||||
|  | ||||
|  | ||||
| class SCIMProviderViewSet(UsedByMixin, ModelViewSet): | ||||
| @ -65,8 +65,12 @@ class SCIMProviderViewSet(UsedByMixin, ModelViewSet): | ||||
|     def sync_status(self, request: Request, pk: int) -> Response: | ||||
|         """Get provider's sync status""" | ||||
|         provider: SCIMProvider = self.get_object() | ||||
|         task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}") | ||||
|         tasks = [task] if task else [] | ||||
|         tasks = list( | ||||
|             get_objects_for_user(request.user, "authentik_events.view_systemtask").filter( | ||||
|                 name="scim_sync", | ||||
|                 uid=slugify(provider.name), | ||||
|             ) | ||||
|         ) | ||||
|         status = { | ||||
|             "tasks": tasks, | ||||
|             "is_running": provider.sync_lock.locked(), | ||||
|  | ||||
| @ -10,7 +10,8 @@ from pydanticscim.responses import PatchOp | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.lib.utils.reflection import path_to_class | ||||
| from authentik.providers.scim.clients import PAGE_SIZE, PAGE_TIMEOUT | ||||
| from authentik.providers.scim.clients.base import SCIMClient | ||||
| @ -39,8 +40,8 @@ def scim_sync_all(): | ||||
|         scim_sync.delay(provider.pk) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| def scim_sync(self: MonitoredTask, provider_pk: int) -> None: | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| def scim_sync(self: SystemTask, provider_pk: int) -> None: | ||||
|     """Run SCIM full sync for provider""" | ||||
|     provider: SCIMProvider = SCIMProvider.objects.filter( | ||||
|         pk=provider_pk, backchannel_application__isnull=False | ||||
| @ -52,8 +53,8 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None: | ||||
|         LOGGER.debug("SCIM sync locked, skipping task", source=provider.name) | ||||
|         return | ||||
|     self.set_uid(slugify(provider.name)) | ||||
|     result = TaskResult(TaskResultStatus.SUCCESSFUL, []) | ||||
|     result.messages.append(_("Starting full SCIM sync")) | ||||
|     messages = [] | ||||
|     messages.append(_("Starting full SCIM sync")) | ||||
|     LOGGER.debug("Starting SCIM sync") | ||||
|     users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE) | ||||
|     groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE) | ||||
| @ -63,17 +64,17 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None: | ||||
|     with allow_join_result(): | ||||
|         try: | ||||
|             for page in users_paginator.page_range: | ||||
|                 result.messages.append(_("Syncing page %(page)d of users" % {"page": page})) | ||||
|                 messages.append(_("Syncing page %(page)d of users" % {"page": page})) | ||||
|                 for msg in scim_sync_users.delay(page, provider_pk).get(): | ||||
|                     result.messages.append(msg) | ||||
|                     messages.append(msg) | ||||
|             for page in groups_paginator.page_range: | ||||
|                 result.messages.append(_("Syncing page %(page)d of groups" % {"page": page})) | ||||
|                 messages.append(_("Syncing page %(page)d of groups" % {"page": page})) | ||||
|                 for msg in scim_sync_group.delay(page, provider_pk).get(): | ||||
|                     result.messages.append(msg) | ||||
|                     messages.append(msg) | ||||
|         except StopSync as exc: | ||||
|             self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|             self.set_error(exc) | ||||
|             return | ||||
|     self.set_status(result) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|  | ||||
| @ -6,6 +6,7 @@ from django_filters.filters import AllValuesMultipleFilter | ||||
| from django_filters.filterset import FilterSet | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.exceptions import ValidationError | ||||
| from rest_framework.fields import BooleanField, DictField, ListField, SerializerMethodField | ||||
| @ -14,13 +15,12 @@ from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.admin.api.tasks import TaskSerializer | ||||
| from authentik.core.api.propertymappings import PropertyMappingSerializer | ||||
| from authentik.core.api.sources import SourceSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.events.monitored_tasks import TaskInfo | ||||
| from authentik.events.api.tasks import SystemTaskSerializer | ||||
| from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | ||||
| from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES | ||||
|  | ||||
| @ -91,7 +91,7 @@ class LDAPSyncStatusSerializer(PassiveSerializer): | ||||
|     """LDAP Source sync status""" | ||||
|  | ||||
|     is_running = BooleanField(read_only=True) | ||||
|     tasks = TaskSerializer(many=True, read_only=True) | ||||
|     tasks = SystemTaskSerializer(many=True, read_only=True) | ||||
|  | ||||
|  | ||||
| class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | ||||
| @ -136,7 +136,12 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet): | ||||
|     def sync_status(self, request: Request, slug: str) -> Response: | ||||
|         """Get source's sync status""" | ||||
|         source: LDAPSource = self.get_object() | ||||
|         tasks = TaskInfo.by_name(f"ldap_sync:{source.slug}:*") or [] | ||||
|         tasks = list( | ||||
|             get_objects_for_user(request.user, "authentik_events.view_systemtask").filter( | ||||
|                 name="ldap_sync", | ||||
|                 uid__startswith=source.slug, | ||||
|             ) | ||||
|         ) | ||||
|         status = { | ||||
|             "tasks": tasks, | ||||
|             "is_running": source.sync_lock.locked(), | ||||
|  | ||||
| @ -1,9 +1,7 @@ | ||||
| """FreeIPA specific""" | ||||
| from datetime import datetime | ||||
| from datetime import datetime, timezone | ||||
| from typing import Any, Generator | ||||
|  | ||||
| from pytz import UTC | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer, flatten | ||||
|  | ||||
| @ -27,7 +25,7 @@ class FreeIPA(BaseLDAPSynchronizer): | ||||
|         if "krbLastPwdChange" not in attributes: | ||||
|             return | ||||
|         pwd_last_set: datetime = attributes.get("krbLastPwdChange", datetime.now()) | ||||
|         pwd_last_set = pwd_last_set.replace(tzinfo=UTC) | ||||
|         pwd_last_set = pwd_last_set.replace(tzinfo=timezone.utc) | ||||
|         if created or pwd_last_set >= user.password_change_date: | ||||
|             self.message(f"'{user.username}': Reset user's password") | ||||
|             self._logger.debug( | ||||
|  | ||||
							
								
								
									
										6
									
								
								authentik/sources/ldap/sync/vendor/ms_ad.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								authentik/sources/ldap/sync/vendor/ms_ad.py
									
									
									
									
										vendored
									
									
								
							| @ -1,10 +1,8 @@ | ||||
| """Active Directory specific""" | ||||
| from datetime import datetime | ||||
| from datetime import datetime, timezone | ||||
| from enum import IntFlag | ||||
| from typing import Any, Generator | ||||
|  | ||||
| from pytz import UTC | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer | ||||
|  | ||||
| @ -58,7 +56,7 @@ class MicrosoftActiveDirectory(BaseLDAPSynchronizer): | ||||
|         if "pwdLastSet" not in attributes: | ||||
|             return | ||||
|         pwd_last_set: datetime = attributes.get("pwdLastSet", datetime.now()) | ||||
|         pwd_last_set = pwd_last_set.replace(tzinfo=UTC) | ||||
|         pwd_last_set = pwd_last_set.replace(tzinfo=timezone.utc) | ||||
|         if created or pwd_last_set >= user.password_change_date: | ||||
|             self.message(f"'{user.username}': Reset user's password") | ||||
|             self._logger.debug( | ||||
|  | ||||
| @ -8,8 +8,9 @@ from ldap3.core.exceptions import LDAPException | ||||
| from redis.exceptions import LockError | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.monitored_tasks import CACHE_KEY_PREFIX as CACHE_KEY_PREFIX_TASKS | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.events.models import SystemTask as DBSystemTask | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.reflection import class_to_path, path_to_class | ||||
| @ -34,7 +35,7 @@ CACHE_KEY_STATUS = "goauthentik.io/sources/ldap/status/" | ||||
| def ldap_sync_all(): | ||||
|     """Sync all sources""" | ||||
|     for source in LDAPSource.objects.filter(enabled=True): | ||||
|         ldap_sync_single.apply_async(args=[source.pk]) | ||||
|         ldap_sync_single.apply_async(args=[str(source.pk)]) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| @ -69,8 +70,7 @@ def ldap_sync_single(source_pk: str): | ||||
|     try: | ||||
|         with lock: | ||||
|             # Delete all sync tasks from the cache | ||||
|             keys = cache.keys(f"{CACHE_KEY_PREFIX_TASKS}ldap_sync:{source.slug}*") | ||||
|             cache.delete_many(keys) | ||||
|             DBSystemTask.objects.filter(name="ldap_sync", uid__startswith=source.slug).delete() | ||||
|             task = chain( | ||||
|                 # User and group sync can happen at once, they have no dependencies on each other | ||||
|                 group( | ||||
| @ -96,18 +96,18 @@ def ldap_sync_paginator(source: LDAPSource, sync: type[BaseLDAPSynchronizer]) -> | ||||
|     for page in sync_inst.get_objects(): | ||||
|         page_cache_key = CACHE_KEY_PREFIX + str(uuid4()) | ||||
|         cache.set(page_cache_key, page, 60 * 60 * CONFIG.get_int("ldap.task_timeout_hours")) | ||||
|         page_sync = ldap_sync.si(source.pk, class_to_path(sync), page_cache_key) | ||||
|         page_sync = ldap_sync.si(str(source.pk), class_to_path(sync), page_cache_key) | ||||
|         signatures.append(page_sync) | ||||
|     return signatures | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     bind=True, | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
|     soft_time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"), | ||||
|     task_time_limit=60 * 60 * CONFIG.get_int("ldap.task_timeout_hours"), | ||||
| ) | ||||
| def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_key: str): | ||||
| def ldap_sync(self: SystemTask, source_pk: str, sync_class: str, page_cache_key: str): | ||||
|     """Synchronization of an LDAP Source""" | ||||
|     self.result_timeout_hours = CONFIG.get_int("ldap.task_timeout_hours") | ||||
|     source: LDAPSource = LDAPSource.objects.filter(pk=source_pk).first() | ||||
| @ -127,20 +127,18 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str, page_cache_k | ||||
|                 + "Try increasing ldap.task_timeout_hours" | ||||
|             ) | ||||
|             LOGGER.warning(error_message) | ||||
|             self.set_status(TaskResult(TaskResultStatus.ERROR, [error_message])) | ||||
|             self.set_status(TaskStatus.ERROR, error_message) | ||||
|             return | ||||
|         cache.touch(page_cache_key) | ||||
|         count = sync_inst.sync(page) | ||||
|         messages = sync_inst.messages | ||||
|         messages.append(f"Synced {count} objects.") | ||||
|         self.set_status( | ||||
|             TaskResult( | ||||
|                 TaskResultStatus.SUCCESSFUL, | ||||
|                 messages, | ||||
|             ) | ||||
|             TaskStatus.SUCCESSFUL, | ||||
|             *messages, | ||||
|         ) | ||||
|         cache.delete(page_cache_key) | ||||
|     except LDAPException as exc: | ||||
|         # No explicit event is created here as .set_status with an error will do that | ||||
|         LOGGER.warning(exception_to_string(exc)) | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|  | ||||
| @ -7,8 +7,8 @@ from django.test import TestCase | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus | ||||
| from authentik.events.models import Event, EventAction, SystemTask | ||||
| from authentik.events.system_tasks import TaskStatus | ||||
| from authentik.lib.generators import generate_id, generate_key | ||||
| from authentik.lib.utils.reflection import class_to_path | ||||
| from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | ||||
| @ -40,9 +40,9 @@ class LDAPSyncTests(TestCase): | ||||
|         """Test sync with missing page""" | ||||
|         connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) | ||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||
|             ldap_sync.delay(self.source.pk, class_to_path(UserLDAPSynchronizer), "foo").get() | ||||
|         status = TaskInfo.by_name("ldap_sync:ldap:users:foo") | ||||
|         self.assertEqual(status.result.status, TaskResultStatus.ERROR) | ||||
|             ldap_sync.delay(str(self.source.pk), class_to_path(UserLDAPSynchronizer), "foo").get() | ||||
|         task = SystemTask.objects.filter(name="ldap_sync", uid="ldap:users:foo").first() | ||||
|         self.assertEqual(task.status, TaskStatus.ERROR) | ||||
|  | ||||
|     def test_sync_error(self): | ||||
|         """Test user sync""" | ||||
|  | ||||
| @ -4,7 +4,8 @@ from json import dumps | ||||
| from requests import RequestException | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.events.models import TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.sources.oauth.models import OAuthSource | ||||
| @ -12,11 +13,11 @@ from authentik.sources.oauth.models import OAuthSource | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| def update_well_known_jwks(self: MonitoredTask): | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| def update_well_known_jwks(self: SystemTask): | ||||
|     """Update OAuth sources' config from well_known, and JWKS info from the configured URL""" | ||||
|     session = get_http_session() | ||||
|     result = TaskResult(TaskResultStatus.SUCCESSFUL, []) | ||||
|     messages = [] | ||||
|     for source in OAuthSource.objects.all().exclude(oidc_well_known_url=""): | ||||
|         try: | ||||
|             well_known_config = session.get(source.oidc_well_known_url) | ||||
| @ -24,7 +25,7 @@ def update_well_known_jwks(self: MonitoredTask): | ||||
|         except RequestException as exc: | ||||
|             text = exc.response.text if exc.response else str(exc) | ||||
|             LOGGER.warning("Failed to update well_known", source=source, exc=exc, text=text) | ||||
|             result.messages.append(f"Failed to update OIDC configuration for {source.slug}") | ||||
|             messages.append(f"Failed to update OIDC configuration for {source.slug}") | ||||
|             continue | ||||
|         config = well_known_config.json() | ||||
|         try: | ||||
| @ -47,7 +48,7 @@ def update_well_known_jwks(self: MonitoredTask): | ||||
|                 source=source, | ||||
|                 exc=exc, | ||||
|             ) | ||||
|             result.messages.append(f"Failed to update OIDC configuration for {source.slug}") | ||||
|             messages.append(f"Failed to update OIDC configuration for {source.slug}") | ||||
|             continue | ||||
|         if dirty: | ||||
|             LOGGER.info("Updating sources' OpenID Configuration", source=source) | ||||
| @ -60,11 +61,11 @@ def update_well_known_jwks(self: MonitoredTask): | ||||
|         except RequestException as exc: | ||||
|             text = exc.response.text if exc.response else str(exc) | ||||
|             LOGGER.warning("Failed to update JWKS", source=source, exc=exc, text=text) | ||||
|             result.messages.append(f"Failed to update JWKS for {source.slug}") | ||||
|             messages.append(f"Failed to update JWKS for {source.slug}") | ||||
|             continue | ||||
|         config = jwks_config.json() | ||||
|         if dumps(source.oidc_jwks, sort_keys=True) != dumps(config, sort_keys=True): | ||||
|             source.oidc_jwks = config | ||||
|             LOGGER.info("Updating sources' JWKS", source=source) | ||||
|             source.save() | ||||
|     self.set_status(result) | ||||
|     self.set_status(TaskStatus.SUCCESSFUL, *messages) | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| """Plex tasks""" | ||||
| from requests import RequestException | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.events.models import Event, EventAction, TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.sources.plex.models import PlexSource | ||||
| @ -16,8 +16,8 @@ def check_plex_token_all(): | ||||
|         check_plex_token.delay(source.slug) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) | ||||
| def check_plex_token(self: MonitoredTask, source_slug: int): | ||||
| @CELERY_APP.task(bind=True, base=SystemTask) | ||||
| def check_plex_token(self: SystemTask, source_slug: int): | ||||
|     """Check the validity of a Plex source.""" | ||||
|     sources = PlexSource.objects.filter(slug=source_slug) | ||||
|     if not sources.exists(): | ||||
| @ -27,16 +27,15 @@ def check_plex_token(self: MonitoredTask, source_slug: int): | ||||
|     auth = PlexAuth(source, source.plex_token) | ||||
|     try: | ||||
|         auth.get_user_info() | ||||
|         self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."])) | ||||
|         self.set_status(TaskStatus.SUCCESSFUL, "Plex token is valid.") | ||||
|     except RequestException as exc: | ||||
|         error = exception_to_string(exc) | ||||
|         if len(source.plex_token) > 0: | ||||
|             error = error.replace(source.plex_token, "$PLEX_TOKEN") | ||||
|         self.set_status( | ||||
|             TaskResult( | ||||
|                 TaskResultStatus.ERROR, | ||||
|                 ["Plex token is invalid/an error occurred:", error], | ||||
|             ) | ||||
|             TaskStatus.ERROR, | ||||
|             "Plex token is invalid/an error occurred:", | ||||
|             error, | ||||
|         ) | ||||
|         Event.new( | ||||
|             EventAction.CONFIGURATION_ERROR, | ||||
|  | ||||
| @ -9,8 +9,8 @@ from django.core.mail.utils import DNS_NAME | ||||
| from django.utils.text import slugify | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.events.models import Event, EventAction, TaskStatus | ||||
| from authentik.events.system_tasks import SystemTask | ||||
| from authentik.root.celery import CELERY_APP | ||||
| from authentik.stages.email.models import EmailStage | ||||
| from authentik.stages.email.utils import logo_data | ||||
| @ -22,7 +22,7 @@ def send_mails(stage: EmailStage, *messages: list[EmailMultiAlternatives]): | ||||
|     """Wrapper to convert EmailMessage to dict and send it from worker""" | ||||
|     tasks = [] | ||||
|     for message in messages: | ||||
|         tasks.append(send_mail.s(message.__dict__, stage.pk)) | ||||
|         tasks.append(send_mail.s(message.__dict__, str(stage.pk))) | ||||
|     lazy_group = group(*tasks) | ||||
|     promise = lazy_group() | ||||
|     return promise | ||||
| @ -44,9 +44,9 @@ def get_email_body(email: EmailMultiAlternatives) -> str: | ||||
|         OSError, | ||||
|     ), | ||||
|     retry_backoff=True, | ||||
|     base=MonitoredTask, | ||||
|     base=SystemTask, | ||||
| ) | ||||
| def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Optional[int] = None): | ||||
| def send_mail(self: SystemTask, message: dict[Any, Any], email_stage_pk: Optional[str] = None): | ||||
|     """Send Email for Email Stage. Retries are scheduled automatically.""" | ||||
|     self.save_on_success = False | ||||
|     message_id = make_msgid(domain=DNS_NAME) | ||||
| @ -58,10 +58,8 @@ def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Opti | ||||
|             stages = EmailStage.objects.filter(pk=email_stage_pk) | ||||
|             if not stages.exists(): | ||||
|                 self.set_status( | ||||
|                     TaskResult( | ||||
|                         TaskResultStatus.WARNING, | ||||
|                         messages=["Email stage does not exist anymore. Discarding message."], | ||||
|                     ) | ||||
|                     TaskStatus.WARNING, | ||||
|                     "Email stage does not exist anymore. Discarding message.", | ||||
|                 ) | ||||
|                 return | ||||
|             stage: EmailStage = stages.first() | ||||
| @ -69,7 +67,7 @@ def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Opti | ||||
|             backend = stage.backend | ||||
|         except ValueError as exc: | ||||
|             LOGGER.warning("failed to get email backend", exc=exc) | ||||
|             self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|             self.set_error(exc) | ||||
|             return | ||||
|         backend.open() | ||||
|         # Since django's EmailMessage objects are not JSON serialisable, | ||||
| @ -97,12 +95,10 @@ def send_mail(self: MonitoredTask, message: dict[Any, Any], email_stage_pk: Opti | ||||
|             to_email=message_object.to, | ||||
|         ).save() | ||||
|         self.set_status( | ||||
|             TaskResult( | ||||
|                 TaskResultStatus.SUCCESSFUL, | ||||
|                 messages=["Successfully sent Mail."], | ||||
|             ) | ||||
|             TaskStatus.SUCCESSFUL, | ||||
|             "Successfully sent Mail.", | ||||
|         ) | ||||
|     except (SMTPException, ConnectionError, OSError) as exc: | ||||
|         LOGGER.debug("Error sending email, retrying...", exc=exc) | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         self.set_error(exc) | ||||
|         raise exc | ||||
|  | ||||
| @ -707,43 +707,6 @@ | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
|                             "model", | ||||
|                             "identifiers" | ||||
|                         ], | ||||
|                         "properties": { | ||||
|                             "model": { | ||||
|                                 "const": "authentik_policies_reputation.reputation" | ||||
|                             }, | ||||
|                             "id": { | ||||
|                                 "type": "string" | ||||
|                             }, | ||||
|                             "state": { | ||||
|                                 "type": "string", | ||||
|                                 "enum": [ | ||||
|                                     "absent", | ||||
|                                     "present", | ||||
|                                     "created", | ||||
|                                     "must_created" | ||||
|                                 ], | ||||
|                                 "default": "present" | ||||
|                             }, | ||||
|                             "conditions": { | ||||
|                                 "type": "array", | ||||
|                                 "items": { | ||||
|                                     "type": "boolean" | ||||
|                                 } | ||||
|                             }, | ||||
|                             "attrs": { | ||||
|                                 "$ref": "#/$defs/model_authentik_policies_reputation.reputation" | ||||
|                             }, | ||||
|                             "identifiers": { | ||||
|                                 "$ref": "#/$defs/model_authentik_policies_reputation.reputation" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
| @ -892,117 +855,6 @@ | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
|                             "model", | ||||
|                             "identifiers" | ||||
|                         ], | ||||
|                         "properties": { | ||||
|                             "model": { | ||||
|                                 "const": "authentik_providers_oauth2.authorizationcode" | ||||
|                             }, | ||||
|                             "id": { | ||||
|                                 "type": "string" | ||||
|                             }, | ||||
|                             "state": { | ||||
|                                 "type": "string", | ||||
|                                 "enum": [ | ||||
|                                     "absent", | ||||
|                                     "present", | ||||
|                                     "created", | ||||
|                                     "must_created" | ||||
|                                 ], | ||||
|                                 "default": "present" | ||||
|                             }, | ||||
|                             "conditions": { | ||||
|                                 "type": "array", | ||||
|                                 "items": { | ||||
|                                     "type": "boolean" | ||||
|                                 } | ||||
|                             }, | ||||
|                             "attrs": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.authorizationcode" | ||||
|                             }, | ||||
|                             "identifiers": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.authorizationcode" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
|                             "model", | ||||
|                             "identifiers" | ||||
|                         ], | ||||
|                         "properties": { | ||||
|                             "model": { | ||||
|                                 "const": "authentik_providers_oauth2.accesstoken" | ||||
|                             }, | ||||
|                             "id": { | ||||
|                                 "type": "string" | ||||
|                             }, | ||||
|                             "state": { | ||||
|                                 "type": "string", | ||||
|                                 "enum": [ | ||||
|                                     "absent", | ||||
|                                     "present", | ||||
|                                     "created", | ||||
|                                     "must_created" | ||||
|                                 ], | ||||
|                                 "default": "present" | ||||
|                             }, | ||||
|                             "conditions": { | ||||
|                                 "type": "array", | ||||
|                                 "items": { | ||||
|                                     "type": "boolean" | ||||
|                                 } | ||||
|                             }, | ||||
|                             "attrs": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.accesstoken" | ||||
|                             }, | ||||
|                             "identifiers": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.accesstoken" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
|                             "model", | ||||
|                             "identifiers" | ||||
|                         ], | ||||
|                         "properties": { | ||||
|                             "model": { | ||||
|                                 "const": "authentik_providers_oauth2.refreshtoken" | ||||
|                             }, | ||||
|                             "id": { | ||||
|                                 "type": "string" | ||||
|                             }, | ||||
|                             "state": { | ||||
|                                 "type": "string", | ||||
|                                 "enum": [ | ||||
|                                     "absent", | ||||
|                                     "present", | ||||
|                                     "created", | ||||
|                                     "must_created" | ||||
|                                 ], | ||||
|                                 "default": "present" | ||||
|                             }, | ||||
|                             "conditions": { | ||||
|                                 "type": "array", | ||||
|                                 "items": { | ||||
|                                     "type": "boolean" | ||||
|                                 } | ||||
|                             }, | ||||
|                             "attrs": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.refreshtoken" | ||||
|                             }, | ||||
|                             "identifiers": { | ||||
|                                 "$ref": "#/$defs/model_authentik_providers_oauth2.refreshtoken" | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         "type": "object", | ||||
|                         "required": [ | ||||
| @ -3678,14 +3530,10 @@ | ||||
|                         "authentik_policies_expression.expressionpolicy", | ||||
|                         "authentik_policies_password.passwordpolicy", | ||||
|                         "authentik_policies_reputation.reputationpolicy", | ||||
|                         "authentik_policies_reputation.reputation", | ||||
|                         "authentik_policies.policybinding", | ||||
|                         "authentik_providers_ldap.ldapprovider", | ||||
|                         "authentik_providers_oauth2.scopemapping", | ||||
|                         "authentik_providers_oauth2.oauth2provider", | ||||
|                         "authentik_providers_oauth2.authorizationcode", | ||||
|                         "authentik_providers_oauth2.accesstoken", | ||||
|                         "authentik_providers_oauth2.refreshtoken", | ||||
|                         "authentik_providers_proxy.proxyprovider", | ||||
|                         "authentik_providers_radius.radiusprovider", | ||||
|                         "authentik_providers_saml.samlprovider", | ||||
| @ -3909,43 +3757,6 @@ | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_policies_reputation.reputation": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|                 "pk": { | ||||
|                     "type": "string", | ||||
|                     "format": "uuid", | ||||
|                     "title": "Reputation uuid" | ||||
|                 }, | ||||
|                 "identifier": { | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
|                     "title": "Identifier" | ||||
|                 }, | ||||
|                 "ip": { | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
|                     "title": "Ip" | ||||
|                 }, | ||||
|                 "ip_geo_data": { | ||||
|                     "type": "object", | ||||
|                     "additionalProperties": true, | ||||
|                     "title": "Ip geo data" | ||||
|                 }, | ||||
|                 "ip_asn_data": { | ||||
|                     "type": "object", | ||||
|                     "additionalProperties": true, | ||||
|                     "title": "Ip asn data" | ||||
|                 }, | ||||
|                 "score": { | ||||
|                     "type": "integer", | ||||
|                     "minimum": -9223372036854775808, | ||||
|                     "maximum": 9223372036854775807, | ||||
|                     "title": "Score" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_policies.policybinding": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
| @ -4224,617 +4035,6 @@ | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_providers_oauth2.authorizationcode": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|                 "provider": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Name" | ||||
|                         }, | ||||
|                         "authentication_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authentication flow", | ||||
|                             "description": "Flow used for authentication when the associated application is accessed by an un-authenticated user." | ||||
|                         }, | ||||
|                         "authorization_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authorization flow", | ||||
|                             "description": "Flow used when authorizing this provider." | ||||
|                         }, | ||||
|                         "property_mappings": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Property mappings" | ||||
|                         }, | ||||
|                         "client_type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "confidential", | ||||
|                                 "public" | ||||
|                             ], | ||||
|                             "title": "Client Type", | ||||
|                             "description": "Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable" | ||||
|                         }, | ||||
|                         "client_id": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Client ID" | ||||
|                         }, | ||||
|                         "client_secret": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "title": "Client Secret" | ||||
|                         }, | ||||
|                         "access_code_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access code validity", | ||||
|                             "description": "Access codes not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "access_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "refresh_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Refresh token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "include_claims_in_id_token": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Include claims in id_token", | ||||
|                             "description": "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||
|                         }, | ||||
|                         "signing_key": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Signing Key", | ||||
|                             "description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." | ||||
|                         }, | ||||
|                         "redirect_uris": { | ||||
|                             "type": "string", | ||||
|                             "title": "Redirect URIs", | ||||
|                             "description": "Enter each URI on a new line." | ||||
|                         }, | ||||
|                         "sub_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "hashed_user_id", | ||||
|                                 "user_id", | ||||
|                                 "user_uuid", | ||||
|                                 "user_username", | ||||
|                                 "user_email", | ||||
|                                 "user_upn" | ||||
|                             ], | ||||
|                             "title": "Sub mode", | ||||
|                             "description": "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." | ||||
|                         }, | ||||
|                         "issuer_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "global", | ||||
|                                 "per_provider" | ||||
|                             ], | ||||
|                             "title": "Issuer mode", | ||||
|                             "description": "Configure how the issuer field of the ID Token should be filled." | ||||
|                         }, | ||||
|                         "jwks_sources": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer", | ||||
|                                 "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                             }, | ||||
|                             "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "name", | ||||
|                         "authorization_flow" | ||||
|                     ], | ||||
|                     "title": "Provider" | ||||
|                 }, | ||||
|                 "user": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "username": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 150, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Username" | ||||
|                         }, | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "title": "Name", | ||||
|                             "description": "User's display name." | ||||
|                         }, | ||||
|                         "is_active": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Active", | ||||
|                             "description": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." | ||||
|                         }, | ||||
|                         "last_login": { | ||||
|                             "type": [ | ||||
|                                 "string", | ||||
|                                 "null" | ||||
|                             ], | ||||
|                             "format": "date-time", | ||||
|                             "title": "Last login" | ||||
|                         }, | ||||
|                         "groups": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Groups" | ||||
|                         }, | ||||
|                         "email": { | ||||
|                             "type": "string", | ||||
|                             "format": "email", | ||||
|                             "maxLength": 254, | ||||
|                             "title": "Email address" | ||||
|                         }, | ||||
|                         "attributes": { | ||||
|                             "type": "object", | ||||
|                             "additionalProperties": true, | ||||
|                             "title": "Attributes" | ||||
|                         }, | ||||
|                         "path": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Path" | ||||
|                         }, | ||||
|                         "type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "internal", | ||||
|                                 "external", | ||||
|                                 "service_account", | ||||
|                                 "internal_service_account" | ||||
|                             ], | ||||
|                             "title": "Type" | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "username", | ||||
|                         "name" | ||||
|                     ], | ||||
|                     "title": "User" | ||||
|                 }, | ||||
|                 "expires": { | ||||
|                     "type": "string", | ||||
|                     "format": "date-time", | ||||
|                     "title": "Expires" | ||||
|                 }, | ||||
|                 "scope": { | ||||
|                     "type": "array", | ||||
|                     "items": { | ||||
|                         "type": "string", | ||||
|                         "minLength": 1 | ||||
|                     }, | ||||
|                     "title": "Scope" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_providers_oauth2.accesstoken": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|                 "provider": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Name" | ||||
|                         }, | ||||
|                         "authentication_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authentication flow", | ||||
|                             "description": "Flow used for authentication when the associated application is accessed by an un-authenticated user." | ||||
|                         }, | ||||
|                         "authorization_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authorization flow", | ||||
|                             "description": "Flow used when authorizing this provider." | ||||
|                         }, | ||||
|                         "property_mappings": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Property mappings" | ||||
|                         }, | ||||
|                         "client_type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "confidential", | ||||
|                                 "public" | ||||
|                             ], | ||||
|                             "title": "Client Type", | ||||
|                             "description": "Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable" | ||||
|                         }, | ||||
|                         "client_id": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Client ID" | ||||
|                         }, | ||||
|                         "client_secret": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "title": "Client Secret" | ||||
|                         }, | ||||
|                         "access_code_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access code validity", | ||||
|                             "description": "Access codes not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "access_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "refresh_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Refresh token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "include_claims_in_id_token": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Include claims in id_token", | ||||
|                             "description": "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||
|                         }, | ||||
|                         "signing_key": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Signing Key", | ||||
|                             "description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." | ||||
|                         }, | ||||
|                         "redirect_uris": { | ||||
|                             "type": "string", | ||||
|                             "title": "Redirect URIs", | ||||
|                             "description": "Enter each URI on a new line." | ||||
|                         }, | ||||
|                         "sub_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "hashed_user_id", | ||||
|                                 "user_id", | ||||
|                                 "user_uuid", | ||||
|                                 "user_username", | ||||
|                                 "user_email", | ||||
|                                 "user_upn" | ||||
|                             ], | ||||
|                             "title": "Sub mode", | ||||
|                             "description": "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." | ||||
|                         }, | ||||
|                         "issuer_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "global", | ||||
|                                 "per_provider" | ||||
|                             ], | ||||
|                             "title": "Issuer mode", | ||||
|                             "description": "Configure how the issuer field of the ID Token should be filled." | ||||
|                         }, | ||||
|                         "jwks_sources": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer", | ||||
|                                 "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                             }, | ||||
|                             "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "name", | ||||
|                         "authorization_flow" | ||||
|                     ], | ||||
|                     "title": "Provider" | ||||
|                 }, | ||||
|                 "user": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "username": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 150, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Username" | ||||
|                         }, | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "title": "Name", | ||||
|                             "description": "User's display name." | ||||
|                         }, | ||||
|                         "is_active": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Active", | ||||
|                             "description": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." | ||||
|                         }, | ||||
|                         "last_login": { | ||||
|                             "type": [ | ||||
|                                 "string", | ||||
|                                 "null" | ||||
|                             ], | ||||
|                             "format": "date-time", | ||||
|                             "title": "Last login" | ||||
|                         }, | ||||
|                         "groups": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Groups" | ||||
|                         }, | ||||
|                         "email": { | ||||
|                             "type": "string", | ||||
|                             "format": "email", | ||||
|                             "maxLength": 254, | ||||
|                             "title": "Email address" | ||||
|                         }, | ||||
|                         "attributes": { | ||||
|                             "type": "object", | ||||
|                             "additionalProperties": true, | ||||
|                             "title": "Attributes" | ||||
|                         }, | ||||
|                         "path": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Path" | ||||
|                         }, | ||||
|                         "type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "internal", | ||||
|                                 "external", | ||||
|                                 "service_account", | ||||
|                                 "internal_service_account" | ||||
|                             ], | ||||
|                             "title": "Type" | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "username", | ||||
|                         "name" | ||||
|                     ], | ||||
|                     "title": "User" | ||||
|                 }, | ||||
|                 "expires": { | ||||
|                     "type": "string", | ||||
|                     "format": "date-time", | ||||
|                     "title": "Expires" | ||||
|                 }, | ||||
|                 "scope": { | ||||
|                     "type": "array", | ||||
|                     "items": { | ||||
|                         "type": "string", | ||||
|                         "minLength": 1 | ||||
|                     }, | ||||
|                     "title": "Scope" | ||||
|                 }, | ||||
|                 "revoked": { | ||||
|                     "type": "boolean", | ||||
|                     "title": "Revoked" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_providers_oauth2.refreshtoken": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|                 "provider": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Name" | ||||
|                         }, | ||||
|                         "authentication_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authentication flow", | ||||
|                             "description": "Flow used for authentication when the associated application is accessed by an un-authenticated user." | ||||
|                         }, | ||||
|                         "authorization_flow": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Authorization flow", | ||||
|                             "description": "Flow used when authorizing this provider." | ||||
|                         }, | ||||
|                         "property_mappings": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Property mappings" | ||||
|                         }, | ||||
|                         "client_type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "confidential", | ||||
|                                 "public" | ||||
|                             ], | ||||
|                             "title": "Client Type", | ||||
|                             "description": "Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable" | ||||
|                         }, | ||||
|                         "client_id": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Client ID" | ||||
|                         }, | ||||
|                         "client_secret": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 255, | ||||
|                             "title": "Client Secret" | ||||
|                         }, | ||||
|                         "access_code_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access code validity", | ||||
|                             "description": "Access codes not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "access_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Access token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "refresh_token_validity": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Refresh token validity", | ||||
|                             "description": "Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3)." | ||||
|                         }, | ||||
|                         "include_claims_in_id_token": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Include claims in id_token", | ||||
|                             "description": "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint." | ||||
|                         }, | ||||
|                         "signing_key": { | ||||
|                             "type": "integer", | ||||
|                             "title": "Signing Key", | ||||
|                             "description": "Key used to sign the tokens. Only required when JWT Algorithm is set to RS256." | ||||
|                         }, | ||||
|                         "redirect_uris": { | ||||
|                             "type": "string", | ||||
|                             "title": "Redirect URIs", | ||||
|                             "description": "Enter each URI on a new line." | ||||
|                         }, | ||||
|                         "sub_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "hashed_user_id", | ||||
|                                 "user_id", | ||||
|                                 "user_uuid", | ||||
|                                 "user_username", | ||||
|                                 "user_email", | ||||
|                                 "user_upn" | ||||
|                             ], | ||||
|                             "title": "Sub mode", | ||||
|                             "description": "Configure what data should be used as unique User Identifier. For most cases, the default should be fine." | ||||
|                         }, | ||||
|                         "issuer_mode": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "global", | ||||
|                                 "per_provider" | ||||
|                             ], | ||||
|                             "title": "Issuer mode", | ||||
|                             "description": "Configure how the issuer field of the ID Token should be filled." | ||||
|                         }, | ||||
|                         "jwks_sources": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer", | ||||
|                                 "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                             }, | ||||
|                             "title": "Any JWT signed by the JWK of the selected source can be used to authenticate." | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "name", | ||||
|                         "authorization_flow" | ||||
|                     ], | ||||
|                     "title": "Provider" | ||||
|                 }, | ||||
|                 "user": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "username": { | ||||
|                             "type": "string", | ||||
|                             "maxLength": 150, | ||||
|                             "minLength": 1, | ||||
|                             "title": "Username" | ||||
|                         }, | ||||
|                         "name": { | ||||
|                             "type": "string", | ||||
|                             "title": "Name", | ||||
|                             "description": "User's display name." | ||||
|                         }, | ||||
|                         "is_active": { | ||||
|                             "type": "boolean", | ||||
|                             "title": "Active", | ||||
|                             "description": "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." | ||||
|                         }, | ||||
|                         "last_login": { | ||||
|                             "type": [ | ||||
|                                 "string", | ||||
|                                 "null" | ||||
|                             ], | ||||
|                             "format": "date-time", | ||||
|                             "title": "Last login" | ||||
|                         }, | ||||
|                         "groups": { | ||||
|                             "type": "array", | ||||
|                             "items": { | ||||
|                                 "type": "integer" | ||||
|                             }, | ||||
|                             "title": "Groups" | ||||
|                         }, | ||||
|                         "email": { | ||||
|                             "type": "string", | ||||
|                             "format": "email", | ||||
|                             "maxLength": 254, | ||||
|                             "title": "Email address" | ||||
|                         }, | ||||
|                         "attributes": { | ||||
|                             "type": "object", | ||||
|                             "additionalProperties": true, | ||||
|                             "title": "Attributes" | ||||
|                         }, | ||||
|                         "path": { | ||||
|                             "type": "string", | ||||
|                             "minLength": 1, | ||||
|                             "title": "Path" | ||||
|                         }, | ||||
|                         "type": { | ||||
|                             "type": "string", | ||||
|                             "enum": [ | ||||
|                                 "internal", | ||||
|                                 "external", | ||||
|                                 "service_account", | ||||
|                                 "internal_service_account" | ||||
|                             ], | ||||
|                             "title": "Type" | ||||
|                         } | ||||
|                     }, | ||||
|                     "required": [ | ||||
|                         "username", | ||||
|                         "name" | ||||
|                     ], | ||||
|                     "title": "User" | ||||
|                 }, | ||||
|                 "expires": { | ||||
|                     "type": "string", | ||||
|                     "format": "date-time", | ||||
|                     "title": "Expires" | ||||
|                 }, | ||||
|                 "scope": { | ||||
|                     "type": "array", | ||||
|                     "items": { | ||||
|                         "type": "string", | ||||
|                         "minLength": 1 | ||||
|                     }, | ||||
|                     "title": "Scope" | ||||
|                 }, | ||||
|                 "revoked": { | ||||
|                     "type": "boolean", | ||||
|                     "title": "Revoked" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|         }, | ||||
|         "model_authentik_providers_proxy.proxyprovider": { | ||||
|             "type": "object", | ||||
|             "properties": { | ||||
|  | ||||
| @ -8,7 +8,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2024-01-23 08:02+0000\n" | ||||
| "POT-Creation-Date: 2024-01-24 12:44+0000\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @ -18,11 +18,6 @@ msgstr "" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n != 1);\n" | ||||
|  | ||||
| #: authentik/admin/api/tasks.py:127 | ||||
| #, python-format | ||||
| msgid "Successfully re-scheduled Task %(name)s!" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/api/schema.py:25 | ||||
| msgid "Generic API Error" | ||||
| msgstr "" | ||||
| @ -69,7 +64,7 @@ msgstr "" | ||||
| msgid "authentik Export - %(date)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/blueprints/v1/tasks.py:151 authentik/crypto/tasks.py:93 | ||||
| #: authentik/blueprints/v1/tasks.py:145 authentik/crypto/tasks.py:87 | ||||
| #, python-format | ||||
| msgid "Successfully imported %(count)d files." | ||||
| msgstr "" | ||||
| @ -108,162 +103,162 @@ msgstr "" | ||||
| msgid "No empty segments in user path allowed." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:85 | ||||
| #: authentik/core/models.py:92 | ||||
| msgid "name" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:87 | ||||
| #: authentik/core/models.py:94 | ||||
| msgid "Users added to this group will be superusers." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:161 | ||||
| #: authentik/core/models.py:168 | ||||
| msgid "Group" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:162 | ||||
| #: authentik/core/models.py:169 | ||||
| msgid "Groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:177 | ||||
| #: authentik/core/models.py:184 | ||||
| msgid "User's display name." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:273 authentik/providers/oauth2/models.py:295 | ||||
| #: authentik/core/models.py:280 authentik/providers/oauth2/models.py:295 | ||||
| msgid "User" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:274 | ||||
| #: authentik/core/models.py:281 | ||||
| msgid "Users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:276 | ||||
| #: authentik/core/models.py:283 | ||||
| #: authentik/stages/email/templates/email/password_reset.html:28 | ||||
| msgid "Reset Password" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:277 | ||||
| #: authentik/core/models.py:284 | ||||
| msgid "Can impersonate other users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:278 authentik/rbac/models.py:54 | ||||
| #: authentik/core/models.py:285 authentik/rbac/models.py:54 | ||||
| msgid "Can assign permissions to users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:279 authentik/rbac/models.py:55 | ||||
| #: authentik/core/models.py:286 authentik/rbac/models.py:55 | ||||
| msgid "Can unassign permissions from users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:293 | ||||
| #: authentik/core/models.py:308 | ||||
| msgid "" | ||||
| "Flow used for authentication when the associated application is accessed by " | ||||
| "an un-authenticated user." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:303 | ||||
| #: authentik/core/models.py:318 | ||||
| msgid "Flow used when authorizing this provider." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:315 | ||||
| #: authentik/core/models.py:330 | ||||
| msgid "" | ||||
| "Accessed from applications; optional backchannel providers for protocols " | ||||
| "like LDAP and SCIM." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:370 | ||||
| #: authentik/core/models.py:385 | ||||
| msgid "Application's display Name." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:371 | ||||
| #: authentik/core/models.py:386 | ||||
| msgid "Internal application name, used in URLs." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:383 | ||||
| #: authentik/core/models.py:398 | ||||
| msgid "Open launch URL in a new browser tab or window." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:447 | ||||
| #: authentik/core/models.py:462 | ||||
| msgid "Application" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:448 | ||||
| #: authentik/core/models.py:463 | ||||
| msgid "Applications" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:454 | ||||
| #: authentik/core/models.py:469 | ||||
| msgid "Use the source-specific identifier" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:456 | ||||
| #: authentik/core/models.py:471 | ||||
| msgid "" | ||||
| "Link to a user with identical email address. Can have security implications " | ||||
| "when a source doesn't validate email addresses." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:460 | ||||
| #: authentik/core/models.py:475 | ||||
| msgid "" | ||||
| "Use the user's email address, but deny enrollment when the email address " | ||||
| "already exists." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:463 | ||||
| #: authentik/core/models.py:478 | ||||
| msgid "" | ||||
| "Link to a user with identical username. Can have security implications when " | ||||
| "a username is used with another source." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:467 | ||||
| #: authentik/core/models.py:482 | ||||
| msgid "" | ||||
| "Use the user's username, but deny enrollment when the username already " | ||||
| "exists." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:474 | ||||
| #: authentik/core/models.py:489 | ||||
| msgid "Source's display Name." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:475 | ||||
| #: authentik/core/models.py:490 | ||||
| msgid "Internal source name, used in URLs." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:494 | ||||
| #: authentik/core/models.py:509 | ||||
| msgid "Flow to use when authenticating existing users." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:503 | ||||
| #: authentik/core/models.py:518 | ||||
| msgid "Flow to use when enrolling new users." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:511 | ||||
| #: authentik/core/models.py:526 | ||||
| msgid "" | ||||
| "How the source determines if an existing user should be authenticated or a " | ||||
| "new user enrolled." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:683 | ||||
| #: authentik/core/models.py:698 | ||||
| msgid "Token" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:684 | ||||
| #: authentik/core/models.py:699 | ||||
| msgid "Tokens" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:689 | ||||
| #: authentik/core/models.py:704 | ||||
| msgid "View token's key" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:725 | ||||
| #: authentik/core/models.py:740 | ||||
| msgid "Property Mapping" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:726 | ||||
| #: authentik/core/models.py:741 | ||||
| msgid "Property Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:763 | ||||
| #: authentik/core/models.py:778 | ||||
| msgid "Authenticated Session" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/core/models.py:764 | ||||
| #: authentik/core/models.py:779 | ||||
| msgid "Authenticated Sessions" | ||||
| msgstr "" | ||||
|  | ||||
| @ -439,6 +434,11 @@ msgstr "" | ||||
| msgid "(You are already connected in another tab/window)" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/api/tasks.py:99 | ||||
| #, python-format | ||||
| msgid "Successfully re-scheduled Task %(name)s!" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py:289 | ||||
| msgid "Event" | ||||
| msgstr "" | ||||
| @ -541,7 +541,19 @@ msgstr "" | ||||
| msgid "Webhook Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/monitored_tasks.py:207 | ||||
| #: authentik/events/models.py:642 | ||||
| msgid "Rerun task" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py:643 | ||||
| msgid "System Task" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py:644 | ||||
| msgid "System Tasks" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/monitored_tasks.py:128 | ||||
| msgid "Task has not been run yet." | ||||
| msgstr "" | ||||
|  | ||||
| @ -1573,31 +1585,31 @@ msgstr "" | ||||
| msgid "SCIM Mappings" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:56 | ||||
| #: authentik/providers/scim/tasks.py:57 | ||||
| msgid "Starting full SCIM sync" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:66 | ||||
| #: authentik/providers/scim/tasks.py:67 | ||||
| #, python-format | ||||
| msgid "Syncing page %(page)d of users" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:70 | ||||
| #: authentik/providers/scim/tasks.py:71 | ||||
| #, python-format | ||||
| msgid "Syncing page %(page)d of groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:102 | ||||
| #: authentik/providers/scim/tasks.py:103 | ||||
| #, python-format | ||||
| msgid "Failed to sync user %(user_name)s due to remote error: %(error)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:113 authentik/providers/scim/tasks.py:154 | ||||
| #: authentik/providers/scim/tasks.py:114 authentik/providers/scim/tasks.py:155 | ||||
| #, python-format | ||||
| msgid "Stopping sync due to error: %(error)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/scim/tasks.py:143 | ||||
| #: authentik/providers/scim/tasks.py:144 | ||||
| #, python-format | ||||
| msgid "Failed to sync group %(group_name)s due to remote error: %(error)s" | ||||
| msgstr "" | ||||
|  | ||||
							
								
								
									
										431
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										431
									
								
								schema.yml
									
									
									
									
									
								
							| @ -236,103 +236,6 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /admin/system_tasks/: | ||||
|     get: | ||||
|       operationId: admin_system_tasks_list | ||||
|       description: List system tasks | ||||
|       tags: | ||||
|       - admin | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: array | ||||
|                 items: | ||||
|                   $ref: '#/components/schemas/Task' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /admin/system_tasks/{id}/: | ||||
|     get: | ||||
|       operationId: admin_system_tasks_retrieve | ||||
|       description: Get a single system task | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: id | ||||
|         schema: | ||||
|           type: string | ||||
|         required: true | ||||
|       tags: | ||||
|       - admin | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Task' | ||||
|           description: '' | ||||
|         '404': | ||||
|           description: Task not found | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /admin/system_tasks/{id}/retry/: | ||||
|     post: | ||||
|       operationId: admin_system_tasks_retry_create | ||||
|       description: Retry task | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: id | ||||
|         schema: | ||||
|           type: string | ||||
|         required: true | ||||
|       tags: | ||||
|       - admin | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '204': | ||||
|           description: Task retried successfully | ||||
|         '404': | ||||
|           description: Task not found | ||||
|         '500': | ||||
|           description: Failed to retry task | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /admin/version/: | ||||
|     get: | ||||
|       operationId: admin_version_retrieve | ||||
| @ -7017,6 +6920,150 @@ paths: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /events/system_tasks/: | ||||
|     get: | ||||
|       operationId: events_system_tasks_list | ||||
|       description: Read-only view set that returns all background tasks | ||||
|       parameters: | ||||
|       - in: query | ||||
|         name: name | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: ordering | ||||
|         required: false | ||||
|         in: query | ||||
|         description: Which field to use when ordering the results. | ||||
|         schema: | ||||
|           type: string | ||||
|       - name: page | ||||
|         required: false | ||||
|         in: query | ||||
|         description: A page number within the paginated result set. | ||||
|         schema: | ||||
|           type: integer | ||||
|       - name: page_size | ||||
|         required: false | ||||
|         in: query | ||||
|         description: Number of results to return per page. | ||||
|         schema: | ||||
|           type: integer | ||||
|       - name: search | ||||
|         required: false | ||||
|         in: query | ||||
|         description: A search term. | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: query | ||||
|         name: status | ||||
|         schema: | ||||
|           type: string | ||||
|           enum: | ||||
|           - error | ||||
|           - successful | ||||
|           - unknown | ||||
|           - warning | ||||
|         description: |- | ||||
|           * `unknown` - Unknown | ||||
|           * `successful` - Successful | ||||
|           * `warning` - Warning | ||||
|           * `error` - Error | ||||
|       - in: query | ||||
|         name: uid | ||||
|         schema: | ||||
|           type: string | ||||
|       tags: | ||||
|       - events | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/PaginatedSystemTaskList' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /events/system_tasks/{uuid}/: | ||||
|     get: | ||||
|       operationId: events_system_tasks_retrieve | ||||
|       description: Read-only view set that returns all background tasks | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this System Task. | ||||
|         required: true | ||||
|       tags: | ||||
|       - events | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '200': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/SystemTask' | ||||
|           description: '' | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /events/system_tasks/{uuid}/run/: | ||||
|     post: | ||||
|       operationId: events_system_tasks_run_create | ||||
|       description: Run task | ||||
|       parameters: | ||||
|       - in: path | ||||
|         name: uuid | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this System Task. | ||||
|         required: true | ||||
|       tags: | ||||
|       - events | ||||
|       security: | ||||
|       - authentik: [] | ||||
|       responses: | ||||
|         '204': | ||||
|           description: Task retried successfully | ||||
|         '404': | ||||
|           description: Task not found | ||||
|         '500': | ||||
|           description: Failed to retry task | ||||
|         '400': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/ValidationError' | ||||
|           description: '' | ||||
|         '403': | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/GenericError' | ||||
|           description: '' | ||||
|   /events/transports/: | ||||
|     get: | ||||
|       operationId: events_transports_list | ||||
| @ -18172,13 +18219,9 @@ paths: | ||||
|           - authentik_policies_expiry.passwordexpirypolicy | ||||
|           - authentik_policies_expression.expressionpolicy | ||||
|           - authentik_policies_password.passwordpolicy | ||||
|           - authentik_policies_reputation.reputation | ||||
|           - authentik_policies_reputation.reputationpolicy | ||||
|           - authentik_providers_ldap.ldapprovider | ||||
|           - authentik_providers_oauth2.accesstoken | ||||
|           - authentik_providers_oauth2.authorizationcode | ||||
|           - authentik_providers_oauth2.oauth2provider | ||||
|           - authentik_providers_oauth2.refreshtoken | ||||
|           - authentik_providers_oauth2.scopemapping | ||||
|           - authentik_providers_proxy.proxyprovider | ||||
|           - authentik_providers_rac.endpoint | ||||
| @ -18245,14 +18288,10 @@ paths: | ||||
|           * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|           * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|           * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|           * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|           * `authentik_policies.policybinding` - Policy Binding | ||||
|           * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|           * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|           * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|           * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|           * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|           * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|           * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|           * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|           * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -18272,7 +18311,7 @@ paths: | ||||
|           * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|           * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|           * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|           * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|           * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -18468,13 +18507,9 @@ paths: | ||||
|           - authentik_policies_expiry.passwordexpirypolicy | ||||
|           - authentik_policies_expression.expressionpolicy | ||||
|           - authentik_policies_password.passwordpolicy | ||||
|           - authentik_policies_reputation.reputation | ||||
|           - authentik_policies_reputation.reputationpolicy | ||||
|           - authentik_providers_ldap.ldapprovider | ||||
|           - authentik_providers_oauth2.accesstoken | ||||
|           - authentik_providers_oauth2.authorizationcode | ||||
|           - authentik_providers_oauth2.oauth2provider | ||||
|           - authentik_providers_oauth2.refreshtoken | ||||
|           - authentik_providers_oauth2.scopemapping | ||||
|           - authentik_providers_proxy.proxyprovider | ||||
|           - authentik_providers_rac.endpoint | ||||
| @ -18541,14 +18576,10 @@ paths: | ||||
|           * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|           * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|           * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|           * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|           * `authentik_policies.policybinding` - Policy Binding | ||||
|           * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|           * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|           * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|           * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|           * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|           * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|           * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|           * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|           * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -18568,7 +18599,7 @@ paths: | ||||
|           * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|           * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|           * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|           * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|           * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|           * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -23265,7 +23296,7 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Static Authenticator Stage. | ||||
|         description: A UUID string identifying this Static Authenticator Setup Stage. | ||||
|         required: true | ||||
|       tags: | ||||
|       - stages | ||||
| @ -23299,7 +23330,7 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Static Authenticator Stage. | ||||
|         description: A UUID string identifying this Static Authenticator Setup Stage. | ||||
|         required: true | ||||
|       tags: | ||||
|       - stages | ||||
| @ -23339,7 +23370,7 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Static Authenticator Stage. | ||||
|         description: A UUID string identifying this Static Authenticator Setup Stage. | ||||
|         required: true | ||||
|       tags: | ||||
|       - stages | ||||
| @ -23378,7 +23409,7 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Static Authenticator Stage. | ||||
|         description: A UUID string identifying this Static Authenticator Setup Stage. | ||||
|         required: true | ||||
|       tags: | ||||
|       - stages | ||||
| @ -23409,7 +23440,7 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|         description: A UUID string identifying this Static Authenticator Stage. | ||||
|         description: A UUID string identifying this Static Authenticator Setup Stage. | ||||
|         required: true | ||||
|       tags: | ||||
|       - stages | ||||
| @ -32444,14 +32475,10 @@ components: | ||||
|             * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|             * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|             * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|             * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|             * `authentik_policies.policybinding` - Policy Binding | ||||
|             * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|             * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|             * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|             * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|             * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|             * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|             * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|             * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|             * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -32471,7 +32498,7 @@ components: | ||||
|             * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|             * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|             * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -32647,14 +32674,10 @@ components: | ||||
|             * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|             * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|             * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|             * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|             * `authentik_policies.policybinding` - Policy Binding | ||||
|             * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|             * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|             * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|             * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|             * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|             * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|             * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|             * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|             * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -32674,7 +32697,7 @@ components: | ||||
|             * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|             * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|             * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -34787,7 +34810,7 @@ components: | ||||
|         tasks: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/Task' | ||||
|             $ref: '#/components/schemas/SystemTask' | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - is_running | ||||
| @ -34966,14 +34989,10 @@ components: | ||||
|       - authentik_policies_expression.expressionpolicy | ||||
|       - authentik_policies_password.passwordpolicy | ||||
|       - authentik_policies_reputation.reputationpolicy | ||||
|       - authentik_policies_reputation.reputation | ||||
|       - authentik_policies.policybinding | ||||
|       - authentik_providers_ldap.ldapprovider | ||||
|       - authentik_providers_oauth2.scopemapping | ||||
|       - authentik_providers_oauth2.oauth2provider | ||||
|       - authentik_providers_oauth2.authorizationcode | ||||
|       - authentik_providers_oauth2.accesstoken | ||||
|       - authentik_providers_oauth2.refreshtoken | ||||
|       - authentik_providers_proxy.proxyprovider | ||||
|       - authentik_providers_radius.radiusprovider | ||||
|       - authentik_providers_saml.samlprovider | ||||
| @ -35046,14 +35065,10 @@ components: | ||||
|         * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|         * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|         * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|         * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|         * `authentik_policies.policybinding` - Policy Binding | ||||
|         * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|         * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|         * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|         * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|         * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|         * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|         * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|         * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|         * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -35073,7 +35088,7 @@ components: | ||||
|         * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|         * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|         * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|         * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|         * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|         * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|         * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|         * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -37047,6 +37062,18 @@ components: | ||||
|       required: | ||||
|       - pagination | ||||
|       - results | ||||
|     PaginatedSystemTaskList: | ||||
|       type: object | ||||
|       properties: | ||||
|         pagination: | ||||
|           $ref: '#/components/schemas/Pagination' | ||||
|         results: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/SystemTask' | ||||
|       required: | ||||
|       - pagination | ||||
|       - results | ||||
|     PaginatedTOTPDeviceList: | ||||
|       type: object | ||||
|       properties: | ||||
| @ -38274,14 +38301,10 @@ components: | ||||
|             * `authentik_policies_expression.expressionpolicy` - Expression Policy | ||||
|             * `authentik_policies_password.passwordpolicy` - Password Policy | ||||
|             * `authentik_policies_reputation.reputationpolicy` - Reputation Policy | ||||
|             * `authentik_policies_reputation.reputation` - Reputation Score | ||||
|             * `authentik_policies.policybinding` - Policy Binding | ||||
|             * `authentik_providers_ldap.ldapprovider` - LDAP Provider | ||||
|             * `authentik_providers_oauth2.scopemapping` - Scope Mapping | ||||
|             * `authentik_providers_oauth2.oauth2provider` - OAuth2/OpenID Provider | ||||
|             * `authentik_providers_oauth2.authorizationcode` - Authorization Code | ||||
|             * `authentik_providers_oauth2.accesstoken` - OAuth2 Access Token | ||||
|             * `authentik_providers_oauth2.refreshtoken` - OAuth2 Refresh Token | ||||
|             * `authentik_providers_proxy.proxyprovider` - Proxy Provider | ||||
|             * `authentik_providers_radius.radiusprovider` - Radius Provider | ||||
|             * `authentik_providers_saml.samlprovider` - SAML Provider | ||||
| @ -38301,7 +38324,7 @@ components: | ||||
|             * `authentik_stages_authenticator_duo.duodevice` - Duo Device | ||||
|             * `authentik_stages_authenticator_sms.authenticatorsmsstage` - SMS Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_sms.smsdevice` - SMS Device | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Stage | ||||
|             * `authentik_stages_authenticator_static.authenticatorstaticstage` - Static Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_static.staticdevice` - Static Device | ||||
|             * `authentik_stages_authenticator_totp.authenticatortotpstage` - TOTP Authenticator Setup Stage | ||||
|             * `authentik_stages_authenticator_totp.totpdevice` - TOTP Device | ||||
| @ -42794,7 +42817,7 @@ components: | ||||
|         tasks: | ||||
|           type: array | ||||
|           items: | ||||
|             $ref: '#/components/schemas/Task' | ||||
|             $ref: '#/components/schemas/SystemTask' | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - is_running | ||||
| @ -43517,6 +43540,67 @@ components: | ||||
|       - http_is_secure | ||||
|       - runtime | ||||
|       - server_time | ||||
|     SystemTask: | ||||
|       type: object | ||||
|       description: Serialize TaskInfo and TaskResult | ||||
|       properties: | ||||
|         uuid: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           readOnly: true | ||||
|         name: | ||||
|           type: string | ||||
|         full_name: | ||||
|           type: string | ||||
|           description: Get full name with UID | ||||
|           readOnly: true | ||||
|         uid: | ||||
|           type: string | ||||
|         description: | ||||
|           type: string | ||||
|         start_timestamp: | ||||
|           type: string | ||||
|           format: date-time | ||||
|           description: Timestamp when the task started | ||||
|           readOnly: true | ||||
|         finish_timestamp: | ||||
|           type: string | ||||
|           format: date-time | ||||
|           description: Timestamp when the task finished | ||||
|           readOnly: true | ||||
|         duration: | ||||
|           type: number | ||||
|           format: double | ||||
|           description: Get the duration a task took to run | ||||
|           readOnly: true | ||||
|         status: | ||||
|           $ref: '#/components/schemas/SystemTaskStatusEnum' | ||||
|         messages: | ||||
|           type: array | ||||
|           items: | ||||
|             type: string | ||||
|       required: | ||||
|       - description | ||||
|       - duration | ||||
|       - finish_timestamp | ||||
|       - full_name | ||||
|       - messages | ||||
|       - name | ||||
|       - start_timestamp | ||||
|       - status | ||||
|       - uuid | ||||
|     SystemTaskStatusEnum: | ||||
|       enum: | ||||
|       - unknown | ||||
|       - successful | ||||
|       - warning | ||||
|       - error | ||||
|       type: string | ||||
|       description: |- | ||||
|         * `unknown` - UNKNOWN | ||||
|         * `successful` - SUCCESSFUL | ||||
|         * `warning` - WARNING | ||||
|         * `error` - ERROR | ||||
|     TOTPDevice: | ||||
|       type: object | ||||
|       description: Serializer for totp authenticator devices | ||||
| @ -43543,45 +43627,6 @@ components: | ||||
|           maxLength: 64 | ||||
|       required: | ||||
|       - name | ||||
|     Task: | ||||
|       type: object | ||||
|       description: Serialize TaskInfo and TaskResult | ||||
|       properties: | ||||
|         task_name: | ||||
|           type: string | ||||
|         task_description: | ||||
|           type: string | ||||
|         task_finish_timestamp: | ||||
|           type: string | ||||
|           format: date-time | ||||
|         task_duration: | ||||
|           type: integer | ||||
|           description: Get the duration a task took to run | ||||
|           readOnly: true | ||||
|         status: | ||||
|           $ref: '#/components/schemas/TaskStatusEnum' | ||||
|         messages: | ||||
|           type: array | ||||
|           items: {} | ||||
|       required: | ||||
|       - messages | ||||
|       - status | ||||
|       - task_description | ||||
|       - task_duration | ||||
|       - task_finish_timestamp | ||||
|       - task_name | ||||
|     TaskStatusEnum: | ||||
|       enum: | ||||
|       - SUCCESSFUL | ||||
|       - WARNING | ||||
|       - ERROR | ||||
|       - UNKNOWN | ||||
|       type: string | ||||
|       description: |- | ||||
|         * `SUCCESSFUL` - SUCCESSFUL | ||||
|         * `WARNING` - WARNING | ||||
|         * `ERROR` - ERROR | ||||
|         * `UNKNOWN` - UNKNOWN | ||||
|     Tenant: | ||||
|       type: object | ||||
|       description: Tenant Serializer | ||||
| @ -43853,16 +43898,16 @@ components: | ||||
|       - pk | ||||
|     UsedByActionEnum: | ||||
|       enum: | ||||
|       - CASCADE | ||||
|       - CASCADE_MANY | ||||
|       - SET_NULL | ||||
|       - SET_DEFAULT | ||||
|       - cascade | ||||
|       - cascade_many | ||||
|       - set_null | ||||
|       - set_default | ||||
|       type: string | ||||
|       description: |- | ||||
|         * `CASCADE` - CASCADE | ||||
|         * `CASCADE_MANY` - CASCADE_MANY | ||||
|         * `SET_NULL` - SET_NULL | ||||
|         * `SET_DEFAULT` - SET_DEFAULT | ||||
|         * `cascade` - CASCADE | ||||
|         * `cascade_many` - CASCADE_MANY | ||||
|         * `set_null` - SET_NULL | ||||
|         * `set_default` - SET_DEFAULT | ||||
|     User: | ||||
|       type: object | ||||
|       description: User Serializer | ||||
|  | ||||
| @ -183,7 +183,12 @@ class TestProviderLDAP(SeleniumTestCase): | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.LOGIN_FAILED, | ||||
|                 user={"pk": anon.pk, "email": anon.email, "username": anon.username}, | ||||
|                 user={ | ||||
|                     "pk": anon.pk, | ||||
|                     "email": anon.email, | ||||
|                     "username": anon.username, | ||||
|                     "is_anonymous": True, | ||||
|                 }, | ||||
|             ).exists(), | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| import { EventGeo } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| import { truncate } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/components/ak-event-info"; | ||||
| import "@goauthentik/elements/Tabs"; | ||||
| import "@goauthentik/elements/buttons/Dropdown"; | ||||
| @ -11,7 +10,7 @@ import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| @ -71,20 +70,7 @@ export class RecentEventsCard extends Table<Event> { | ||||
|         return [ | ||||
|             html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div> | ||||
|                 <small>${item.app}</small>`, | ||||
|             item.user?.username | ||||
|                 ? html`<div> | ||||
|                           <a href="#/identity/users/${item.user.pk}" | ||||
|                               >${truncate(item.user?.username, 15)}</a | ||||
|                           > | ||||
|                       </div> | ||||
|                       ${item.user.on_behalf_of | ||||
|                           ? html`<small> | ||||
|                                 <a href="#/identity/users/${item.user.on_behalf_of.pk}" | ||||
|                                     >${msg(str`On behalf of ${item.user.on_behalf_of.username}`)}</a | ||||
|                                 > | ||||
|                             </small>` | ||||
|                           : html``}` | ||||
|                 : html`-`, | ||||
|             EventUser(item), | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html` <div>${item.clientIp || msg("-")}</div> | ||||
|                 <small>${EventGeo(item)}</small>`, | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { ChartData, ChartOptions } from "chart.js"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { ProvidersApi, SourcesApi, TaskStatusEnum } from "@goauthentik/api"; | ||||
| import { ProvidersApi, SourcesApi, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export interface SyncStatus { | ||||
|     healthy: number; | ||||
| @ -49,12 +49,12 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | ||||
|                     }); | ||||
|  | ||||
|                     health.tasks.forEach((task) => { | ||||
|                         if (task.status !== TaskStatusEnum.Successful) { | ||||
|                         if (task.status !== SystemTaskStatusEnum.Successful) { | ||||
|                             metrics.failed += 1; | ||||
|                         } | ||||
|                         const now = new Date().getTime(); | ||||
|                         const maxDelta = 3600000; // 1 hour | ||||
|                         if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) { | ||||
|                         if (!health || now - task.finishTimestamp.getTime() > maxDelta) { | ||||
|                             metrics.unsynced += 1; | ||||
|                         } else { | ||||
|                             metrics.healthy += 1; | ||||
| @ -94,12 +94,12 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | ||||
|                         id: element.pk, | ||||
|                     }); | ||||
|                     health.tasks.forEach((task) => { | ||||
|                         if (task.status !== TaskStatusEnum.Successful) { | ||||
|                         if (task.status !== SystemTaskStatusEnum.Successful) { | ||||
|                             sourceKey = "failed"; | ||||
|                         } | ||||
|                         const now = new Date().getTime(); | ||||
|                         const maxDelta = 3600000; // 1 hour | ||||
|                         if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) { | ||||
|                         if (!health || now - task.finishTimestamp.getTime() > maxDelta) { | ||||
|                             sourceKey = "unsynced"; | ||||
|                         } | ||||
|                     }); | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import "@goauthentik/admin/events/EventVolumeChart"; | ||||
| import { EventGeo } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -10,7 +10,7 @@ import { TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import { TablePage } from "@goauthentik/elements/table/TablePage"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| @ -81,18 +81,7 @@ export class EventListPage extends TablePage<Event> { | ||||
|         return [ | ||||
|             html`<div>${actionToLabel(item.action)}</div> | ||||
|                 <small>${item.app}</small>`, | ||||
|             item.user?.username | ||||
|                 ? html`<div> | ||||
|                           <a href="#/identity/users/${item.user.pk}">${item.user?.username}</a> | ||||
|                       </div> | ||||
|                       ${item.user.on_behalf_of | ||||
|                           ? html`<small> | ||||
|                                 <a href="#/identity/users/${item.user.on_behalf_of.pk}" | ||||
|                                     >${msg(str`On behalf of ${item.user.on_behalf_of.username}`)}</a | ||||
|                                 > | ||||
|                             </small>` | ||||
|                           : html``}` | ||||
|                 : html`-`, | ||||
|             EventUser(item), | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<div>${item.clientIp || msg("-")}</div> | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventGeo } from "@goauthentik/admin/events/utils"; | ||||
| import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -87,27 +87,7 @@ export class EventViewPage extends AKElement { | ||||
|                                     </dt> | ||||
|                                     <dd class="pf-c-description-list__description"> | ||||
|                                         <div class="pf-c-description-list__text"> | ||||
|                                             ${this.event.user?.username | ||||
|                                                 ? html`<div> | ||||
|                                                           <a | ||||
|                                                               href="#/identity/users/${this.event | ||||
|                                                                   .user.pk}" | ||||
|                                                               >${this.event.user?.username}</a | ||||
|                                                           > | ||||
|                                                       </div> | ||||
|                                                       ${this.event.user.on_behalf_of | ||||
|                                                           ? html`<small> | ||||
|                                                                 <a | ||||
|                                                                     href="#/identity/users/${this | ||||
|                                                                         .event.user.on_behalf_of | ||||
|                                                                         .pk}" | ||||
|                                                                     >${msg( | ||||
|                                                                         str`On behalf of ${this.event.user.on_behalf_of.username}`, | ||||
|                                                                     )}</a | ||||
|                                                                 > | ||||
|                                                             </small>` | ||||
|                                                           : html``}` | ||||
|                                                 : html`-`} | ||||
|                                             ${EventUser(this.event)} | ||||
|                                         </div> | ||||
|                                     </dd> | ||||
|                                 </div> | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| import { truncate } from "@goauthentik/app/common/utils"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { KeyUnknown } from "@goauthentik/elements/forms/Form"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| export function EventGeo(event: EventWithContext): TemplateResult { | ||||
| @ -14,3 +16,29 @@ export function EventGeo(event: EventWithContext): TemplateResult { | ||||
|     } | ||||
|     return html``; | ||||
| } | ||||
|  | ||||
| export function EventUser(event: EventWithContext, truncateUsername?: number): TemplateResult { | ||||
|     if (!event.user.username) { | ||||
|         return html`-`; | ||||
|     } | ||||
|     let body = html``; | ||||
|     if (event.user.is_anonymous) { | ||||
|         body = html`<div>${msg("Anonymous user")}</div>`; | ||||
|     } else { | ||||
|         body = html`<div> | ||||
|             <a href="#/identity/users/${event.user.pk}" | ||||
|                 >${truncateUsername | ||||
|                     ? truncate(event.user?.username, truncateUsername) | ||||
|                     : event.user?.username}</a | ||||
|             > | ||||
|         </div>`; | ||||
|     } | ||||
|     if (event.user.on_behalf_of) { | ||||
|         body = html`${body}<small> | ||||
|                 <a href="#/identity/users/${event.user.on_behalf_of.pk}" | ||||
|                     >${msg(str`On behalf of ${event.user.on_behalf_of.username}`)}</a | ||||
|                 > | ||||
|             </small>`; | ||||
|     } | ||||
|     return body; | ||||
| } | ||||
|  | ||||
| @ -94,7 +94,7 @@ export class ReputationListPage extends TablePage<Reputation> { | ||||
|             html`${item.updated.toLocaleString()}`, | ||||
|             html` | ||||
|                 <ak-rbac-object-permission-modal | ||||
|                     model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputation} | ||||
|                     model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputationpolicy} | ||||
|                     objectPk=${item.pk || ""} | ||||
|                 > | ||||
|                 </ak-rbac-object-permission-modal> | ||||
|  | ||||
| @ -32,7 +32,7 @@ import { | ||||
|     RbacPermissionsAssignedByUsersListModelEnum, | ||||
|     SCIMProvider, | ||||
|     SCIMSyncStatus, | ||||
|     TaskStatusEnum, | ||||
|     SystemTaskStatusEnum, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-provider-scim-view") | ||||
| @ -143,15 +143,15 @@ export class SCIMProviderViewPage extends AKElement { | ||||
|             <ul class="pf-c-list"> | ||||
|                 ${this.syncState.tasks.map((task) => { | ||||
|                     let header = ""; | ||||
|                     if (task.status === TaskStatusEnum.Warning) { | ||||
|                     if (task.status === SystemTaskStatusEnum.Warning) { | ||||
|                         header = msg("Task finished with warnings"); | ||||
|                     } else if (task.status === TaskStatusEnum.Error) { | ||||
|                     } else if (task.status === SystemTaskStatusEnum.Error) { | ||||
|                         header = msg("Task finished with errors"); | ||||
|                     } else { | ||||
|                         header = msg(str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`); | ||||
|                         header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`); | ||||
|                     } | ||||
|                     return html`<li> | ||||
|                         <p>${task.taskName}</p> | ||||
|                         <p>${task.name}</p> | ||||
|                         <ul class="pf-c-list"> | ||||
|                             <li>${header}</li> | ||||
|                             ${task.messages.map((m) => { | ||||
|  | ||||
| @ -29,7 +29,7 @@ import { | ||||
|     LDAPSyncStatus, | ||||
|     RbacPermissionsAssignedByUsersListModelEnum, | ||||
|     SourcesApi, | ||||
|     TaskStatusEnum, | ||||
|     SystemTaskStatusEnum, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-source-ldap-view") | ||||
| @ -77,15 +77,15 @@ export class LDAPSourceViewPage extends AKElement { | ||||
|             <ul class="pf-c-list"> | ||||
|                 ${this.syncState.tasks.map((task) => { | ||||
|                     let header = ""; | ||||
|                     if (task.status === TaskStatusEnum.Warning) { | ||||
|                     if (task.status === SystemTaskStatusEnum.Warning) { | ||||
|                         header = msg("Task finished with warnings"); | ||||
|                     } else if (task.status === TaskStatusEnum.Error) { | ||||
|                     } else if (task.status === SystemTaskStatusEnum.Error) { | ||||
|                         header = msg("Task finished with errors"); | ||||
|                     } else { | ||||
|                         header = msg(str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`); | ||||
|                         header = msg(str`Last sync: ${task.finishTimestamp.toLocaleString()}`); | ||||
|                     } | ||||
|                     return html`<li> | ||||
|                         <p>${task.taskName}</p> | ||||
|                         <p>${task.name}</p> | ||||
|                         <ul class="pf-c-list"> | ||||
|                             <li>${header}</li> | ||||
|                             ${task.messages.map((m) => { | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { uiConfig } from "@goauthentik/app/common/ui/config"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EVENT_REFRESH } from "@goauthentik/common/constants"; | ||||
| import { PFColor } from "@goauthentik/elements/Label"; | ||||
| @ -14,13 +15,10 @@ import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; | ||||
|  | ||||
| import { AdminApi, Task, TaskStatusEnum } from "@goauthentik/api"; | ||||
| import { EventsApi, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-system-task-list") | ||||
| export class SystemTaskListPage extends TablePage<Task> { | ||||
|     searchEnabled(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| export class SystemTaskListPage extends TablePage<SystemTask> { | ||||
|     pageTitle(): string { | ||||
|         return msg("System Tasks"); | ||||
|     } | ||||
| @ -34,53 +32,45 @@ export class SystemTaskListPage extends TablePage<Task> { | ||||
|     expandable = true; | ||||
|  | ||||
|     @property() | ||||
|     order = "slug"; | ||||
|     order = "name"; | ||||
|  | ||||
|     static get styles(): CSSResult[] { | ||||
|         return super.styles.concat(PFDescriptionList); | ||||
|     } | ||||
|  | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Task>> { | ||||
|         return new AdminApi(DEFAULT_CONFIG).adminSystemTasksList().then((tasks) => { | ||||
|             return { | ||||
|                 pagination: { | ||||
|                     count: tasks.length, | ||||
|                     totalPages: 1, | ||||
|                     startIndex: 1, | ||||
|                     endIndex: tasks.length, | ||||
|                     current: page, | ||||
|                     next: 0, | ||||
|                     previous: 0, | ||||
|                 }, | ||||
|                 results: tasks, | ||||
|             }; | ||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<SystemTask>> { | ||||
|         return new EventsApi(DEFAULT_CONFIG).eventsSystemTasksList({ | ||||
|             ordering: this.order, | ||||
|             page: page, | ||||
|             pageSize: (await uiConfig()).pagination.perPage, | ||||
|             search: this.search || "", | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     columns(): TableColumn[] { | ||||
|         return [ | ||||
|             new TableColumn(msg("Identifier")), | ||||
|             new TableColumn(msg("Identifier"), "name"), | ||||
|             new TableColumn(msg("Description")), | ||||
|             new TableColumn(msg("Last run")), | ||||
|             new TableColumn(msg("Status")), | ||||
|             new TableColumn(msg("Status"), "status"), | ||||
|             new TableColumn(msg("Actions")), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     taskStatus(task: Task): TemplateResult { | ||||
|     taskStatus(task: SystemTask): TemplateResult { | ||||
|         switch (task.status) { | ||||
|             case TaskStatusEnum.Successful: | ||||
|             case SystemTaskStatusEnum.Successful: | ||||
|                 return html`<ak-label color=${PFColor.Green}>${msg("Successful")}</ak-label>`; | ||||
|             case TaskStatusEnum.Warning: | ||||
|             case SystemTaskStatusEnum.Warning: | ||||
|                 return html`<ak-label color=${PFColor.Orange}>${msg("Warning")}</ak-label>`; | ||||
|             case TaskStatusEnum.Error: | ||||
|             case SystemTaskStatusEnum.Error: | ||||
|                 return html`<ak-label color=${PFColor.Red}>${msg("Error")}</ak-label>`; | ||||
|             default: | ||||
|                 return html`<ak-label color=${PFColor.Grey}>${msg("Unknown")}</ak-label>`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     renderExpanded(item: Task): TemplateResult { | ||||
|     renderExpanded(item: SystemTask): TemplateResult { | ||||
|         return html` <td role="cell" colspan="3"> | ||||
|                 <div class="pf-c-table__expandable-row-content"> | ||||
|                     <dl class="pf-c-description-list pf-m-horizontal"> | ||||
| @ -90,7 +80,7 @@ export class SystemTaskListPage extends TablePage<Task> { | ||||
|                             </dt> | ||||
|                             <dd class="pf-c-description-list__description"> | ||||
|                                 <div class="pf-c-description-list__text"> | ||||
|                                     ${msg(str`${item.taskDuration.toFixed(2)} seconds`)} | ||||
|                                     ${msg(str`${item.duration.toFixed(2)} seconds`)} | ||||
|                                 </div> | ||||
|                             </dd> | ||||
|                         </div> | ||||
| @ -113,18 +103,18 @@ export class SystemTaskListPage extends TablePage<Task> { | ||||
|             <td></td>`; | ||||
|     } | ||||
|  | ||||
|     row(item: Task): TemplateResult[] { | ||||
|     row(item: SystemTask): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${item.taskName}`, | ||||
|             html`${item.taskDescription}`, | ||||
|             html`${item.taskFinishTimestamp.toLocaleString()}`, | ||||
|             html`${item.name}${item.uid ? `:${item.uid}` : ""}`, | ||||
|             html`${item.description}`, | ||||
|             html`${item.finishTimestamp.toLocaleString()}`, | ||||
|             this.taskStatus(item), | ||||
|             html`<ak-action-button | ||||
|                 class="pf-m-plain" | ||||
|                 .apiRequest=${() => { | ||||
|                     return new AdminApi(DEFAULT_CONFIG) | ||||
|                         .adminSystemTasksRetryCreate({ | ||||
|                             id: item.taskName, | ||||
|                     return new EventsApi(DEFAULT_CONFIG) | ||||
|                         .eventsSystemTasksRunCreate({ | ||||
|                             uuid: item.uuid, | ||||
|                         }) | ||||
|                         .then(() => { | ||||
|                             this.dispatchEvent( | ||||
|  | ||||
| @ -5,6 +5,7 @@ export interface EventUser { | ||||
|     email?: string; | ||||
|     username: string; | ||||
|     on_behalf_of?: EventUser; | ||||
|     is_anonymous?: boolean; | ||||
| } | ||||
|  | ||||
| export interface EventContext { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { EventGeo } from "@goauthentik/app/admin/events/utils"; | ||||
| import { EventGeo, EventUser } from "@goauthentik/app/admin/events/utils"; | ||||
| import { actionToLabel } from "@goauthentik/app/common/labels"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| @ -11,7 +11,7 @@ import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import { PaginatedResponse } from "@goauthentik/elements/table/Table"; | ||||
| import { Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| @ -76,12 +76,7 @@ export class ObjectChangelog extends Table<Event> { | ||||
|     row(item: EventWithContext): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${actionToLabel(item.action)}`, | ||||
|             html`<div>${item.user?.username}</div> | ||||
|                 ${item.user.on_behalf_of | ||||
|                     ? html`<small> | ||||
|                           ${msg(str`On behalf of ${item.user.on_behalf_of.username}`)} | ||||
|                       </small>` | ||||
|                     : html``}`, | ||||
|             EventUser(item), | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<div>${item.clientIp || msg("-")}</div> | ||||
|  | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { EventUser } from "@goauthentik/app/admin/events/utils"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { EventWithContext } from "@goauthentik/common/events"; | ||||
| import { actionToLabel } from "@goauthentik/common/labels"; | ||||
| @ -9,7 +10,7 @@ import "@goauthentik/elements/buttons/ModalButton"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
|  | ||||
| import { msg, str } from "@lit/localize"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| @ -46,12 +47,7 @@ export class UserEvents extends Table<Event> { | ||||
|     row(item: EventWithContext): TemplateResult[] { | ||||
|         return [ | ||||
|             html`${actionToLabel(item.action)}`, | ||||
|             html`<div>${item.user?.username}</div> | ||||
|                 ${item.user.on_behalf_of | ||||
|                     ? html`<small> | ||||
|                           ${msg(str`On behalf of ${item.user.on_behalf_of.username}`)} | ||||
|                       </small>` | ||||
|                     : html``}`, | ||||
|             EventUser(item), | ||||
|             html`<span>${item.created?.toLocaleString()}</span>`, | ||||
|             html`<span>${item.clientIp || msg("-")}</span>`, | ||||
|         ]; | ||||
|  | ||||
| @ -359,9 +359,9 @@ | ||||
|         <source>Recent events</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>Im Namen von | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -598,7 +598,7 @@ | ||||
|         <source>Duration</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2553,9 +2553,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>Aufgabe mit Fehlern beendet</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Letzte Synchronisierung: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6328,6 +6328,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -363,9 +363,9 @@ | ||||
|         <target>Recent events</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>On behalf of | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -613,9 +613,9 @@ | ||||
|         <target>Duration</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/>seconds</target> | ||||
|         <x id="0" equiv-text="${item.duration.toFixed(2)}"/>seconds</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2675,9 +2675,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>Task finished with errors</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Last sync: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6603,6 +6603,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -352,9 +352,9 @@ | ||||
|         <source>Recent events</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>En nombre de | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -590,7 +590,7 @@ | ||||
|         <source>Duration</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2513,9 +2513,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>La tarea ha finalizado con errores</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Última sincronización: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6244,6 +6244,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -451,9 +451,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>Au nom de | ||||
| <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
| <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -608,9 +608,9 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saa0e2675da69651b"> | ||||
|         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> | ||||
|         <target>L'URL "  | ||||
|         <x id="0" equiv-text="${this.url}"/>" n'a pas été trouvée.</target> | ||||
|         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> | ||||
|         <target>L'URL " | ||||
|         <x id="0" equiv-text="${this.url}"/>" n'a pas été trouvée.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s58cd9c2fe836d9c6"> | ||||
| @ -761,9 +761,9 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/>secondes</target> | ||||
|         <x id="0" equiv-text="${item.duration.toFixed(2)}"/>secondes</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -1052,8 +1052,8 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa8384c9c26731f83"> | ||||
|         <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source> | ||||
|         <target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir.</target> | ||||
|         <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source> | ||||
|         <target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s55787f4dfcdce52b"> | ||||
| @ -1625,7 +1625,7 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s33ed903c210a6209"> | ||||
|         <source>Token to authenticate with. Currently only bearer authentication is supported.</source> | ||||
|         <target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge.</target> | ||||
|         <target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sfc8bb104e2c05af8"> | ||||
| @ -1793,8 +1793,8 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa90b7809586c35ce"> | ||||
|         <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source> | ||||
|         <target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test".</target> | ||||
|         <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source> | ||||
|         <target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test".</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s0410779cb47de312"> | ||||
| @ -2887,7 +2887,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s33683c3b1dbaf264"> | ||||
|         <source>To use SSL instead, use 'ldaps://' and disable this option.</source> | ||||
|         <target>Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option.</target> | ||||
|         <target>Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s2221fef80f4753a2"> | ||||
| @ -2976,8 +2976,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s76768bebabb7d543"> | ||||
|         <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> | ||||
|         <target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'</target> | ||||
|         <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> | ||||
|         <target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s026555347e589f0e"> | ||||
| @ -3272,7 +3272,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s3198c384c2f68b08"> | ||||
|         <source>Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually.</source> | ||||
|         <target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement.</target> | ||||
|         <target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sb32e9c1faa0b8673"> | ||||
| @ -3336,9 +3336,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Dernière synchro : | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -3440,7 +3440,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s9f8aac89fe318acc"> | ||||
|         <source>Optionally set the 'FriendlyName' value of the Assertion attribute.</source> | ||||
|         <target>Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel)</target> | ||||
|         <target>Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s851c108679653d2a"> | ||||
| @ -3754,8 +3754,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s7b1fba26d245cb1c"> | ||||
|         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> | ||||
|         <target>En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5".</target> | ||||
|         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> | ||||
|         <target>En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5".</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s44536d20bb5c8257"> | ||||
| @ -3931,10 +3931,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa95a538bfbb86111"> | ||||
|         <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source> | ||||
|         <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source> | ||||
|         <target>Êtes-vous sûr de vouloir mettre à jour | ||||
|         <x id="0" equiv-text="${this.objectLabel}"/>"  | ||||
|         <x id="1" equiv-text="${this.obj?.name}"/>" ?</target> | ||||
|         <x id="0" equiv-text="${this.objectLabel}"/>" | ||||
|         <x id="1" equiv-text="${this.obj?.name}"/>" ?</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc92d7cfb6ee1fec6"> | ||||
| @ -5015,8 +5015,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sdf1d8edef27236f0"> | ||||
|         <source>A "roaming" authenticator, like a YubiKey</source> | ||||
|         <target>Un authentificateur "itinérant", comme une YubiKey</target> | ||||
|         <source>A "roaming" authenticator, like a YubiKey</source> | ||||
|         <target>Un authentificateur "itinérant", comme une YubiKey</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sfffba7b23d8fb40c"> | ||||
| @ -5341,7 +5341,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s5170f9ef331949c0"> | ||||
|         <source>Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.</source> | ||||
|         <target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data".</target> | ||||
|         <target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data".</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s36cb242ac90353bc"> | ||||
| @ -5350,10 +5350,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s2d5f69929bb7221d"> | ||||
|         <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source> | ||||
|         <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${prompt.name}"/>("  | ||||
|         <x id="1" equiv-text="${prompt.fieldKey}"/>", de type  | ||||
|         <x id="0" equiv-text="${prompt.name}"/>(" | ||||
|         <x id="1" equiv-text="${prompt.fieldKey}"/>", de type | ||||
|         <x id="2" equiv-text="${prompt.type}"/>)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
| @ -5402,8 +5402,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s1608b2f94fa0dbd4"> | ||||
|         <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source> | ||||
|         <target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target> | ||||
|         <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source> | ||||
|         <target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s542a71bb8f41e057"> | ||||
| @ -6187,7 +6187,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa7fcf026bd25f231"> | ||||
|         <source>Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.</source> | ||||
|         <target>Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant.</target> | ||||
|         <target>Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant.</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf1d289e3137c2ea"> | ||||
| @ -7494,7 +7494,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
| </trans-unit> | ||||
| <trans-unit id="sff0ac1ace2d90709"> | ||||
|   <source>Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).</source> | ||||
|   <target>Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target> | ||||
|   <target>Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb58b8a60cad8762"> | ||||
|   <source>Default relay state</source> | ||||
| @ -7904,7 +7904,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
|   <target>Utilisateur créé et ajouté au groupe <x id="0" equiv-text="${this.group.name}"/> avec succès</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s824e0943a7104668"> | ||||
|   <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source> | ||||
|   <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source> | ||||
|   <target>Cet utilisateur sera ajouté au groupe &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s62e7f6ed7d9cb3ca"> | ||||
| @ -8328,6 +8328,24 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
|   <target>Réglages système</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -450,8 +450,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <target><x id="0" equiv-text="${item.user.on_behalf_of.username}"/> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target><x id="0" equiv-text="${event.user.on_behalf_of.username}"/> | ||||
| 을 대신하여</target> | ||||
|  | ||||
|       </trans-unit> | ||||
| @ -758,9 +758,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> 초</target> | ||||
|         <x id="0" equiv-text="${item.duration.toFixed(2)}"/> 초</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -3327,8 +3327,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <target><x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/>에 마지막으로 동기화 됨</target> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target><x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/>에 마지막으로 동기화 됨</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -8202,6 +8202,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -447,8 +447,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <target>Namens <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>Namens <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -747,8 +747,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <target><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconden</target> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconden</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -3315,8 +3315,8 @@ slaagt niet wanneer een of beide geselecteerde opties gelijk zijn aan of boven d | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Laatste synchronisatie: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Laatste synchronisatie: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -8042,6 +8042,24 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -361,9 +361,9 @@ | ||||
|         <source>Recent events</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>W imieniu | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -600,7 +600,7 @@ | ||||
|         <source>Duration</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2597,9 +2597,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>Zadanie zakończone z błędami</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Ostatnia synchronizacja: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6451,6 +6451,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -448,8 +448,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|   <target>Ōń ƀēĥàĺƒ ōƒ <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|   <target>Ōń ƀēĥàĺƒ ōƒ <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -748,8 +748,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|   <target><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> śēćōńďś</target> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|   <target><x id="0" equiv-text="${item.duration.toFixed(2)}"/> śēćōńďś</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -3315,8 +3315,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|   <target>Ĺàśţ śŷńć: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|   <target>Ĺàśţ śŷńć: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -8177,4 +8177,22 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
| </body></file></xliff> | ||||
|  | ||||
| @ -352,9 +352,9 @@ | ||||
|         <source>Recent events</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/>adına</target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/>adına</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -590,7 +590,7 @@ | ||||
|         <source>Duration</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2512,9 +2512,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>Görev hatalarla tamamlandı</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>Son senkronizasyon: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6237,6 +6237,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -294,7 +294,7 @@ | ||||
|   <source>Recent events</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sc35581d9c1cd67ff"> | ||||
|   <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|   <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
| </trans-unit> | ||||
| <trans-unit id="saf63a04c86018698"> | ||||
|   <source>-</source> | ||||
| @ -498,7 +498,7 @@ | ||||
|   <source>Duration</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="se7e1ababbc4868b8"> | ||||
|   <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|   <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s14bf17e2a1a2c381"> | ||||
|   <source>Restart task</source> | ||||
| @ -2029,7 +2029,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|   <source>Task finished with errors</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sbedb77365a066648"> | ||||
|   <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|   <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s2f0f6691de0b0388"> | ||||
|   <source>Warning: Provider is not assigned to an application as backchannel provider.</source> | ||||
| @ -5146,6 +5146,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
| </body> | ||||
| </file> | ||||
| </xliff> | ||||
|  | ||||
| @ -451,9 +451,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>代表 | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -608,9 +608,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saa0e2675da69651b"> | ||||
|         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> | ||||
|         <target>未找到 URL "  | ||||
|         <x id="0" equiv-text="${this.url}"/>"。</target> | ||||
|         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> | ||||
|         <target>未找到 URL " | ||||
|         <x id="0" equiv-text="${this.url}"/>"。</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s58cd9c2fe836d9c6"> | ||||
| @ -761,9 +761,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/>秒</target> | ||||
|         <x id="0" equiv-text="${item.duration.toFixed(2)}"/>秒</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -1052,8 +1052,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa8384c9c26731f83"> | ||||
|         <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source> | ||||
|         <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target> | ||||
|         <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source> | ||||
|         <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s55787f4dfcdce52b"> | ||||
| @ -1794,8 +1794,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa90b7809586c35ce"> | ||||
|         <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source> | ||||
|         <target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target> | ||||
|         <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source> | ||||
|         <target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s0410779cb47de312"> | ||||
| @ -2978,8 +2978,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s76768bebabb7d543"> | ||||
|         <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> | ||||
|         <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target> | ||||
|         <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> | ||||
|         <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s026555347e589f0e"> | ||||
| @ -3338,9 +3338,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>上次同步: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -3756,8 +3756,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s7b1fba26d245cb1c"> | ||||
|         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> | ||||
|         <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target> | ||||
|         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> | ||||
|         <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s44536d20bb5c8257"> | ||||
| @ -3933,10 +3933,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sa95a538bfbb86111"> | ||||
|         <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source> | ||||
|         <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source> | ||||
|         <target>您确定要更新 | ||||
|         <x id="0" equiv-text="${this.objectLabel}"/>"  | ||||
|         <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target> | ||||
|         <x id="0" equiv-text="${this.objectLabel}"/>" | ||||
|         <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc92d7cfb6ee1fec6"> | ||||
| @ -5017,7 +5017,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sdf1d8edef27236f0"> | ||||
|         <source>A "roaming" authenticator, like a YubiKey</source> | ||||
|         <source>A "roaming" authenticator, like a YubiKey</source> | ||||
|         <target>像 YubiKey 这样的“漫游”身份验证器</target> | ||||
|  | ||||
|       </trans-unit> | ||||
| @ -5352,10 +5352,10 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s2d5f69929bb7221d"> | ||||
|         <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source> | ||||
|         <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${prompt.name}"/>("  | ||||
|         <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为  | ||||
|         <x id="0" equiv-text="${prompt.name}"/>(" | ||||
|         <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为 | ||||
|         <x id="2" equiv-text="${prompt.type}"/>)</target> | ||||
|  | ||||
|       </trans-unit> | ||||
| @ -5404,7 +5404,7 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="s1608b2f94fa0dbd4"> | ||||
|         <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source> | ||||
|         <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source> | ||||
|         <target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target> | ||||
|  | ||||
|       </trans-unit> | ||||
| @ -7906,7 +7906,7 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
|   <target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s824e0943a7104668"> | ||||
|   <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source> | ||||
|   <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source> | ||||
|   <target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s62e7f6ed7d9cb3ca"> | ||||
| @ -8330,6 +8330,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
|   <target>系统设置</target> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -359,9 +359,9 @@ | ||||
|         <source>Recent events</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>代表 | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
|         <source>-</source> | ||||
| @ -597,7 +597,7 @@ | ||||
|         <source>Duration</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
|         <source>Authentication</source> | ||||
| @ -2534,9 +2534,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|         <target>任务已完成,但出现错误</target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>上次同步: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|         <source>OAuth Source <x id="0" equiv-text="${this.source.name}"/></source> | ||||
| @ -6285,6 +6285,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -451,9 +451,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>代表 | ||||
|         <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -761,9 +761,9 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target> | ||||
|         <x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/>秒</target> | ||||
|         <x id="0" equiv-text="${item.duration.toFixed(2)}"/>秒</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -3338,9 +3338,9 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>上次同步: | ||||
|         <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
|  | ||||
| @ -447,8 +447,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc35581d9c1cd67ff"> | ||||
|         <source>On behalf of <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></source> | ||||
|         <target>代表 <x id="0" equiv-text="${item.user.on_behalf_of.username}"/></target> | ||||
|         <source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source> | ||||
|         <target>代表 <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="saf63a04c86018698"> | ||||
| @ -749,8 +749,8 @@ | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="se7e1ababbc4868b8"> | ||||
|         <source><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> seconds</source> | ||||
|         <target><x id="0" equiv-text="${item.taskDuration.toFixed(2)}"/> 秒</target> | ||||
|         <source><x id="0" equiv-text="${item.duration.toFixed(2)}"/> seconds</source> | ||||
|         <target><x id="0" equiv-text="${item.duration.toFixed(2)}"/> 秒</target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sc25edca57df81461"> | ||||
| @ -3315,8 +3315,8 @@ doesn't pass when either or both of the selected options are equal or above the | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sbedb77365a066648"> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>上次同步: <x id="0" equiv-text="${task.taskFinishTimestamp.toLocaleString()}"/></target> | ||||
|         <source>Last sync: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></source> | ||||
|         <target>上次同步: <x id="0" equiv-text="${task.finishTimestamp.toLocaleString()}"/></target> | ||||
|  | ||||
|       </trans-unit> | ||||
|       <trans-unit id="sf3fec8353106ac2f"> | ||||
| @ -8161,6 +8161,24 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="see1eb81c1f734079"> | ||||
|   <source>System settings</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s47fb5504f693775b"> | ||||
|   <source>Changes made:</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s506b6a19d12f414c"> | ||||
|   <source>Key</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4fc9dc73245eab09"> | ||||
|   <source>Previous value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="scb3b0671d7b7f640"> | ||||
|   <source>New value</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s4a642406b0745917"> | ||||
|   <source>Raw event info</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="sa65e7bc7ddd3484d"> | ||||
|   <source>Anonymous user</source> | ||||
| </trans-unit> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L