providers/scim: use lock for sync (#7948)
* providers/scim: use lock for sync 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:
		| @ -2,6 +2,7 @@ | |||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
|  | from rest_framework.fields import BooleanField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
| @ -9,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from authentik.admin.api.tasks import TaskSerializer | from authentik.admin.api.tasks import TaskSerializer | ||||||
| from authentik.core.api.providers import ProviderSerializer | from authentik.core.api.providers import ProviderSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | 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.monitored_tasks import TaskInfo | ||||||
| from authentik.providers.scim.models import SCIMProvider | from authentik.providers.scim.models import SCIMProvider | ||||||
|  |  | ||||||
| @ -37,6 +39,13 @@ class SCIMProviderSerializer(ProviderSerializer): | |||||||
|         extra_kwargs = {} |         extra_kwargs = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SCIMSyncStatusSerializer(PassiveSerializer): | ||||||
|  |     """SCIM Provider sync status""" | ||||||
|  |  | ||||||
|  |     is_running = BooleanField(read_only=True) | ||||||
|  |     tasks = TaskSerializer(many=True, read_only=True) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMProviderViewSet(UsedByMixin, ModelViewSet): | class SCIMProviderViewSet(UsedByMixin, ModelViewSet): | ||||||
|     """SCIMProvider Viewset""" |     """SCIMProvider Viewset""" | ||||||
|  |  | ||||||
| @ -48,15 +57,18 @@ class SCIMProviderViewSet(UsedByMixin, ModelViewSet): | |||||||
|  |  | ||||||
|     @extend_schema( |     @extend_schema( | ||||||
|         responses={ |         responses={ | ||||||
|             200: TaskSerializer(), |             200: SCIMSyncStatusSerializer(), | ||||||
|             404: OpenApiResponse(description="Task not found"), |             404: OpenApiResponse(description="Task not found"), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     @action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[]) |     @action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[]) | ||||||
|     def sync_status(self, request: Request, pk: int) -> Response: |     def sync_status(self, request: Request, pk: int) -> Response: | ||||||
|         """Get provider's sync status""" |         """Get provider's sync status""" | ||||||
|         provider = self.get_object() |         provider: SCIMProvider = self.get_object() | ||||||
|         task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}") |         task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}") | ||||||
|         if not task: |         tasks = [task] if task else [] | ||||||
|             return Response(status=404) |         status = { | ||||||
|         return Response(TaskSerializer(task).data) |             "tasks": tasks, | ||||||
|  |             "is_running": provider.sync_lock.locked(), | ||||||
|  |         } | ||||||
|  |         return Response(SCIMSyncStatusSerializer(status).data) | ||||||
|  | |||||||
| @ -1,11 +1,14 @@ | |||||||
| """SCIM Provider models""" | """SCIM Provider models""" | ||||||
|  | from django.core.cache import cache | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.db.models import QuerySet | from django.db.models import QuerySet | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import get_anonymous_user | ||||||
|  | from redis.lock import Lock | ||||||
| from rest_framework.serializers import Serializer | from rest_framework.serializers import Serializer | ||||||
|  |  | ||||||
| from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes | from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes | ||||||
|  | from authentik.providers.scim.clients import PAGE_TIMEOUT | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMProvider(BackchannelProvider): | class SCIMProvider(BackchannelProvider): | ||||||
| @ -27,6 +30,15 @@ class SCIMProvider(BackchannelProvider): | |||||||
|         help_text=_("Property mappings used for group creation/updating."), |         help_text=_("Property mappings used for group creation/updating."), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def sync_lock(self) -> Lock: | ||||||
|  |         """Redis lock for syncing SCIM to prevent multiple parallel syncs happening""" | ||||||
|  |         return Lock( | ||||||
|  |             cache.client.get_client(), | ||||||
|  |             name=f"goauthentik.io/providers/scim/sync-{str(self.pk)}", | ||||||
|  |             timeout=(60 * 60 * PAGE_TIMEOUT) * 3, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def get_user_qs(self) -> QuerySet[User]: |     def get_user_qs(self) -> QuerySet[User]: | ||||||
|         """Get queryset of all users with consistent ordering |         """Get queryset of all users with consistent ordering | ||||||
|         according to the provider's settings""" |         according to the provider's settings""" | ||||||
|  | |||||||
| @ -47,6 +47,10 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None: | |||||||
|     ).first() |     ).first() | ||||||
|     if not provider: |     if not provider: | ||||||
|         return |         return | ||||||
|  |     lock = provider.sync_lock | ||||||
|  |     if lock.locked(): | ||||||
|  |         LOGGER.debug("SCIM sync locked, skipping task", source=provider.name) | ||||||
|  |         return | ||||||
|     self.set_uid(slugify(provider.name)) |     self.set_uid(slugify(provider.name)) | ||||||
|     result = TaskResult(TaskResultStatus.SUCCESSFUL, []) |     result = TaskResult(TaskResultStatus.SUCCESSFUL, []) | ||||||
|     result.messages.append(_("Starting full SCIM sync")) |     result.messages.append(_("Starting full SCIM sync")) | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								schema.yml
									
									
									
									
									
								
							| @ -17079,7 +17079,7 @@ paths: | |||||||
|           content: |           content: | ||||||
|             application/json: |             application/json: | ||||||
|               schema: |               schema: | ||||||
|                 $ref: '#/components/schemas/Task' |                 $ref: '#/components/schemas/SCIMSyncStatus' | ||||||
|           description: '' |           description: '' | ||||||
|         '404': |         '404': | ||||||
|           description: Task not found |           description: Task not found | ||||||
| @ -40645,6 +40645,21 @@ components: | |||||||
|       - name |       - name | ||||||
|       - token |       - token | ||||||
|       - url |       - url | ||||||
|  |     SCIMSyncStatus: | ||||||
|  |       type: object | ||||||
|  |       description: SCIM Provider sync status | ||||||
|  |       properties: | ||||||
|  |         is_running: | ||||||
|  |           type: boolean | ||||||
|  |           readOnly: true | ||||||
|  |         tasks: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             $ref: '#/components/schemas/Task' | ||||||
|  |           readOnly: true | ||||||
|  |       required: | ||||||
|  |       - is_running | ||||||
|  |       - tasks | ||||||
|     SMSDevice: |     SMSDevice: | ||||||
|       type: object |       type: object | ||||||
|       description: Serializer for sms authenticator devices |       description: Serializer for sms authenticator devices | ||||||
|  | |||||||
| @ -93,15 +93,16 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | |||||||
|                     const health = await api.providersScimSyncStatusRetrieve({ |                     const health = await api.providersScimSyncStatusRetrieve({ | ||||||
|                         id: element.pk, |                         id: element.pk, | ||||||
|                     }); |                     }); | ||||||
|  |                     health.tasks.forEach((task) => { | ||||||
|                     if (health.status !== TaskStatusEnum.Successful) { |                         if (task.status !== TaskStatusEnum.Successful) { | ||||||
|                         sourceKey = "failed"; |                             sourceKey = "failed"; | ||||||
|                     } |                         } | ||||||
|                     const now = new Date().getTime(); |                         const now = new Date().getTime(); | ||||||
|                     const maxDelta = 3600000; // 1 hour |                         const maxDelta = 3600000; // 1 hour | ||||||
|                     if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) { |                         if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) { | ||||||
|                         sourceKey = "unsynced"; |                             sourceKey = "unsynced"; | ||||||
|                     } |                         } | ||||||
|  |                     }); | ||||||
|                 } catch { |                 } catch { | ||||||
|                     sourceKey = "unsynced"; |                     sourceKey = "unsynced"; | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ import "@goauthentik/elements/Tabs"; | |||||||
| import "@goauthentik/elements/buttons/ActionButton"; | import "@goauthentik/elements/buttons/ActionButton"; | ||||||
| import "@goauthentik/elements/buttons/ModalButton"; | import "@goauthentik/elements/buttons/ModalButton"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg, str } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, html } from "lit"; | import { CSSResult, TemplateResult, html } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators.js"; | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| @ -31,7 +31,8 @@ import { | |||||||
|     ProvidersApi, |     ProvidersApi, | ||||||
|     RbacPermissionsAssignedByUsersListModelEnum, |     RbacPermissionsAssignedByUsersListModelEnum, | ||||||
|     SCIMProvider, |     SCIMProvider, | ||||||
|     Task, |     SCIMSyncStatus, | ||||||
|  |     TaskStatusEnum, | ||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-provider-scim-view") | @customElement("ak-provider-scim-view") | ||||||
| @ -54,7 +55,7 @@ export class SCIMProviderViewPage extends AKElement { | |||||||
|     provider?: SCIMProvider; |     provider?: SCIMProvider; | ||||||
|  |  | ||||||
|     @state() |     @state() | ||||||
|     syncState?: Task; |     syncState?: SCIMSyncStatus; | ||||||
|  |  | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
| @ -128,6 +129,41 @@ export class SCIMProviderViewPage extends AKElement { | |||||||
|         </ak-tabs>`; |         </ak-tabs>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderSyncStatus(): TemplateResult { | ||||||
|  |         if (!this.syncState) { | ||||||
|  |             return html`${msg("No sync status.")}`; | ||||||
|  |         } | ||||||
|  |         if (this.syncState.isRunning) { | ||||||
|  |             return html`${msg("Sync currently running.")}`; | ||||||
|  |         } | ||||||
|  |         if (this.syncState.tasks.length < 1) { | ||||||
|  |             return html`${msg("Not synced yet.")}`; | ||||||
|  |         } | ||||||
|  |         return html` | ||||||
|  |             <ul class="pf-c-list"> | ||||||
|  |                 ${this.syncState.tasks.map((task) => { | ||||||
|  |                     let header = ""; | ||||||
|  |                     if (task.status === TaskStatusEnum.Warning) { | ||||||
|  |                         header = msg("Task finished with warnings"); | ||||||
|  |                     } else if (task.status === TaskStatusEnum.Error) { | ||||||
|  |                         header = msg("Task finished with errors"); | ||||||
|  |                     } else { | ||||||
|  |                         header = msg(str`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`); | ||||||
|  |                     } | ||||||
|  |                     return html`<li> | ||||||
|  |                         <p>${task.taskName}</p> | ||||||
|  |                         <ul class="pf-c-list"> | ||||||
|  |                             <li>${header}</li> | ||||||
|  |                             ${task.messages.map((m) => { | ||||||
|  |                                 return html`<li>${m}</li>`; | ||||||
|  |                             })} | ||||||
|  |                         </ul> | ||||||
|  |                     </li> `; | ||||||
|  |                 })} | ||||||
|  |             </ul> | ||||||
|  |         `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderTabOverview(): TemplateResult { |     renderTabOverview(): TemplateResult { | ||||||
|         if (!this.provider) { |         if (!this.provider) { | ||||||
|             return html``; |             return html``; | ||||||
| @ -186,16 +222,7 @@ export class SCIMProviderViewPage extends AKElement { | |||||||
|                         <div class="pf-c-card__title"> |                         <div class="pf-c-card__title"> | ||||||
|                             <p>${msg("Sync status")}</p> |                             <p>${msg("Sync status")}</p> | ||||||
|                         </div> |                         </div> | ||||||
|                         <div class="pf-c-card__body"> |                         <div class="pf-c-card__body">${this.renderSyncStatus()}</div> | ||||||
|                             ${this.syncState |  | ||||||
|                                 ? html` <ul class="pf-c-list"> |  | ||||||
|                                       ${this.syncState.messages.map((m) => { |  | ||||||
|                                           return html`<li>${m}</li>`; |  | ||||||
|                                       })} |  | ||||||
|                                   </ul>` |  | ||||||
|                                 : html` ${msg("Sync not run yet.")} `} |  | ||||||
|                         </div> |  | ||||||
|  |  | ||||||
|                         <div class="pf-c-card__footer"> |                         <div class="pf-c-card__footer"> | ||||||
|                             <ak-action-button |                             <ak-action-button | ||||||
|                                 class="pf-m-secondary" |                                 class="pf-m-secondary" | ||||||
|  | |||||||
| @ -1688,9 +1688,6 @@ | |||||||
|       <trans-unit id="sc6c575c5ff64cdb1"> |       <trans-unit id="sc6c575c5ff64cdb1"> | ||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>Synchronisation erneut ausführen</target> |         <target>Synchronisation erneut ausführen</target> | ||||||
|  | |||||||
| @ -1779,10 +1779,6 @@ | |||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|         <target>Update SCIM Provider</target> |         <target>Update SCIM Provider</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|         <target>Sync not run yet.</target> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>Run sync again</target> |         <target>Run sync again</target> | ||||||
|  | |||||||
| @ -1660,9 +1660,6 @@ | |||||||
|       <trans-unit id="sc6c575c5ff64cdb1"> |       <trans-unit id="sc6c575c5ff64cdb1"> | ||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>Vuelve a ejecutar la sincronización</target> |         <target>Vuelve a ejecutar la sincronización</target> | ||||||
|  | |||||||
| @ -2216,11 +2216,6 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target> | |||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|         <target>Mettre à jour le fournisseur SCIM</target> |         <target>Mettre à jour le fournisseur SCIM</target> | ||||||
|          |          | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|         <target>La synchronisation n'a pas encore été lancée.</target> |  | ||||||
|          |  | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|  | |||||||
| @ -1714,9 +1714,6 @@ | |||||||
|       <trans-unit id="sc6c575c5ff64cdb1"> |       <trans-unit id="sc6c575c5ff64cdb1"> | ||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>Uruchom ponownie synchronizację</target> |         <target>Uruchom ponownie synchronizację</target> | ||||||
|  | |||||||
| @ -2196,11 +2196,6 @@ | |||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|   <target>Ũƥďàţē ŚĆĨM Ƥŕōvĩďēŕ</target> |   <target>Ũƥďàţē ŚĆĨM Ƥŕōvĩďēŕ</target> | ||||||
|          |          | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|   <target>Śŷńć ńōţ ŕũń ŷēţ.</target> |  | ||||||
|          |  | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|  | |||||||
| @ -1659,9 +1659,6 @@ | |||||||
|       <trans-unit id="sc6c575c5ff64cdb1"> |       <trans-unit id="sc6c575c5ff64cdb1"> | ||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>Eşzamanlamayı tekrar çalıştır</target> |         <target>Eşzamanlamayı tekrar çalıştır</target> | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| <?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> | <?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> | ||||||
|   <file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext"> |   <file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext"> | ||||||
|     <body> |     <body> | ||||||
|       <trans-unit id="s4caed5b7a7e5d89b"> |       <trans-unit id="s4caed5b7a7e5d89b"> | ||||||
| @ -613,9 +613,9 @@ | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="saa0e2675da69651b"> |       <trans-unit id="saa0e2675da69651b"> | ||||||
|         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> |         <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source> | ||||||
|         <target>未找到 URL "  |         <target>未找到 URL "  | ||||||
|         <x id="0" equiv-text="${this.url}"/>"。</target> |         <x id="0" equiv-text="${this.url}"/>"。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s58cd9c2fe836d9c6"> |       <trans-unit id="s58cd9c2fe836d9c6"> | ||||||
| @ -1057,8 +1057,8 @@ | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sa8384c9c26731f83"> |       <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> |         <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source> | ||||||
|         <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target> |         <target>要允许任何重定向 URI,请将此值设置为 ".*"。请注意这可能带来的安全影响。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s55787f4dfcdce52b"> |       <trans-unit id="s55787f4dfcdce52b"> | ||||||
| @ -1799,8 +1799,8 @@ | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sa90b7809586c35ce"> |       <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> |         <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> |         <target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s0410779cb47de312"> |       <trans-unit id="s0410779cb47de312"> | ||||||
| @ -2217,11 +2217,6 @@ | |||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|         <target>更新 SCIM 提供程序</target> |         <target>更新 SCIM 提供程序</target> | ||||||
|          |          | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|         <target>尚未同步过。</target> |  | ||||||
|          |  | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
| @ -2988,8 +2983,8 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s76768bebabb7d543"> |       <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> |         <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> |         <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s026555347e589f0e"> |       <trans-unit id="s026555347e589f0e"> | ||||||
| @ -3781,8 +3776,8 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7b1fba26d245cb1c"> |       <trans-unit id="s7b1fba26d245cb1c"> | ||||||
|         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> |         <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source> | ||||||
|         <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target> |         <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s44536d20bb5c8257"> |       <trans-unit id="s44536d20bb5c8257"> | ||||||
| @ -3791,8 +3786,8 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s3bb51cabb02b997e"> |       <trans-unit id="s3bb51cabb02b997e"> | ||||||
|         <source>Format: "weeks=3;days=2;hours=3,seconds=2".</source> |         <source>Format: "weeks=3;days=2;hours=3,seconds=2".</source> | ||||||
|         <target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target> |         <target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s04bfd02201db5ab8"> |       <trans-unit id="s04bfd02201db5ab8"> | ||||||
| @ -3988,10 +3983,10 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sa95a538bfbb86111"> |       <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>您确定要更新  |         <target>您确定要更新  | ||||||
|         <x id="0" equiv-text="${this.objectLabel}"/>"  |         <x id="0" equiv-text="${this.objectLabel}"/>"  | ||||||
|         <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target> |         <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sc92d7cfb6ee1fec6"> |       <trans-unit id="sc92d7cfb6ee1fec6"> | ||||||
| @ -5077,7 +5072,7 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sdf1d8edef27236f0"> |       <trans-unit id="sdf1d8edef27236f0"> | ||||||
|         <source>A "roaming" authenticator, like a YubiKey</source> |         <source>A "roaming" authenticator, like a YubiKey</source> | ||||||
|         <target>像 YubiKey 这样的“漫游”身份验证器</target> |         <target>像 YubiKey 这样的“漫游”身份验证器</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @ -5412,10 +5407,10 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s2d5f69929bb7221d"> |       <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> |         <target> | ||||||
|         <x id="0" equiv-text="${prompt.name}"/>("  |         <x id="0" equiv-text="${prompt.name}"/>("  | ||||||
|         <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为  |         <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为  | ||||||
|         <x id="2" equiv-text="${prompt.type}"/>)</target> |         <x id="2" equiv-text="${prompt.type}"/>)</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @ -5464,7 +5459,7 @@ doesn't pass when either or both of the selected options are equal or above the | |||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s1608b2f94fa0dbd4"> |       <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> |         <target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target> | ||||||
|          |          | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @ -7970,7 +7965,7 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
|   <target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target> |   <target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s824e0943a7104668"> | <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> |   <target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s62e7f6ed7d9cb3ca"> | <trans-unit id="s62e7f6ed7d9cb3ca"> | ||||||
| @ -8051,4 +8046,4 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
| </xliff> | </xliff> | ||||||
|  | |||||||
| @ -1673,9 +1673,6 @@ | |||||||
|       <trans-unit id="sc6c575c5ff64cdb1"> |       <trans-unit id="sc6c575c5ff64cdb1"> | ||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|         <target>再次运行同步</target> |         <target>再次运行同步</target> | ||||||
|  | |||||||
| @ -2198,11 +2198,6 @@ | |||||||
|         <source>Update SCIM Provider</source> |         <source>Update SCIM Provider</source> | ||||||
|         <target>更新 SCIM 供應商</target> |         <target>更新 SCIM 供應商</target> | ||||||
|          |          | ||||||
|       </trans-unit> |  | ||||||
|       <trans-unit id="s7da38af36522ff6a"> |  | ||||||
|         <source>Sync not run yet.</source> |  | ||||||
|         <target>尚未執行同步。</target> |  | ||||||
|          |  | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="sbecf8dc03c978d15"> |       <trans-unit id="sbecf8dc03c978d15"> | ||||||
|         <source>Run sync again</source> |         <source>Run sync again</source> | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L