diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md index a62bd82..e09a1bd 100644 --- a/docs/INTERFACE_PATTERN_MIGRATION.md +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -52,7 +52,12 @@ through declared capabilities or metadata. - `access.workflow.grant-user-access` covers the user, group, and role path. - `access.reference.admin-access-fields` covers accounts, roles, API keys, and - reusable credentials. + their backing administration fields. +- `access.workflow.manage-api-keys` owns exact help for accountable ownership, + bounded scopes and expiry, one-time secret custody, and immediate revocation. +- `access.workflow.manage-service-account-credentials` owns exact help for the + account ceiling, activation state, credential rotation/revocation, one-time + secret custody, concurrency, and retirement consequences. - `access.reference.external-function-role-mappings` explains the Organizations, IDM, and Access responsibility split. - Files and Mail blockers link to documentation supplied by the owning module. diff --git a/docs/SERVICE_ACCOUNTS.md b/docs/SERVICE_ACCOUNTS.md index e8071a4..e33741b 100644 --- a/docs/SERVICE_ACCOUNTS.md +++ b/docs/SERVICE_ACCOUNTS.md @@ -39,3 +39,14 @@ deactivates the principal and revokes all active credentials. Credential list responses never contain a secret. Create and rotate responses contain it once. Audit records include identifiers, prefixes, scopes, and the new service-account revision, but never the secret or its hash. + +## Contextual help + +F1 on the service-account page, its editors, scope controls, one-time secret, +rotation and revocation actions, activation state, or retirement confirmation +resolves to the Access-owned `access.workflow.manage-service-account-credentials` +topic. The German reference content distinguishes reversible deactivation from +retirement, explains immediate client impact, and states that secrets cannot be +recovered. Tenant API-key controls resolve separately to +`access.workflow.manage-api-keys`, because their effective authorization also +depends on the accountable human owner's current permissions. diff --git a/package.json b/package.json index 9fa64a6..4ecc804 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/access-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "webui/src/index.ts", diff --git a/pyproject.toml b/pyproject.toml index f2b9b5f..244e6a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-access" -version = "0.1.18" +version = "0.1.19" description = "GovOPlaN access platform module with identity, auth, RBAC, and scope primitives." readme = "README.md" requires-python = ">=3.12" diff --git a/src/govoplan_access/__init__.py b/src/govoplan_access/__init__.py index 682273f..2eaf23e 100644 --- a/src/govoplan_access/__init__.py +++ b/src/govoplan_access/__init__.py @@ -1,3 +1,3 @@ """GovOPlaN access platform module.""" -__version__ = "0.1.18" +__version__ = "0.1.19" diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index 95d81c8..4ce31b9 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -556,8 +556,6 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( "access.admin.tenant-users", "access.admin.tenant-groups", "access.admin.tenant-roles", - "access.admin.api-keys", - "access.admin.service-accounts", ], "route": "/admin", "screen": "Admin", @@ -625,6 +623,85 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( ], }, ), + DocumentationTopic( + id="access.workflow.manage-api-keys", + title="Create and revoke tenant API keys", + summary="Issue a one-time automation secret for an accountable tenant user, constrain its scope and lifetime, and revoke it when access must stop.", + body=( + "Tenant API keys are non-interactive automation credentials owned by an existing tenant user. Select an owner whose current effective permissions contain every requested scope; authorization continues to intersect the stored key scopes with that owner's current permissions, so removing the owner's access also narrows the key. Set the shortest practical expiry and grant only the scopes the client needs. " + "The secret is displayed once after creation. GovOPlaN then retains only its one-way hash and visible prefix, so administrators cannot display or recover it later. Record the value directly in an approved external secret manager and close the one-time dialog only after custody is confirmed. " + "Revocation is immediate and irreversible for that key: existing clients lose access and must be configured with a newly issued credential. Inspect and audit views expose metadata, scopes, timestamps, and the non-authenticating prefix but never secret material." + ), + layer="configured", + documentation_types=("admin", "user"), + audience=("tenant_admin", "access_admin", "operator"), + order=32, + conditions=( + DocumentationCondition( + required_modules=("access",), + any_scopes=( + "access:api_key:read", + "access:api_key:create", + "access:api_key:revoke", + "admin:api_keys:read", + "admin:api_keys:create", + "admin:api_keys:revoke", + ), + ), + ), + links=( + DocumentationLink(label="Tenant API keys", href="/admin?section=tenant-api-keys", kind="runtime"), + DocumentationLink(label="API keys API", href="/api/v1/admin/api-keys", kind="api"), + ), + translations={ + "de": { + "title": "Mandanten-API-Schlüssel erstellen und widerrufen", + "summary": "Geben Sie ein einmal sichtbares Automatisierungsgeheimnis für eine verantwortliche Person aus, begrenzen Sie Umfang und Laufzeit und widerrufen Sie den Schlüssel, sobald der Zugriff enden muss.", + "body": "Mandanten-API-Schlüssel sind nicht interaktive Automatisierungszugänge einer vorhandenen Person im Mandanten. Wählen Sie eine verantwortliche Person, deren aktuelle wirksame Berechtigungen alle gewünschten Scopes enthalten. Bei jeder Nutzung werden die gespeicherten Schlüssel-Scopes weiterhin mit den aktuellen Berechtigungen dieser Person geschnitten; ein Entzug ihrer Berechtigungen schränkt daher auch den Schlüssel ein. Legen Sie die kürzeste praktikable Laufzeit fest und vergeben Sie nur die Scopes, die der Client tatsächlich benötigt. Das Geheimnis wird nach der Erstellung genau einmal angezeigt. Danach speichert GovOPlaN nur einen Einweg-Hash und das sichtbare, nicht zur Anmeldung geeignete Präfix; eine spätere Anzeige oder Wiederherstellung ist nicht möglich. Übertragen Sie den Wert unmittelbar in einen freigegebenen externen Geheimnismanager und schließen Sie den Einmal-Dialog erst nach bestätigter Verwahrung. Ein Widerruf wirkt sofort und kann für diesen Schlüssel nicht rückgängig gemacht werden: Bestehende Clients verlieren den Zugriff und benötigen einen neu ausgegebenen Zugang. Detail- und Auditansichten zeigen Metadaten, Scopes, Zeitpunkte und das Präfix, aber niemals das Geheimnis.", + } + }, + metadata={ + "kind": "workflow", + "route": "/admin", + "screen": "Tenant API keys", + "help_contexts": [ + "access.admin.api-keys", + "access.api-keys.action.reload", + "access.api-keys.action.create", + "access.api-keys.action.inspect", + "access.api-keys.action.revoke", + "access.api-keys.field.show-revoked", + "access.api-keys.field.name", + "access.api-keys.field.owner", + "access.api-keys.field.expiry", + "access.api-keys.field.scopes", + "access.api-keys.secret", + "access.api-keys.confirm-revoke", + ], + "prerequisites": [ + "The tenant permits API credentials.", + "The actor may create or revoke API keys and may delegate every selected scope.", + "An approved external secret manager and accountable owner are known.", + ], + "steps": [ + "Choose the accountable owner and the narrowest required scopes.", + "Set the shortest practical expiry before creating the key.", + "Transfer the one-time secret directly into the approved secret manager.", + "Revoke the key when its client, owner, or purpose is no longer valid.", + ], + "outcome": "The automation client has a time-bounded credential whose effective access cannot exceed either its stored scopes or its owner's current permissions.", + "limitations": [ + "A one-time secret cannot be displayed or recovered after its creation dialog closes.", + "Changing the owner, expiry, or scopes requires a replacement key.", + "Revocation does not update external clients; operators must install a replacement where needed.", + ], + "consequences": [ + "Revocation immediately rejects subsequent requests made with the key.", + "Removing permissions from the owner immediately narrows effective key access.", + ], + "verification": "Reload the key directory, verify owner, prefix, scopes, expiry, and status, then test the intended client without copying secret material into evidence.", + }, + ), DocumentationTopic( id="access.workflow.manage-reusable-credentials", title="Manage reusable credentials safely", @@ -738,9 +815,41 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( DocumentationLink(label="Service accounts API", href="/api/v1/admin/service-accounts", kind="api"), DocumentationLink(label="Credential lifecycle API", href="/api/v1/admin/service-accounts/{service_account_id}/credentials", kind="api"), ), + translations={ + "de": { + "title": "Dienstkonten und ihre Zugangsdaten verwalten", + "summary": "Erstellen Sie nicht interaktive Automatisierungsidentitäten, begrenzen Sie deren aktuellen Berechtigungsrahmen und rotieren Sie einmal sichtbare Zugangsdaten ohne menschliche Anmeldung.", + "body": "Dienstkonten sind mandanteneigene Automatisierungsidentitäten ohne Passwort und ohne interaktive Sitzung. Administrierende legen zuerst den Berechtigungsrahmen des Kontos fest und erstellen danach eine oder mehrere unabhängig widerrufbare Zugangsdaten. Jede Zugangsdaten-Berechtigung muss innerhalb dieses Rahmens liegen. Bei jeder Anfrage wird sie erneut mit dem aktuellen Rahmen geschnitten; eine Verkleinerung des Rahmens oder eine Deaktivierung wirkt deshalb sofort. Das Geheimnis wird nur bei Erstellung oder Rotation einmal angezeigt. GovOPlaN speichert anschließend ausschließlich einen Einweg-Hash und das sichtbare Präfix; das Geheimnis kann weder angezeigt noch wiederhergestellt werden. Eine Rotation erzeugt in einer Transaktion den Ersatz und widerruft die vorherigen Zugangsdaten. Ein Widerruf unterbricht bestehende Clients sofort. Die Deaktivierung stoppt alle Anmeldungen des Dienstkontos, kann aber wieder aufgehoben werden. Das endgültige Stilllegen deaktiviert die zugrunde liegende Identität und widerruft sämtliche aktiven Zugangsdaten. Jede Änderung verwendet die aktuelle Revision des Dienstkontos; bei einem Konflikt muss die Ansicht neu geladen werden, damit keine parallele Änderung überschrieben wird.", + } + }, metadata={ "kind": "workflow", - "help_contexts": ["access.admin.service-accounts"], + "help_contexts": [ + "access.admin.service-accounts", + "access.service-accounts.action.reload", + "access.service-accounts.action.create", + "access.service-accounts.action.manage", + "access.service-accounts.action.edit", + "access.service-accounts.action.save", + "access.service-accounts.action.activation", + "access.service-accounts.action.retire", + "access.service-accounts.account-editor", + "access.service-accounts.field.name", + "access.service-accounts.field.description", + "access.service-accounts.field.scope-ceiling", + "access.service-accounts.field.show-revoked", + "access.service-accounts.credential-editor", + "access.service-accounts.action.create-credential", + "access.service-accounts.action.save-credential", + "access.service-accounts.action.rotate-credential", + "access.service-accounts.action.revoke-credential", + "access.service-accounts.field.credential-name", + "access.service-accounts.field.credential-expiry", + "access.service-accounts.field.credential-scopes", + "access.service-accounts.secret", + "access.service-accounts.confirm-revoke-credential", + "access.service-accounts.confirm-retire", + ], "prerequisites": [ "The tenant permits API credentials.", "You have service-account write permission and may delegate every selected scope.", @@ -751,6 +860,17 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( "Record the one-time secret in an external secret manager.", "Rotate credentials before expiry and revoke credentials that are no longer used.", ], + "outcome": "The automation principal remains non-interactive and can authenticate only through an active credential whose grant is within the account's current scope ceiling.", + "limitations": [ + "One-time credential secrets cannot be displayed or recovered after the creation dialog closes.", + "Deactivation and a reduced scope ceiling affect clients immediately but do not rewrite their external configuration.", + "Retirement revokes every active credential and requires a new service account for later reuse.", + ], + "consequences": [ + "Rotation revokes the previous credential in the same transaction that creates its replacement.", + "Credential revocation, account deactivation, and retirement immediately reject affected client requests.", + "A stale revision is rejected so a concurrent administration change is not overwritten.", + ], "verification": "The administration table shows the expected active credential count, last-use timestamp, revision, and audit events without exposing secret material.", }, ), @@ -1134,7 +1254,7 @@ def _people_search(context: ModuleContext) -> object: manifest = ModuleManifest( id="access", name="Access", - version="0.1.18", + version="0.1.19", optional_dependencies=("identity", "organizations", "tenancy", "idm"), provides_interfaces=( ModuleInterfaceProvider(name=CAPABILITY_ACCESS_PEOPLE_SEARCH, version="0.1.0"), diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py index 08c8d29..f05711d 100644 --- a/tests/test_interface_documentation_contract.py +++ b/tests/test_interface_documentation_contract.py @@ -22,8 +22,16 @@ class InterfaceDocumentationContractTests(unittest.TestCase): "access.admin.tenant-users", "access.admin.tenant-groups", "access.admin.tenant-roles", + }, + "access.workflow.manage-api-keys": { "access.admin.api-keys", - "access.admin.service-accounts", + "access.api-keys.action.create", + "access.api-keys.action.revoke", + "access.api-keys.field.owner", + "access.api-keys.field.expiry", + "access.api-keys.field.scopes", + "access.api-keys.secret", + "access.api-keys.confirm-revoke", }, "access.workflow.manage-reusable-credentials": { "access.admin.system-credentials", @@ -44,6 +52,16 @@ class InterfaceDocumentationContractTests(unittest.TestCase): }, "access.workflow.manage-service-account-credentials": { "access.admin.service-accounts", + "access.service-accounts.action.create", + "access.service-accounts.action.activation", + "access.service-accounts.action.retire", + "access.service-accounts.field.scope-ceiling", + "access.service-accounts.action.rotate-credential", + "access.service-accounts.action.revoke-credential", + "access.service-accounts.field.credential-expiry", + "access.service-accounts.field.credential-scopes", + "access.service-accounts.secret", + "access.service-accounts.confirm-retire", }, "access.workflow.manage-sessions": { "access.settings.sessions", @@ -71,6 +89,28 @@ class InterfaceDocumentationContractTests(unittest.TestCase): credential_topic.translations["de"]["body"], ) + api_key_topic = topics["access.workflow.manage-api-keys"] + self.assertEqual( + {"title", "summary", "body"}, + set(api_key_topic.translations["de"]), + ) + self.assertIn( + "sofort und kann für diesen Schlüssel nicht rückgängig gemacht werden", + api_key_topic.translations["de"]["body"], + ) + + service_account_topic = topics[ + "access.workflow.manage-service-account-credentials" + ] + self.assertEqual( + {"title", "summary", "body"}, + set(service_account_topic.translations["de"]), + ) + self.assertIn( + "widerruft sämtliche aktiven Zugangsdaten", + service_account_topic.translations["de"]["body"], + ) + def test_access_admin_surfaces_remain_declared(self) -> None: surface_ids = { surface.id for surface in manifest.frontend.view_surfaces diff --git a/webui/package.json b/webui/package.json index 30f9ff6..2132160 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/access-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "scripts": { diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs index 7f13ce6..84a5491 100644 --- a/webui/scripts/test-interface-pattern-language.mjs +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -49,6 +49,33 @@ assert.match(serviceAccounts, /rotateServiceAccountCredential/); assert.match(serviceAccounts, /revokeServiceAccountCredential/); assert.match(serviceAccounts, /Secrets are shown once/); assert.match(serviceAccounts, / row.last_used_at || "", render: (row) => formatDateTime(row.last_used_at) }, { id: "expires", header: "i18n:govoplan-access.expires.a99be3da", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.expires_at || "", render: (row) => row.expires_at ? formatDateTime(row.expires_at) : "i18n:govoplan-access.no_expiry.39d436aa" }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 108, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, - { id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: , variant: "danger", applicable: !row.revoked_at, disabled: !canRevoke, disabledReason: row.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : !canRevoke ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setRevoking(row) } + { id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.name }), icon: , helpContextId: "access.api-keys.action.inspect", helpModuleId: "access", onClick: () => setViewing(row) }, + { id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: , variant: "danger", helpContextId: "access.api-keys.action.revoke", helpModuleId: "access", applicable: !row.revoked_at, disabled: !canRevoke, disabledReason: row.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : !canRevoke ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setRevoking(row) } ]} /> }], [canRevoke]); @@ -153,20 +153,20 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: { return ( <> - } variant="primary" onClick={openCreate} disabled={!canCreate || !users.length} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : !users.length ? ACCESS_INTERFACE_I18N.selectUserAndScopes : undefined} />}> + } variant="primary" helpContextId="access.api-keys.action.create" helpModuleId="access" onClick={openCreate} disabled={!canCreate || !users.length} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : !users.length ? ACCESS_INTERFACE_I18N.selectUserAndScopes : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_api_keys_found.1f377128" />
- !busy && setCreating(false)} className="" footer={<>}> + !busy && setCreating(false)} className="" footer={<>}> - setDraft({ ...draft, name: event.target.value })} /> - - setDraft({ ...draft, expiresAt: value })} /> + setDraft({ ...draft, name: event.target.value })} /> + + setDraft({ ...draft, expiresAt: value })} /> -
i18n:govoplan-access.allowed_scopes.d94515ff ({ id: permission.scope, label: permission.label, description: i18nMessage("i18n:govoplan-access.value_value.0e2772ed", { value0: permission.scope, value1: permission.description }) }))} selected={draft.scopes} onChange={(scopes) => setDraft({ ...draft, scopes })} emptyText="i18n:govoplan-access.the_selected_user_has_no_tenant_permissions_avai.96985ec7" />
+
i18n:govoplan-access.allowed_scopes.d94515ff ({ id: permission.scope, label: permission.label, description: i18nMessage("i18n:govoplan-access.value_value.0e2772ed", { value0: permission.scope, value1: permission.description }) }))} selected={draft.scopes} onChange={(scopes) => setDraft({ ...draft, scopes })} emptyText="i18n:govoplan-access.the_selected_user_has_no_tenant_permissions_avai.96985ec7" />
- setViewing(null)} className="" footer={}> + setViewing(null)} className="" footer={}> {viewing && <> i18n:govoplan-access.name.709a2322}>{viewing.name}i18n:govoplan-access.prefix.90eceb01}>{viewing.prefix}… i18n:govoplan-access.owner.89ff3122}>{viewing.user_email}i18n:govoplan-access.status.bae7d5be}>{viewing.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : "i18n:govoplan-access.active.a733b809"} @@ -175,11 +175,11 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: {

i18n:govoplan-access.scopes.c23540e5

{viewing.scopes.map((scope) => {scope})}
}
- setSecret(null)} className="" footer={}> + setSecret(null)} className="" footer={}> {secret && <>

i18n:govoplan-access.the_secret_for.c60737ef {secret.name} i18n:govoplan-access.is_shown_once.af2b1235

{secret.value}

i18n:govoplan-access.store_it_in_a_secret_manager_only_its_prefix_and.796ac588

}
- setRevoking(null)} onConfirm={() => void revoke()} /> + setRevoking(null)} onConfirm={() => void revoke()} /> ); } diff --git a/webui/src/features/admin/ServiceAccountsPanel.tsx b/webui/src/features/admin/ServiceAccountsPanel.tsx index 18cf472..bb67ebb 100644 --- a/webui/src/features/admin/ServiceAccountsPanel.tsx +++ b/webui/src/features/admin/ServiceAccountsPanel.tsx @@ -214,7 +214,7 @@ export default function ServiceAccountsPanel({ resizable: false, align: "right", render: (row) => , onClick: () => void openManager(row) } + { id: "manage", label: `Manage ${row.name}`, icon: , helpContextId: "access.service-accounts.action.manage", helpModuleId: "access", onClick: () => void openManager(row) } ]} /> } ], []); @@ -274,6 +274,8 @@ export default function ServiceAccountsPanel({ id: "rotate", label: `Rotate ${row.name}`, icon: , + helpContextId: "access.service-accounts.action.rotate-credential", + helpModuleId: "access", applicable: !row.revoked_at, disabled: !canWrite || !managing?.is_active, disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing?.is_active ? "Activate the service account first." : undefined, @@ -284,6 +286,8 @@ export default function ServiceAccountsPanel({ label: `Revoke ${row.name}`, icon: , variant: "danger", + helpContextId: "access.service-accounts.action.revoke-credential", + helpModuleId: "access", applicable: !row.revoked_at, disabled: !canWrite, disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, @@ -445,10 +449,12 @@ export default function ServiceAccountsPanel({ loading={loading} error={error} success={success} + helpContextId="access.admin.service-accounts" + helpModuleId="access" actions={<> - - } variant="primary" onClick={openCreateAccount} disabled={!canWrite} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined} /> + + } variant="primary" helpContextId="access.service-accounts.action.create" helpModuleId="access" onClick={openCreateAccount} disabled={!canWrite} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined} /> } >
@@ -459,15 +465,17 @@ export default function ServiceAccountsPanel({ !busy && setAccountEditor(null)} className="" - footer={<>} + footer={<>} > - setAccountDraft({ ...accountDraft, name: event.target.value })} /> - setAccountDraft({ ...accountDraft, description: event.target.value })} /> + setAccountDraft({ ...accountDraft, name: event.target.value })} /> + setAccountDraft({ ...accountDraft, description: event.target.value })} /> -
+
Scope ceiling ({ id: permission.scope, label: permission.label, description: `${permission.scope} - ${permission.description}` }))} selected={accountDraft.scopes} onChange={(scopes) => setAccountDraft({ ...accountDraft, scopes })} emptyText="No tenant scopes can be delegated by your current account." />
@@ -476,6 +484,8 @@ export default function ServiceAccountsPanel({ !busy && setManaging(null)} className="" footer={} @@ -488,13 +498,13 @@ export default function ServiceAccountsPanel({ - - - - } variant="primary" onClick={() => openCredentialEditor("create")} disabled={!canWrite || !managing.is_active} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing.is_active ? "Activate the service account first." : undefined} /> + + + + } variant="primary" helpContextId="access.service-accounts.action.create-credential" helpModuleId="access" onClick={() => openCredentialEditor("create")} disabled={!canWrite || !managing.is_active} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : !managing.is_active ? "Activate the service account first." : undefined} /> - +
row.id} emptyText="No credentials found." /> @@ -506,27 +516,29 @@ export default function ServiceAccountsPanel({ !busy && setCredentialEditor(null)} className="" - footer={<>} + footer={<>} > {credentialEditor?.mode === "rotate" &&

Rotation creates a new secret and revokes the previous credential in the same transaction.

} - setCredentialDraft({ ...credentialDraft, name: event.target.value })} /> - setCredentialDraft({ ...credentialDraft, expiresAt: value })} /> + setCredentialDraft({ ...credentialDraft, name: event.target.value })} /> + setCredentialDraft({ ...credentialDraft, expiresAt: value })} /> -
+
Credential scopes ({ id: permission.scope, label: permission.label, description: `${permission.scope} - ${permission.description}` }))} selected={credentialDraft.scopes} onChange={(scopes) => setCredentialDraft({ ...credentialDraft, scopes })} emptyText="The service account has no credential scopes available." />
- setSecret(null)} className="" footer={}> + setSecret(null)} className="" footer={}> {secret && <>

The secret for {secret.name} is shown once.

{secret.value}

Store it in a secret manager. GovOPlaN retains only a one-way hash and the visible prefix.

}
- setRevoking(null)} onConfirm={() => void revokeCredential()} /> - setRetiring(false)} onConfirm={() => void retire()} /> + setRevoking(null)} onConfirm={() => void revokeCredential()} /> + setRetiring(false)} onConfirm={() => void retire()} /> ); }