diff --git a/.changeset/i18n-sso-scim-userposition-importjob-coverage.md b/.changeset/i18n-sso-scim-userposition-importjob-coverage.md new file mode 100644 index 0000000000..e77d03311e --- /dev/null +++ b/.changeset/i18n-sso-scim-userposition-importjob-coverage.md @@ -0,0 +1,24 @@ +--- +"@objectstack/platform-objects": patch +"@objectstack/plugin-security": patch +--- + +fix(i18n): translate the SSO / SCIM / user-position / import-job admin objects + +Four live, UI-facing system objects were registered but never added to their +package's i18n extract config, so non-English admins saw raw English `label` +metadata: + +- `sys_sso_provider`, `sys_scim_provider` (platform-objects) — identity-provider + admin grids plus the register / verify-domain actions. +- `sys_user_position` (plugin-security) — delegated position assignment + (`userActions` create/edit/delete); its sibling `sys_user_permission_set` was + already translated, so this closes an inconsistency. +- `sys_import_job` (platform-objects) — import history / progress, alongside the + already-translated `sys_job` / `sys_job_run`. + +Adds each object to its package's `scripts/i18n-extract.config.ts` and supplies +real zh-CN / ja-JP / es-ES translations across all four locale bundles, and +extends the bundle-ownership guards' `OWNED_OBJECTS` to cover them. The +orphan-only guards from #3502 could not catch this "owned-and-live-but-never- +extracted" gap. diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index a909e590a1..2e51807fa2 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -51,6 +51,8 @@ import { SysOauthClientResource, SysOauthClientAssertion, SysJwks, + SysSsoProvider, + SysScimProvider, } from '../src/identity/index.js'; // ── Security ────────────────────────────────────────────────────────────── @@ -80,6 +82,7 @@ import { SysJob, SysJobRun, SysJobQueue, + SysImportJob, } from '../src/audit/index.js'; // ── Integration ─────────────────────────────────────────────────────────── @@ -151,6 +154,8 @@ export default defineStack({ SysOauthClientResource, SysOauthClientAssertion, SysJwks, + SysSsoProvider, + SysScimProvider, // Security: RBAC moved to @objectstack/plugin-security, sharing to // @objectstack/plugin-sharing (ADR-0029 K2 / D8). @@ -168,6 +173,7 @@ export default defineStack({ SysJob, SysJobRun, SysJobQueue, + SysImportJob, // Integration: sys_webhook moved to @objectstack/plugin-webhooks (ADR-0029 D8). diff --git a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts index a198fd3629..5764158a2d 100644 --- a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts +++ b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts @@ -24,9 +24,12 @@ const OWNED_OBJECTS = new Set([ 'sys_user_preference', 'sys_oauth_application', 'sys_oauth_access_token', 'sys_oauth_refresh_token', 'sys_oauth_consent', 'sys_oauth_resource', 'sys_oauth_client_resource', 'sys_oauth_client_assertion', 'sys_jwks', + // identity — external SSO / SCIM providers (admin-facing, better-auth-managed) + 'sys_sso_provider', 'sys_scim_provider', // audit / messaging-adjacent (still owned here) 'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template', 'sys_saved_report', 'sys_report_schedule', 'sys_job', 'sys_job_run', 'sys_job_queue', + 'sys_import_job', // metadata 'sys_metadata', 'sys_metadata_history', 'sys_view_definition', 'sys_metadata_audit', // system diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index bf10073dfa..f1befafec5 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1667,6 +1667,203 @@ export const enObjects: NonNullable = { } } }, + sys_sso_provider: { + label: "SSO Provider", + pluralLabel: "SSO Providers", + description: "External SSO identity providers (OIDC / SAML) this environment federates login to", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "Provider ID", + help: "Stable provider identifier (unique within the environment)" + }, + issuer: { + label: "Issuer", + help: "IdP issuer URL" + }, + domain: { + label: "Email Domain", + help: "Email domain routed to this IdP (e.g. acme.com)" + }, + domain_verified: { + label: "Domain Verified", + help: "Whether DNS ownership of the email domain has been proven (ADR-0024 ②). Set by “Verify Domain” after the DNS TXT record resolves. Managed by better-auth — not directly editable. Only enforced when domain verification is enabled for the environment." + }, + oidc_config: { + label: "OIDC Config", + help: "JSON: clientId, clientSecret, endpoints, scopes, mapping, pkce (managed by better-auth)" + }, + saml_config: { + label: "SAML Config", + help: "JSON: entryPoint, cert, identifierFormat, mapping (managed by better-auth)" + }, + user_id: { + label: "Registered By", + help: "User who registered this provider" + }, + organization_id: { + label: "Organization", + help: "Organization scope (when org-scoped SSO is used)" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All", + emptyState: { + title: "No SSO providers yet", + message: "Register your organization’s external IdP — OIDC (Okta, Entra, Auth0, …) with “Register SSO Provider”, or SAML 2.0 with “Register SAML Provider”. Members whose email domain matches can then sign in through it." + } + } + }, + _actions: { + register_sso_provider: { + label: "Register SSO Provider", + params: { + providerId: { + label: "Provider ID", + helpText: "Stable identifier, e.g. \"okta\" or \"acme-entra\"." + }, + issuer: { + label: "Issuer URL", + helpText: "IdP issuer, e.g. https://acme.okta.com. Discovery is fetched from here unless an explicit URL is given below." + }, + domain: { + label: "Email Domain", + helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." + }, + clientId: { + label: "Client ID", + helpText: "OAuth client ID issued by the IdP for this environment." + }, + clientSecret: { + label: "Client Secret", + helpText: "OAuth client secret (stored encrypted by better-auth)." + }, + discoveryEndpoint: { + label: "Discovery URL", + helpText: "Optional. OIDC discovery document URL. Leave blank to derive `/.well-known/openid-configuration`." + }, + scopes: { + label: "Scopes", + helpText: "Optional. Space- or comma-separated OAuth scopes. Defaults to \"openid email profile\".", + placeholder: "openid email profile" + }, + mapId: { + label: "Map: User ID claim", + helpText: "Optional. ID-token claim mapped to the user ID. Defaults to \"sub\".", + placeholder: "sub" + }, + mapEmail: { + label: "Map: Email claim", + helpText: "Optional. Claim mapped to email. Defaults to \"email\".", + placeholder: "email" + }, + mapName: { + label: "Map: Name claim", + helpText: "Optional. Claim mapped to display name. Defaults to \"name\".", + placeholder: "name" + } + } + }, + register_saml_provider: { + label: "Register SAML Provider", + params: { + providerId: { + label: "Provider ID", + helpText: "Stable identifier, e.g. \"acme-saml\"." + }, + issuer: { + label: "IdP Entity ID", + helpText: "The IdP’s SAML EntityID (issuer), e.g. https://saml.acme.com/entityid." + }, + domain: { + label: "Email Domain", + helpText: "Users with this email domain are routed to this IdP, e.g. acme.com." + }, + entryPoint: { + label: "IdP SSO URL", + helpText: "The IdP’s SAML single sign-on (redirect) endpoint that receives the SAMLRequest." + }, + cert: { + label: "IdP Signing Certificate", + helpText: "The IdP’s X.509 signing certificate (PEM body). Used to verify assertion signatures." + }, + identifierFormat: { + label: "NameID Format", + helpText: "Optional. Requested SAML NameID format. Defaults to the IdP’s configured format.", + placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + }, + request_domain_verification: { + label: "Request Domain Verification", + resultDialog: { + title: "Verify your domain", + description: "Add the DNS TXT record below at your domain’s DNS provider, then run “Verify Domain”. The token is shown once.", + acknowledge: "Done", + fields: { + dnsRecordType: "Record type", + dnsRecordName: "Name / Host", + dnsRecordValue: "Value" + } + } + }, + verify_domain: { + label: "Verify Domain", + successMessage: "Domain ownership verified" + }, + delete_sso_provider: { + label: "Delete SSO Provider", + confirmText: "Delete this SSO provider? Users from its domain will no longer be able to sign in through it.", + successMessage: "SSO provider deleted" + } + } + }, + sys_scim_provider: { + label: "SCIM Provider", + pluralLabel: "SCIM Providers", + description: "SCIM 2.0 connections (bearer tokens) external IdPs use to provision/deprovision this environment's users", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "Provider ID", + help: "Stable SCIM provider identifier (e.g. \"okta-scim\")" + }, + scim_token: { + label: "SCIM Token (hash)", + help: "Hashed bearer credential for this SCIM connection — the plaintext is shown once at generate-token. Sensitive; do not expose." + }, + organization_id: { + label: "Organization", + help: "Organization scope of this token (org-scoped tokens restrict provisioning to that org)" + }, + user_id: { + label: "Owned By", + help: "User who generated this token (when provider-ownership is enabled)" + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + }, + _views: { + all: { + label: "All" + } + } + }, sys_notification: { label: "Notification", pluralLabel: "Notifications", @@ -2236,6 +2433,92 @@ export const enObjects: NonNullable = { } } }, + sys_import_job: { + label: "Import Job", + pluralLabel: "Import Jobs", + description: "Asynchronous bulk-import job state, progress, and history", + fields: { + id: { + label: "Job ID" + }, + object_name: { + label: "Object", + help: "API name of the object being imported into" + }, + status: { + label: "Status", + options: { + pending: "pending", + running: "running", + succeeded: "succeeded", + failed: "failed", + cancelled: "cancelled" + } + }, + total_rows: { + label: "Total Rows" + }, + processed_rows: { + label: "Processed Rows" + }, + created_count: { + label: "Created" + }, + updated_count: { + label: "Updated" + }, + skipped_count: { + label: "Skipped" + }, + error_count: { + label: "Errors" + }, + write_mode: { + label: "Write Mode", + options: { + insert: "insert", + update: "update", + upsert: "upsert" + } + }, + dry_run: { + label: "Dry Run" + }, + run_automations: { + label: "Run Automations" + }, + treat_as_historical: { + label: "Treat As Historical" + }, + error: { + label: "Fatal Error" + }, + results: { + label: "Row Results (sample)", + help: "Capped sample of per-row results (failures first) for the UI" + }, + undo_log: { + label: "Undo Log", + help: "Reversal instructions ({created:[ids], updated:[{id,before}]}) captured for small non-dry-run jobs so the import can be undone" + }, + reverted_at: { + label: "Reverted At", + help: "Set when the import was undone (created records deleted, updated records restored)" + }, + started_at: { + label: "Started At" + }, + completed_at: { + label: "Completed At" + }, + created_by: { + label: "Created By" + }, + created_at: { + label: "Created At" + } + } + }, sys_metadata: { label: "System Metadata", pluralLabel: "System Metadata", diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index 492d3c01e2..812d4df237 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -1667,6 +1667,203 @@ export const esESObjects: NonNullable = { } } }, + sys_sso_provider: { + label: "Proveedor SSO", + pluralLabel: "Proveedores SSO", + description: "Proveedores de identidad SSO externos (OIDC / SAML) a los que este entorno federa el inicio de sesión", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "ID de proveedor", + help: "Identificador de proveedor estable (único dentro del entorno)" + }, + issuer: { + label: "Emisor", + help: "URL del emisor del IdP" + }, + domain: { + label: "Dominio de correo", + help: "Dominio de correo enrutado a este IdP (p. ej. acme.com)" + }, + domain_verified: { + label: "Dominio verificado", + help: "Si se ha probado la propiedad DNS del dominio de correo (ADR-0024 ②). Lo establece «Verificar dominio» tras resolverse el registro DNS TXT. Gestionado por better-auth: no editable directamente. Solo se aplica cuando la verificación de dominio está habilitada para el entorno." + }, + oidc_config: { + label: "Configuración OIDC", + help: "JSON: clientId, clientSecret, endpoints, scopes, mapeo, pkce (gestionado por better-auth)" + }, + saml_config: { + label: "Configuración SAML", + help: "JSON: entryPoint, cert, identifierFormat, mapeo (gestionado por better-auth)" + }, + user_id: { + label: "Registrado por", + help: "Usuario que registró este proveedor" + }, + organization_id: { + label: "Organización", + help: "Ámbito de organización (cuando se usa SSO por organización)" + }, + created_at: { + label: "Creado el" + }, + updated_at: { + label: "Actualizado el" + } + }, + _views: { + all: { + label: "Todos", + emptyState: { + title: "Aún no hay proveedores SSO", + message: "Registre el IdP externo de su organización: OIDC (Okta, Entra, Auth0, …) con «Registrar proveedor SSO», o SAML 2.0 con «Registrar proveedor SAML». Los miembros cuyo dominio de correo coincida podrán iniciar sesión a través de él." + } + } + }, + _actions: { + register_sso_provider: { + label: "Registrar proveedor SSO", + params: { + providerId: { + label: "ID de proveedor", + helpText: "Identificador estable, p. ej. «okta» o «acme-entra»." + }, + issuer: { + label: "URL del emisor", + helpText: "Emisor del IdP, p. ej. https://acme.okta.com. El descubrimiento se obtiene de aquí salvo que se indique una URL explícita abajo." + }, + domain: { + label: "Dominio de correo", + helpText: "Los usuarios con este dominio de correo se enrutan a este IdP, p. ej. acme.com." + }, + clientId: { + label: "ID de cliente", + helpText: "ID de cliente OAuth emitido por el IdP para este entorno." + }, + clientSecret: { + label: "Secreto de cliente", + helpText: "Secreto de cliente OAuth (almacenado cifrado por better-auth)." + }, + discoveryEndpoint: { + label: "URL de descubrimiento", + helpText: "Opcional. URL del documento de descubrimiento OIDC. Déjelo en blanco para derivar `/.well-known/openid-configuration`." + }, + scopes: { + label: "Ámbitos (scopes)", + helpText: "Opcional. Ámbitos OAuth separados por espacios o comas. Valor predeterminado: «openid email profile».", + placeholder: "openid email profile" + }, + mapId: { + label: "Mapeo: reclamo de ID de usuario", + helpText: "Opcional. Reclamo del token de ID asignado al ID de usuario. Valor predeterminado: «sub».", + placeholder: "sub" + }, + mapEmail: { + label: "Mapeo: reclamo de correo", + helpText: "Opcional. Reclamo asignado al correo. Valor predeterminado: «email».", + placeholder: "email" + }, + mapName: { + label: "Mapeo: reclamo de nombre", + helpText: "Opcional. Reclamo asignado al nombre visible. Valor predeterminado: «name».", + placeholder: "name" + } + } + }, + register_saml_provider: { + label: "Registrar proveedor SAML", + params: { + providerId: { + label: "ID de proveedor", + helpText: "Identificador estable, p. ej. «acme-saml»." + }, + issuer: { + label: "ID de entidad del IdP", + helpText: "El EntityID SAML del IdP (emisor), p. ej. https://saml.acme.com/entityid." + }, + domain: { + label: "Dominio de correo", + helpText: "Los usuarios con este dominio de correo se enrutan a este IdP, p. ej. acme.com." + }, + entryPoint: { + label: "URL de SSO del IdP", + helpText: "Endpoint de inicio de sesión único (redirección) SAML del IdP que recibe la SAMLRequest." + }, + cert: { + label: "Certificado de firma del IdP", + helpText: "Certificado de firma X.509 del IdP (cuerpo PEM). Se usa para verificar las firmas de las aserciones." + }, + identifierFormat: { + label: "Formato de NameID", + helpText: "Opcional. Formato de NameID SAML solicitado. Por defecto, el formato configurado en el IdP.", + placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + }, + request_domain_verification: { + label: "Solicitar verificación de dominio", + resultDialog: { + title: "Verifique su dominio", + description: "Añada el siguiente registro DNS TXT en el proveedor DNS de su dominio y luego ejecute «Verificar dominio». El token se muestra una sola vez.", + acknowledge: "Hecho", + fields: { + dnsRecordType: "Tipo de registro", + dnsRecordName: "Nombre / Host", + dnsRecordValue: "Valor" + } + } + }, + verify_domain: { + label: "Verificar dominio", + successMessage: "Propiedad del dominio verificada" + }, + delete_sso_provider: { + label: "Eliminar proveedor SSO", + confirmText: "¿Eliminar este proveedor SSO? Los usuarios de su dominio ya no podrán iniciar sesión a través de él.", + successMessage: "Proveedor SSO eliminado" + } + } + }, + sys_scim_provider: { + label: "Proveedor SCIM", + pluralLabel: "Proveedores SCIM", + description: "Conexiones SCIM 2.0 (tokens bearer) que los IdP externos usan para aprovisionar/desaprovisionar los usuarios de este entorno", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "ID de proveedor", + help: "Identificador de proveedor SCIM estable (p. ej. «okta-scim»)" + }, + scim_token: { + label: "Token SCIM (hash)", + help: "Credencial bearer con hash de esta conexión SCIM: el texto plano se muestra una sola vez al generar el token. Sensible; no lo exponga." + }, + organization_id: { + label: "Organización", + help: "Ámbito de organización de este token (los tokens por organización limitan el aprovisionamiento a esa organización)" + }, + user_id: { + label: "Propiedad de", + help: "Usuario que generó este token (cuando la propiedad de proveedor está habilitada)" + }, + created_at: { + label: "Creado el" + }, + updated_at: { + label: "Actualizado el" + } + }, + _views: { + all: { + label: "Todos" + } + } + }, sys_notification: { label: "Notificación", pluralLabel: "Notificaciones", @@ -2236,6 +2433,92 @@ export const esESObjects: NonNullable = { } } }, + sys_import_job: { + label: "Trabajo de importación", + pluralLabel: "Trabajos de importación", + description: "Estado, progreso e historial de trabajos de importación masiva asíncrona", + fields: { + id: { + label: "ID de trabajo" + }, + object_name: { + label: "Objeto", + help: "Nombre de API del objeto en el que se importa" + }, + status: { + label: "Estado", + options: { + pending: "Pendiente", + running: "En ejecución", + succeeded: "Completado", + failed: "Fallido", + cancelled: "Cancelado" + } + }, + total_rows: { + label: "Filas totales" + }, + processed_rows: { + label: "Filas procesadas" + }, + created_count: { + label: "Creados" + }, + updated_count: { + label: "Actualizados" + }, + skipped_count: { + label: "Omitidos" + }, + error_count: { + label: "Errores" + }, + write_mode: { + label: "Modo de escritura", + options: { + insert: "Insertar", + update: "Actualizar", + upsert: "Insertar o actualizar" + } + }, + dry_run: { + label: "Simulación" + }, + run_automations: { + label: "Ejecutar automatizaciones" + }, + treat_as_historical: { + label: "Tratar como histórico" + }, + error: { + label: "Error fatal" + }, + results: { + label: "Resultados por fila (muestra)", + help: "Muestra limitada de resultados por fila (primero los fallos) para la interfaz" + }, + undo_log: { + label: "Registro de deshacer", + help: "Instrucciones de reversión ({created:[ids], updated:[{id,before}]}) capturadas para trabajos pequeños que no son simulación, de modo que la importación pueda deshacerse" + }, + reverted_at: { + label: "Deshecho el", + help: "Se establece cuando se deshizo la importación (los registros creados se eliminan, los actualizados se restauran)" + }, + started_at: { + label: "Iniciado el" + }, + completed_at: { + label: "Completado el" + }, + created_by: { + label: "Creado por" + }, + created_at: { + label: "Creado el" + } + } + }, sys_metadata: { label: "Metadatos del sistema", pluralLabel: "Metadatos del sistema", diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index f530c54907..a51fa04d01 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -1667,6 +1667,203 @@ export const jaJPObjects: NonNullable = { } } }, + sys_sso_provider: { + label: "SSO プロバイダー", + pluralLabel: "SSO プロバイダー", + description: "この環境がログインをフェデレーションする外部 SSO ID プロバイダー(OIDC / SAML)", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "プロバイダー ID", + help: "安定したプロバイダー識別子(環境内で一意)" + }, + issuer: { + label: "発行者", + help: "IdP 発行者 URL" + }, + domain: { + label: "メールドメイン", + help: "この IdP にルーティングされるメールドメイン(例: acme.com)" + }, + domain_verified: { + label: "ドメイン検証済み", + help: "メールドメインの DNS 所有権が証明済みかどうか(ADR-0024 ②)。DNS TXT レコードが解決した後、「ドメインを検証」により設定されます。better-auth が管理し、直接編集はできません。環境でドメイン検証が有効な場合にのみ適用されます。" + }, + oidc_config: { + label: "OIDC 設定", + help: "JSON: clientId、clientSecret、エンドポイント、scopes、マッピング、pkce(better-auth が管理)" + }, + saml_config: { + label: "SAML 設定", + help: "JSON: entryPoint、cert、identifierFormat、マッピング(better-auth が管理)" + }, + user_id: { + label: "登録者", + help: "このプロバイダーを登録したユーザー" + }, + organization_id: { + label: "組織", + help: "組織スコープ(組織スコープの SSO を使用する場合)" + }, + created_at: { + label: "作成日時" + }, + updated_at: { + label: "更新日時" + } + }, + _views: { + all: { + label: "すべて", + emptyState: { + title: "SSO プロバイダーがまだありません", + message: "組織の外部 IdP を登録します——「SSO プロバイダーを登録」で OIDC(Okta、Entra、Auth0 など)を、「SAML プロバイダーを登録」で SAML 2.0 を接続します。メールドメインが一致するメンバーは、それを通じてサインインできます。" + } + } + }, + _actions: { + register_sso_provider: { + label: "SSO プロバイダーを登録", + params: { + providerId: { + label: "プロバイダー ID", + helpText: "安定した識別子(例: 「okta」や「acme-entra」)。" + }, + issuer: { + label: "発行者 URL", + helpText: "IdP 発行者(例: https://acme.okta.com)。下で明示的な URL を指定しない限り、ここからディスカバリを取得します。" + }, + domain: { + label: "メールドメイン", + helpText: "このメールドメインのユーザーはこの IdP にルーティングされます(例: acme.com)。" + }, + clientId: { + label: "クライアント ID", + helpText: "この環境向けに IdP が発行した OAuth クライアント ID。" + }, + clientSecret: { + label: "クライアントシークレット", + helpText: "OAuth クライアントシークレット(better-auth により暗号化して保存)。" + }, + discoveryEndpoint: { + label: "ディスカバリ URL", + helpText: "任意。OIDC ディスカバリドキュメントの URL。空欄の場合は `/.well-known/openid-configuration` を導出します。" + }, + scopes: { + label: "スコープ", + helpText: "任意。スペースまたはカンマ区切りの OAuth スコープ。既定値は「openid email profile」。", + placeholder: "openid email profile" + }, + mapId: { + label: "マッピング: ユーザー ID クレーム", + helpText: "任意。ユーザー ID にマッピングする ID トークンのクレーム。既定値は「sub」。", + placeholder: "sub" + }, + mapEmail: { + label: "マッピング: メールクレーム", + helpText: "任意。メールにマッピングするクレーム。既定値は「email」。", + placeholder: "email" + }, + mapName: { + label: "マッピング: 名前クレーム", + helpText: "任意。表示名にマッピングするクレーム。既定値は「name」。", + placeholder: "name" + } + } + }, + register_saml_provider: { + label: "SAML プロバイダーを登録", + params: { + providerId: { + label: "プロバイダー ID", + helpText: "安定した識別子(例: 「acme-saml」)。" + }, + issuer: { + label: "IdP エンティティ ID", + helpText: "IdP の SAML EntityID(発行者)(例: https://saml.acme.com/entityid)。" + }, + domain: { + label: "メールドメイン", + helpText: "このメールドメインのユーザーはこの IdP にルーティングされます(例: acme.com)。" + }, + entryPoint: { + label: "IdP SSO URL", + helpText: "SAMLRequest を受け取る IdP の SAML シングルサインオン(リダイレクト)エンドポイント。" + }, + cert: { + label: "IdP 署名証明書", + helpText: "IdP の X.509 署名証明書(PEM 本文)。アサーション署名の検証に使用します。" + }, + identifierFormat: { + label: "NameID フォーマット", + helpText: "任意。要求する SAML NameID フォーマット。既定では IdP に設定されたフォーマットを使用します。", + placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + }, + request_domain_verification: { + label: "ドメイン検証をリクエスト", + resultDialog: { + title: "ドメインを検証", + description: "以下の DNS TXT レコードをドメインの DNS プロバイダーに追加し、「ドメインを検証」を実行してください。トークンは一度だけ表示されます。", + acknowledge: "完了", + fields: { + dnsRecordType: "レコードタイプ", + dnsRecordName: "名前 / ホスト", + dnsRecordValue: "値" + } + } + }, + verify_domain: { + label: "ドメインを検証", + successMessage: "ドメイン所有権を確認しました" + }, + delete_sso_provider: { + label: "SSO プロバイダーを削除", + confirmText: "この SSO プロバイダーを削除しますか?そのドメインのユーザーは、これを通じてサインインできなくなります。", + successMessage: "SSO プロバイダーを削除しました" + } + } + }, + sys_scim_provider: { + label: "SCIM プロバイダー", + pluralLabel: "SCIM プロバイダー", + description: "外部 IdP がこの環境のユーザーをプロビジョニング/デプロビジョニングするために使用する SCIM 2.0 接続(ベアラートークン)", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "プロバイダー ID", + help: "安定した SCIM プロバイダー識別子(例: 「okta-scim」)" + }, + scim_token: { + label: "SCIM トークン(ハッシュ)", + help: "この SCIM 接続のハッシュ化されたベアラー資格情報——平文はトークン生成時に一度だけ表示されます。機密情報のため、公開しないでください。" + }, + organization_id: { + label: "組織", + help: "このトークンの組織スコープ(組織スコープのトークンはプロビジョニングをその組織に限定します)" + }, + user_id: { + label: "所有者", + help: "このトークンを生成したユーザー(プロバイダー所有権が有効な場合)" + }, + created_at: { + label: "作成日時" + }, + updated_at: { + label: "更新日時" + } + }, + _views: { + all: { + label: "すべて" + } + } + }, sys_notification: { label: "通知", pluralLabel: "通知", @@ -2236,6 +2433,92 @@ export const jaJPObjects: NonNullable = { } } }, + sys_import_job: { + label: "インポートジョブ", + pluralLabel: "インポートジョブ", + description: "非同期一括インポートジョブの状態、進捗、履歴", + fields: { + id: { + label: "ジョブ ID" + }, + object_name: { + label: "オブジェクト", + help: "インポート先オブジェクトの API 名" + }, + status: { + label: "ステータス", + options: { + pending: "保留中", + running: "実行中", + succeeded: "成功", + failed: "失敗", + cancelled: "キャンセル済み" + } + }, + total_rows: { + label: "総行数" + }, + processed_rows: { + label: "処理済み行数" + }, + created_count: { + label: "作成済み" + }, + updated_count: { + label: "更新済み" + }, + skipped_count: { + label: "スキップ済み" + }, + error_count: { + label: "エラー数" + }, + write_mode: { + label: "書き込みモード", + options: { + insert: "挿入", + update: "更新", + upsert: "挿入または更新" + } + }, + dry_run: { + label: "ドライラン" + }, + run_automations: { + label: "自動化を実行" + }, + treat_as_historical: { + label: "履歴データとして扱う" + }, + error: { + label: "致命的なエラー" + }, + results: { + label: "行ごとの結果(サンプル)", + help: "UI 用の行ごとの結果サンプル(失敗を優先、件数に上限あり)" + }, + undo_log: { + label: "取り消しログ", + help: "インポートを取り消せるように、小規模な非ドライランジョブ向けに記録された巻き戻し手順({created:[ids], updated:[{id,before}]})" + }, + reverted_at: { + label: "取り消し日時", + help: "インポートが取り消されたときに設定されます(作成されたレコードは削除され、更新されたレコードは復元されます)" + }, + started_at: { + label: "開始日時" + }, + completed_at: { + label: "完了日時" + }, + created_by: { + label: "作成者" + }, + created_at: { + label: "作成日時" + } + } + }, sys_metadata: { label: "システムメタデータ", pluralLabel: "システムメタデータ", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 14bc242281..197809894e 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -1667,6 +1667,203 @@ export const zhCNObjects: NonNullable = { } } }, + sys_sso_provider: { + label: "SSO 提供方", + pluralLabel: "SSO 提供方", + description: "本环境用于联合登录的外部 SSO 身份提供方(OIDC / SAML)", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "提供方 ID", + help: "稳定的提供方标识符(在本环境内唯一)" + }, + issuer: { + label: "颁发者", + help: "IdP 颁发者 URL" + }, + domain: { + label: "邮箱域名", + help: "路由到该 IdP 的邮箱域名(如 acme.com)" + }, + domain_verified: { + label: "域名已验证", + help: "是否已证明对该邮箱域名的 DNS 所有权(ADR-0024 ②)。在 DNS TXT 记录解析后由“验证域名”设置。由 better-auth 管理——不可直接编辑。仅在本环境启用域名验证时强制执行。" + }, + oidc_config: { + label: "OIDC 配置", + help: "JSON:clientId、clientSecret、端点、scopes、映射、pkce(由 better-auth 管理)" + }, + saml_config: { + label: "SAML 配置", + help: "JSON:entryPoint、cert、identifierFormat、映射(由 better-auth 管理)" + }, + user_id: { + label: "注册人", + help: "注册该提供方的用户" + }, + organization_id: { + label: "组织", + help: "组织范围(使用组织级 SSO 时)" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + }, + _views: { + all: { + label: "全部", + emptyState: { + title: "尚无 SSO 提供方", + message: "注册贵组织的外部 IdP——使用“注册 SSO 提供方”接入 OIDC(Okta、Entra、Auth0 等),或使用“注册 SAML 提供方”接入 SAML 2.0。邮箱域名匹配的成员即可通过它登录。" + } + } + }, + _actions: { + register_sso_provider: { + label: "注册 SSO 提供方", + params: { + providerId: { + label: "提供方 ID", + helpText: "稳定标识符,如 “okta” 或 “acme-entra”。" + }, + issuer: { + label: "颁发者 URL", + helpText: "IdP 颁发者,如 https://acme.okta.com。除非在下方提供显式 URL,否则从此处获取发现文档。" + }, + domain: { + label: "邮箱域名", + helpText: "使用该邮箱域名的用户将被路由到此 IdP,如 acme.com。" + }, + clientId: { + label: "客户端 ID", + helpText: "IdP 为本环境签发的 OAuth 客户端 ID。" + }, + clientSecret: { + label: "客户端密钥", + helpText: "OAuth 客户端密钥(由 better-auth 加密存储)。" + }, + discoveryEndpoint: { + label: "发现文档 URL", + helpText: "可选。OIDC 发现文档 URL。留空则推导为 `/.well-known/openid-configuration`。" + }, + scopes: { + label: "授权范围", + helpText: "可选。以空格或逗号分隔的 OAuth 授权范围(scopes)。默认为 “openid email profile”。", + placeholder: "openid email profile" + }, + mapId: { + label: "映射:用户 ID claim", + helpText: "可选。映射到用户 ID 的 ID-token claim。默认为 “sub”。", + placeholder: "sub" + }, + mapEmail: { + label: "映射:邮箱 claim", + helpText: "可选。映射到邮箱的 claim。默认为 “email”。", + placeholder: "email" + }, + mapName: { + label: "映射:姓名 claim", + helpText: "可选。映射到显示名称的 claim。默认为 “name”。", + placeholder: "name" + } + } + }, + register_saml_provider: { + label: "注册 SAML 提供方", + params: { + providerId: { + label: "提供方 ID", + helpText: "稳定标识符,如 “acme-saml”。" + }, + issuer: { + label: "IdP 实体 ID", + helpText: "IdP 的 SAML EntityID(颁发者),如 https://saml.acme.com/entityid。" + }, + domain: { + label: "邮箱域名", + helpText: "使用该邮箱域名的用户将被路由到此 IdP,如 acme.com。" + }, + entryPoint: { + label: "IdP SSO URL", + helpText: "接收 SAMLRequest 的 IdP SAML 单点登录(重定向)端点。" + }, + cert: { + label: "IdP 签名证书", + helpText: "IdP 的 X.509 签名证书(PEM 主体)。用于验证断言签名。" + }, + identifierFormat: { + label: "NameID 格式", + helpText: "可选。请求的 SAML NameID 格式。默认为 IdP 配置的格式。", + placeholder: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + } + } + }, + request_domain_verification: { + label: "申请域名验证", + resultDialog: { + title: "验证您的域名", + description: "在您域名的 DNS 提供商处添加下方 DNS TXT 记录,然后运行“验证域名”。该令牌仅显示一次。", + acknowledge: "完成", + fields: { + dnsRecordType: "记录类型", + dnsRecordName: "名称 / 主机", + dnsRecordValue: "值" + } + } + }, + verify_domain: { + label: "验证域名", + successMessage: "域名所有权已验证" + }, + delete_sso_provider: { + label: "删除 SSO 提供方", + confirmText: "删除该 SSO 提供方吗?其域名下的用户将无法再通过它登录。", + successMessage: "SSO 提供方已删除" + } + } + }, + sys_scim_provider: { + label: "SCIM 提供方", + pluralLabel: "SCIM 提供方", + description: "外部 IdP 用于开通/停用本环境用户的 SCIM 2.0 连接(Bearer 令牌)", + fields: { + id: { + label: "ID" + }, + provider_id: { + label: "提供方 ID", + help: "稳定的 SCIM 提供方标识符(如 “okta-scim”)" + }, + scim_token: { + label: "SCIM 令牌(哈希)", + help: "该 SCIM 连接的哈希 Bearer 凭据——明文仅在生成令牌时显示一次。敏感信息,请勿泄露。" + }, + organization_id: { + label: "组织", + help: "该令牌的组织范围(组织级令牌将开通限制在该组织内)" + }, + user_id: { + label: "所有者", + help: "生成该令牌的用户(启用提供方归属时)" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + }, + _views: { + all: { + label: "全部" + } + } + }, sys_notification: { label: "通知", pluralLabel: "通知", @@ -2236,6 +2433,92 @@ export const zhCNObjects: NonNullable = { } } }, + sys_import_job: { + label: "导入任务", + pluralLabel: "导入任务", + description: "异步批量导入任务的状态、进度与历史", + fields: { + id: { + label: "任务 ID" + }, + object_name: { + label: "对象", + help: "导入目标对象的 API 名称" + }, + status: { + label: "状态", + options: { + pending: "待处理", + running: "运行中", + succeeded: "成功", + failed: "失败", + cancelled: "已取消" + } + }, + total_rows: { + label: "总行数" + }, + processed_rows: { + label: "已处理行数" + }, + created_count: { + label: "已创建" + }, + updated_count: { + label: "已更新" + }, + skipped_count: { + label: "已跳过" + }, + error_count: { + label: "错误数" + }, + write_mode: { + label: "写入模式", + options: { + insert: "插入", + update: "更新", + upsert: "插入或更新" + } + }, + dry_run: { + label: "试运行" + }, + run_automations: { + label: "运行自动化" + }, + treat_as_historical: { + label: "视为历史数据" + }, + error: { + label: "致命错误" + }, + results: { + label: "行结果(样本)", + help: "供 UI 使用的按行结果样本(失败项优先,数量有上限)" + }, + undo_log: { + label: "撤销日志", + help: "为小型非试运行任务记录的回滚指令({created:[ids], updated:[{id,before}]}),以便撤销导入" + }, + reverted_at: { + label: "撤销时间", + help: "在导入被撤销时设置(已创建记录被删除,已更新记录被还原)" + }, + started_at: { + label: "开始时间" + }, + completed_at: { + label: "完成时间" + }, + created_by: { + label: "创建人" + }, + created_at: { + label: "创建时间" + } + } + }, sys_metadata: { label: "系统元数据", pluralLabel: "系统元数据", diff --git a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts index 55eb32f106..17edd7537b 100644 --- a/packages/plugins/plugin-security/scripts/i18n-extract.config.ts +++ b/packages/plugins/plugin-security/scripts/i18n-extract.config.ts @@ -17,7 +17,7 @@ import { defineStack } from '@objectstack/spec'; // must stay in this list so `os i18n extract` keeps emitting it (dropping it // here silently deletes those strings on the next run — the sys_audit_log // incident). Enforced by src/translations/bundle-ownership.test.ts. -import { SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet } from '../src/objects/index.js'; +import { SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet, SysUserPosition } from '../src/objects/index.js'; import { enObjects } from '../src/translations/en.objects.generated.js'; import { zhCNObjects } from '../src/translations/zh-CN.objects.generated.js'; import { jaJPObjects } from '../src/translations/ja-JP.objects.generated.js'; @@ -25,7 +25,7 @@ import { esESObjects } from '../src/translations/es-ES.objects.generated.js'; export default defineStack({ name: 'plugin-security-i18n-extract', - objects: [SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet] as any, + objects: [SysPosition, SysCapability, SysPermissionSet, SysUserPermissionSet, SysPositionPermissionSet, SysUserPosition] as any, translations: [ { en: { objects: enObjects } }, { 'zh-CN': { objects: zhCNObjects } }, diff --git a/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts b/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts index ec27c27830..cb5550b902 100644 --- a/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts +++ b/packages/plugins/plugin-security/src/translations/bundle-ownership.test.ts @@ -11,12 +11,12 @@ // moved/removed object's keys were left behind — remove them from the bundles // (or migrate them to the owning package), then keep this list in sync. // -// NB: this package defines more objects (sys_user_position, -// sys_audience_binding_suggestion) that the extract config deliberately does NOT -// translate — absent from both the config and the bundles, correctly out of -// scope. `sys_capability` IS translated (curated strings live in the bundles), -// so it must stay in the extract config; this guard is what caught it being -// dropped from that list. +// NB: this package also defines sys_audience_binding_suggestion, which the +// extract config deliberately does NOT translate — an engine-owned suggestion +// queue with no admin UI surface, absent from both the config and the bundles, +// correctly out of scope. `sys_capability` and `sys_user_position` ARE translated +// (curated strings live in the bundles), so they must stay in the extract config; +// this guard is what caught sys_capability being dropped from that list. import { describe, it, expect } from 'vitest'; import { enObjects } from './en.objects.generated.js'; @@ -29,6 +29,7 @@ const OWNED_OBJECTS = new Set([ 'sys_permission_set', 'sys_user_permission_set', 'sys_position_permission_set', + 'sys_user_position', ]); describe('objects translation bundle ownership (ADR-0029 D8)', () => { diff --git a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts index 052c8fe55c..d89fe50525 100644 --- a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts @@ -253,5 +253,66 @@ export const enObjects: NonNullable = { label: "Updated At" } } + }, + sys_user_position: { + label: "User Position", + pluralLabel: "User Positions", + description: "Assigns a position (sys_position.name) to a user. Platform-owned (ADR-0057 D4, ADR-0090 D3).", + fields: { + id: { + label: "Assignment ID", + help: "UUID of the user-position assignment." + }, + user_id: { + label: "User", + help: "Foreign key to sys_user." + }, + position: { + label: "Position", + help: "Position machine name (references sys_position.name)." + }, + business_unit_id: { + label: "Business Unit", + help: "[ADR-0090 Addendum] Assignment-level BU anchor: where this position assignment applies. Depth anchor for readScope/writeScope, delegated-admin boundary (D12), and audit fact. Null = unanchored (legacy/tenant-wide); delegated admins MUST anchor assignments inside their subtree." + }, + organization_id: { + label: "Organization", + help: "Tenant that owns this assignment; null = global (cross-tenant)." + }, + granted_by: { + label: "Granted By", + help: "User who granted this position assignment (stamped by the delegated-admin gate for delegate writes)." + }, + valid_from: { + label: "Valid From", + help: "[ADR-0091 D1] Grant is inactive before this instant. Null = active immediately. Enforced fail-closed at resolution time (D2) — never by a background job." + }, + valid_until: { + label: "Valid Until", + help: "[ADR-0091 D1] Grant is inactive AT and AFTER this instant (half-open [from, until), UTC). Null = never expires. Mandatory on delegation rows (D3). Enforced at resolution time (D2)." + }, + reason: { + label: "Reason", + help: "[ADR-0091 D1] Why this grant exists. Free text; REQUIRED on delegation (D3) and break-glass (D4) rows." + }, + delegated_from: { + label: "Delegated From", + help: "[ADR-0091 D3] The delegator whose authority this row carries (职务代理). A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways." + }, + last_certified_at: { + label: "Last Certified At", + help: "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified." + }, + certified_by: { + label: "Certified By", + help: "[ADR-0091 D5] Reviewer who last attested this grant." + }, + created_at: { + label: "Created At" + }, + updated_at: { + label: "Updated At" + } + } } }; diff --git a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts index d0ff03efaa..e58c151f54 100644 --- a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts @@ -253,5 +253,66 @@ export const esESObjects: NonNullable = { label: "Actualizado el" } } + }, + sys_user_position: { + label: "Puesto de usuario", + pluralLabel: "Puestos de usuario", + description: "Asigna un puesto (sys_position.name) a un usuario. Gestionado por la plataforma (ADR-0057 D4, ADR-0090 D3).", + fields: { + id: { + label: "ID de asignación", + help: "UUID de la asignación usuario-puesto." + }, + user_id: { + label: "Usuario", + help: "Clave foránea a sys_user." + }, + position: { + label: "Puesto", + help: "Nombre de máquina del puesto (referencia a sys_position.name)." + }, + business_unit_id: { + label: "Unidad de negocio", + help: "[Anexo ADR-0090] Ancla de UN a nivel de asignación: dónde se aplica esta asignación de puesto. Ancla de profundidad para readScope/writeScope, límite de administración delegada (D12) y hecho de auditoría. Nulo = sin anclar (heredado/todo el inquilino); los administradores delegados DEBEN anclar las asignaciones dentro de su subárbol." + }, + organization_id: { + label: "Organización", + help: "Inquilino propietario de esta asignación; nulo = global (entre inquilinos)." + }, + granted_by: { + label: "Otorgado por", + help: "Usuario que otorgó esta asignación de puesto (registrado por la puerta de administración delegada en escrituras delegadas)." + }, + valid_from: { + label: "Válido desde", + help: "[ADR-0091 D1] La concesión está inactiva antes de este instante. Nulo = activa de inmediato. Se aplica en modo fail-closed en el momento de resolución (D2), nunca mediante un trabajo en segundo plano." + }, + valid_until: { + label: "Válido hasta", + help: "[ADR-0091 D1] La concesión está inactiva EN y DESPUÉS de este instante (intervalo semiabierto [from, until), UTC). Nulo = nunca expira. Obligatorio en filas de delegación (D3). Se aplica en el momento de resolución (D2)." + }, + reason: { + label: "Motivo", + help: "[ADR-0091 D1] Por qué existe esta concesión. Texto libre; OBLIGATORIO en filas de delegación (D3) y de acceso de emergencia (D4)." + }, + delegated_from: { + label: "Delegado desde", + help: "[ADR-0091 D3] El delegador cuya autoridad porta esta fila (职务代理). Una fila con delegated_from establecido no es delegable ni autorrenovable: las cadenas se cortan en ambos sentidos." + }, + last_certified_at: { + label: "Última certificación", + help: "[ADR-0091 D5] Cuándo se atestiguó por última vez esta concesión en una revisión de recertificación. Nulo = nunca certificada." + }, + certified_by: { + label: "Certificado por", + help: "[ADR-0091 D5] Revisor que atestiguó por última vez esta concesión." + }, + created_at: { + label: "Creado el" + }, + updated_at: { + label: "Actualizado el" + } + } } }; diff --git a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts index 6aa68f5a7a..5f935b8ad3 100644 --- a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts @@ -253,5 +253,66 @@ export const jaJPObjects: NonNullable = { label: "更新日時" } } + }, + sys_user_position: { + label: "ユーザーポジション", + pluralLabel: "ユーザーポジション", + description: "ポジション(sys_position.name)をユーザーに割り当てます。プラットフォーム管理(ADR-0057 D4、ADR-0090 D3)。", + fields: { + id: { + label: "割り当て ID", + help: "ユーザーポジション割り当ての UUID。" + }, + user_id: { + label: "ユーザー", + help: "sys_user への外部キー。" + }, + position: { + label: "ポジション", + help: "ポジションのマシン名(sys_position.name を参照)。" + }, + business_unit_id: { + label: "ビジネスユニット", + help: "[ADR-0090 補遺] 割り当てレベルの BU アンカー:このポジション割り当てが適用される範囲。readScope/writeScope の深さアンカー、委任管理の境界(D12)、監査事実として機能します。Null = アンカーなし(レガシー/テナント全体)。委任管理者は割り当てを自身のサブツリー内にアンカーする必要があります。" + }, + organization_id: { + label: "組織", + help: "この割り当てを所有するテナント。Null = グローバル(テナント横断)。" + }, + granted_by: { + label: "付与者", + help: "このポジション割り当てを付与したユーザー(委任書き込み時に委任管理ゲートが記録)。" + }, + valid_from: { + label: "有効開始", + help: "[ADR-0091 D1] この時点より前は付与が無効です。Null = 即時有効。解決時にフェイルクローズで適用されます(D2)——バックグラウンドジョブでは行いません。" + }, + valid_until: { + label: "有効終了", + help: "[ADR-0091 D1] この時点以降は付与が無効です(半開区間 [from, until)、UTC)。Null = 無期限。委任行では必須(D3)。解決時に適用されます(D2)。" + }, + reason: { + label: "理由", + help: "[ADR-0091 D1] この付与が存在する理由。自由記述。委任(D3)およびブレークグラス(D4)の行では必須。" + }, + delegated_from: { + label: "委任元", + help: "[ADR-0091 D3] この行が担う権限の委任者(職務代理)。delegated_from が設定された行は、それ自体を再委任できず、自己更新もできません——連鎖は双方向で断ち切られます。" + }, + last_certified_at: { + label: "最終認証日時", + help: "[ADR-0091 D5] この付与が再認証レビューで最後に証明された日時。Null = 未認証。" + }, + certified_by: { + label: "認証者", + help: "[ADR-0091 D5] この付与を最後に証明したレビュアー。" + }, + created_at: { + label: "作成日時" + }, + updated_at: { + label: "更新日時" + } + } } }; diff --git a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts index b8a9b9b102..cdda5a2b3f 100644 --- a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts @@ -253,5 +253,66 @@ export const zhCNObjects: NonNullable = { label: "更新时间" } } + }, + sys_user_position: { + label: "用户岗位", + pluralLabel: "用户岗位", + description: "将岗位(sys_position.name)分配给用户。由平台管理(ADR-0057 D4、ADR-0090 D3)。", + fields: { + id: { + label: "分配 ID", + help: "用户-岗位分配的 UUID。" + }, + user_id: { + label: "用户", + help: "指向 sys_user 的外键。" + }, + position: { + label: "岗位", + help: "岗位机器名(引用 sys_position.name)。" + }, + business_unit_id: { + label: "业务单元", + help: "[ADR-0090 附录] 分配级 BU 锚点:此岗位分配适用的范围。作为 readScope/writeScope 的深度锚点、委派管理边界(D12)和审计事实。为空 = 未锚定(遗留/租户级);委派管理员必须将分配锚定在其子树内。" + }, + organization_id: { + label: "组织", + help: "拥有此分配的租户;为空 = 全局(跨租户)。" + }, + granted_by: { + label: "授予人", + help: "授予此岗位分配的用户(委派写入时由委派管理门标记)。" + }, + valid_from: { + label: "生效时间", + help: "[ADR-0091 D1] 在此时刻之前授予无效。为空 = 立即生效。在解析时以 fail-closed 方式强制执行(D2)——绝不依赖后台任务。" + }, + valid_until: { + label: "失效时间", + help: "[ADR-0091 D1] 在此时刻及之后授予无效(半开区间 [from, until),UTC)。为空 = 永不过期。委派行必填(D3)。在解析时强制执行(D2)。" + }, + reason: { + label: "原因", + help: "[ADR-0091 D1] 此授予存在的原因。自由文本;委派(D3)和紧急破窗(D4)行必填。" + }, + delegated_from: { + label: "委派自", + help: "[ADR-0091 D3] 此行所承载权限的委派人(职务代理)。设置了 delegated_from 的行本身不可再委派、不可自续期——双向切断链条。" + }, + last_certified_at: { + label: "最近认证时间", + help: "[ADR-0091 D5] 此授予在重新认证复核中最近一次被认证的时间。为空 = 从未认证。" + }, + certified_by: { + label: "认证人", + help: "[ADR-0091 D5] 最近一次认证此授予的复核人。" + }, + created_at: { + label: "创建时间" + }, + updated_at: { + label: "更新时间" + } + } } };