Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8a05f8b9 | ||
|
|
e55434f406 | ||
|
|
a889071b71 | ||
|
|
8a43b9b676 |
@@ -66,6 +66,9 @@ This module will own:
|
|||||||
capabilities, including the bounded `access.governanceProjection.v1` bulk
|
capabilities, including the bounded `access.governanceProjection.v1` bulk
|
||||||
reconciliation contract used by Admin for idempotent per-assignment outcomes
|
reconciliation contract used by Admin for idempotent per-assignment outcomes
|
||||||
- access-owned migrations
|
- access-owned migrations
|
||||||
|
- a provider-neutral tenant-erasure contribution that removes tenant-scoped
|
||||||
|
credentials and authorization projections while preserving shared global
|
||||||
|
accounts and identities
|
||||||
|
|
||||||
The governance-template routes under `/admin/system/governance-templates` are
|
The governance-template routes under `/admin/system/governance-templates` are
|
||||||
contributed by `govoplan-admin`; access must not register those routes.
|
contributed by `govoplan-admin`; access must not register those routes.
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ contracts.
|
|||||||
- tenant owner provisioning and default access bootstrap
|
- tenant owner provisioning and default access bootstrap
|
||||||
- materializing governance templates into access-owned groups and roles
|
- materializing governance templates into access-owned groups and roles
|
||||||
- access-owned SQLAlchemy metadata and migrations for `access_*` tables
|
- access-owned SQLAlchemy metadata and migrations for `access_*` tables
|
||||||
|
- the `tenancy.erasure_provider.access` contribution, which previews and
|
||||||
|
idempotently removes only target-tenant credentials and authorization rows
|
||||||
|
while retaining global accounts and identities shared with other tenants
|
||||||
|
|
||||||
The active access tables use the `access_*` namespace while the model classes
|
The active access tables use the `access_*` namespace while the model classes
|
||||||
live in this module: `access_accounts`, `access_users`, `access_groups`,
|
live in this module: `access_accounts`, `access_users`, `access_groups`,
|
||||||
@@ -64,6 +67,22 @@ Access declares tenancy as an optional module integration. It uses the
|
|||||||
core-owned `core_scopes` table as the scope table, but it must not import
|
core-owned `core_scopes` table as the scope table, but it must not import
|
||||||
`govoplan_tenancy` or require the tenancy package to start.
|
`govoplan_tenancy` or require the tenancy package to start.
|
||||||
|
|
||||||
|
## Tenant-Erasure Boundary
|
||||||
|
|
||||||
|
Access implements the Core tenant-erasure provider contract without importing
|
||||||
|
Tenancy. Its preview counts every Access table with a tenant boundary. The
|
||||||
|
first destructive step removes target-tenant sessions and API keys; the second
|
||||||
|
removes service accounts, memberships, groups, tenant roles, organization
|
||||||
|
units, functions, assignments, and delegations in dependency-safe order.
|
||||||
|
Both steps are database-transactional and idempotent, so reconciliation can
|
||||||
|
repeat them after an interrupted response.
|
||||||
|
|
||||||
|
Global accounts, system-role assignments, identities, and identity-account
|
||||||
|
links are intentionally retained: they are installation-wide facts and may be
|
||||||
|
used by another tenant. Provider previews and receipts contain counts and
|
||||||
|
stable references only, never password hashes, session tokens, API-key hashes,
|
||||||
|
email addresses, or other credential material.
|
||||||
|
|
||||||
## Core-Only Startup Contract
|
## Core-Only Startup Contract
|
||||||
|
|
||||||
A core-only installation must be able to start far enough to expose process
|
A core-only installation must be able to start far enough to expose process
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.24",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
|
|||||||
+2
-2
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-access"
|
name = "govoplan-access"
|
||||||
version = "0.1.20"
|
version = "0.1.24"
|
||||||
description = "GovOPlaN access platform module with identity, auth, RBAC, and scope primitives."
|
description = "GovOPlaN access platform module with identity, auth, RBAC, and scope primitives."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.18",
|
"govoplan-core>=0.1.43",
|
||||||
"redis>=5,<6",
|
"redis>=5,<6",
|
||||||
"SQLAlchemy>=2,<3",
|
"SQLAlchemy>=2,<3",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""GovOPlaN access platform module."""
|
"""GovOPlaN access platform module."""
|
||||||
|
|
||||||
__version__ = "0.1.20"
|
__version__ = "0.1.24"
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
"""German translations for public structured documentation metadata."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
GERMAN_STRUCTURED_TRANSLATIONS: dict[str, dict[str, Any]] = {'access.reference.admin-access-fields': {'fields': [{'admin_description': 'Auf dem Konto und den '
|
||||||
|
'Mitglieder-Payloads '
|
||||||
|
'gespeichert. Es muss '
|
||||||
|
'normalisiert und '
|
||||||
|
'eindeutig für das '
|
||||||
|
'entsprechende '
|
||||||
|
'Login-Konto sein.',
|
||||||
|
'api_field': 'email',
|
||||||
|
'api_path': '/api/v1/admin/users',
|
||||||
|
'field_id': 'access.user.email',
|
||||||
|
'label': 'E-Mail',
|
||||||
|
'permission_scope': 'access:membership:create',
|
||||||
|
'provenance': 'Mandantenmitgliedschaft oder '
|
||||||
|
'Kontosuche.',
|
||||||
|
'user_description': 'Die Adresse, die '
|
||||||
|
'verwendet wird, um die '
|
||||||
|
'Person zu '
|
||||||
|
'identifizieren, wenn '
|
||||||
|
'sie sich anmelden.',
|
||||||
|
'validation': 'Muss eine gültige '
|
||||||
|
'E-Mail-Adresse sein.'},
|
||||||
|
{'admin_description': 'Wird, sofern verfügbar, '
|
||||||
|
'in Benutzer- und '
|
||||||
|
'Kontoantworten als '
|
||||||
|
'display_name ausgegeben.',
|
||||||
|
'api_field': 'display_name',
|
||||||
|
'api_path': '/api/v1/admin/users',
|
||||||
|
'field_id': 'access.user.display_name',
|
||||||
|
'label': 'Anzeigename',
|
||||||
|
'permission_scope': 'access:membership:update',
|
||||||
|
'provenance': 'Profil der '
|
||||||
|
'Mandantenmitgliedschaft.',
|
||||||
|
'user_description': 'Der lesbare Name, der '
|
||||||
|
'in Benutzerlisten und '
|
||||||
|
'Bewertungsbildschirmen '
|
||||||
|
'angezeigt wird.',
|
||||||
|
'validation': 'Menschenlesbarer Text; Halten '
|
||||||
|
'Sie ihn für Administratoren '
|
||||||
|
'erkennbar.'},
|
||||||
|
{'admin_description': 'Wird beim Aktualisieren '
|
||||||
|
'einer Benutzer- oder '
|
||||||
|
'Gruppenmitgliedschaft '
|
||||||
|
'als group_ids übertragen.',
|
||||||
|
'api_field': 'group_ids',
|
||||||
|
'api_path': '/api/v1/admin/users/{user_id}',
|
||||||
|
'field_id': 'access.user.groups',
|
||||||
|
'label': 'Gruppen',
|
||||||
|
'permission_scope': 'access:group:manage_members',
|
||||||
|
'provenance': 'Benutzergruppenmitgliedschaftszeilen.',
|
||||||
|
'user_description': 'Gemeinsame Zugriffsbündel, '
|
||||||
|
'die Rollen für viele '
|
||||||
|
'Personen gleichzeitig '
|
||||||
|
'hinzufügen können.',
|
||||||
|
'validation': 'Gruppen müssen zum gleichen '
|
||||||
|
'Mandant gehören.'},
|
||||||
|
{'admin_description': 'Wird bei '
|
||||||
|
'Aktualisierungsanforderungen '
|
||||||
|
'für Benutzer- und '
|
||||||
|
'Gruppenrollen als role_ids '
|
||||||
|
'übertragen.',
|
||||||
|
'api_field': 'role_ids',
|
||||||
|
'api_path': '/api/v1/admin/users/{user_id}',
|
||||||
|
'field_id': 'access.user.roles',
|
||||||
|
'label': 'Rollen',
|
||||||
|
'permission_scope': 'access:role:assign',
|
||||||
|
'provenance': 'Direkte Benutzerrollen plus '
|
||||||
|
'Gruppenrollenvererbung.',
|
||||||
|
'user_description': 'Direktzugangszuschüsse, '
|
||||||
|
'die einer Person '
|
||||||
|
'zugewiesen oder von '
|
||||||
|
'Gruppen geerbt wurden.',
|
||||||
|
'validation': 'Rollen müssen zuordenbar sein '
|
||||||
|
'und dürfen das '
|
||||||
|
'Delegationslimit der '
|
||||||
|
'handelnden Person nicht '
|
||||||
|
'überschreiten.'},
|
||||||
|
{'admin_description': 'Bildet Scopes beim '
|
||||||
|
'Erstellen eines '
|
||||||
|
'API-Schlüssel zu und '
|
||||||
|
'wird mit den aktuellen '
|
||||||
|
'Berechtigungen des '
|
||||||
|
'Besitzers geschnitten.',
|
||||||
|
'api_field': 'scopes',
|
||||||
|
'api_path': '/api/v1/admin/api-keys',
|
||||||
|
'field_id': 'access.api_key.scopes',
|
||||||
|
'label': 'Anwendungsbereiche',
|
||||||
|
'permission_scope': 'access:api_key:create',
|
||||||
|
'provenance': 'API-Schlüssel Grant plus '
|
||||||
|
'Eigentümerdelegation.',
|
||||||
|
'user_description': 'Die Aktionen, die ein '
|
||||||
|
'API-Schlüssel ausführen '
|
||||||
|
'kann.',
|
||||||
|
'validation': 'Verwenden Sie möglichst enge '
|
||||||
|
'Berechtigungsbereiche.'}]},
|
||||||
|
'access.reference.personal-navigation': {'outcome': 'Die Seitenschiene des Benutzers spiegelt die '
|
||||||
|
'persönlichen Präferenzen wider, während '
|
||||||
|
'verschlossene und unzugängliche Einträge '
|
||||||
|
'durch übergeordnete Richtlinien geregelt '
|
||||||
|
'bleiben.'},
|
||||||
|
'access.workflow.configuration-packages': {'limitations': ['Die Paketübernahme installiert keine '
|
||||||
|
'fehlenden Module.',
|
||||||
|
'Die anbieterübergreifende Übernahme ist keine atomar '
|
||||||
|
'verteilte Transaktion.',
|
||||||
|
'Generisches Rollback hängt von einem '
|
||||||
|
'beibehaltenen '
|
||||||
|
'vor der Übernahme erstellten Datenbank-Snapshot ab.'],
|
||||||
|
'operational_consequences': ['Ein abgestandener oder '
|
||||||
|
'blockierter Preflight '
|
||||||
|
'muss vor der Anwendung '
|
||||||
|
'erneut durchgeführt '
|
||||||
|
'werden.',
|
||||||
|
'Eine teilweise Anwendung '
|
||||||
|
'erfordert eine '
|
||||||
|
'Wiederherstellung, bevor '
|
||||||
|
'das Paket erneut '
|
||||||
|
'getestet wird.',
|
||||||
|
'Geheimwerte bleiben '
|
||||||
|
'außerhalb tragbarer '
|
||||||
|
'Fragmente und '
|
||||||
|
'Herkunft.']},
|
||||||
|
'access.workflow.data-subject-request': {'limitations': ['Module ohne DSAR-Anbieter werden als '
|
||||||
|
'Deckungslücken gemeldet.',
|
||||||
|
'Globale Konten und Identitäten werden '
|
||||||
|
'nicht automatisch gelöscht.']},
|
||||||
|
'access.workflow.grant-user-access': {'outcome': 'Eine Person kann sich beim Mandant anmelden und '
|
||||||
|
'erhält den beabsichtigten Zugang durch Gruppen '
|
||||||
|
'und Rollen.',
|
||||||
|
'prerequisites': ['Sie können Admin öffnen.',
|
||||||
|
'Sie können Benutzer, Gruppen und Rollen '
|
||||||
|
'lesen.',
|
||||||
|
'Schreib- oder Zuweisungsaktionen '
|
||||||
|
'erfordern übereinstimmende '
|
||||||
|
'Verwaltungsberechtigungen.'],
|
||||||
|
'result': 'Die Mitgliedschaft hat die beabsichtigten '
|
||||||
|
'effektiven Berechtigungen und keine breiteren '
|
||||||
|
'Rollen als nötig.',
|
||||||
|
'steps': ['Öffnen Sie Admin und gehen Sie zu Benutzern.',
|
||||||
|
'Finden Sie die bestehende Person oder erstellen '
|
||||||
|
'Sie eine Mitgliedschaft mit ihrer E-Mail-Adresse '
|
||||||
|
'und dem Anzeigenamen.',
|
||||||
|
'Überprüfen Sie aktuelle Gruppen und direkte '
|
||||||
|
'Rollen, bevor Sie etwas ändern.',
|
||||||
|
'Fügen Sie die Person der kleinsten Gruppe hinzu, '
|
||||||
|
'die den erforderlichen gemeinsamen Zugriff '
|
||||||
|
'gewährt.',
|
||||||
|
'Weisen Sie direkte Rollen nur zu, wenn eine '
|
||||||
|
'Gruppe nicht mit dem Fall übereinstimmt.',
|
||||||
|
'Speichern und überprüfen Sie eine '
|
||||||
|
'Blockernachricht, bevor Sie einen System- oder '
|
||||||
|
'Mandantbesitzer um Hilfe bitten.'],
|
||||||
|
'verification': 'Öffnen Sie den Benutzer erneut und '
|
||||||
|
'vergleichen Sie Gruppen, direkte Rollen '
|
||||||
|
'und effektive Berechtigungen mit der '
|
||||||
|
'Anforderung.'},
|
||||||
|
'access.workflow.manage-api-keys': {'consequences': ['Der Widerruf lehnt nachfolgende Anfragen, '
|
||||||
|
'die mit dem Schlüssel gestellt wurden, '
|
||||||
|
'sofort ab.',
|
||||||
|
'Durch das Entfernen von Berechtigungen vom '
|
||||||
|
'Besitzer wird der effektive '
|
||||||
|
'Schlüsselzugriff sofort eingeschränkt.'],
|
||||||
|
'limitations': ['Ein einmaliges Geheimnis kann nach dem '
|
||||||
|
'Schließen des Erstellungsdialogs nicht '
|
||||||
|
'angezeigt oder wiederhergestellt werden.',
|
||||||
|
'Ändern des Besitzers, Ablauf oder Scopes '
|
||||||
|
'erfordert einen Ersatzschlüssel.',
|
||||||
|
'Der Widerruf aktualisiert keine externen '
|
||||||
|
'Clients; die Betreiber müssen bei Bedarf '
|
||||||
|
'einen Ersatz installieren.'],
|
||||||
|
'outcome': 'Der Automatisierungsclient verfügt über einen '
|
||||||
|
'zeitlich begrenzten Berechtigungsnachweis, dessen '
|
||||||
|
'effektiver Zugriff weder seine gespeicherten '
|
||||||
|
'Berechtigungsbereiche noch die aktuellen '
|
||||||
|
'Berechtigungen seines Besitzers überschreiten '
|
||||||
|
'kann.',
|
||||||
|
'prerequisites': ['Der Mandant erlaubt '
|
||||||
|
'API-Anmeldeinformationen.',
|
||||||
|
'Die handelnde Person kann API-Schlüssel '
|
||||||
|
'erstellen oder widerrufen und jeden '
|
||||||
|
'ausgewählten Bereich delegieren.',
|
||||||
|
'Ein zugelassener externer Geheimmanager '
|
||||||
|
'und rechenschaftspflichtiger Eigentümer '
|
||||||
|
'sind bekannt.'],
|
||||||
|
'steps': ['Wählen Sie den verantwortlichen Eigentümer und die '
|
||||||
|
'engsten erforderlichen Berechtigungsbereiche.',
|
||||||
|
'Legen Sie den kürzesten praktischen Ablauf fest, '
|
||||||
|
'bevor Sie den Schlüssel erstellen.',
|
||||||
|
'Übertragen Sie das einmalige Geheimnis direkt in '
|
||||||
|
'den genehmigten Geheimmanager.',
|
||||||
|
'Widerrufen Sie den Schlüssel, wenn sein Client, '
|
||||||
|
'Eigentümer oder Zweck nicht mehr gültig ist.'],
|
||||||
|
'verification': 'Laden Sie das Schlüsselverzeichnis neu, '
|
||||||
|
'überprüfen Sie Eigentümer, Präfix, '
|
||||||
|
'Berechtigungsumfang, Ablauf und Status und '
|
||||||
|
'testen Sie dann den beabsichtigten Client, '
|
||||||
|
'ohne geheimes Material in Nachweise zu '
|
||||||
|
'kopieren.'},
|
||||||
|
'access.workflow.manage-reusable-credentials': {'limitations': ['GovOPlaN kann ein konfiguriertes '
|
||||||
|
'Geheimnis nicht anzeigen oder '
|
||||||
|
'wiederherstellen.',
|
||||||
|
'Eine leere Modul- oder '
|
||||||
|
'Serverbeschränkung bedeutet '
|
||||||
|
'jeden Wert, der nach '
|
||||||
|
'Berechtigungsumfang zulässig '
|
||||||
|
'ist.',
|
||||||
|
'Das Löschen oder Leeren eines '
|
||||||
|
'Geheimnisses schreibt keine '
|
||||||
|
'abhängigen Verbindungsreferenzen '
|
||||||
|
'neu.'],
|
||||||
|
'outcome': 'Die Zugangsdaten bleiben '
|
||||||
|
'schreibgeschützt und sind nur '
|
||||||
|
'innerhalb seines aktiven '
|
||||||
|
'Berechtigungsumfangs, Moduls, Servers '
|
||||||
|
'und Autorisierungsgrenzen verwendbar.',
|
||||||
|
'prerequisites': ['Der beabsichtigte '
|
||||||
|
'Berechtigungsinhaber wird '
|
||||||
|
'ausgewählt.',
|
||||||
|
'Die handelnde Person kann '
|
||||||
|
'Anmeldeinformationen lesen und '
|
||||||
|
'hat Schreibautorität für '
|
||||||
|
'Mutationen.',
|
||||||
|
'Der externe '
|
||||||
|
'Secret-Manager-Eigentümer und '
|
||||||
|
'abhängige Verbindungen sind '
|
||||||
|
'bekannt.'],
|
||||||
|
'steps': ['Wählen Sie den engsten Besitzumfang '
|
||||||
|
'und Anmeldetyp.',
|
||||||
|
'Beschränken Sie Module und Server '
|
||||||
|
'explizit, wenn eine breite Nutzung '
|
||||||
|
'nicht beabsichtigt ist.',
|
||||||
|
'Speichern Sie ein neues oder '
|
||||||
|
'Ersatzgeheimnis, ohne zu erwarten, '
|
||||||
|
'dass es erneut angezeigt wird.',
|
||||||
|
'Überprüfen Sie abhängige Verbindungen '
|
||||||
|
'vor der Deaktivierung, geheimen '
|
||||||
|
'Löschung oder Löschung.'],
|
||||||
|
'verification': 'Laden Sie die Liste der '
|
||||||
|
'Zugangsdaten neu, bestätigen Sie '
|
||||||
|
'deren Berechtigungsumfang und '
|
||||||
|
'Verfügbarkeit und testen Sie '
|
||||||
|
'dann jede beabsichtigte '
|
||||||
|
'abhängige Verbindung, ohne das '
|
||||||
|
'Geheimnis zu enthüllen.'},
|
||||||
|
'access.workflow.manage-service-account-credentials': {'consequences': ['Rotation widerruft den '
|
||||||
|
'vorherigen Nachweis in '
|
||||||
|
'der gleichen '
|
||||||
|
'Transaktion, die seinen '
|
||||||
|
'Ersatz schafft.',
|
||||||
|
'Der Widerruf, die '
|
||||||
|
'Deaktivierung des Kontos '
|
||||||
|
'und der Ruhestand lehnen '
|
||||||
|
'betroffene '
|
||||||
|
'Kundenanfragen sofort '
|
||||||
|
'ab.',
|
||||||
|
'Eine veraltete Revision '
|
||||||
|
'wird abgelehnt, so dass '
|
||||||
|
'ein gleichzeitiger '
|
||||||
|
'Verwaltungswechsel nicht '
|
||||||
|
'überschrieben wird.'],
|
||||||
|
'limitations': ['Einmalige '
|
||||||
|
'Anmeldegeheimnisse können '
|
||||||
|
'nach dem Schließen des '
|
||||||
|
'Erstellungsdialogs nicht '
|
||||||
|
'angezeigt oder '
|
||||||
|
'wiederhergestellt werden.',
|
||||||
|
'Deaktivierung und eine '
|
||||||
|
'reduzierte '
|
||||||
|
'Berechtigungsumfangsobergrenze '
|
||||||
|
'betreffen Clients sofort, '
|
||||||
|
'schreiben ihre externe '
|
||||||
|
'Konfiguration jedoch '
|
||||||
|
'nicht neu.',
|
||||||
|
'Der Ruhestand widerruft '
|
||||||
|
'alle aktiven '
|
||||||
|
'Anmeldeinformationen und '
|
||||||
|
'erfordert ein neues '
|
||||||
|
'Servicekonto für die '
|
||||||
|
'spätere '
|
||||||
|
'Wiederverwendung.'],
|
||||||
|
'outcome': 'Der Automatisierungsprinzipal '
|
||||||
|
'bleibt nicht interaktiv und '
|
||||||
|
'kann sich nur durch einen '
|
||||||
|
'aktiven Berechtigungsnachweis '
|
||||||
|
'authentifizieren, dessen '
|
||||||
|
'Gewährung innerhalb der '
|
||||||
|
'aktuellen '
|
||||||
|
'Berechtigungsumfangsobergrenze '
|
||||||
|
'des Kontos liegt.',
|
||||||
|
'prerequisites': ['Der Mandant erlaubt '
|
||||||
|
'API-Anmeldeinformationen.',
|
||||||
|
'Sie haben eine '
|
||||||
|
'Service-Account-Schreibberechtigung '
|
||||||
|
'und können jeden '
|
||||||
|
'ausgewählten Bereich '
|
||||||
|
'delegieren.'],
|
||||||
|
'steps': ['Erstellen Sie ein Servicekonto '
|
||||||
|
'und definieren Sie die engste '
|
||||||
|
'Nutzumfangsobergrenze.',
|
||||||
|
'Öffnen Sie das Konto und '
|
||||||
|
'erstellen Sie einen '
|
||||||
|
'Berechtigungsnachweis mit einem '
|
||||||
|
'gleichen oder engeren '
|
||||||
|
'Berechtigungsumfang.',
|
||||||
|
'Notieren Sie das einmalige '
|
||||||
|
'Geheimnis in einem externen '
|
||||||
|
'Geheimmanager.',
|
||||||
|
'Anmeldeinformationen vor Ablauf '
|
||||||
|
'drehen und Anmeldeinformationen '
|
||||||
|
'widerrufen, die nicht mehr '
|
||||||
|
'verwendet werden.'],
|
||||||
|
'verification': 'Die Verwaltungstabelle '
|
||||||
|
'zeigt die erwartete '
|
||||||
|
'Anzahl der aktiven '
|
||||||
|
'Anmeldeinformationen, den '
|
||||||
|
'Zeitstempel für die '
|
||||||
|
'letzte Verwendung, die '
|
||||||
|
'Revision und die '
|
||||||
|
'Audit-Ereignisse, ohne '
|
||||||
|
'geheimes Material '
|
||||||
|
'preiszugeben.'}}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from govoplan_access.backend.db.models import (
|
||||||
|
ApiKey,
|
||||||
|
AuthSession,
|
||||||
|
ExternalFunctionRoleAssignment,
|
||||||
|
Function,
|
||||||
|
FunctionAssignment,
|
||||||
|
FunctionDelegation,
|
||||||
|
FunctionRoleAssignment,
|
||||||
|
Group,
|
||||||
|
GroupRoleAssignment,
|
||||||
|
OrganizationUnit,
|
||||||
|
Role,
|
||||||
|
ServiceAccount,
|
||||||
|
User,
|
||||||
|
UserGroupMembership,
|
||||||
|
UserRoleAssignment,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.tenant_erasure import (
|
||||||
|
TENANT_ERASURE_PROVIDER_CAPABILITY_PREFIX,
|
||||||
|
TenantErasurePreview,
|
||||||
|
TenantErasureResource,
|
||||||
|
TenantErasureStep,
|
||||||
|
TenantErasureStepResult,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ACCESS_TENANT_ERASURE_CAPABILITY = (
|
||||||
|
f"{TENANT_ERASURE_PROVIDER_CAPABILITY_PREFIX}access"
|
||||||
|
)
|
||||||
|
|
||||||
|
_CREDENTIAL_MODELS = (AuthSession, ApiKey)
|
||||||
|
_TENANT_ACCESS_MODELS = (
|
||||||
|
ServiceAccount,
|
||||||
|
FunctionDelegation,
|
||||||
|
ExternalFunctionRoleAssignment,
|
||||||
|
FunctionRoleAssignment,
|
||||||
|
UserGroupMembership,
|
||||||
|
UserRoleAssignment,
|
||||||
|
GroupRoleAssignment,
|
||||||
|
FunctionAssignment,
|
||||||
|
Function,
|
||||||
|
OrganizationUnit,
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
Role,
|
||||||
|
)
|
||||||
|
_ALL_MODELS = _CREDENTIAL_MODELS + _TENANT_ACCESS_MODELS
|
||||||
|
|
||||||
|
|
||||||
|
def _counts(session: Session, tenant_id: str) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
model.__tablename__: session.query(model)
|
||||||
|
.filter(model.tenant_id == tenant_id)
|
||||||
|
.count()
|
||||||
|
for model in _ALL_MODELS
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_models(
|
||||||
|
session: Session,
|
||||||
|
tenant_id: str,
|
||||||
|
models: tuple[type, ...],
|
||||||
|
) -> int:
|
||||||
|
deleted = 0
|
||||||
|
for model in models:
|
||||||
|
deleted += (
|
||||||
|
session.query(model)
|
||||||
|
.filter(model.tenant_id == tenant_id)
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTenantErasureProvider:
|
||||||
|
module_id = "access"
|
||||||
|
|
||||||
|
def preview_tenant_erasure(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
tenant_id: str,
|
||||||
|
) -> TenantErasurePreview:
|
||||||
|
if not isinstance(session, Session):
|
||||||
|
raise TypeError("Access tenant erasure requires a database session.")
|
||||||
|
counts = _counts(session, tenant_id)
|
||||||
|
credential_count = sum(
|
||||||
|
counts[model.__tablename__] for model in _CREDENTIAL_MODELS
|
||||||
|
)
|
||||||
|
access_count = sum(
|
||||||
|
counts[model.__tablename__] for model in _TENANT_ACCESS_MODELS
|
||||||
|
)
|
||||||
|
resources = tuple(
|
||||||
|
TenantErasureResource(
|
||||||
|
resource_type=table_name,
|
||||||
|
count=count,
|
||||||
|
disposition="erase",
|
||||||
|
summary=f"{count} tenant-scoped Access records will be erased.",
|
||||||
|
)
|
||||||
|
for table_name, count in sorted(counts.items())
|
||||||
|
)
|
||||||
|
steps: list[TenantErasureStep] = []
|
||||||
|
if credential_count:
|
||||||
|
steps.append(
|
||||||
|
TenantErasureStep(
|
||||||
|
step_id="revoke-tenant-credentials",
|
||||||
|
kind="erase",
|
||||||
|
summary="Revoke tenant sessions and erase tenant API keys.",
|
||||||
|
destructive=True,
|
||||||
|
irreversible=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if access_count:
|
||||||
|
steps.append(
|
||||||
|
TenantErasureStep(
|
||||||
|
step_id="erase-tenant-access",
|
||||||
|
kind="erase",
|
||||||
|
summary=(
|
||||||
|
"Erase tenant memberships, service accounts, groups, roles, "
|
||||||
|
"organization units, functions, assignments, and delegations."
|
||||||
|
),
|
||||||
|
destructive=True,
|
||||||
|
irreversible=True,
|
||||||
|
depends_on=(
|
||||||
|
("revoke-tenant-credentials",) if credential_count else ()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return TenantErasurePreview(
|
||||||
|
module_id=self.module_id,
|
||||||
|
complete=True,
|
||||||
|
resources=resources,
|
||||||
|
steps=tuple(steps),
|
||||||
|
warnings=(
|
||||||
|
"Global accounts and identity links are retained because they may belong to other tenants.",
|
||||||
|
),
|
||||||
|
provider_revision="access-tenant-erasure-v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_tenant_erasure_step(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
tenant_id: str,
|
||||||
|
step_id: str,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> TenantErasureStepResult:
|
||||||
|
if not isinstance(session, Session):
|
||||||
|
raise TypeError("Access tenant erasure requires a database session.")
|
||||||
|
if not idempotency_key.strip():
|
||||||
|
raise ValueError("Access tenant erasure requires an idempotency key.")
|
||||||
|
if step_id == "revoke-tenant-credentials":
|
||||||
|
deleted = _delete_models(session, tenant_id, _CREDENTIAL_MODELS)
|
||||||
|
summary = "Tenant sessions and API keys were erased."
|
||||||
|
elif step_id == "erase-tenant-access":
|
||||||
|
deleted = _delete_models(session, tenant_id, _TENANT_ACCESS_MODELS)
|
||||||
|
summary = "Tenant-scoped Access records were erased."
|
||||||
|
else:
|
||||||
|
return TenantErasureStepResult(
|
||||||
|
state="blocked",
|
||||||
|
summary="Access tenant erasure step is unknown.",
|
||||||
|
)
|
||||||
|
return TenantErasureStepResult(
|
||||||
|
state="completed",
|
||||||
|
summary=summary,
|
||||||
|
receipt_ref=f"access:tenant-erasure:{tenant_id}:{step_id}",
|
||||||
|
metrics={"deleted": deleted},
|
||||||
|
)
|
||||||
|
|
||||||
|
def reconcile_tenant_erasure_step(
|
||||||
|
self,
|
||||||
|
session: object,
|
||||||
|
tenant_id: str,
|
||||||
|
step_id: str,
|
||||||
|
idempotency_key: str,
|
||||||
|
) -> TenantErasureStepResult:
|
||||||
|
return self.execute_tenant_erasure_step(
|
||||||
|
session,
|
||||||
|
tenant_id,
|
||||||
|
step_id,
|
||||||
|
idempotency_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ACCESS_TENANT_ERASURE_CAPABILITY",
|
||||||
|
"AccessTenantErasureProvider",
|
||||||
|
]
|
||||||
@@ -6,6 +6,18 @@ from govoplan_access.backend.manifest import manifest
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceDocumentationContractTests(unittest.TestCase):
|
class InterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_all_static_topics_have_complete_german_content(self) -> None:
|
||||||
|
for topic in manifest.documentation:
|
||||||
|
german = (topic.translations or {}).get("de", {})
|
||||||
|
self.assertEqual(
|
||||||
|
{"title", "summary", "body"},
|
||||||
|
set(german),
|
||||||
|
topic.id,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
all(str(value).strip() for value in german.values()), topic.id
|
||||||
|
)
|
||||||
|
|
||||||
def test_access_admin_topics_publish_stable_help_contexts(self) -> None:
|
def test_access_admin_topics_publish_stable_help_contexts(self) -> None:
|
||||||
topics = {topic.id: topic for topic in manifest.documentation}
|
topics = {topic.id: topic for topic in manifest.documentation}
|
||||||
|
|
||||||
@@ -112,9 +124,7 @@ class InterfaceDocumentationContractTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_access_admin_surfaces_remain_declared(self) -> None:
|
def test_access_admin_surfaces_remain_declared(self) -> None:
|
||||||
surface_ids = {
|
surface_ids = {surface.id for surface in manifest.frontend.view_surfaces}
|
||||||
surface.id for surface in manifest.frontend.view_surfaces
|
|
||||||
}
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
{
|
{
|
||||||
"access.admin.system-roles",
|
"access.admin.system-roles",
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from govoplan_access.backend.db.base import AccessBase
|
||||||
|
from govoplan_access.backend.db.models import (
|
||||||
|
Account,
|
||||||
|
ApiKey,
|
||||||
|
AuthSession,
|
||||||
|
Group,
|
||||||
|
Role,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
from govoplan_access.backend.tenant_erasure_provider import (
|
||||||
|
AccessTenantErasureProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_erasure_is_tenant_bounded_and_retains_global_account() -> None:
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||||
|
AccessBase.metadata.create_all(engine)
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
with Session(engine) as session:
|
||||||
|
account = Account(
|
||||||
|
email="shared@example.test",
|
||||||
|
normalized_email="shared@example.test",
|
||||||
|
is_active=True,
|
||||||
|
auth_provider="local",
|
||||||
|
)
|
||||||
|
session.add(account)
|
||||||
|
session.flush()
|
||||||
|
first = User(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
account_id=account.id,
|
||||||
|
email="shared@example.test",
|
||||||
|
is_active=True,
|
||||||
|
is_tenant_admin=True,
|
||||||
|
auth_provider="local",
|
||||||
|
)
|
||||||
|
second = User(
|
||||||
|
tenant_id="tenant-2",
|
||||||
|
account_id=account.id,
|
||||||
|
email="shared@example.test",
|
||||||
|
is_active=True,
|
||||||
|
is_tenant_admin=False,
|
||||||
|
auth_provider="local",
|
||||||
|
)
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
Group(tenant_id="tenant-1", slug="group", name="Group"),
|
||||||
|
Group(tenant_id="tenant-2", slug="group", name="Group"),
|
||||||
|
Role(tenant_id="tenant-1", slug="role", name="Role"),
|
||||||
|
Role(tenant_id="tenant-2", slug="role", name="Role"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.flush()
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
ApiKey(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
user_id=first.id,
|
||||||
|
name="key",
|
||||||
|
prefix="prefix",
|
||||||
|
key_hash="hash",
|
||||||
|
scopes=[],
|
||||||
|
),
|
||||||
|
AuthSession(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
user_id=first.id,
|
||||||
|
account_id=account.id,
|
||||||
|
token_hash="token-hash",
|
||||||
|
expires_at=now + timedelta(hours=1),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
provider = AccessTenantErasureProvider()
|
||||||
|
preview = provider.preview_tenant_erasure(session, "tenant-1")
|
||||||
|
|
||||||
|
assert preview.allowed
|
||||||
|
assert [step.step_id for step in preview.steps] == [
|
||||||
|
"revoke-tenant-credentials",
|
||||||
|
"erase-tenant-access",
|
||||||
|
]
|
||||||
|
assert "revoke-tenant-credentials" in preview.steps[1].depends_on
|
||||||
|
|
||||||
|
revoked = provider.execute_tenant_erasure_step(
|
||||||
|
session,
|
||||||
|
"tenant-1",
|
||||||
|
"revoke-tenant-credentials",
|
||||||
|
"operation:access:credentials",
|
||||||
|
)
|
||||||
|
erased = provider.execute_tenant_erasure_step(
|
||||||
|
session,
|
||||||
|
"tenant-1",
|
||||||
|
"erase-tenant-access",
|
||||||
|
"operation:access:tenant",
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
assert revoked.state == "completed"
|
||||||
|
assert erased.state == "completed"
|
||||||
|
assert provider.preview_tenant_erasure(session, "tenant-1").steps == ()
|
||||||
|
assert session.scalar(select(Account).where(Account.id == account.id)) is not None
|
||||||
|
assert session.scalar(select(User).where(User.tenant_id == "tenant-2")) is not None
|
||||||
|
assert session.scalar(select(Group).where(Group.tenant_id == "tenant-2")) is not None
|
||||||
|
assert session.scalar(select(Role).where(Role.tenant_id == "tenant-2")) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_access_erasure_replay_is_idempotent() -> None:
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||||
|
AccessBase.metadata.create_all(engine)
|
||||||
|
provider = AccessTenantErasureProvider()
|
||||||
|
with Session(engine) as session:
|
||||||
|
first = provider.execute_tenant_erasure_step(
|
||||||
|
session,
|
||||||
|
"tenant-1",
|
||||||
|
"erase-tenant-access",
|
||||||
|
"operation:access:tenant",
|
||||||
|
)
|
||||||
|
second = provider.reconcile_tenant_erasure_step(
|
||||||
|
session,
|
||||||
|
"tenant-1",
|
||||||
|
"erase-tenant-access",
|
||||||
|
"operation:access:tenant",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert first.metrics == {"deleted": 0}
|
||||||
|
assert second.metrics == {"deleted": 0}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.24",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -239,8 +239,10 @@ export default function SystemUsersPanel({
|
|||||||
<FormField label="i18n:govoplan-access.email.84add5b2"><input value={draft.email} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, email: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-access.email.84add5b2"><input value={draft.email} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, email: event.target.value })} /></FormField>
|
||||||
<FormField label="i18n:govoplan-access.display_name.c7874aaa"><input value={draft.displayName} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, displayName: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-access.display_name.c7874aaa"><input value={draft.displayName} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, displayName: event.target.value })} /></FormField>
|
||||||
{editing === "new" &&
|
{editing === "new" &&
|
||||||
<FormField label="i18n:govoplan-access.initial_password.2278be8c">
|
<FormField label="i18n:govoplan-access.initial_password.2278be8c" helpContextId="access.admin.system-users.initial-password" helpModuleId="access">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
helpContextId="access.admin.system-users.initial-password"
|
||||||
|
helpModuleId="access"
|
||||||
value={draft.password}
|
value={draft.password}
|
||||||
placeholder="i18n:govoplan-access.leave_empty_to_generate.e58222d8"
|
placeholder="i18n:govoplan-access.leave_empty_to_generate.e58222d8"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
|||||||
id: "revoke-session",
|
id: "revoke-session",
|
||||||
label: "i18n:govoplan-access.revoke_session.5e551007",
|
label: "i18n:govoplan-access.revoke_session.5e551007",
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
|
helpContextId: "access.sessions.action.revoke",
|
||||||
|
helpModuleId: "access",
|
||||||
applicable: !row.current,
|
applicable: !row.current,
|
||||||
disabled: busy || !canRevokeSessions,
|
disabled: busy || !canRevokeSessions,
|
||||||
disabledReason: !canRevokeSessions ? "i18n:govoplan-access.session_revocation_permission_required.5e551016" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined,
|
disabledReason: !canRevokeSessions ? "i18n:govoplan-access.session_revocation_permission_required.5e551016" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined,
|
||||||
@@ -283,8 +285,10 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
|||||||
<FormField label="i18n:govoplan-access.email.84add5b2"><input value={draft.email} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, email: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-access.email.84add5b2"><input value={draft.email} disabled={editing !== "new"} onChange={(event) => setDraft({ ...draft, email: event.target.value })} /></FormField>
|
||||||
<FormField label="i18n:govoplan-access.display_name.c7874aaa"><input value={draft.displayName} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, displayName: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-access.display_name.c7874aaa"><input value={draft.displayName} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, displayName: event.target.value })} /></FormField>
|
||||||
{editing === "new" &&
|
{editing === "new" &&
|
||||||
<FormField label="i18n:govoplan-access.initial_password.2278be8c">
|
<FormField label="i18n:govoplan-access.initial_password.2278be8c" helpContextId="access.admin.tenant-users.initial-password" helpModuleId="access">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
helpContextId="access.admin.tenant-users.initial-password"
|
||||||
|
helpModuleId="access"
|
||||||
value={draft.password}
|
value={draft.password}
|
||||||
placeholder="i18n:govoplan-access.leave_empty_to_generate.e58222d8"
|
placeholder="i18n:govoplan-access.leave_empty_to_generate.e58222d8"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
@@ -294,7 +298,7 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
|||||||
}
|
}
|
||||||
<FormField label="i18n:govoplan-access.membership_status.b77fc732"><select value={draft.isActive ? "active" : "inactive"} disabled={Boolean(editing && editing !== "new" && (!canSuspend || editing.is_last_active_owner))} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-access.active.a733b809</option><option value="inactive">i18n:govoplan-access.inactive.09af574c</option></select></FormField>
|
<FormField label="i18n:govoplan-access.membership_status.b77fc732"><select value={draft.isActive ? "active" : "inactive"} disabled={Boolean(editing && editing !== "new" && (!canSuspend || editing.is_last_active_owner))} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-access.active.a733b809</option><option value="inactive">i18n:govoplan-access.inactive.09af574c</option></select></FormField>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
{editing === "new" && <ToggleSwitch label="i18n:govoplan-access.require_password_change_when_account_settings_ar.69bce7a3" checked={draft.passwordResetRequired} onChange={(passwordResetRequired) => setDraft({ ...draft, passwordResetRequired })} />}
|
{editing === "new" && <ToggleSwitch label="i18n:govoplan-access.require_password_change_when_account_settings_ar.69bce7a3" checked={draft.passwordResetRequired} helpContextId="access.admin.tenant-users.require-password-change" helpModuleId="access" onChange={(passwordResetRequired) => setDraft({ ...draft, passwordResetRequired })} />}
|
||||||
{editing && editing !== "new" && editing.is_last_active_owner && <p className="admin-protection-note">i18n:govoplan-access.this_membership_is_the_tenant_s_last_active_oper.072b247f</p>}
|
{editing && editing !== "new" && editing.is_last_active_owner && <p className="admin-protection-note">i18n:govoplan-access.this_membership_is_the_tenant_s_last_active_oper.072b247f</p>}
|
||||||
<ContentGrid columns={2} spacing="block" collapseAt="wide">
|
<ContentGrid columns={2} spacing="block" collapseAt="wide">
|
||||||
<div><span className="form-label">i18n:govoplan-access.groups.ae9629f4</span><AdminSelectionList options={groups.filter((group) => group.is_active).map((group) => ({ id: group.id, label: group.name, description: group.description, disabled: !canManageGroups }))} selected={draft.groupIds} onChange={(groupIds) => setDraft({ ...draft, groupIds })} emptyText="i18n:govoplan-access.no_groups_exist_yet.9cd029f6" /></div>
|
<div><span className="form-label">i18n:govoplan-access.groups.ae9629f4</span><AdminSelectionList options={groups.filter((group) => group.is_active).map((group) => ({ id: group.id, label: group.name, description: group.description, disabled: !canManageGroups }))} selected={draft.groupIds} onChange={(groupIds) => setDraft({ ...draft, groupIds })} emptyText="i18n:govoplan-access.no_groups_exist_yet.9cd029f6" /></div>
|
||||||
@@ -320,11 +324,11 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu
|
|||||||
</>}
|
</>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog variant="administration" size="large" open={Boolean(revokingSession)} title="i18n:govoplan-access.revoke_session.5e551007" onClose={() => !busy && setRevokingSession(null)} className="" footer={<><Button onClick={() => setRevokingSession(null)} disabled={busy}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="danger" onClick={() => void revokeSelectedSession()} disabled={busy || !reauthorizationPassword} disabledReason={!reauthorizationPassword ? "i18n:govoplan-access.current_password_required.5e551019" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined}>i18n:govoplan-access.revoke_session.5e551007</Button></>}>
|
<Dialog variant="administration" size="large" open={Boolean(revokingSession)} title="i18n:govoplan-access.revoke_session.5e551007" helpContextId="access.sessions.action.revoke" helpModuleId="access" onClose={() => !busy && setRevokingSession(null)} className="" footer={<><Button onClick={() => setRevokingSession(null)} disabled={busy}>i18n:govoplan-access.cancel.77dfd213</Button><Button variant="danger" helpContextId="access.sessions.action.revoke" helpModuleId="access" onClick={() => void revokeSelectedSession()} disabled={busy || !reauthorizationPassword} disabledReason={!reauthorizationPassword ? "i18n:govoplan-access.current_password_required.5e551019" : busy ? ACCESS_INTERFACE_I18N.operationInProgress : undefined}>i18n:govoplan-access.revoke_session.5e551007</Button></>}>
|
||||||
{sessionError && <p className="admin-protection-note">{sessionError}</p>}
|
{sessionError && <p className="admin-protection-note">{sessionError}</p>}
|
||||||
<p>i18n:govoplan-access.admin_session_revocation_confirmation.5e551020</p>
|
<p>i18n:govoplan-access.admin_session_revocation_confirmation.5e551020</p>
|
||||||
<FormField label="i18n:govoplan-access.current_password.5e551021">
|
<FormField label="i18n:govoplan-access.current_password.5e551021" helpContextId="access.sessions.field.current-password" helpModuleId="access">
|
||||||
<PasswordField value={reauthorizationPassword} autoComplete="current-password" onValueChange={setReauthorizationPassword} />
|
<PasswordField helpContextId="access.sessions.field.current-password" helpModuleId="access" value={reauthorizationPassword} autoComplete="current-password" onValueChange={setReauthorizationPassword} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export default function SessionSettingsPanel({
|
|||||||
id: "revoke",
|
id: "revoke",
|
||||||
label: "i18n:govoplan-access.revoke_session.5e551007",
|
label: "i18n:govoplan-access.revoke_session.5e551007",
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
|
helpContextId: "access.sessions.action.revoke",
|
||||||
|
helpModuleId: "access",
|
||||||
applicable: !row.current,
|
applicable: !row.current,
|
||||||
disabled: busy,
|
disabled: busy,
|
||||||
disabledReason: busy
|
disabledReason: busy
|
||||||
@@ -208,6 +210,8 @@ export default function SessionSettingsPanel({
|
|||||||
destructiveActions={
|
destructiveActions={
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
helpContextId="access.sessions.action.revoke-others"
|
||||||
|
helpModuleId="access"
|
||||||
disabled={busy || sessions.filter((item) => !item.current).length === 0}
|
disabled={busy || sessions.filter((item) => !item.current).length === 0}
|
||||||
disabledReason={
|
disabledReason={
|
||||||
busy
|
busy
|
||||||
@@ -243,6 +247,8 @@ export default function SessionSettingsPanel({
|
|||||||
confirmLabel="i18n:govoplan-access.revoke_session.5e551007"
|
confirmLabel="i18n:govoplan-access.revoke_session.5e551007"
|
||||||
tone="danger"
|
tone="danger"
|
||||||
busy={busy}
|
busy={busy}
|
||||||
|
helpContextId="access.sessions.action.revoke"
|
||||||
|
helpModuleId="access"
|
||||||
onCancel={() => setRevoking(null)}
|
onCancel={() => setRevoking(null)}
|
||||||
onConfirm={() => void revokeOne()}
|
onConfirm={() => void revokeOne()}
|
||||||
/>
|
/>
|
||||||
@@ -253,6 +259,8 @@ export default function SessionSettingsPanel({
|
|||||||
confirmLabel="i18n:govoplan-access.revoke_all_other_sessions.5e551012"
|
confirmLabel="i18n:govoplan-access.revoke_all_other_sessions.5e551012"
|
||||||
tone="danger"
|
tone="danger"
|
||||||
busy={busy}
|
busy={busy}
|
||||||
|
helpContextId="access.sessions.action.revoke-others"
|
||||||
|
helpModuleId="access"
|
||||||
onCancel={() => setRevokingOthers(false)}
|
onCancel={() => setRevokingOthers(false)}
|
||||||
onConfirm={() => void revokeOthers()}
|
onConfirm={() => void revokeOthers()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user