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:
parent
ec8f2d4bf9
commit
2521073dba
|
@ -2,6 +2,7 @@
|
|||
from django.utils.text import slugify
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
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
|
||||
|
@ -9,6 +10,7 @@ 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.providers.scim.models import SCIMProvider
|
||||
|
||||
|
@ -37,6 +39,13 @@ class SCIMProviderSerializer(ProviderSerializer):
|
|||
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):
|
||||
"""SCIMProvider Viewset"""
|
||||
|
||||
|
@ -48,15 +57,18 @@ class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
|
|||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: TaskSerializer(),
|
||||
200: SCIMSyncStatusSerializer(),
|
||||
404: OpenApiResponse(description="Task not found"),
|
||||
}
|
||||
)
|
||||
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
|
||||
def sync_status(self, request: Request, pk: int) -> Response:
|
||||
"""Get provider's sync status"""
|
||||
provider = self.get_object()
|
||||
provider: SCIMProvider = self.get_object()
|
||||
task = TaskInfo.by_name(f"scim_sync:{slugify(provider.name)}")
|
||||
if not task:
|
||||
return Response(status=404)
|
||||
return Response(TaskSerializer(task).data)
|
||||
tasks = [task] if task else []
|
||||
status = {
|
||||
"tasks": tasks,
|
||||
"is_running": provider.sync_lock.locked(),
|
||||
}
|
||||
return Response(SCIMSyncStatusSerializer(status).data)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
"""SCIM Provider models"""
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from redis.lock import Lock
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
|
||||
from authentik.providers.scim.clients import PAGE_TIMEOUT
|
||||
|
||||
|
||||
class SCIMProvider(BackchannelProvider):
|
||||
|
@ -27,6 +30,15 @@ class SCIMProvider(BackchannelProvider):
|
|||
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]:
|
||||
"""Get queryset of all users with consistent ordering
|
||||
according to the provider's settings"""
|
||||
|
|
|
@ -47,6 +47,10 @@ def scim_sync(self: MonitoredTask, provider_pk: int) -> None:
|
|||
).first()
|
||||
if not provider:
|
||||
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))
|
||||
result = TaskResult(TaskResultStatus.SUCCESSFUL, [])
|
||||
result.messages.append(_("Starting full SCIM sync"))
|
||||
|
|
17
schema.yml
17
schema.yml
|
@ -17079,7 +17079,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Task'
|
||||
$ref: '#/components/schemas/SCIMSyncStatus'
|
||||
description: ''
|
||||
'404':
|
||||
description: Task not found
|
||||
|
@ -40645,6 +40645,21 @@ components:
|
|||
- name
|
||||
- token
|
||||
- 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:
|
||||
type: object
|
||||
description: Serializer for sms authenticator devices
|
||||
|
|
|
@ -93,15 +93,16 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> {
|
|||
const health = await api.providersScimSyncStatusRetrieve({
|
||||
id: element.pk,
|
||||
});
|
||||
|
||||
if (health.status !== TaskStatusEnum.Successful) {
|
||||
sourceKey = "failed";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - health.taskFinishTimestamp.getTime() > maxDelta) {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
health.tasks.forEach((task) => {
|
||||
if (task.status !== TaskStatusEnum.Successful) {
|
||||
sourceKey = "failed";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const maxDelta = 3600000; // 1 hour
|
||||
if (!health || now - task.taskFinishTimestamp.getTime() > maxDelta) {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
sourceKey = "unsynced";
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import "@goauthentik/elements/Tabs";
|
|||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
|
@ -31,7 +31,8 @@ import {
|
|||
ProvidersApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
SCIMProvider,
|
||||
Task,
|
||||
SCIMSyncStatus,
|
||||
TaskStatusEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-scim-view")
|
||||
|
@ -54,7 +55,7 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
provider?: SCIMProvider;
|
||||
|
||||
@state()
|
||||
syncState?: Task;
|
||||
syncState?: SCIMSyncStatus;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
|
@ -128,6 +129,41 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
</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 {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
|
@ -186,16 +222,7 @@ export class SCIMProviderViewPage extends AKElement {
|
|||
<div class="pf-c-card__title">
|
||||
<p>${msg("Sync status")}</p>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
${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__body">${this.renderSyncStatus()}</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
|
|
|
@ -1688,9 +1688,6 @@
|
|||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<target>Synchronisation erneut ausführen</target>
|
||||
|
|
|
@ -1779,10 +1779,6 @@
|
|||
<source>Update SCIM Provider</source>
|
||||
<target>Update SCIM Provider</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
<target>Sync not run yet.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<target>Run sync again</target>
|
||||
|
|
|
@ -1660,9 +1660,6 @@
|
|||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<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>
|
||||
<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 id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
|
|
|
@ -1714,9 +1714,6 @@
|
|||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<target>Uruchom ponownie synchronizację</target>
|
||||
|
|
|
@ -2196,11 +2196,6 @@
|
|||
<source>Update SCIM Provider</source>
|
||||
<target>Ũƥďàţē ŚĆĨM Ƥŕōvĩďēŕ</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
<target>Śŷńć ńōţ ŕũń ŷēţ.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
|
|
|
@ -1659,9 +1659,6 @@
|
|||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<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">
|
||||
<body>
|
||||
<trans-unit id="s4caed5b7a7e5d89b">
|
||||
|
@ -613,9 +613,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">
|
||||
|
@ -1057,8 +1057,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">
|
||||
|
@ -1799,8 +1799,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">
|
||||
|
@ -2217,11 +2217,6 @@
|
|||
<source>Update SCIM Provider</source>
|
||||
<target>更新 SCIM 提供程序</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
<target>尚未同步过。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<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 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">
|
||||
|
@ -3781,8 +3776,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">
|
||||
|
@ -3791,8 +3786,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
|||
|
||||
</trans-unit>
|
||||
<trans-unit id="s3bb51cabb02b997e">
|
||||
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
|
||||
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
|
||||
<source>Format: "weeks=3;days=2;hours=3,seconds=2".</source>
|
||||
<target>格式:"weeks=3;days=2;hours=3,seconds=2"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<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 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">
|
||||
|
@ -5077,7 +5072,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>
|
||||
|
@ -5412,10 +5407,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>
|
||||
|
@ -5464,7 +5459,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>
|
||||
|
@ -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>
|
||||
</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">
|
||||
|
|
|
@ -1673,9 +1673,6 @@
|
|||
<trans-unit id="sc6c575c5ff64cdb1">
|
||||
<source>Update SCIM Provider</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
<target>再次运行同步</target>
|
||||
|
|
|
@ -2198,11 +2198,6 @@
|
|||
<source>Update SCIM Provider</source>
|
||||
<target>更新 SCIM 供應商</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7da38af36522ff6a">
|
||||
<source>Sync not run yet.</source>
|
||||
<target>尚未執行同步。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sbecf8dc03c978d15">
|
||||
<source>Run sync again</source>
|
||||
|
|
Reference in New Issue