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:
Jens L 2023-12-21 14:43:40 +01:00 committed by GitHub
parent ec8f2d4bf9
commit 2521073dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 123 additions and 91 deletions

View File

@ -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)

View File

@ -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"""

View File

@ -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"))

View File

@ -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

View File

@ -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";
} }

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -613,9 +613,9 @@
</trans-unit> </trans-unit>
<trans-unit id="saa0e2675da69651b"> <trans-unit id="saa0e2675da69651b">
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source> <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL &quot; <target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>&quot;。</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 &quot;.*&quot;. 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请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</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 &quot;fa-test&quot;.</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 图标 &quot;fa-test&quot;。</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 &quot;memberUid&quot; 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>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'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 &quot;minutes=5&quot;.</source> <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</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: &quot;weeks=3;days=2;hours=3,seconds=2&quot;.</source> <source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
<target>格式:&quot;weeks=3;days=2;hours=3,seconds=2&quot;。</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}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</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}"/>&quot; <x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</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 &quot;roaming&quot; 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}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, 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}"/>&quot; <x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为 <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 &quot;stay signed in&quot;, 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 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source> <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target> <target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit> </trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca"> <trans-unit id="s62e7f6ed7d9cb3ca">

View File

@ -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>

View File

@ -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>