Compare commits
3
Commits
5b8baa6cde
..
v0.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2afcaa09c | ||
|
|
d08a41fb56 | ||
|
|
002d12e417 |
@@ -1,5 +1,9 @@
|
|||||||
# GovOPlaN Access
|
# GovOPlaN Access
|
||||||
|
|
||||||
|
<!-- govoplan-repository-type:start -->
|
||||||
|
**Repository type:** module (platform).
|
||||||
|
<!-- govoplan-repository-type:end -->
|
||||||
|
|
||||||
`govoplan-access` is the platform module for GovOPlaN identity,
|
`govoplan-access` is the platform module for GovOPlaN identity,
|
||||||
authentication, sessions, API keys, RBAC, groups, users, and access
|
authentication, sessions, API keys, RBAC, groups, users, and access
|
||||||
administration.
|
administration.
|
||||||
@@ -98,7 +102,7 @@ available:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /mnt/DATA/git/govoplan-core
|
cd /mnt/DATA/git/govoplan-core
|
||||||
./scripts/gitea-sync-labels.py --root /mnt/DATA/git/govoplan-access --apply
|
/mnt/DATA/git/govoplan/tools/gitea/gitea-sync-labels.py --root /mnt/DATA/git/govoplan-access --apply
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Install
|
## Development Install
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.8",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
+2
-2
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-access"
|
name = "govoplan-access"
|
||||||
version = "0.1.6"
|
version = "0.1.8"
|
||||||
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.6",
|
"govoplan-core>=0.1.8",
|
||||||
"SQLAlchemy>=2,<3",
|
"SQLAlchemy>=2,<3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from govoplan_access.backend.db.models import (
|
|||||||
UserGroupMembership,
|
UserGroupMembership,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import OrganizationDirectory
|
||||||
from govoplan_access.backend.permissions.catalog import effective_permission_count, expand_scopes
|
from govoplan_access.backend.permissions.catalog import effective_permission_count, expand_scopes
|
||||||
|
|
||||||
|
|
||||||
@@ -143,13 +144,22 @@ def _user_item(
|
|||||||
*,
|
*,
|
||||||
owner_ids: set[str] | None = None,
|
owner_ids: set[str] | None = None,
|
||||||
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> UserAdminItem:
|
) -> UserAdminItem:
|
||||||
account = session.get(Account, user.account_id)
|
account = session.get(Account, user.account_id)
|
||||||
if account is None:
|
if account is None:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="User account is missing")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="User account is missing")
|
||||||
groups = collect_user_groups(session, user)
|
groups = collect_user_groups(session, user)
|
||||||
roles = collect_direct_user_roles(session, user)
|
roles = collect_direct_user_roles(session, user)
|
||||||
external_roles = collect_external_function_roles(session, user, idm_assignments) if idm_assignments else []
|
external_roles = (
|
||||||
|
collect_external_function_roles(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_assignments,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
|
if idm_assignments else []
|
||||||
|
)
|
||||||
effective_scopes = set(collect_user_scopes(session, user, include_system=False))
|
effective_scopes = set(collect_user_scopes(session, user, include_system=False))
|
||||||
for role in external_roles:
|
for role in external_roles:
|
||||||
effective_scopes.update(role.permissions or [])
|
effective_scopes.update(role.permissions or [])
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from govoplan_core.api.v1.schemas import (
|
|||||||
UserUiPreferences,
|
UserUiPreferences,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
|
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
|
||||||
from govoplan_core.admin.settings import get_system_settings
|
from govoplan_core.admin.settings import get_system_settings
|
||||||
from govoplan_core.audit.logging import audit_from_principal
|
from govoplan_core.audit.logging import audit_from_principal
|
||||||
@@ -176,6 +178,16 @@ def _language_context(session: Session, *, tenant: Tenant, user: User) -> dict[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_directory_from_request(request: Request) -> IdentityDirectory | None:
|
||||||
|
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||||
|
if not isinstance(registry, PlatformRegistry) or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
if not isinstance(capability, IdentityDirectory):
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
def _resolve_login_user(session: Session, payload: LoginRequest) -> tuple[Account, User, Tenant]:
|
def _resolve_login_user(session: Session, payload: LoginRequest) -> tuple[Account, User, Tenant]:
|
||||||
account = (
|
account = (
|
||||||
session.query(Account)
|
session.query(Account)
|
||||||
@@ -215,6 +227,8 @@ def _me_response(
|
|||||||
service_account_id: str | None = None,
|
service_account_id: str | None = None,
|
||||||
include_system: bool = True,
|
include_system: bool = True,
|
||||||
include_all_memberships: bool = True,
|
include_all_memberships: bool = True,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
identity_id: str | None = None,
|
||||||
) -> MeResponse:
|
) -> MeResponse:
|
||||||
tenant_roles = collect_user_roles(session, user)
|
tenant_roles = collect_user_roles(session, user)
|
||||||
system_roles = collect_system_roles(session, account) if include_system else []
|
system_roles = collect_system_roles(session, account) if include_system else []
|
||||||
@@ -248,7 +262,7 @@ def _me_response(
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=tenant.id,
|
tenant_id=tenant.id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id or identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(scopes),
|
scopes=frozenset(scopes),
|
||||||
group_ids=frozenset(group.id for group in groups),
|
group_ids=frozenset(group.id for group in groups),
|
||||||
role_ids=frozenset(role.id for role in tenant_roles + system_roles),
|
role_ids=frozenset(role.id for role in tenant_roles + system_roles),
|
||||||
@@ -271,7 +285,8 @@ def _me_response(
|
|||||||
@router.post("/login", response_model=LoginResponse)
|
@router.post("/login", response_model=LoginResponse)
|
||||||
def login(payload: LoginRequest, request: Request, response: Response, session: Session = Depends(get_session)):
|
def login(payload: LoginRequest, request: Request, response: Response, session: Session = Depends(get_session)):
|
||||||
account, user, tenant = _resolve_login_user(session, payload)
|
account, user, tenant = _resolve_login_user(session, payload)
|
||||||
me_payload = _me_response(session, account=account, user=user, tenant=tenant)
|
identity_directory = _identity_directory_from_request(request)
|
||||||
|
me_payload = _me_response(session, account=account, user=user, tenant=tenant, identity_directory=identity_directory)
|
||||||
maintenance_mode = saved_maintenance_mode(session)
|
maintenance_mode = saved_maintenance_mode(session)
|
||||||
if maintenance_mode.enabled and not scopes_grant(me_payload.scopes, MAINTENANCE_ACCESS_SCOPE):
|
if maintenance_mode.enabled and not scopes_grant(me_payload.scopes, MAINTENANCE_ACCESS_SCOPE):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -300,6 +315,7 @@ def login(payload: LoginRequest, request: Request, response: Response, session:
|
|||||||
effective_scopes=me_payload.scopes,
|
effective_scopes=me_payload.scopes,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
session_id=created.model.id,
|
session_id=created.model.id,
|
||||||
|
identity_directory=identity_directory,
|
||||||
).model_dump(),
|
).model_dump(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -321,6 +337,7 @@ def me(principal: ApiPrincipal = Depends(get_api_principal), session: Session =
|
|||||||
service_account_id=principal.principal.service_account_id,
|
service_account_id=principal.principal.service_account_id,
|
||||||
include_system=principal.auth_session is not None,
|
include_system=principal.auth_session is not None,
|
||||||
include_all_memberships=principal.auth_session is not None,
|
include_all_memberships=principal.auth_session is not None,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +418,7 @@ def update_profile(
|
|||||||
session_id=principal.session_id,
|
session_id=principal.session_id,
|
||||||
include_system=True,
|
include_system=True,
|
||||||
include_all_memberships=True,
|
include_all_memberships=True,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -430,6 +448,7 @@ def switch_tenant(
|
|||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
session_id=principal.session_id,
|
session_id=principal.session_id,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ from govoplan_core.core.configuration_control import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
||||||
from govoplan_core.core.access import CAPABILITY_ACCESS_EXPLANATION, AccessExplanationService, AccessDecisionProvenance, PrincipalRef
|
from govoplan_core.core.access import CAPABILITY_ACCESS_EXPLANATION, AccessExplanationService, AccessDecisionProvenance, PrincipalRef
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||||
@@ -507,6 +508,16 @@ def _optional_organization_directory() -> OrganizationDirectory | None:
|
|||||||
return capability
|
return capability
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_identity_directory() -> IdentityDirectory | None:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
if not isinstance(capability, IdentityDirectory):
|
||||||
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
def _optional_idm_directory() -> IdmDirectory | None:
|
def _optional_idm_directory() -> IdmDirectory | None:
|
||||||
registry = get_registry()
|
registry = get_registry()
|
||||||
if registry is None or not registry.has_capability(CAPABILITY_IDM_DIRECTORY):
|
if registry is None or not registry.has_capability(CAPABILITY_IDM_DIRECTORY):
|
||||||
@@ -527,6 +538,7 @@ def _access_explanation_service_or_error() -> AccessExplanationService:
|
|||||||
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
||||||
|
|
||||||
return SqlAccessExplanationService(
|
return SqlAccessExplanationService(
|
||||||
|
identity_directory=_optional_identity_directory(),
|
||||||
idm_directory=_optional_idm_directory(),
|
idm_directory=_optional_idm_directory(),
|
||||||
organization_directory=_optional_organization_directory(),
|
organization_directory=_optional_organization_directory(),
|
||||||
)
|
)
|
||||||
@@ -541,12 +553,19 @@ def _idm_assignments_for_user(
|
|||||||
return tuple(idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id))
|
return tuple(idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id))
|
||||||
|
|
||||||
|
|
||||||
def _principal_ref_for_user(session: Session, user: User, *, idm_directory: IdmDirectory | None = None) -> PrincipalRef:
|
def _principal_ref_for_user(
|
||||||
|
session: Session,
|
||||||
|
user: User,
|
||||||
|
*,
|
||||||
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
|
) -> PrincipalRef:
|
||||||
account = session.get(Account, user.account_id)
|
account = session.get(Account, user.account_id)
|
||||||
if account is None:
|
if account is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
scopes = set(collect_user_scopes(session, user, include_system=True))
|
scopes = set(collect_user_scopes(session, user, include_system=True))
|
||||||
for role in idm_roles:
|
for role in idm_roles:
|
||||||
scopes.update(role.permissions or [])
|
scopes.update(role.permissions or [])
|
||||||
@@ -558,7 +577,7 @@ def _principal_ref_for_user(session: Session, user: User, *, idm_directory: IdmD
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=user.tenant_id,
|
tenant_id=user.tenant_id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(sorted(scopes)),
|
scopes=frozenset(sorted(scopes)),
|
||||||
group_ids=frozenset(group.id for group in collect_user_groups(session, user)),
|
group_ids=frozenset(group.id for group in collect_user_groups(session, user)),
|
||||||
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
||||||
@@ -1937,8 +1956,9 @@ def _full_users_delta_response(session: Session, tenant: Tenant) -> UserListDelt
|
|||||||
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
return UserListDeltaResponse(
|
return UserListDeltaResponse(
|
||||||
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in users],
|
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in users],
|
||||||
deleted=[],
|
deleted=[],
|
||||||
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_USERS_COLLECTION,)),
|
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_USERS_COLLECTION,)),
|
||||||
has_more=False,
|
has_more=False,
|
||||||
@@ -1960,13 +1980,14 @@ def _users_delta_response(session: Session, tenant: Tenant, *, since: str, limit
|
|||||||
}
|
}
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
deleted = [
|
deleted = [
|
||||||
_delta_deleted_item(entry)
|
_delta_deleted_item(entry)
|
||||||
for entry in entries
|
for entry in entries
|
||||||
if entry.resource_type == "access_user" and entry.resource_id not in visible
|
if entry.resource_type == "access_user" and entry.resource_id not in visible
|
||||||
]
|
]
|
||||||
return UserListDeltaResponse(
|
return UserListDeltaResponse(
|
||||||
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in visible.values()],
|
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in visible.values()],
|
||||||
deleted=deleted,
|
deleted=deleted,
|
||||||
watermark=_access_delta_response_watermark(session, tenant_id=tenant.id, collections=(ACCESS_USERS_COLLECTION,), entries=entries, has_more=has_more),
|
watermark=_access_delta_response_watermark(session, tenant_id=tenant.id, collections=(ACCESS_USERS_COLLECTION,), entries=entries, has_more=has_more),
|
||||||
has_more=has_more,
|
has_more=has_more,
|
||||||
@@ -1980,9 +2001,16 @@ def _user_item_for_response(
|
|||||||
*,
|
*,
|
||||||
owner_ids: set[str] | None = None,
|
owner_ids: set[str] | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> UserAdminItem:
|
) -> UserAdminItem:
|
||||||
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
||||||
return _user_item(session, user, owner_ids=owner_ids, idm_assignments=idm_assignments)
|
return _user_item(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
owner_ids=owner_ids,
|
||||||
|
idm_assignments=idm_assignments,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/delta", response_model=UserListDeltaResponse)
|
@router.get("/users/delta", response_model=UserListDeltaResponse)
|
||||||
@@ -2009,7 +2037,8 @@ def list_users(
|
|||||||
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
return UserListResponse(users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in users])
|
organization_directory = _optional_organization_directory()
|
||||||
|
return UserListResponse(users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in users])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/{user_id}/access-explanation", response_model=UserAccessExplanationResponse)
|
@router.get("/users/{user_id}/access-explanation", response_model=UserAccessExplanationResponse)
|
||||||
@@ -2024,15 +2053,16 @@ def get_user_access_explanation(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
explanation = build_user_access_explanation(
|
explanation = build_user_access_explanation(
|
||||||
session,
|
session,
|
||||||
user,
|
user,
|
||||||
idm_directory=idm_directory,
|
idm_directory=idm_directory,
|
||||||
organization_directory=_optional_organization_directory(),
|
organization_directory=organization_directory,
|
||||||
include_system=False,
|
include_system=False,
|
||||||
)
|
)
|
||||||
return UserAccessExplanationResponse(
|
return UserAccessExplanationResponse(
|
||||||
user=_user_item_for_response(session, user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
role_sources=[source.to_dict() for source in explanation.role_sources],
|
role_sources=[source.to_dict() for source in explanation.role_sources],
|
||||||
scopes=[scope.to_dict() for scope in explanation.scopes],
|
scopes=[scope.to_dict() for scope in explanation.scopes],
|
||||||
function_facts=[fact.to_dict() for fact in explanation.function_facts],
|
function_facts=[fact.to_dict() for fact in explanation.function_facts],
|
||||||
@@ -2054,7 +2084,15 @@ def get_resource_access_explanation(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
target_principal = _principal_ref_for_user(session, user, idm_directory=idm_directory)
|
identity_directory = _optional_identity_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
|
target_principal = _principal_ref_for_user(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=idm_directory,
|
||||||
|
identity_directory=identity_directory,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
provenance = _access_explanation_service_or_error().explain_resource_provenance(
|
provenance = _access_explanation_service_or_error().explain_resource_provenance(
|
||||||
target_principal,
|
target_principal,
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
@@ -2062,7 +2100,7 @@ def get_resource_access_explanation(
|
|||||||
action=action,
|
action=action,
|
||||||
)
|
)
|
||||||
return ResourceAccessExplanationResponse(
|
return ResourceAccessExplanationResponse(
|
||||||
user=_user_item_for_response(session, user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
resource_id=resource_id,
|
resource_id=resource_id,
|
||||||
action=action,
|
action=action,
|
||||||
@@ -2152,8 +2190,9 @@ def create_user(
|
|||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
return UserCreateResponse(
|
return UserCreateResponse(
|
||||||
user=_user_item_for_response(session, result.user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, result.user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
account_created=result.account_created,
|
account_created=result.account_created,
|
||||||
temporary_password=result.temporary_password,
|
temporary_password=result.temporary_password,
|
||||||
)
|
)
|
||||||
@@ -2234,7 +2273,12 @@ def update_user(
|
|||||||
details=payload.model_dump(exclude_none=True),
|
details=payload.model_dump(exclude_none=True),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _user_item_for_response(session, user, idm_directory=_optional_idm_directory())
|
return _user_item_for_response(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=_optional_idm_directory(),
|
||||||
|
organization_directory=_optional_organization_directory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _full_groups_delta_response(session: Session, tenant: Tenant) -> GroupListDeltaResponse:
|
def _full_groups_delta_response(session: Session, tenant: Tenant) -> GroupListDeltaResponse:
|
||||||
@@ -3311,7 +3355,12 @@ def create_tenant_api_key(
|
|||||||
user = session.query(User).filter(User.id == user_id, User.tenant_id == tenant.id, User.is_active.is_(True)).one_or_none()
|
user = session.query(User).filter(User.id == user_id, User.tenant_id == tenant.id, User.is_active.is_(True)).one_or_none()
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Active user not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Active user not found")
|
||||||
user_scopes = _user_item_for_response(session, user, idm_directory=_optional_idm_directory()).effective_scopes
|
user_scopes = _user_item_for_response(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=_optional_idm_directory(),
|
||||||
|
organization_directory=_optional_organization_directory(),
|
||||||
|
).effective_scopes
|
||||||
requested = payload.scopes or ["campaign:read"]
|
requested = payload.scopes or ["campaign:read"]
|
||||||
invalid = [scope for scope in requested if scope.startswith("system:") or not scopes_grant(user_scopes, scope)]
|
invalid = [scope for scope in requested if scope.startswith("system:") or not scopes_grant(user_scopes, scope)]
|
||||||
if invalid:
|
if invalid:
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ from govoplan_core.core.access import (
|
|||||||
PrincipalRef,
|
PrincipalRef,
|
||||||
PrincipalResolver,
|
PrincipalResolver,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import OrganizationDirectory
|
||||||
from govoplan_core.core.modules import AccessDecision
|
from govoplan_core.core.modules import AccessDecision
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
|
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
|
||||||
@@ -63,6 +65,7 @@ def _build_principal_ref(
|
|||||||
api_key: ApiKey | None = None,
|
api_key: ApiKey | None = None,
|
||||||
auth_session: AuthSession | None = None,
|
auth_session: AuthSession | None = None,
|
||||||
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
extra_roles: tuple[Role, ...] = (),
|
extra_roles: tuple[Role, ...] = (),
|
||||||
) -> PrincipalRef:
|
) -> PrincipalRef:
|
||||||
function_assignment_ids = list(collect_function_assignment_ids(session, user))
|
function_assignment_ids = list(collect_function_assignment_ids(session, user))
|
||||||
@@ -74,7 +77,7 @@ def _build_principal_ref(
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(scopes),
|
scopes=frozenset(scopes),
|
||||||
group_ids=_principal_group_ids(session, user),
|
group_ids=_principal_group_ids(session, user),
|
||||||
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
||||||
@@ -128,6 +131,8 @@ def _resolve_legacy_principal_ref(
|
|||||||
authorization: str | None,
|
authorization: str | None,
|
||||||
x_api_key: str | None,
|
x_api_key: str | None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> PrincipalRef:
|
) -> PrincipalRef:
|
||||||
token, source = _extract_token(request, authorization, x_api_key)
|
token, source = _extract_token(request, authorization, x_api_key)
|
||||||
if not token:
|
if not token:
|
||||||
@@ -147,7 +152,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
):
|
):
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive or inconsistent API-key principal")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive or inconsistent API-key principal")
|
||||||
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=api_key.tenant_id)
|
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=api_key.tenant_id)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
user_scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=False), idm_roles)
|
user_scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=False), idm_roles)
|
||||||
effective_scopes = intersect_api_key_scopes(user_scopes, api_key.scopes or [])
|
effective_scopes = intersect_api_key_scopes(user_scopes, api_key.scopes or [])
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -160,6 +165,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
scopes=effective_scopes,
|
scopes=effective_scopes,
|
||||||
auth_method="api_key",
|
auth_method="api_key",
|
||||||
idm_assignments=idm_assignments,
|
idm_assignments=idm_assignments,
|
||||||
|
identity_directory=identity_directory,
|
||||||
extra_roles=idm_roles,
|
extra_roles=idm_roles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,7 +187,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
if not header_token or not cookie_token or header_token != cookie_token or not verify_auth_session_csrf(auth_session, header_token):
|
if not header_token or not cookie_token or header_token != cookie_token or not verify_auth_session_csrf(auth_session, header_token):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or missing CSRF token")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or missing CSRF token")
|
||||||
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=user.tenant_id)
|
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=user.tenant_id)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=True), idm_roles)
|
scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=True), idm_roles)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _build_principal_ref(
|
return _build_principal_ref(
|
||||||
@@ -193,6 +199,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
idm_assignments=idm_assignments,
|
idm_assignments=idm_assignments,
|
||||||
|
identity_directory=identity_directory,
|
||||||
extra_roles=idm_roles,
|
extra_roles=idm_roles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,8 +264,15 @@ def _permission_evaluator_from_request(request: Request) -> PermissionEvaluator
|
|||||||
|
|
||||||
|
|
||||||
class LegacyPrincipalResolver:
|
class LegacyPrincipalResolver:
|
||||||
def __init__(self, idm_directory: IdmDirectory | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
|
) -> None:
|
||||||
self._idm_directory = idm_directory
|
self._idm_directory = idm_directory
|
||||||
|
self._identity_directory = identity_directory
|
||||||
|
self._organization_directory = organization_directory
|
||||||
|
|
||||||
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
|
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
|
||||||
if not isinstance(request, Request):
|
if not isinstance(request, Request):
|
||||||
@@ -272,6 +286,8 @@ class LegacyPrincipalResolver:
|
|||||||
authorization=authorization,
|
authorization=authorization,
|
||||||
x_api_key=x_api_key,
|
x_api_key=x_api_key,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
with get_database().session() as managed_session:
|
with get_database().session() as managed_session:
|
||||||
return _resolve_legacy_principal_ref(
|
return _resolve_legacy_principal_ref(
|
||||||
@@ -280,6 +296,8 @@ class LegacyPrincipalResolver:
|
|||||||
authorization=authorization,
|
authorization=authorization,
|
||||||
x_api_key=x_api_key,
|
x_api_key=x_api_key,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ from govoplan_core.core.access import (
|
|||||||
OrganizationUnitRef,
|
OrganizationUnitRef,
|
||||||
UserRef,
|
UserRef,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory, IdentityRef as DirectoryIdentityRef
|
||||||
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef as IdmFunctionAssignmentRef
|
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef as IdmFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import (
|
||||||
|
ORGANIZATIONS_MODULE_ID,
|
||||||
|
OrganizationDirectory,
|
||||||
|
OrganizationFunctionRef as DirectoryFunctionRef,
|
||||||
|
OrganizationUnitRef as DirectoryOrganizationUnitRef,
|
||||||
|
)
|
||||||
from govoplan_access.backend.db.models import (
|
from govoplan_access.backend.db.models import (
|
||||||
Account,
|
Account,
|
||||||
Function,
|
Function,
|
||||||
@@ -73,6 +80,16 @@ def _identity_ref(identity: Identity, account_links: list[IdentityAccountLink])
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _directory_identity_ref(identity: DirectoryIdentityRef) -> IdentityRef:
|
||||||
|
return IdentityRef(
|
||||||
|
id=identity.id,
|
||||||
|
display_name=identity.display_name,
|
||||||
|
primary_account_id=identity.primary_account_id,
|
||||||
|
account_ids=tuple(identity.account_ids),
|
||||||
|
status=identity.status, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
|
def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
|
||||||
return OrganizationUnitRef(
|
return OrganizationUnitRef(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
@@ -83,6 +100,16 @@ def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _directory_organization_unit_ref(item: DirectoryOrganizationUnitRef) -> OrganizationUnitRef:
|
||||||
|
return OrganizationUnitRef(
|
||||||
|
id=item.id,
|
||||||
|
tenant_id=item.tenant_id,
|
||||||
|
name=item.name,
|
||||||
|
parent_id=item.parent_id,
|
||||||
|
status=item.status, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef:
|
def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef:
|
||||||
return FunctionRef(
|
return FunctionRef(
|
||||||
id=function.id,
|
id=function.id,
|
||||||
@@ -97,6 +124,20 @@ def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _directory_function_ref(function: DirectoryFunctionRef, role_ids: Iterable[str]) -> FunctionRef:
|
||||||
|
return FunctionRef(
|
||||||
|
id=function.id,
|
||||||
|
tenant_id=function.tenant_id,
|
||||||
|
organization_unit_id=function.organization_unit_id,
|
||||||
|
slug=function.slug,
|
||||||
|
name=function.name,
|
||||||
|
role_ids=tuple(role_ids),
|
||||||
|
delegable=function.delegable,
|
||||||
|
act_in_place_allowed=function.act_in_place_allowed,
|
||||||
|
status=function.status, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _function_assignment_ref(item: FunctionAssignment) -> FunctionAssignmentRef:
|
def _function_assignment_ref(item: FunctionAssignment) -> FunctionAssignmentRef:
|
||||||
return FunctionAssignmentRef(
|
return FunctionAssignmentRef(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
@@ -134,8 +175,15 @@ def _idm_function_assignment_ref(item: IdmFunctionAssignmentRef, *, account_id:
|
|||||||
|
|
||||||
|
|
||||||
class SqlAccessDirectory(AccessSemanticDirectory):
|
class SqlAccessDirectory(AccessSemanticDirectory):
|
||||||
def __init__(self, idm_directory: IdmDirectory | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
|
) -> None:
|
||||||
self._idm_directory = idm_directory
|
self._idm_directory = idm_directory
|
||||||
|
self._identity_directory = identity_directory
|
||||||
|
self._organization_directory = organization_directory
|
||||||
|
|
||||||
def get_account(self, account_id: str) -> AccountRef | None:
|
def get_account(self, account_id: str) -> AccountRef | None:
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
@@ -230,6 +278,10 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return subject.label or subject.id
|
return subject.label or subject.id
|
||||||
|
|
||||||
def get_identity(self, identity_id: str) -> IdentityRef | None:
|
def get_identity(self, identity_id: str) -> IdentityRef | None:
|
||||||
|
if self._identity_directory is not None:
|
||||||
|
identity = self._identity_directory.get_identity(identity_id)
|
||||||
|
if identity is not None:
|
||||||
|
return _directory_identity_ref(identity)
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
identity = session.get(Identity, identity_id)
|
identity = session.get(Identity, identity_id)
|
||||||
if identity is None:
|
if identity is None:
|
||||||
@@ -243,6 +295,16 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return _identity_ref(identity, links)
|
return _identity_ref(identity, links)
|
||||||
|
|
||||||
def accounts_for_identity(self, identity_id: str) -> tuple[AccountRef, ...]:
|
def accounts_for_identity(self, identity_id: str) -> tuple[AccountRef, ...]:
|
||||||
|
if self._identity_directory is not None:
|
||||||
|
links = tuple(self._identity_directory.accounts_for_identity(identity_id))
|
||||||
|
if links:
|
||||||
|
account_ids = [link.account_id for link in links]
|
||||||
|
with get_database().session() as session:
|
||||||
|
accounts = {
|
||||||
|
account.id: account
|
||||||
|
for account in session.query(Account).filter(Account.id.in_(account_ids)).all()
|
||||||
|
}
|
||||||
|
return tuple(_account_ref(accounts[account_id]) for account_id in account_ids if account_id in accounts)
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
accounts = (
|
accounts = (
|
||||||
session.query(Account)
|
session.query(Account)
|
||||||
@@ -254,11 +316,19 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return tuple(_account_ref(account) for account in accounts)
|
return tuple(_account_ref(account) for account in accounts)
|
||||||
|
|
||||||
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
|
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
|
||||||
|
if self._organization_directory is not None:
|
||||||
|
item = self._organization_directory.get_organization_unit(organization_unit_id)
|
||||||
|
if item is not None:
|
||||||
|
return _directory_organization_unit_ref(item)
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
item = session.get(OrganizationUnit, organization_unit_id)
|
item = session.get(OrganizationUnit, organization_unit_id)
|
||||||
return _organization_unit_ref(item) if item is not None else None
|
return _organization_unit_ref(item) if item is not None else None
|
||||||
|
|
||||||
def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]:
|
def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]:
|
||||||
|
if self._organization_directory is not None:
|
||||||
|
items = tuple(self._organization_directory.organization_units_for_tenant(tenant_id))
|
||||||
|
if items:
|
||||||
|
return tuple(_directory_organization_unit_ref(item) for item in items)
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
items = (
|
items = (
|
||||||
session.query(OrganizationUnit)
|
session.query(OrganizationUnit)
|
||||||
@@ -269,6 +339,10 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return tuple(_organization_unit_ref(item) for item in items)
|
return tuple(_organization_unit_ref(item) for item in items)
|
||||||
|
|
||||||
def get_function(self, function_id: str) -> FunctionRef | None:
|
def get_function(self, function_id: str) -> FunctionRef | None:
|
||||||
|
if self._organization_directory is not None:
|
||||||
|
function = self._organization_directory.get_function(function_id)
|
||||||
|
if function is not None:
|
||||||
|
return _directory_function_ref(function, self._external_function_role_ids(function.id, tenant_id=function.tenant_id))
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
function = session.get(Function, function_id)
|
function = session.get(Function, function_id)
|
||||||
if function is None:
|
if function is None:
|
||||||
@@ -288,6 +362,22 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
*,
|
*,
|
||||||
include_subunits: bool = False,
|
include_subunits: bool = False,
|
||||||
) -> tuple[FunctionRef, ...]:
|
) -> tuple[FunctionRef, ...]:
|
||||||
|
if self._organization_directory is not None:
|
||||||
|
functions = tuple(
|
||||||
|
self._organization_directory.functions_for_organization_unit(
|
||||||
|
organization_unit_id,
|
||||||
|
include_subunits=include_subunits,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if functions:
|
||||||
|
role_ids_by_function = self._external_function_role_ids_by_function(
|
||||||
|
[item.id for item in functions],
|
||||||
|
tenant_id=functions[0].tenant_id,
|
||||||
|
)
|
||||||
|
return tuple(
|
||||||
|
_directory_function_ref(item, role_ids_by_function.get(item.id, ()))
|
||||||
|
for item in functions
|
||||||
|
)
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
unit_ids = {organization_unit_id}
|
unit_ids = {organization_unit_id}
|
||||||
if include_subunits:
|
if include_subunits:
|
||||||
@@ -345,3 +435,33 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
)
|
)
|
||||||
deduped = {item.id: item for item in assignments}
|
deduped = {item.id: item for item in assignments}
|
||||||
return tuple(deduped.values())
|
return tuple(deduped.values())
|
||||||
|
|
||||||
|
def _external_function_role_ids(self, function_id: str, *, tenant_id: str) -> tuple[str, ...]:
|
||||||
|
return self._external_function_role_ids_by_function([function_id], tenant_id=tenant_id).get(function_id, ())
|
||||||
|
|
||||||
|
def _external_function_role_ids_by_function(
|
||||||
|
self,
|
||||||
|
function_ids: Iterable[str],
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
) -> dict[str, tuple[str, ...]]:
|
||||||
|
ids = sorted({str(function_id) for function_id in function_ids if function_id})
|
||||||
|
if not ids:
|
||||||
|
return {}
|
||||||
|
from govoplan_access.backend.db.models import ExternalFunctionRoleAssignment
|
||||||
|
|
||||||
|
with get_database().session() as session:
|
||||||
|
rows = (
|
||||||
|
session.query(ExternalFunctionRoleAssignment.function_id, ExternalFunctionRoleAssignment.role_id)
|
||||||
|
.filter(
|
||||||
|
ExternalFunctionRoleAssignment.tenant_id == tenant_id,
|
||||||
|
ExternalFunctionRoleAssignment.source_module == ORGANIZATIONS_MODULE_ID,
|
||||||
|
ExternalFunctionRoleAssignment.function_id.in_(ids),
|
||||||
|
)
|
||||||
|
.order_by(ExternalFunctionRoleAssignment.created_at.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
result: dict[str, list[str]] = {}
|
||||||
|
for function_id, role_id in rows:
|
||||||
|
result.setdefault(function_id, []).append(role_id)
|
||||||
|
return {function_id: tuple(role_ids) for function_id, role_ids in result.items()}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ from govoplan_access.backend.semantic import (
|
|||||||
identity_id_for_account,
|
identity_id_for_account,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.access import AccessDecisionProvenance, AccessExplanationService, PrincipalRef, ResourceAccessExplanationProvider
|
from govoplan_core.core.access import AccessDecisionProvenance, AccessExplanationService, PrincipalRef, ResourceAccessExplanationProvider
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
from govoplan_core.core.organizations import OrganizationDirectory
|
from govoplan_core.core.organizations import ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.db.session import get_database
|
from govoplan_core.db.session import get_database
|
||||||
from govoplan_access.backend.permissions.catalog import expand_scopes, scopes_grant
|
from govoplan_access.backend.permissions.catalog import expand_scopes, scopes_grant
|
||||||
|
|
||||||
@@ -159,10 +160,12 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
organization_directory: OrganizationDirectory | None = None,
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
resource_explanation_providers: Iterable[ResourceAccessExplanationProvider] = (),
|
resource_explanation_providers: Iterable[ResourceAccessExplanationProvider] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._identity_directory = identity_directory
|
||||||
self._idm_directory = idm_directory
|
self._idm_directory = idm_directory
|
||||||
self._organization_directory = organization_directory
|
self._organization_directory = organization_directory
|
||||||
self._resource_explanation_providers = tuple(resource_explanation_providers)
|
self._resource_explanation_providers = tuple(resource_explanation_providers)
|
||||||
@@ -178,6 +181,7 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
required_scope,
|
required_scope,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
organization_directory=self._organization_directory,
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
@@ -196,6 +200,7 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
action,
|
action,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
organization_directory=self._organization_directory,
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
@@ -250,12 +255,17 @@ def _scope_provenance(
|
|||||||
principal: PrincipalRef,
|
principal: PrincipalRef,
|
||||||
required_scope: str,
|
required_scope: str,
|
||||||
*,
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
organization_directory: OrganizationDirectory | None = None,
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> list[AccessDecisionProvenance]:
|
) -> list[AccessDecisionProvenance]:
|
||||||
items: list[AccessDecisionProvenance] = []
|
items: list[AccessDecisionProvenance] = []
|
||||||
account = session.get(Account, principal.account_id)
|
account = session.get(Account, principal.account_id)
|
||||||
identity_id = principal.identity_id or identity_id_for_account(session, principal.account_id)
|
identity_id = principal.identity_id or identity_id_for_account(
|
||||||
|
session,
|
||||||
|
principal.account_id,
|
||||||
|
identity_directory=identity_directory,
|
||||||
|
)
|
||||||
if identity_id:
|
if identity_id:
|
||||||
items.append(AccessDecisionProvenance(kind="identity", id=identity_id, source="identity_account_link"))
|
items.append(AccessDecisionProvenance(kind="identity", id=identity_id, source="identity_account_link"))
|
||||||
items.append(
|
items.append(
|
||||||
@@ -421,6 +431,12 @@ def _idm_function_role_sources(
|
|||||||
for assignment in idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id)
|
for assignment in idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id)
|
||||||
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
||||||
]
|
]
|
||||||
|
if organization_directory is not None:
|
||||||
|
assignments = [
|
||||||
|
assignment
|
||||||
|
for assignment in assignments
|
||||||
|
if _organization_function_active(organization_directory, assignment)
|
||||||
|
]
|
||||||
if not assignments:
|
if not assignments:
|
||||||
return [], []
|
return [], []
|
||||||
function_ids = sorted({assignment.function_id for assignment in assignments})
|
function_ids = sorted({assignment.function_id for assignment in assignments})
|
||||||
@@ -429,6 +445,7 @@ def _idm_function_role_sources(
|
|||||||
.join(Role, ExternalFunctionRoleAssignment.role_id == Role.id)
|
.join(Role, ExternalFunctionRoleAssignment.role_id == Role.id)
|
||||||
.filter(
|
.filter(
|
||||||
ExternalFunctionRoleAssignment.tenant_id == user.tenant_id,
|
ExternalFunctionRoleAssignment.tenant_id == user.tenant_id,
|
||||||
|
ExternalFunctionRoleAssignment.source_module == ORGANIZATIONS_MODULE_ID,
|
||||||
ExternalFunctionRoleAssignment.function_id.in_(function_ids),
|
ExternalFunctionRoleAssignment.function_id.in_(function_ids),
|
||||||
Role.tenant_id == user.tenant_id,
|
Role.tenant_id == user.tenant_id,
|
||||||
)
|
)
|
||||||
@@ -446,7 +463,7 @@ def _idm_function_role_sources(
|
|||||||
mappings = mappings_by_function.get(assignment.function_id, [])
|
mappings = mappings_by_function.get(assignment.function_id, [])
|
||||||
function_facts.append(
|
function_facts.append(
|
||||||
FunctionFactExplanation(
|
FunctionFactExplanation(
|
||||||
source_module="organizations",
|
source_module=ORGANIZATIONS_MODULE_ID,
|
||||||
assignment_id=assignment.id,
|
assignment_id=assignment.id,
|
||||||
tenant_id=assignment.tenant_id,
|
tenant_id=assignment.tenant_id,
|
||||||
identity_id=assignment.identity_id,
|
identity_id=assignment.identity_id,
|
||||||
@@ -501,6 +518,14 @@ def _organization_labels(
|
|||||||
return (function.name if function is not None else None, unit.name if unit is not None else None)
|
return (function.name if function is not None else None, unit.name if unit is not None else None)
|
||||||
|
|
||||||
|
|
||||||
|
def _organization_function_active(
|
||||||
|
organization_directory: OrganizationDirectory,
|
||||||
|
assignment: OrganizationFunctionAssignmentRef,
|
||||||
|
) -> bool:
|
||||||
|
function = organization_directory.get_function(assignment.function_id)
|
||||||
|
return function is not None and function.tenant_id == assignment.tenant_id and function.status == "active"
|
||||||
|
|
||||||
|
|
||||||
def _system_role_sources(session: Session, account: Account) -> list[AccessRoleSourceExplanation]:
|
def _system_role_sources(session: Session, account: Account) -> list[AccessRoleSourceExplanation]:
|
||||||
roles = (
|
roles = (
|
||||||
session.query(Role)
|
session.query(Role)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from govoplan_core.core.access import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.campaigns import CAPABILITY_CAMPAIGNS_ACCESS
|
from govoplan_core.core.campaigns import CAPABILITY_CAMPAIGNS_ACCESS
|
||||||
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS
|
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
|
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
|
||||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory
|
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory
|
||||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||||
@@ -455,8 +456,11 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = (
|
|||||||
def _legacy_principal_resolver(context: ModuleContext) -> object:
|
def _legacy_principal_resolver(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.auth.dependencies import LegacyPrincipalResolver
|
from govoplan_access.backend.auth.dependencies import LegacyPrincipalResolver
|
||||||
|
|
||||||
idm_directory = _optional_idm_directory(context)
|
return LegacyPrincipalResolver(
|
||||||
return LegacyPrincipalResolver(idm_directory=idm_directory)
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _legacy_permission_evaluator(context: ModuleContext) -> object:
|
def _legacy_permission_evaluator(context: ModuleContext) -> object:
|
||||||
@@ -483,13 +487,30 @@ def _tenant_context_switcher(context: ModuleContext) -> object:
|
|||||||
def _access_directory(context: ModuleContext) -> object:
|
def _access_directory(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.directory import SqlAccessDirectory
|
from govoplan_access.backend.directory import SqlAccessDirectory
|
||||||
|
|
||||||
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
|
return SqlAccessDirectory(
|
||||||
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _access_semantic_directory(context: ModuleContext) -> object:
|
def _access_semantic_directory(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.directory import SqlAccessDirectory
|
from govoplan_access.backend.directory import SqlAccessDirectory
|
||||||
|
|
||||||
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
|
return SqlAccessDirectory(
|
||||||
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_identity_directory(context: ModuleContext) -> IdentityDirectory | None:
|
||||||
|
if not context.registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
capability = context.registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
if not isinstance(capability, IdentityDirectory):
|
||||||
|
raise RuntimeError(f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
def _optional_idm_directory(context: ModuleContext) -> IdmDirectory | None:
|
def _optional_idm_directory(context: ModuleContext) -> IdmDirectory | None:
|
||||||
@@ -525,6 +546,7 @@ def _access_explanation_service(context: ModuleContext) -> object:
|
|||||||
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
||||||
|
|
||||||
return SqlAccessExplanationService(
|
return SqlAccessExplanationService(
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
idm_directory=_optional_idm_directory(context),
|
idm_directory=_optional_idm_directory(context),
|
||||||
organization_directory=_optional_organization_directory(context),
|
organization_directory=_optional_organization_directory(context),
|
||||||
resource_explanation_providers=_resource_explanation_providers(context),
|
resource_explanation_providers=_resource_explanation_providers(context),
|
||||||
@@ -578,7 +600,7 @@ def _route_factory(context: ModuleContext):
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="access",
|
id="access",
|
||||||
name="Access",
|
name="Access",
|
||||||
version="0.1.6",
|
version="0.1.8",
|
||||||
optional_dependencies=("identity", "organizations", "tenancy", "idm"),
|
optional_dependencies=("identity", "organizations", "tenancy", "idm"),
|
||||||
permissions=ACCESS_PERMISSIONS,
|
permissions=ACCESS_PERMISSIONS,
|
||||||
role_templates=ACCESS_ROLE_TEMPLATES,
|
role_templates=ACCESS_ROLE_TEMPLATES,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
"""v0.1.7 access baseline
|
||||||
|
|
||||||
|
Revision ID: 4a5b6c7d8e9f
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2026-07-11 00:00:00.000000
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = '4a5b6c7d8e9f'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = '4f2a9c8e7b6d'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table('access_identities',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('display_name', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('external_subject', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('source', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_identities'))
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_identities_external_subject'), 'access_identities', ['external_subject'], unique=False)
|
||||||
|
op.create_table('access_organization_units',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('parent_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('slug', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['parent_id'], ['access_organization_units.id'], name=op.f('fk_access_organization_units_parent_id_access_organization_units'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_organization_units_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_organization_units')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'slug', name='uq_organization_units_tenant_slug')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_organization_units_parent_id'), 'access_organization_units', ['parent_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_organization_units_tenant_id'), 'access_organization_units', ['tenant_id'], unique=False)
|
||||||
|
op.create_table('access_external_function_role_assignments',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('source_module', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('function_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('role_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['access_roles.id'], name=op.f('fk_access_external_function_role_assignments_role_id_access_roles'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_external_function_role_assignments_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_external_function_role_assignments')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'source_module', 'function_id', 'role_id', name='uq_external_function_role_assignments')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_external_function_role_assignments_function_id'), 'access_external_function_role_assignments', ['function_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_external_function_role_assignments_role_id'), 'access_external_function_role_assignments', ['role_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_external_function_role_assignments_source_module'), 'access_external_function_role_assignments', ['source_module'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_external_function_role_assignments_tenant_id'), 'access_external_function_role_assignments', ['tenant_id'], unique=False)
|
||||||
|
op.create_table('access_functions',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('organization_unit_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('slug', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('delegable', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('act_in_place_allowed', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['organization_unit_id'], ['access_organization_units.id'], name=op.f('fk_access_functions_organization_unit_id_access_organization_units'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_functions_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_functions')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'organization_unit_id', 'slug', name='uq_functions_tenant_ou_slug')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_functions_organization_unit_id'), 'access_functions', ['organization_unit_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_functions_tenant_id'), 'access_functions', ['tenant_id'], unique=False)
|
||||||
|
op.create_table('access_identity_account_links',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('identity_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('account_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('is_primary', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('source', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['account_id'], ['access_accounts.id'], name=op.f('fk_access_identity_account_links_account_id_access_accounts'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['identity_id'], ['access_identities.id'], name=op.f('fk_access_identity_account_links_identity_id_access_identities'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_identity_account_links')),
|
||||||
|
sa.UniqueConstraint('identity_id', 'account_id', name='uq_identity_account_links_identity_account')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_identity_account_links_account_id'), 'access_identity_account_links', ['account_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_identity_account_links_identity_id'), 'access_identity_account_links', ['identity_id'], unique=False)
|
||||||
|
op.create_index('uq_identity_account_links_primary_account', 'access_identity_account_links', ['account_id'], unique=True, sqlite_where=sa.text('is_primary = 1'), postgresql_where=sa.text('is_primary IS TRUE'))
|
||||||
|
op.create_index('uq_identity_account_links_primary_identity', 'access_identity_account_links', ['identity_id'], unique=True, sqlite_where=sa.text('is_primary = 1'), postgresql_where=sa.text('is_primary IS TRUE'))
|
||||||
|
op.create_table('access_function_assignments',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('account_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('identity_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('function_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('organization_unit_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('applies_to_subunits', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('source', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('delegated_from_assignment_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('acting_for_account_id', sa.String(length=36), nullable=True),
|
||||||
|
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['account_id'], ['access_accounts.id'], name=op.f('fk_access_function_assignments_account_id_access_accounts'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['acting_for_account_id'], ['access_accounts.id'], name=op.f('fk_access_function_assignments_acting_for_account_id_access_accounts'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['delegated_from_assignment_id'], ['access_function_assignments.id'], name=op.f('fk_access_function_assignments_delegated_from_assignment_id_access_function_assignments'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['function_id'], ['access_functions.id'], name=op.f('fk_access_function_assignments_function_id_access_functions'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['identity_id'], ['access_identities.id'], name=op.f('fk_access_function_assignments_identity_id_access_identities'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_unit_id'], ['access_organization_units.id'], name=op.f('fk_access_function_assignments_organization_unit_id_access_organization_units'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_function_assignments_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_function_assignments')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'account_id', 'function_id', 'organization_unit_id', name='uq_function_assignments_account_scope')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_account_id'), 'access_function_assignments', ['account_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_acting_for_account_id'), 'access_function_assignments', ['acting_for_account_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_delegated_from_assignment_id'), 'access_function_assignments', ['delegated_from_assignment_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_function_id'), 'access_function_assignments', ['function_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_identity_id'), 'access_function_assignments', ['identity_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_organization_unit_id'), 'access_function_assignments', ['organization_unit_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_assignments_tenant_id'), 'access_function_assignments', ['tenant_id'], unique=False)
|
||||||
|
op.create_table('access_function_role_assignments',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('function_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('role_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['function_id'], ['access_functions.id'], name=op.f('fk_access_function_role_assignments_function_id_access_functions'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['access_roles.id'], name=op.f('fk_access_function_role_assignments_role_id_access_roles'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_function_role_assignments_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_function_role_assignments')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'function_id', 'role_id', name='uq_function_role_assignments')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_function_role_assignments_function_id'), 'access_function_role_assignments', ['function_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_role_assignments_role_id'), 'access_function_role_assignments', ['role_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_role_assignments_tenant_id'), 'access_function_role_assignments', ['tenant_id'], unique=False)
|
||||||
|
op.create_table('access_function_delegations',
|
||||||
|
sa.Column('id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('function_assignment_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('delegator_account_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('delegate_account_id', sa.String(length=36), nullable=False),
|
||||||
|
sa.Column('mode', sa.String(length=30), nullable=False),
|
||||||
|
sa.Column('reason', sa.Text(), nullable=True),
|
||||||
|
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['delegate_account_id'], ['access_accounts.id'], name=op.f('fk_access_function_delegations_delegate_account_id_access_accounts'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['delegator_account_id'], ['access_accounts.id'], name=op.f('fk_access_function_delegations_delegator_account_id_access_accounts'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['function_assignment_id'], ['access_function_assignments.id'], name=op.f('fk_access_function_delegations_function_assignment_id_access_function_assignments'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['core_scopes.id'], name=op.f('fk_access_function_delegations_tenant_id_scopes'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_access_function_delegations')),
|
||||||
|
sa.UniqueConstraint('tenant_id', 'function_assignment_id', 'delegate_account_id', 'mode', name='uq_function_delegations_assignment_delegate_mode')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_access_function_delegations_delegate_account_id'), 'access_function_delegations', ['delegate_account_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_delegations_delegator_account_id'), 'access_function_delegations', ['delegator_account_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_delegations_function_assignment_id'), 'access_function_delegations', ['function_assignment_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_delegations_revoked_at'), 'access_function_delegations', ['revoked_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_access_function_delegations_tenant_id'), 'access_function_delegations', ['tenant_id'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('access_function_delegations')
|
||||||
|
op.drop_table('access_function_role_assignments')
|
||||||
|
op.drop_table('access_function_assignments')
|
||||||
|
op.drop_table('access_identity_account_links')
|
||||||
|
op.drop_table('access_functions')
|
||||||
|
op.drop_table('access_external_function_role_assignments')
|
||||||
|
op.drop_table('access_organization_units')
|
||||||
|
op.drop_table('access_identities')
|
||||||
@@ -17,7 +17,9 @@ from govoplan_access.backend.db.models import (
|
|||||||
Role,
|
Role,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.security.time import utc_now
|
from govoplan_core.security.time import utc_now
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +37,16 @@ def primary_identity_for_account(session: Session, account_id: str) -> Identity
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def identity_id_for_account(session: Session, account_id: str) -> str | None:
|
def identity_id_for_account(
|
||||||
|
session: Session,
|
||||||
|
account_id: str,
|
||||||
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
if identity_directory is not None:
|
||||||
|
identity = identity_directory.identity_for_account(account_id)
|
||||||
|
if identity is not None and identity.status == "active":
|
||||||
|
return identity.id
|
||||||
identity = primary_identity_for_account(session, account_id)
|
identity = primary_identity_for_account(session, account_id)
|
||||||
return identity.id if identity is not None else None
|
return identity.id if identity is not None else None
|
||||||
|
|
||||||
@@ -163,13 +174,20 @@ def collect_external_function_roles(
|
|||||||
user: User,
|
user: User,
|
||||||
assignments: Iterable[OrganizationFunctionAssignmentRef],
|
assignments: Iterable[OrganizationFunctionAssignmentRef],
|
||||||
*,
|
*,
|
||||||
source_module: str = "organizations",
|
source_module: str = ORGANIZATIONS_MODULE_ID,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> list[Role]:
|
) -> list[Role]:
|
||||||
function_ids = sorted({
|
function_ids = sorted({
|
||||||
assignment.function_id
|
assignment.function_id
|
||||||
for assignment in assignments
|
for assignment in assignments
|
||||||
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
||||||
})
|
})
|
||||||
|
if organization_directory is not None and source_module == ORGANIZATIONS_MODULE_ID:
|
||||||
|
function_ids = [
|
||||||
|
function_id
|
||||||
|
for function_id in function_ids
|
||||||
|
if _organization_function_active(organization_directory, function_id, tenant_id=user.tenant_id)
|
||||||
|
]
|
||||||
if not function_ids:
|
if not function_ids:
|
||||||
return []
|
return []
|
||||||
return (
|
return (
|
||||||
@@ -184,3 +202,13 @@ def collect_external_function_roles(
|
|||||||
.order_by(Role.name.asc())
|
.order_by(Role.name.asc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _organization_function_active(
|
||||||
|
organization_directory: OrganizationDirectory,
|
||||||
|
function_id: str,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
) -> bool:
|
||||||
|
function = organization_directory.get_function(function_id)
|
||||||
|
return function is not None and function.tenant_id == tenant_id and function.status == "active"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.8",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -205,7 +205,6 @@ export type PrivacyRetentionPolicyFieldKey =
|
|||||||
| "audit_detail_level";
|
| "audit_detail_level";
|
||||||
|
|
||||||
export type PrivacyRetentionLimitPermissions = Record<PrivacyRetentionPolicyFieldKey, boolean>;
|
export type PrivacyRetentionLimitPermissions = Record<PrivacyRetentionPolicyFieldKey, boolean>;
|
||||||
export type PrivacyRetentionLimitPermissionPatch = Partial<PrivacyRetentionLimitPermissions>;
|
|
||||||
|
|
||||||
export type PrivacyRetentionPolicy = {
|
export type PrivacyRetentionPolicy = {
|
||||||
store_raw_campaign_json: boolean;
|
store_raw_campaign_json: boolean;
|
||||||
@@ -218,29 +217,6 @@ export type PrivacyRetentionPolicy = {
|
|||||||
allow_lower_level_limits: PrivacyRetentionLimitPermissions;
|
allow_lower_level_limits: PrivacyRetentionLimitPermissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PrivacyRetentionPolicyPatch = Partial<Omit<PrivacyRetentionPolicy, "allow_lower_level_limits">> & {
|
|
||||||
allow_lower_level_limits?: PrivacyRetentionLimitPermissionPatch;
|
|
||||||
};
|
|
||||||
export type PrivacyRetentionPolicyScope = "system" | "tenant" | "user" | "group" | "campaign";
|
|
||||||
|
|
||||||
export type PolicySourceStep = {
|
|
||||||
scope_type: string;
|
|
||||||
scope_id?: string | null;
|
|
||||||
label: string;
|
|
||||||
applied_fields?: string[];
|
|
||||||
policy?: PrivacyRetentionPolicyPatch | PrivacyRetentionPolicy | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PrivacyRetentionPolicyScopeResponse = {
|
|
||||||
scope_type: PrivacyRetentionPolicyScope;
|
|
||||||
scope_id?: string | null;
|
|
||||||
policy: PrivacyRetentionPolicyPatch;
|
|
||||||
effective_policy: PrivacyRetentionPolicy;
|
|
||||||
parent_policy?: PrivacyRetentionPolicy | null;
|
|
||||||
effective_policy_sources?: PolicySourceStep[];
|
|
||||||
parent_policy_sources?: PolicySourceStep[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SystemSettingsItem = {
|
export type SystemSettingsItem = {
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
allow_tenant_custom_groups: boolean;
|
allow_tenant_custom_groups: boolean;
|
||||||
@@ -276,16 +252,6 @@ export type TenantSettingsDeltaSections = Partial<{
|
|||||||
settings: Pick<TenantSettingsItem, "settings">["settings"];
|
settings: Pick<TenantSettingsItem, "settings">["settings"];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type RetentionRunResponse = {
|
|
||||||
result: {
|
|
||||||
dry_run: boolean;
|
|
||||||
policy: PrivacyRetentionPolicy;
|
|
||||||
cutoffs: Record<string, string | null>;
|
|
||||||
effective_policy_scope?: string;
|
|
||||||
counts: Record<string, Record<string, number>>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GovernanceAssignment = {
|
export type GovernanceAssignment = {
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
mode: "available" | "required";
|
mode: "available" | "required";
|
||||||
@@ -329,18 +295,6 @@ export type ExternalFunctionRoleMappingItem = {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuditAdminItem = {
|
|
||||||
id: string;
|
|
||||||
scope: "tenant" | "system";
|
|
||||||
tenant_id?: string | null;
|
|
||||||
actor_email?: string | null;
|
|
||||||
action: string;
|
|
||||||
object_type?: string | null;
|
|
||||||
object_id?: string | null;
|
|
||||||
details: Record<string, unknown>;
|
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DeltaResponseFields = {
|
type DeltaResponseFields = {
|
||||||
deleted: DeltaDeletedItem[];
|
deleted: DeltaDeletedItem[];
|
||||||
watermark?: string | null;
|
watermark?: string | null;
|
||||||
@@ -361,15 +315,6 @@ export type TenantSettingsDeltaResponse = {
|
|||||||
sections: TenantSettingsDeltaSections;
|
sections: TenantSettingsDeltaSections;
|
||||||
changed_sections: string[];
|
changed_sections: string[];
|
||||||
} & DeltaResponseFields;
|
} & DeltaResponseFields;
|
||||||
export type AuditAdminDeltaResponse = {
|
|
||||||
items: AuditAdminItem[];
|
|
||||||
total: number;
|
|
||||||
page: number;
|
|
||||||
page_size: number;
|
|
||||||
pages: number;
|
|
||||||
cursor?: string | null;
|
|
||||||
next_cursor?: string | null;
|
|
||||||
} & DeltaResponseFields;
|
|
||||||
|
|
||||||
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -671,58 +616,6 @@ export function revokeApiKey(settings: ApiSettings, keyId: string): Promise<ApiK
|
|||||||
return apiFetch(settings, `/api/v1/admin/api-keys/${keyId}/revoke`, { method: "POST" });
|
return apiFetch(settings, `/api/v1/admin/api-keys/${keyId}/revoke`, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuditQueryOptions = {
|
|
||||||
tenantId?: string | null;
|
|
||||||
allTenants?: boolean;
|
|
||||||
scope?: "tenant" | "system";
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
cursor?: string | null;
|
|
||||||
sortBy?: "time" | "actor" | "action" | "object" | "tenant";
|
|
||||||
sortDirection?: "asc" | "desc";
|
|
||||||
filters?: Partial<Record<"time" | "actor" | "action" | "object" | "tenant", string>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function fetchAdminAudit(settings: ApiSettings, options: AuditQueryOptions = {}): Promise<{ items: AuditAdminItem[]; total: number; page: number; page_size: number; pages: number; cursor?: string | null; next_cursor?: string | null }> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (options.tenantId) params.set("tenant_id", options.tenantId);
|
|
||||||
if (options.allTenants) params.set("all_tenants", "true");
|
|
||||||
if (options.scope) params.set("scope", options.scope);
|
|
||||||
if (options.limit) params.set("limit", String(options.limit));
|
|
||||||
if (options.offset) params.set("offset", String(options.offset));
|
|
||||||
if (options.page) params.set("page", String(options.page));
|
|
||||||
if (options.pageSize) params.set("page_size", String(options.pageSize));
|
|
||||||
if (options.cursor) params.set("cursor", options.cursor);
|
|
||||||
if (options.sortBy) params.set("sort_by", options.sortBy);
|
|
||||||
if (options.sortDirection) params.set("sort_direction", options.sortDirection);
|
|
||||||
for (const [column, value] of Object.entries(options.filters ?? {})) {
|
|
||||||
if (value?.trim()) params.set(`filter_${column}`, value);
|
|
||||||
}
|
|
||||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return apiFetch(settings, `/api/v1/admin/audit${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchAdminAuditDelta(settings: ApiSettings, options: AuditQueryOptions & { since?: string | null } = {}): Promise<AuditAdminDeltaResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (options.tenantId) params.set("tenant_id", options.tenantId);
|
|
||||||
if (options.allTenants) params.set("all_tenants", "true");
|
|
||||||
if (options.scope) params.set("scope", options.scope);
|
|
||||||
if (options.limit) params.set("limit", String(options.limit));
|
|
||||||
if (options.pageSize) params.set("page_size", String(options.pageSize));
|
|
||||||
if (options.cursor) params.set("cursor", options.cursor);
|
|
||||||
if (options.sortBy) params.set("sort_by", options.sortBy);
|
|
||||||
if (options.sortDirection) params.set("sort_direction", options.sortDirection);
|
|
||||||
if (options.since) params.set("since", options.since);
|
|
||||||
for (const [column, value] of Object.entries(options.filters ?? {})) {
|
|
||||||
if (value?.trim()) params.set(`filter_${column}`, value);
|
|
||||||
}
|
|
||||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return apiFetch(settings, `/api/v1/admin/audit/delta${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function createSystemAccount(settings: ApiSettings, payload: {
|
export function createSystemAccount(settings: ApiSettings, payload: {
|
||||||
email: string;
|
email: string;
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
@@ -759,24 +652,6 @@ export function updateSystemSettings(settings: ApiSettings, payload: SystemSetti
|
|||||||
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrivacyRetentionPolicy(settings: ApiSettings, scope: PrivacyRetentionPolicyScope, scopeId?: string | null): Promise<PrivacyRetentionPolicyScopeResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (scopeId) params.set("scope_id", scopeId);
|
|
||||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return apiFetch(settings, `/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${suffix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updatePrivacyRetentionPolicy(settings: ApiSettings, scope: PrivacyRetentionPolicyScope, policy: PrivacyRetentionPolicyPatch, scopeId?: string | null): Promise<PrivacyRetentionPolicyScopeResponse> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (scopeId) params.set("scope_id", scopeId);
|
|
||||||
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return apiFetch(settings, `/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${suffix}`, { method: "PUT", body: JSON.stringify({ policy }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runRetentionPolicy(settings: ApiSettings, dryRun = true): Promise<RetentionRunResponse> {
|
|
||||||
return apiFetch(settings, "/api/v1/admin/system/retention/run", { method: "POST", body: JSON.stringify({ dry_run: dryRun }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise<GovernanceTemplateItem[]> {
|
export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise<GovernanceTemplateItem[]> {
|
||||||
const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : "";
|
||||||
const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`);
|
const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`);
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { Search } from "lucide-react";
|
|
||||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
|
||||||
import { fetchAdminAudit, fetchAdminAuditDelta, type AuditAdminItem } from "../../api/admin";
|
|
||||||
import { Button } from "@govoplan/core-webui";
|
|
||||||
import { DataGrid, type DataGridColumn, type DataGridQueryState } from "@govoplan/core-webui";
|
|
||||||
import { Dialog } from "@govoplan/core-webui";
|
|
||||||
import { AdminIconButton, AdminPageLayout, adminErrorMessage, formatAdminDateTime as formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui";
|
|
||||||
|
|
||||||
type AuditSortBy = "time" | "actor" | "action" | "object" | "tenant";
|
|
||||||
|
|
||||||
const DEFAULT_QUERY: DataGridQueryState = {
|
|
||||||
sort: { columnId: "time", direction: "desc" },
|
|
||||||
filters: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminAuditPanel({
|
|
||||||
settings,
|
|
||||||
auth,
|
|
||||||
systemMode = false
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}: {settings: ApiSettings;auth: AuthInfo;systemMode?: boolean;}) {
|
|
||||||
const [items, setItems] = useState<AuditAdminItem[]>([]);
|
|
||||||
const itemsRef = useRef<AuditAdminItem[]>([]);
|
|
||||||
const pageItemsRef = useRef<Record<string, AuditAdminItem[]>>({});
|
|
||||||
const pageCursorsRef = useRef<Record<number, string | null>>({ 1: null });
|
|
||||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(10);
|
|
||||||
const [query, setQuery] = useState<DataGridQueryState>(DEFAULT_QUERY);
|
|
||||||
const [selected, setSelected] = useState<AuditAdminItem | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [reloadToken, setReloadToken] = useState(0);
|
|
||||||
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
|
||||||
try {
|
|
||||||
const sortColumn = query.sort?.columnId;
|
|
||||||
const sortBy = sortColumn && ["time", "actor", "action", "object", "tenant"].includes(sortColumn) ?
|
|
||||||
sortColumn as AuditSortBy :
|
|
||||||
"time";
|
|
||||||
const sortDirection = query.sort?.direction ?? "desc";
|
|
||||||
const filters = query.filters;
|
|
||||||
const pageCursor = page === 1 ? null : pageCursorsRef.current[page];
|
|
||||||
const deltaMode = page === 1 || pageCursor !== undefined;
|
|
||||||
const requestOptions = {
|
|
||||||
scope: systemMode ? "system" : "tenant",
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
cursor: pageCursor,
|
|
||||||
sortBy,
|
|
||||||
sortDirection,
|
|
||||||
filters
|
|
||||||
};
|
|
||||||
const deltaKey = `access:audit:${systemMode ? "system" : "tenant"}:${tenantId}:${pageSize}:${page}:${pageCursor ?? "root"}:${JSON.stringify({ sortBy, sortDirection, filters })}`;
|
|
||||||
const response = deltaMode
|
|
||||||
? await fetchAdminAuditDelta(settings, { ...requestOptions, since: getDeltaWatermark(deltaKey) })
|
|
||||||
: await fetchAdminAudit(settings, requestOptions);
|
|
||||||
const baseItems = pageItemsRef.current[deltaKey] ?? [];
|
|
||||||
const nextItems = "full" in response && !response.full
|
|
||||||
? mergeDeltaRows(baseItems, response.items, response.deleted, (item) => item.id, { sort: compareAuditEvents(sortBy, sortDirection) }).slice(0, pageSize)
|
|
||||||
: response.items;
|
|
||||||
pageItemsRef.current[deltaKey] = nextItems;
|
|
||||||
itemsRef.current = nextItems;
|
|
||||||
setItems(nextItems);
|
|
||||||
setTotal(response.total);
|
|
||||||
if (!deltaMode && response.page !== page) setPage(response.page);
|
|
||||||
if (response.cursor !== undefined) pageCursorsRef.current[page] = response.cursor ?? null;
|
|
||||||
if (response.next_cursor !== undefined) {
|
|
||||||
if (response.next_cursor) pageCursorsRef.current[page + 1] = response.next_cursor;
|
|
||||||
else delete pageCursorsRef.current[page + 1];
|
|
||||||
}
|
|
||||||
if ("full" in response && !response.full && page === 1 && (response.items.length > 0 || response.deleted.length > 0)) {
|
|
||||||
pageCursorsRef.current = { 1: null };
|
|
||||||
}
|
|
||||||
if ("watermark" in response) setDeltaWatermark(deltaKey, response.watermark);
|
|
||||||
if (!deltaMode) resetDeltaWatermark(deltaKey);
|
|
||||||
} catch (err) {
|
|
||||||
setError(adminErrorMessage(err));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [settings.accessToken, settings.apiBaseUrl, systemMode, tenantId, page, pageSize, query, reloadToken, getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
itemsRef.current = [];
|
|
||||||
pageItemsRef.current = {};
|
|
||||||
pageCursorsRef.current = { 1: null };
|
|
||||||
resetDeltaWatermark();
|
|
||||||
}, [settings.accessToken, settings.apiBaseUrl, systemMode, tenantId, pageSize, query, resetDeltaWatermark]);
|
|
||||||
|
|
||||||
useEffect(() => {void load();}, [load]);
|
|
||||||
|
|
||||||
const handleQueryChange = useCallback((next: DataGridQueryState) => {
|
|
||||||
setQuery((current) => {
|
|
||||||
if (JSON.stringify(current) === JSON.stringify(next)) return current;
|
|
||||||
setPage(1);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const columns = useMemo<DataGridColumn<AuditAdminItem>[]>(() => [
|
|
||||||
{ id: "time", header: "i18n:govoplan-access.time.6c82e6dd", width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) },
|
|
||||||
{ id: "actor", header: "i18n:govoplan-access.actor.cbd19b5c", width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "i18n:govoplan-access.system.bc0792d8" },
|
|
||||||
{ id: "action", header: "i18n:govoplan-access.action.97c89a4d", width: 250, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => row.action },
|
|
||||||
{ id: "object", header: "i18n:govoplan-access.object.2883f191", width: 300, minWidth: 180, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => `${row.object_type || "—"} ${row.object_id || ""}`.trim() },
|
|
||||||
...(systemMode ? [{ id: "tenant", header: "i18n:govoplan-access.tenant_context.b401a2ad", width: 190, minWidth: 150, maxWidth: 300, resizable: true, sortable: true, filterable: true, value: (row: AuditAdminItem) => row.tenant_id || "—" }] : []),
|
|
||||||
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 70, sticky: "end", resizable: false, align: "right", render: (row) => <div className="admin-icon-actions"><AdminIconButton label="i18n:govoplan-access.inspect_audit_event.5776c1b2" icon={<Search />} onClick={() => setSelected(row)} /></div> }],
|
|
||||||
[systemMode]);
|
|
||||||
|
|
||||||
const firstShown = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
||||||
const lastShown = Math.min(total, page * pageSize);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AdminPageLayout
|
|
||||||
title={systemMode ? "i18n:govoplan-access.system_audit.69c6b424" : "i18n:govoplan-access.tenant_audit.492b9138"}
|
|
||||||
description={systemMode ? i18nMessage("i18n:govoplan-access.system_level_administrative_history_showing_valu.c8a089a1", { value0:
|
|
||||||
firstShown, value1: lastShown, value2: total }) : i18nMessage("i18n:govoplan-access.tenant_level_administrative_history_for_the_acti.2f8fbfff", { value0:
|
|
||||||
firstShown, value1: lastShown, value2: total })}
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
actions={<Button onClick={() => setReloadToken((value) => value + 1)} disabled={loading}>i18n:govoplan-access.reload.cce71553</Button>}>
|
|
||||||
|
|
||||||
<div className="admin-table-surface">
|
|
||||||
<DataGrid
|
|
||||||
id={systemMode ? "admin-system-audit-v5" : "admin-tenant-audit-v5"}
|
|
||||||
rows={items}
|
|
||||||
columns={columns}
|
|
||||||
initialFit="container" getRowKey={(row) => row.id}
|
|
||||||
emptyText="i18n:govoplan-access.no_administrative_audit_records_found.8d128767"
|
|
||||||
className="admin-audit-grid"
|
|
||||||
initialSort={{ columnId: "time", direction: "desc" }}
|
|
||||||
pagination={{
|
|
||||||
mode: "server",
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalRows: total,
|
|
||||||
pageSizeOptions: [10, 25, 50, 100, 250],
|
|
||||||
disabled: loading,
|
|
||||||
onPageChange: setPage,
|
|
||||||
onPageSizeChange: (next) => {setPageSize(next);setPage(1);}
|
|
||||||
}}
|
|
||||||
onQueryChange={handleQueryChange} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</AdminPageLayout>
|
|
||||||
<Dialog open={Boolean(selected)} title="i18n:govoplan-access.audit_event_details.3749b52d" onClose={() => setSelected(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setSelected(null)}>i18n:govoplan-access.close.bbfa773e</Button>}>
|
|
||||||
{selected && <><dl className="admin-details-grid"><div><dt>i18n:govoplan-access.scope.4651a34e</dt><dd>{selected.scope}</dd></div><div><dt>i18n:govoplan-access.action.97c89a4d</dt><dd>{selected.action}</dd></div><div><dt>i18n:govoplan-access.actor.cbd19b5c</dt><dd>{selected.actor_email || "i18n:govoplan-access.system.bc0792d8"}</dd></div><div><dt>i18n:govoplan-access.object.2883f191</dt><dd>{selected.object_type || "—"} {selected.object_id || ""}</dd></div><div><dt>i18n:govoplan-access.tenant_context.b401a2ad</dt><dd>{selected.tenant_id || "—"}</dd></div><div><dt>i18n:govoplan-access.time.6c82e6dd</dt><dd>{formatDateTime(selected.created_at)}</dd></div></dl><pre className="admin-json-preview">{JSON.stringify(selected.details, null, 2)}</pre></>}
|
|
||||||
</Dialog>
|
|
||||||
</>);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareAuditEvents(sortBy: AuditSortBy, sortDirection: "asc" | "desc"): (left: AuditAdminItem, right: AuditAdminItem) => number {
|
|
||||||
return (left, right) => {
|
|
||||||
const primary = compareAuditValues(auditSortValue(left, sortBy), auditSortValue(right, sortBy));
|
|
||||||
const directed = sortDirection === "asc" ? primary : -primary;
|
|
||||||
return directed || right.id.localeCompare(left.id);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function auditSortValue(item: AuditAdminItem, sortBy: AuditSortBy): string | number {
|
|
||||||
if (sortBy === "time") return new Date(item.created_at).getTime();
|
|
||||||
if (sortBy === "actor") return item.actor_email || "System";
|
|
||||||
if (sortBy === "action") return item.action;
|
|
||||||
if (sortBy === "object") return `${item.object_type || ""} ${item.object_id || ""}`;
|
|
||||||
return item.tenant_id || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareAuditValues(left: string | number, right: string | number): number {
|
|
||||||
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
||||||
return String(left).localeCompare(String(right));
|
|
||||||
}
|
|
||||||
@@ -22,11 +22,9 @@ import GroupsPanel from "./GroupsPanel";
|
|||||||
import RolesPanel from "./RolesPanel";
|
import RolesPanel from "./RolesPanel";
|
||||||
import ExternalFunctionRoleMappingsPanel from "./ExternalFunctionRoleMappingsPanel";
|
import ExternalFunctionRoleMappingsPanel from "./ExternalFunctionRoleMappingsPanel";
|
||||||
import ApiKeysPanel from "./ApiKeysPanel";
|
import ApiKeysPanel from "./ApiKeysPanel";
|
||||||
import AdminAuditPanel from "./AdminAuditPanel";
|
|
||||||
import FileConnectorsPanel from "./FileConnectorsPanel";
|
import FileConnectorsPanel from "./FileConnectorsPanel";
|
||||||
import MailProfilesPanel from "./MailProfilesPanel";
|
import MailProfilesPanel from "./MailProfilesPanel";
|
||||||
import RetentionPoliciesPanel from "./RetentionPoliciesPanel";
|
import { usePlatformUiCapabilities, usePlatformUiCapability } from "@govoplan/core-webui";
|
||||||
import { usePlatformModuleInstalled, usePlatformUiCapabilities, usePlatformUiCapability } from "@govoplan/core-webui";
|
|
||||||
|
|
||||||
type AdminSection = string;
|
type AdminSection = string;
|
||||||
type OrderedAdminNavItem = { id: AdminSection; label: string; order: number };
|
type OrderedAdminNavItem = { id: AdminSection; label: string; order: number };
|
||||||
@@ -37,7 +35,6 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
"system-configuration-changes",
|
"system-configuration-changes",
|
||||||
"system-configuration-packages",
|
"system-configuration-packages",
|
||||||
"system-modules",
|
"system-modules",
|
||||||
"system-audit",
|
|
||||||
"system-tenants",
|
"system-tenants",
|
||||||
"system-roles",
|
"system-roles",
|
||||||
"system-role-templates",
|
"system-role-templates",
|
||||||
@@ -45,7 +42,6 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
"system-users",
|
"system-users",
|
||||||
"system-file-connectors",
|
"system-file-connectors",
|
||||||
"system-mail-servers",
|
"system-mail-servers",
|
||||||
"system-retention",
|
|
||||||
"tenant-settings",
|
"tenant-settings",
|
||||||
"tenant-roles",
|
"tenant-roles",
|
||||||
"tenant-function-role-mappings",
|
"tenant-function-role-mappings",
|
||||||
@@ -54,14 +50,10 @@ const handledAdminSectionIds = new Set<string>([
|
|||||||
"tenant-file-connectors",
|
"tenant-file-connectors",
|
||||||
"tenant-mail-servers",
|
"tenant-mail-servers",
|
||||||
"tenant-api-keys",
|
"tenant-api-keys",
|
||||||
"tenant-retention",
|
|
||||||
"tenant-audit",
|
|
||||||
"tenant-group-file-connectors",
|
"tenant-group-file-connectors",
|
||||||
"tenant-group-mail-servers",
|
"tenant-group-mail-servers",
|
||||||
"tenant-group-retention",
|
|
||||||
"tenant-user-file-connectors",
|
"tenant-user-file-connectors",
|
||||||
"tenant-user-mail-servers",
|
"tenant-user-mail-servers"
|
||||||
"tenant-user-retention"
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default function AdminPage({
|
export default function AdminPage({
|
||||||
@@ -79,8 +71,6 @@ export default function AdminPage({
|
|||||||
const adminSectionCapabilities = usePlatformUiCapabilities<AdminSectionsUiCapability>("admin.sections");
|
const adminSectionCapabilities = usePlatformUiCapabilities<AdminSectionsUiCapability>("admin.sections");
|
||||||
const mailProfilesAvailable = Boolean(mailProfilesUi);
|
const mailProfilesAvailable = Boolean(mailProfilesUi);
|
||||||
const fileConnectorsAvailable = Boolean(fileConnectorsUi);
|
const fileConnectorsAvailable = Boolean(fileConnectorsUi);
|
||||||
const auditAvailable = usePlatformModuleInstalled("audit");
|
|
||||||
const policyAvailable = usePlatformModuleInstalled("policy");
|
|
||||||
const contributedSections = useMemo(
|
const contributedSections = useMemo(
|
||||||
() =>
|
() =>
|
||||||
adminSectionCapabilities
|
adminSectionCapabilities
|
||||||
@@ -102,13 +92,11 @@ export default function AdminPage({
|
|||||||
if (canUseContributedSection(auth, section)) sections.add(section.id);
|
if (canUseContributedSection(auth, section)) sections.add(section.id);
|
||||||
}
|
}
|
||||||
if (hasScope(auth, "system:settings:read")) {
|
if (hasScope(auth, "system:settings:read")) {
|
||||||
if (policyAvailable) sections.add("system-retention");
|
|
||||||
if (mailProfilesAvailable) sections.add("system-mail-servers");
|
if (mailProfilesAvailable) sections.add("system-mail-servers");
|
||||||
}
|
}
|
||||||
if (hasScope(auth, "system:tenants:read")) sections.add("system-tenants");
|
if (hasScope(auth, "system:tenants:read")) sections.add("system-tenants");
|
||||||
if (hasAnyScope(auth, ["system:accounts:read", "system:access:read"])) sections.add("system-users");
|
if (hasAnyScope(auth, ["system:accounts:read", "system:access:read"])) sections.add("system-users");
|
||||||
if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles");
|
if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles");
|
||||||
if (auditAvailable && hasScope(auth, "system:audit:read")) sections.add("system-audit");
|
|
||||||
if (hasScope(auth, "admin:users:read")) sections.add("tenant-users");
|
if (hasScope(auth, "admin:users:read")) sections.add("tenant-users");
|
||||||
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-groups");
|
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-groups");
|
||||||
if (hasScope(auth, "admin:roles:read")) sections.add("tenant-roles");
|
if (hasScope(auth, "admin:roles:read")) sections.add("tenant-roles");
|
||||||
@@ -123,15 +111,9 @@ export default function AdminPage({
|
|||||||
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-file-connectors");
|
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-file-connectors");
|
||||||
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-file-connectors");
|
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-file-connectors");
|
||||||
}
|
}
|
||||||
if (policyAvailable && hasScope(auth, "admin:policies:read")) {
|
|
||||||
sections.add("tenant-retention");
|
|
||||||
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-retention");
|
|
||||||
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-retention");
|
|
||||||
}
|
|
||||||
if (hasScope(auth, "admin:settings:read")) sections.add("tenant-settings");
|
if (hasScope(auth, "admin:settings:read")) sections.add("tenant-settings");
|
||||||
if (auditAvailable && hasScope(auth, "audit:read")) sections.add("tenant-audit");
|
|
||||||
return sections;
|
return sections;
|
||||||
}, [auth, auditAvailable, contributedSections, fileConnectorsAvailable, mailProfilesAvailable, organizationFunctionPicker, policyAvailable]);
|
}, [auth, contributedSections, fileConnectorsAvailable, mailProfilesAvailable, organizationFunctionPicker]);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const requestedSection = searchParams.get("section") as AdminSection | null;
|
const requestedSection = searchParams.get("section") as AdminSection | null;
|
||||||
const fallbackSection = available.has("overview") ? "overview" : (Array.from(available)[0] ?? "overview");
|
const fallbackSection = available.has("overview") ? "overview" : (Array.from(available)[0] ?? "overview");
|
||||||
@@ -176,7 +158,6 @@ export default function AdminPage({
|
|||||||
visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20),
|
visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20),
|
||||||
visibleNavItem(available, "system-settings", "i18n:govoplan-access.maintenance.94de303b", 30),
|
visibleNavItem(available, "system-settings", "i18n:govoplan-access.maintenance.94de303b", 30),
|
||||||
visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40),
|
visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40),
|
||||||
visibleNavItem(available, "system-audit", "i18n:govoplan-access.audit.fa1703dd", 90),
|
|
||||||
...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -192,7 +173,6 @@ export default function AdminPage({
|
|||||||
visibleNavItem(available, "system-users", "i18n:govoplan-access.users.57f2b181", 50),
|
visibleNavItem(available, "system-users", "i18n:govoplan-access.users.57f2b181", 50),
|
||||||
visibleNavItem(available, "system-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 60),
|
visibleNavItem(available, "system-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 60),
|
||||||
visibleNavItem(available, "system-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 70),
|
visibleNavItem(available, "system-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 70),
|
||||||
visibleNavItem(available, "system-retention", "i18n:govoplan-access.retention.c7199d9e", 80),
|
|
||||||
...contributedNavItems(contributedSections, available, "GLOBAL", handledAdminSectionIds),
|
...contributedNavItems(contributedSections, available, "GLOBAL", handledAdminSectionIds),
|
||||||
...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
@@ -209,9 +189,7 @@ export default function AdminPage({
|
|||||||
visibleNavItem(available, "tenant-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 50),
|
visibleNavItem(available, "tenant-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 50),
|
||||||
visibleNavItem(available, "tenant-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 60),
|
visibleNavItem(available, "tenant-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 60),
|
||||||
visibleNavItem(available, "tenant-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 70),
|
visibleNavItem(available, "tenant-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 70),
|
||||||
visibleNavItem(available, "tenant-retention", "i18n:govoplan-access.retention.c7199d9e", 80),
|
|
||||||
visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90),
|
visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90),
|
||||||
visibleNavItem(available, "tenant-audit", "i18n:govoplan-access.audit.fa1703dd", 100),
|
|
||||||
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -222,7 +200,6 @@ export default function AdminPage({
|
|||||||
sortNavItems([
|
sortNavItems([
|
||||||
visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
|
visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
|
||||||
visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
|
visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
|
||||||
visibleNavItem(available, "tenant-group-retention", "i18n:govoplan-access.retention.c7199d9e", 30),
|
|
||||||
...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -233,7 +210,6 @@ export default function AdminPage({
|
|||||||
sortNavItems([
|
sortNavItems([
|
||||||
visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
|
visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
|
||||||
visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
|
visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
|
||||||
visibleNavItem(available, "tenant-user-retention", "i18n:govoplan-access.retention.c7199d9e", 30),
|
|
||||||
...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds)
|
...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
@@ -248,7 +224,6 @@ export default function AdminPage({
|
|||||||
<section className="workspace-content">
|
<section className="workspace-content">
|
||||||
<div className="content-pad workspace-data-page">
|
<div className="content-pad workspace-data-page">
|
||||||
{contributedSection && contributedSection.render(contributionContext)}
|
{contributedSection && contributedSection.render(contributionContext)}
|
||||||
{!contributedSection && active === "system-retention" && <RetentionPoliciesPanel settings={settings} scopeType="system" canWrite={hasScope(auth, "system:settings:write")} />}
|
|
||||||
{!contributedSection && active === "system-mail-servers" && (
|
{!contributedSection && active === "system-mail-servers" && (
|
||||||
<MailProfilesPanel settings={settings} scopeType="system" canWriteProfiles={hasScope(auth, "system:settings:write")} canManageCredentials={hasScope(auth, "system:settings:write")} canWritePolicy={hasScope(auth, "system:settings:write")} />
|
<MailProfilesPanel settings={settings} scopeType="system" canWriteProfiles={hasScope(auth, "system:settings:write")} canManageCredentials={hasScope(auth, "system:settings:write")} canWritePolicy={hasScope(auth, "system:settings:write")} />
|
||||||
)}
|
)}
|
||||||
@@ -267,7 +242,6 @@ export default function AdminPage({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!contributedSection && active === "system-roles" && <SystemRolesPanel settings={settings} canWrite={hasScope(auth, "system:roles:write")} onAuthRefresh={refreshAuth} />}
|
{!contributedSection && active === "system-roles" && <SystemRolesPanel settings={settings} canWrite={hasScope(auth, "system:roles:write")} onAuthRefresh={refreshAuth} />}
|
||||||
{!contributedSection && active === "system-audit" && <AdminAuditPanel settings={settings} auth={auth} systemMode />}
|
|
||||||
{!contributedSection && active === "tenant-users" && <UsersPanel settings={settings} auth={auth} canCreate={hasScope(auth, "admin:users:create")} canUpdate={hasScope(auth, "admin:users:update")} canSuspend={hasScope(auth, "admin:users:suspend")} canManageGroups={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
{!contributedSection && active === "tenant-users" && <UsersPanel settings={settings} auth={auth} canCreate={hasScope(auth, "admin:users:create")} canUpdate={hasScope(auth, "admin:users:update")} canSuspend={hasScope(auth, "admin:users:suspend")} canManageGroups={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
||||||
{!contributedSection && active === "tenant-groups" && <GroupsPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:groups:write")} canManageMembers={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
{!contributedSection && active === "tenant-groups" && <GroupsPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:groups:write")} canManageMembers={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
|
||||||
{!contributedSection && active === "tenant-roles" && <RolesPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:roles:write")} onAuthRefresh={refreshAuth} />}
|
{!contributedSection && active === "tenant-roles" && <RolesPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:roles:write")} onAuthRefresh={refreshAuth} />}
|
||||||
@@ -278,11 +252,7 @@ export default function AdminPage({
|
|||||||
{!contributedSection && active === "tenant-group-mail-servers" && <MailProfilesPanel settings={settings} scopeType="group" canWriteProfiles={hasScope(auth, "mail_servers:write")} canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")} canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />}
|
{!contributedSection && active === "tenant-group-mail-servers" && <MailProfilesPanel settings={settings} scopeType="group" canWriteProfiles={hasScope(auth, "mail_servers:write")} canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")} canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />}
|
||||||
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
||||||
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
|
||||||
{!contributedSection && active === "tenant-retention" && <RetentionPoliciesPanel settings={settings} scopeType="tenant" canWrite={hasScope(auth, "admin:policies:write")} />}
|
|
||||||
{!contributedSection && active === "tenant-user-retention" && <RetentionPoliciesPanel settings={settings} scopeType="user" canWrite={hasScope(auth, "admin:policies:write")} />}
|
|
||||||
{!contributedSection && active === "tenant-group-retention" && <RetentionPoliciesPanel settings={settings} scopeType="group" canWrite={hasScope(auth, "admin:policies:write")} />}
|
|
||||||
{!contributedSection && active === "tenant-settings" && <TenantSettingsPanel settings={settings} canWrite={hasScope(auth, "admin:settings:write")} onAuthRefresh={refreshAuth} />}
|
{!contributedSection && active === "tenant-settings" && <TenantSettingsPanel settings={settings} canWrite={hasScope(auth, "admin:settings:write")} onAuthRefresh={refreshAuth} />}
|
||||||
{!contributedSection && active === "tenant-audit" && <AdminAuditPanel settings={settings} auth={auth} />}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import type { ApiSettings } from "@govoplan/core-webui";
|
|
||||||
import { fetchGroupsDelta, fetchUsersDelta, runRetentionPolicy, type GroupSummary, type PrivacyRetentionPolicyScope, type RetentionRunResponse, type UserAdminItem } from "../../api/admin";
|
|
||||||
import { Button } from "@govoplan/core-webui";
|
|
||||||
import { Card } from "@govoplan/core-webui";
|
|
||||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
|
||||||
import { RetentionPolicyScopeManager, type RetentionPolicyTargetOption } from "@govoplan/core-webui";
|
|
||||||
import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks } from "@govoplan/core-webui";
|
|
||||||
import { loadDeltaRows } from "./utils/deltaRows";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
settings: ApiSettings;
|
|
||||||
scopeType: Extract<PrivacyRetentionPolicyScope, "system" | "tenant" | "user" | "group">;
|
|
||||||
canWrite: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copy: Record<Props["scopeType"], {title: string;description: string;targetLabel?: string;policyTitle: string;policyDescription: string;}> = {
|
|
||||||
system: {
|
|
||||||
title: "i18n:govoplan-access.system_retention.4191e7f7",
|
|
||||||
description: "i18n:govoplan-access.instance_wide_privacy_retention_policy_and_lower.646ce224",
|
|
||||||
policyTitle: "i18n:govoplan-access.system_retention_policy.7027f6ba",
|
|
||||||
policyDescription: "i18n:govoplan-access.set_concrete_system_retention_values_the_allow_o.02e1fd13"
|
|
||||||
},
|
|
||||||
tenant: {
|
|
||||||
title: "i18n:govoplan-access.tenant_retention.95b35db0",
|
|
||||||
description: "i18n:govoplan-access.tenant_level_privacy_and_retention_limits_for_th.a6d4108c",
|
|
||||||
policyTitle: "i18n:govoplan-access.tenant_retention_policy.f10893d7",
|
|
||||||
policyDescription: "i18n:govoplan-access.tenant_limits_may_only_narrow_the_system_policy_.de974a77"
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
title: "i18n:govoplan-access.user_retention.f0966bbf",
|
|
||||||
description: "i18n:govoplan-access.user_scoped_retention_limits_for_campaigns_owned.cbc9268b",
|
|
||||||
targetLabel: "i18n:govoplan-access.user.9f8a2389",
|
|
||||||
policyTitle: "i18n:govoplan-access.user_retention_policy.2776f485",
|
|
||||||
policyDescription: "i18n:govoplan-access.user_limits_may_only_narrow_inherited_system_and.b194c7c0"
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
title: "i18n:govoplan-access.group_retention.57cdcdaa",
|
|
||||||
description: "i18n:govoplan-access.group_scoped_retention_limits_for_group_owned_ca.e8e25e04",
|
|
||||||
targetLabel: "i18n:govoplan-access.group.171a0606",
|
|
||||||
policyTitle: "i18n:govoplan-access.group_retention_policy.ad941c0b",
|
|
||||||
policyDescription: "i18n:govoplan-access.group_limits_may_only_narrow_inherited_system_an.32649b6f"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }: Props) {
|
|
||||||
const [targets, setTargets] = useState<RetentionPolicyTargetOption[]>([]);
|
|
||||||
const usersRef = useRef<UserAdminItem[]>([]);
|
|
||||||
const groupsRef = useRef<GroupSummary[]>([]);
|
|
||||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
|
||||||
const [loadingTargets, setLoadingTargets] = useState(scopeType === "user" || scopeType === "group");
|
|
||||||
const [targetError, setTargetError] = useState("");
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const [success, setSuccess] = useState("");
|
|
||||||
const [runError, setRunError] = useState("");
|
|
||||||
const [confirmRetentionRun, setConfirmRetentionRun] = useState(false);
|
|
||||||
const [retentionResult, setRetentionResult] = useState<RetentionRunResponse | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
usersRef.current = [];
|
|
||||||
groupsRef.current = [];
|
|
||||||
resetDeltaWatermark();
|
|
||||||
void loadTargets();
|
|
||||||
}, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, resetDeltaWatermark]);
|
|
||||||
|
|
||||||
async function loadTargets() {
|
|
||||||
if (scopeType !== "user" && scopeType !== "group") {
|
|
||||||
setTargets([]);
|
|
||||||
setLoadingTargets(false);
|
|
||||||
setTargetError("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoadingTargets(true);
|
|
||||||
setTargetError("");
|
|
||||||
try {
|
|
||||||
if (scopeType === "user") {
|
|
||||||
const users = await loadDeltaRows(usersRef.current, "access:retention-users", getDeltaWatermark, setDeltaWatermark, (since) => fetchUsersDelta(settings, { since }), (response) => response.users, (user) => user.id, "access_user", sortUsers);
|
|
||||||
usersRef.current = users;
|
|
||||||
setTargets(users.map((user) => ({
|
|
||||||
id: user.id,
|
|
||||||
label: user.display_name || user.email,
|
|
||||||
secondary: user.display_name ? user.email : null
|
|
||||||
})));
|
|
||||||
} else {
|
|
||||||
const groups = await loadDeltaRows(groupsRef.current, "access:retention-groups", getDeltaWatermark, setDeltaWatermark, (since) => fetchGroupsDelta(settings, { since }), (response) => response.groups, (group) => group.id, "access_group", sortGroups);
|
|
||||||
groupsRef.current = groups;
|
|
||||||
setTargets(groups.map((group) => ({
|
|
||||||
id: group.id,
|
|
||||||
label: group.name,
|
|
||||||
secondary: group.slug
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setTargets([]);
|
|
||||||
setTargetError(adminErrorMessage(err));
|
|
||||||
} finally {
|
|
||||||
setLoadingTargets(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runRetention(dryRun: boolean) {
|
|
||||||
setBusy(true);
|
|
||||||
setRunError("");
|
|
||||||
setSuccess("");
|
|
||||||
try {
|
|
||||||
const response = await runRetentionPolicy(settings, dryRun);
|
|
||||||
setRetentionResult(response);
|
|
||||||
setSuccess(dryRun ? "i18n:govoplan-access.retention_dry_run_completed.91895aee" : "i18n:govoplan-access.retention_policy_applied.7fa4e050");
|
|
||||||
setConfirmRetentionRun(false);
|
|
||||||
} catch (err) {setRunError(adminErrorMessage(err));} finally
|
|
||||||
{setBusy(false);}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = copy[scopeType];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AdminPageLayout title={labels.title} description={labels.description} loading={loadingTargets} error={targetError || runError} success={success}>
|
|
||||||
<RetentionPolicyScopeManager
|
|
||||||
settings={settings}
|
|
||||||
scopeType={scopeType}
|
|
||||||
targetOptions={targets}
|
|
||||||
targetLabel={labels.targetLabel}
|
|
||||||
title={labels.policyTitle}
|
|
||||||
description={labels.policyDescription}
|
|
||||||
canWrite={canWrite} />
|
|
||||||
|
|
||||||
{scopeType === "system" &&
|
|
||||||
<div className="retention-run-card">
|
|
||||||
<Card title="i18n:govoplan-access.retention_execution.84b7105d">
|
|
||||||
<p className="muted small-note">i18n:govoplan-access.run_the_saved_effective_retention_policy_against.cd39a54c</p>
|
|
||||||
<div className="button-row compact-actions subsection-bottom-actions">
|
|
||||||
<Button onClick={() => void runRetention(true)} disabled={!canWrite || busy}>i18n:govoplan-access.dry_run.485a3d15</Button>
|
|
||||||
<Button variant="danger" onClick={() => setConfirmRetentionRun(true)} disabled={!canWrite || busy}>i18n:govoplan-access.apply_retention.5b991811</Button>
|
|
||||||
</div>
|
|
||||||
{retentionResult && <pre className="admin-json-preview">{JSON.stringify(retentionResult.result, null, 2)}</pre>}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</AdminPageLayout>
|
|
||||||
<ConfirmDialog
|
|
||||||
open={confirmRetentionRun}
|
|
||||||
title="i18n:govoplan-access.apply_retention_policy.9e5d32b4"
|
|
||||||
message="i18n:govoplan-access.this_will_redact_or_delete_eligible_retained_dat.e8e80715"
|
|
||||||
confirmLabel="i18n:govoplan-access.apply_retention.5b991811"
|
|
||||||
tone="danger"
|
|
||||||
busy={busy}
|
|
||||||
onCancel={() => setConfirmRetentionRun(false)}
|
|
||||||
onConfirm={() => void runRetention(false)} />
|
|
||||||
|
|
||||||
</>);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortUsers(left: UserAdminItem, right: UserAdminItem): number {
|
|
||||||
return left.email.localeCompare(right.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortGroups(left: GroupSummary, right: GroupSummary): number {
|
|
||||||
return left.name.localeCompare(right.name) || left.slug.localeCompare(right.slug);
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.add_api_key.725d9988": "Add API key",
|
"i18n:govoplan-access.add_api_key.725d9988": "Add API key",
|
||||||
"i18n:govoplan-access.add_global_account.18e4df22": "Add global account",
|
"i18n:govoplan-access.add_global_account.18e4df22": "Add global account",
|
||||||
"i18n:govoplan-access.add_group.2fca464f": "Add group",
|
"i18n:govoplan-access.add_group.2fca464f": "Add group",
|
||||||
"i18n:govoplan-access.add_function_role_mapping.1bc376ac": "Add function role mapping",
|
"i18n:govoplan-access.add_function_role_mapping.1bc376ac": "Add function mapping",
|
||||||
"i18n:govoplan-access.add_role.d8d5d55c": "Add role",
|
"i18n:govoplan-access.add_role.d8d5d55c": "Add role",
|
||||||
"i18n:govoplan-access.add_system_role.f9ef262b": "Add system role",
|
"i18n:govoplan-access.add_system_role.f9ef262b": "Add system role",
|
||||||
"i18n:govoplan-access.add_tenant_user.36f37ce7": "Add tenant user",
|
"i18n:govoplan-access.add_tenant_user.36f37ce7": "Add tenant user",
|
||||||
@@ -55,7 +55,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.create_api_key.d7b30388": "Create API key",
|
"i18n:govoplan-access.create_api_key.d7b30388": "Create API key",
|
||||||
"i18n:govoplan-access.create_global_account.e821f016": "Create global account",
|
"i18n:govoplan-access.create_global_account.e821f016": "Create global account",
|
||||||
"i18n:govoplan-access.create_group.5a0b1c17": "Create group",
|
"i18n:govoplan-access.create_group.5a0b1c17": "Create group",
|
||||||
"i18n:govoplan-access.create_function_role_mapping.3718168d": "Create function role mapping",
|
"i18n:govoplan-access.create_function_role_mapping.3718168d": "Create function mapping",
|
||||||
"i18n:govoplan-access.create_key.e028cb09": "Create key",
|
"i18n:govoplan-access.create_key.e028cb09": "Create key",
|
||||||
"i18n:govoplan-access.create_role.db859bad": "Create role",
|
"i18n:govoplan-access.create_role.db859bad": "Create role",
|
||||||
"i18n:govoplan-access.create_system_role.a1e40b25": "Create system role",
|
"i18n:govoplan-access.create_system_role.a1e40b25": "Create system role",
|
||||||
@@ -80,7 +80,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.deactivate_value.a276a667": "Deactivate {value0}",
|
"i18n:govoplan-access.deactivate_value.a276a667": "Deactivate {value0}",
|
||||||
"i18n:govoplan-access.default_locale.b99d021f": "Default locale",
|
"i18n:govoplan-access.default_locale.b99d021f": "Default locale",
|
||||||
"i18n:govoplan-access.delete_role.fbf0667e": "Delete role",
|
"i18n:govoplan-access.delete_role.fbf0667e": "Delete role",
|
||||||
"i18n:govoplan-access.delete_function_role_mapping.0c0eec6e": "Delete function role mapping",
|
"i18n:govoplan-access.delete_function_role_mapping.0c0eec6e": "Delete function mapping",
|
||||||
"i18n:govoplan-access.delete_function_role_mapping_value.419da2aa": "Delete the mapping for {value0}? Accepted assignments will no longer grant the mapped role.",
|
"i18n:govoplan-access.delete_function_role_mapping_value.419da2aa": "Delete the mapping for {value0}? Accepted assignments will no longer grant the mapped role.",
|
||||||
"i18n:govoplan-access.delete_mapping.0d27d92a": "Delete mapping",
|
"i18n:govoplan-access.delete_mapping.0d27d92a": "Delete mapping",
|
||||||
"i18n:govoplan-access.delete_system_role.e2d84a56": "Delete system role",
|
"i18n:govoplan-access.delete_system_role.e2d84a56": "Delete system role",
|
||||||
@@ -95,7 +95,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.dry_run.485a3d15": "Dry run",
|
"i18n:govoplan-access.dry_run.485a3d15": "Dry run",
|
||||||
"i18n:govoplan-access.edit_global_account.d13b8485": "Edit global account",
|
"i18n:govoplan-access.edit_global_account.d13b8485": "Edit global account",
|
||||||
"i18n:govoplan-access.edit_group.edb57d8e": "Edit group",
|
"i18n:govoplan-access.edit_group.edb57d8e": "Edit group",
|
||||||
"i18n:govoplan-access.edit_function_role_mapping.91ee75af": "Edit function role mapping",
|
"i18n:govoplan-access.edit_function_role_mapping.91ee75af": "Edit function mapping",
|
||||||
"i18n:govoplan-access.edit_role.61dd63e9": "Edit role",
|
"i18n:govoplan-access.edit_role.61dd63e9": "Edit role",
|
||||||
"i18n:govoplan-access.edit_system_role.6ebb7cb0": "Edit system role",
|
"i18n:govoplan-access.edit_system_role.6ebb7cb0": "Edit system role",
|
||||||
"i18n:govoplan-access.edit_tenant_user.99121a61": "Edit tenant user",
|
"i18n:govoplan-access.edit_tenant_user.99121a61": "Edit tenant user",
|
||||||
@@ -114,11 +114,11 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.function_fact.4f7435e4": "Function fact",
|
"i18n:govoplan-access.function_fact.4f7435e4": "Function fact",
|
||||||
"i18n:govoplan-access.function_facts.848b32cc": "Function facts",
|
"i18n:govoplan-access.function_facts.848b32cc": "Function facts",
|
||||||
"i18n:govoplan-access.function_id.e5e08937": "Function ID",
|
"i18n:govoplan-access.function_id.e5e08937": "Function ID",
|
||||||
"i18n:govoplan-access.function_role_mapping_created.7a25eb5a": "Function role mapping created.",
|
"i18n:govoplan-access.function_role_mapping_created.7a25eb5a": "Function mapping created.",
|
||||||
"i18n:govoplan-access.function_role_mapping_deleted.fb180786": "Function role mapping deleted.",
|
"i18n:govoplan-access.function_role_mapping_deleted.fb180786": "Function mapping deleted.",
|
||||||
"i18n:govoplan-access.function_role_mapping_help.0cf9996a": "The source module and function ID identify an accepted external function fact. Access grants the selected tenant role only while IDM reports an active accepted assignment for that fact.",
|
"i18n:govoplan-access.function_role_mapping_help.0cf9996a": "The source module and function ID identify an accepted external function fact. Access grants the selected tenant role only while IDM reports an active accepted assignment for that fact.",
|
||||||
"i18n:govoplan-access.function_role_mapping_updated.76020443": "Function role mapping updated.",
|
"i18n:govoplan-access.function_role_mapping_updated.76020443": "Function mapping updated.",
|
||||||
"i18n:govoplan-access.function_role_mappings.2b64e9c3": "Function role mappings",
|
"i18n:govoplan-access.function_role_mappings.2b64e9c3": "Function mappings",
|
||||||
"i18n:govoplan-access.general.9239ee2c": "General",
|
"i18n:govoplan-access.general.9239ee2c": "General",
|
||||||
"i18n:govoplan-access.global_account_details.0a0cf240": "Global account details",
|
"i18n:govoplan-access.global_account_details.0a0cf240": "Global account details",
|
||||||
"i18n:govoplan-access.global_account_value_created.5100e467": "Global account {value0} created.",
|
"i18n:govoplan-access.global_account_value_created.5100e467": "Global account {value0} created.",
|
||||||
@@ -186,7 +186,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.no_api_keys_found.1f377128": "No API keys found.",
|
"i18n:govoplan-access.no_api_keys_found.1f377128": "No API keys found.",
|
||||||
"i18n:govoplan-access.no_assignable_roles_exist.a4c268c2": "No assignable roles exist.",
|
"i18n:govoplan-access.no_assignable_roles_exist.a4c268c2": "No assignable roles exist.",
|
||||||
"i18n:govoplan-access.no_expiry.39d436aa": "No expiry",
|
"i18n:govoplan-access.no_expiry.39d436aa": "No expiry",
|
||||||
"i18n:govoplan-access.no_function_role_mappings_found.f735ff54": "No function role mappings found.",
|
"i18n:govoplan-access.no_function_role_mappings_found.f735ff54": "No function mappings found.",
|
||||||
"i18n:govoplan-access.no_function_facts_found.b2ecee17": "No function facts found.",
|
"i18n:govoplan-access.no_function_facts_found.b2ecee17": "No function facts found.",
|
||||||
"i18n:govoplan-access.no_global_accounts_found.29d96a9e": "No global accounts found.",
|
"i18n:govoplan-access.no_global_accounts_found.29d96a9e": "No global accounts found.",
|
||||||
"i18n:govoplan-access.no_groups_exist_yet.9cd029f6": "No groups exist yet.",
|
"i18n:govoplan-access.no_groups_exist_yet.9cd029f6": "No groups exist yet.",
|
||||||
@@ -301,7 +301,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.tenant_profiles.4d7281ce": "Tenant profiles",
|
"i18n:govoplan-access.tenant_profiles.4d7281ce": "Tenant profiles",
|
||||||
"i18n:govoplan-access.tenant_retention_policy.f10893d7": "Tenant retention policy",
|
"i18n:govoplan-access.tenant_retention_policy.f10893d7": "Tenant retention policy",
|
||||||
"i18n:govoplan-access.tenant_retention.95b35db0": "Tenant retention",
|
"i18n:govoplan-access.tenant_retention.95b35db0": "Tenant retention",
|
||||||
"i18n:govoplan-access.tenant_role_templates": "Tenant role templates",
|
"i18n:govoplan-access.tenant_role_templates": "Role templates",
|
||||||
"i18n:govoplan-access.tenant_roles.51aca82d": "Tenant roles",
|
"i18n:govoplan-access.tenant_roles.51aca82d": "Tenant roles",
|
||||||
"i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae": "Tenant-scoped automation credentials are capped by their owner's current effective permissions. API keys are immutable after creation and are revoked rather than edited.",
|
"i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae": "Tenant-scoped automation credentials are capped by their owner's current effective permissions. API keys are immutable after creation and are revoked rather than edited.",
|
||||||
"i18n:govoplan-access.tenant_user_details.fbab9079": "Tenant user details",
|
"i18n:govoplan-access.tenant_user_details.fbab9079": "Tenant user details",
|
||||||
@@ -365,7 +365,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.add_api_key.725d9988": "Add API key",
|
"i18n:govoplan-access.add_api_key.725d9988": "Add API key",
|
||||||
"i18n:govoplan-access.add_global_account.18e4df22": "Add global account",
|
"i18n:govoplan-access.add_global_account.18e4df22": "Add global account",
|
||||||
"i18n:govoplan-access.add_group.2fca464f": "Gruppe hinzufügen",
|
"i18n:govoplan-access.add_group.2fca464f": "Gruppe hinzufügen",
|
||||||
"i18n:govoplan-access.add_function_role_mapping.1bc376ac": "Funktions-Rollenzuordnung hinzufügen",
|
"i18n:govoplan-access.add_function_role_mapping.1bc376ac": "Funktionszuordnung hinzufügen",
|
||||||
"i18n:govoplan-access.add_role.d8d5d55c": "Rolle hinzufügen",
|
"i18n:govoplan-access.add_role.d8d5d55c": "Rolle hinzufügen",
|
||||||
"i18n:govoplan-access.add_system_role.f9ef262b": "Add system role",
|
"i18n:govoplan-access.add_system_role.f9ef262b": "Add system role",
|
||||||
"i18n:govoplan-access.add_tenant_user.36f37ce7": "Mandantenbenutzer hinzufügen",
|
"i18n:govoplan-access.add_tenant_user.36f37ce7": "Mandantenbenutzer hinzufügen",
|
||||||
@@ -403,7 +403,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.create_api_key.d7b30388": "API-Schlüssel erstellen",
|
"i18n:govoplan-access.create_api_key.d7b30388": "API-Schlüssel erstellen",
|
||||||
"i18n:govoplan-access.create_global_account.e821f016": "Create global account",
|
"i18n:govoplan-access.create_global_account.e821f016": "Create global account",
|
||||||
"i18n:govoplan-access.create_group.5a0b1c17": "Gruppe erstellen",
|
"i18n:govoplan-access.create_group.5a0b1c17": "Gruppe erstellen",
|
||||||
"i18n:govoplan-access.create_function_role_mapping.3718168d": "Funktions-Rollenzuordnung erstellen",
|
"i18n:govoplan-access.create_function_role_mapping.3718168d": "Funktionszuordnung erstellen",
|
||||||
"i18n:govoplan-access.create_key.e028cb09": "Create key",
|
"i18n:govoplan-access.create_key.e028cb09": "Create key",
|
||||||
"i18n:govoplan-access.create_role.db859bad": "Rolle erstellen",
|
"i18n:govoplan-access.create_role.db859bad": "Rolle erstellen",
|
||||||
"i18n:govoplan-access.create_system_role.a1e40b25": "Create system role",
|
"i18n:govoplan-access.create_system_role.a1e40b25": "Create system role",
|
||||||
@@ -428,7 +428,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.deactivate_value.a276a667": "Deactivate {value0}",
|
"i18n:govoplan-access.deactivate_value.a276a667": "Deactivate {value0}",
|
||||||
"i18n:govoplan-access.default_locale.b99d021f": "Standardsprache",
|
"i18n:govoplan-access.default_locale.b99d021f": "Standardsprache",
|
||||||
"i18n:govoplan-access.delete_role.fbf0667e": "Delete role",
|
"i18n:govoplan-access.delete_role.fbf0667e": "Delete role",
|
||||||
"i18n:govoplan-access.delete_function_role_mapping.0c0eec6e": "Funktions-Rollenzuordnung löschen",
|
"i18n:govoplan-access.delete_function_role_mapping.0c0eec6e": "Funktionszuordnung löschen",
|
||||||
"i18n:govoplan-access.delete_function_role_mapping_value.419da2aa": "Zuordnung für {value0} löschen? Akzeptierte Zuweisungen gewähren die zugeordnete Rolle dann nicht mehr.",
|
"i18n:govoplan-access.delete_function_role_mapping_value.419da2aa": "Zuordnung für {value0} löschen? Akzeptierte Zuweisungen gewähren die zugeordnete Rolle dann nicht mehr.",
|
||||||
"i18n:govoplan-access.delete_mapping.0d27d92a": "Zuordnung löschen",
|
"i18n:govoplan-access.delete_mapping.0d27d92a": "Zuordnung löschen",
|
||||||
"i18n:govoplan-access.delete_system_role.e2d84a56": "Delete system role",
|
"i18n:govoplan-access.delete_system_role.e2d84a56": "Delete system role",
|
||||||
@@ -443,7 +443,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.dry_run.485a3d15": "Trockenlauf",
|
"i18n:govoplan-access.dry_run.485a3d15": "Trockenlauf",
|
||||||
"i18n:govoplan-access.edit_global_account.d13b8485": "Edit global account",
|
"i18n:govoplan-access.edit_global_account.d13b8485": "Edit global account",
|
||||||
"i18n:govoplan-access.edit_group.edb57d8e": "Gruppe bearbeiten",
|
"i18n:govoplan-access.edit_group.edb57d8e": "Gruppe bearbeiten",
|
||||||
"i18n:govoplan-access.edit_function_role_mapping.91ee75af": "Funktions-Rollenzuordnung bearbeiten",
|
"i18n:govoplan-access.edit_function_role_mapping.91ee75af": "Funktionszuordnung bearbeiten",
|
||||||
"i18n:govoplan-access.edit_role.61dd63e9": "Rolle bearbeiten",
|
"i18n:govoplan-access.edit_role.61dd63e9": "Rolle bearbeiten",
|
||||||
"i18n:govoplan-access.edit_system_role.6ebb7cb0": "Edit system role",
|
"i18n:govoplan-access.edit_system_role.6ebb7cb0": "Edit system role",
|
||||||
"i18n:govoplan-access.edit_tenant_user.99121a61": "Mandantenbenutzer bearbeiten",
|
"i18n:govoplan-access.edit_tenant_user.99121a61": "Mandantenbenutzer bearbeiten",
|
||||||
@@ -462,11 +462,11 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.function_fact.4f7435e4": "Funktionsfakt",
|
"i18n:govoplan-access.function_fact.4f7435e4": "Funktionsfakt",
|
||||||
"i18n:govoplan-access.function_facts.848b32cc": "Funktionsfakten",
|
"i18n:govoplan-access.function_facts.848b32cc": "Funktionsfakten",
|
||||||
"i18n:govoplan-access.function_id.e5e08937": "Funktions-ID",
|
"i18n:govoplan-access.function_id.e5e08937": "Funktions-ID",
|
||||||
"i18n:govoplan-access.function_role_mapping_created.7a25eb5a": "Funktions-Rollenzuordnung erstellt.",
|
"i18n:govoplan-access.function_role_mapping_created.7a25eb5a": "Funktionszuordnung erstellt.",
|
||||||
"i18n:govoplan-access.function_role_mapping_deleted.fb180786": "Funktions-Rollenzuordnung gelöscht.",
|
"i18n:govoplan-access.function_role_mapping_deleted.fb180786": "Funktionszuordnung gelöscht.",
|
||||||
"i18n:govoplan-access.function_role_mapping_help.0cf9996a": "Quellmodul und Funktions-ID identifizieren einen akzeptierten externen Funktionsfakt. Access gewährt die ausgewählte Mandantenrolle nur, solange IDM eine aktive akzeptierte Zuweisung für diesen Fakt meldet.",
|
"i18n:govoplan-access.function_role_mapping_help.0cf9996a": "Quellmodul und Funktions-ID identifizieren einen akzeptierten externen Funktionsfakt. Access gewährt die ausgewählte Mandantenrolle nur, solange IDM eine aktive akzeptierte Zuweisung für diesen Fakt meldet.",
|
||||||
"i18n:govoplan-access.function_role_mapping_updated.76020443": "Funktions-Rollenzuordnung aktualisiert.",
|
"i18n:govoplan-access.function_role_mapping_updated.76020443": "Funktionszuordnung aktualisiert.",
|
||||||
"i18n:govoplan-access.function_role_mappings.2b64e9c3": "Funktions-Rollenzuordnungen",
|
"i18n:govoplan-access.function_role_mappings.2b64e9c3": "Funktionszuordnungen",
|
||||||
"i18n:govoplan-access.general.9239ee2c": "Allgemein",
|
"i18n:govoplan-access.general.9239ee2c": "Allgemein",
|
||||||
"i18n:govoplan-access.global_account_details.0a0cf240": "Global account details",
|
"i18n:govoplan-access.global_account_details.0a0cf240": "Global account details",
|
||||||
"i18n:govoplan-access.global_account_value_created.5100e467": "Global account {value0} created.",
|
"i18n:govoplan-access.global_account_value_created.5100e467": "Global account {value0} created.",
|
||||||
@@ -534,7 +534,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.no_api_keys_found.1f377128": "No API keys found.",
|
"i18n:govoplan-access.no_api_keys_found.1f377128": "No API keys found.",
|
||||||
"i18n:govoplan-access.no_assignable_roles_exist.a4c268c2": "Es gibt keine zuweisbaren Rollen.",
|
"i18n:govoplan-access.no_assignable_roles_exist.a4c268c2": "Es gibt keine zuweisbaren Rollen.",
|
||||||
"i18n:govoplan-access.no_expiry.39d436aa": "No expiry",
|
"i18n:govoplan-access.no_expiry.39d436aa": "No expiry",
|
||||||
"i18n:govoplan-access.no_function_role_mappings_found.f735ff54": "Keine Funktions-Rollenzuordnungen gefunden.",
|
"i18n:govoplan-access.no_function_role_mappings_found.f735ff54": "Keine Funktionszuordnungen gefunden.",
|
||||||
"i18n:govoplan-access.no_function_facts_found.b2ecee17": "Keine Funktionsfakten gefunden.",
|
"i18n:govoplan-access.no_function_facts_found.b2ecee17": "Keine Funktionsfakten gefunden.",
|
||||||
"i18n:govoplan-access.no_global_accounts_found.29d96a9e": "No global accounts found.",
|
"i18n:govoplan-access.no_global_accounts_found.29d96a9e": "No global accounts found.",
|
||||||
"i18n:govoplan-access.no_groups_exist_yet.9cd029f6": "Es gibt noch keine Gruppen.",
|
"i18n:govoplan-access.no_groups_exist_yet.9cd029f6": "Es gibt noch keine Gruppen.",
|
||||||
@@ -649,7 +649,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-access.tenant_profiles.4d7281ce": "Tenant profiles",
|
"i18n:govoplan-access.tenant_profiles.4d7281ce": "Tenant profiles",
|
||||||
"i18n:govoplan-access.tenant_retention_policy.f10893d7": "Tenant retention policy",
|
"i18n:govoplan-access.tenant_retention_policy.f10893d7": "Tenant retention policy",
|
||||||
"i18n:govoplan-access.tenant_retention.95b35db0": "Tenant retention",
|
"i18n:govoplan-access.tenant_retention.95b35db0": "Tenant retention",
|
||||||
"i18n:govoplan-access.tenant_role_templates": "Mandantenrollenvorlagen",
|
"i18n:govoplan-access.tenant_role_templates": "Rollenvorlagen",
|
||||||
"i18n:govoplan-access.tenant_roles.51aca82d": "Mandantenrollen",
|
"i18n:govoplan-access.tenant_roles.51aca82d": "Mandantenrollen",
|
||||||
"i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae": "Tenant-scoped automation credentials are capped by their owner's current effective permissions. API keys are immutable after creation and are revoked rather than edited.",
|
"i18n:govoplan-access.tenant_scoped_automation_credentials_are_capped_.9059dcae": "Tenant-scoped automation credentials are capped by their owner's current effective permissions. API keys are immutable after creation and are revoked rather than edited.",
|
||||||
"i18n:govoplan-access.tenant_user_details.fbab9079": "Mandantenbenutzerdetails",
|
"i18n:govoplan-access.tenant_user_details.fbab9079": "Mandantenbenutzerdetails",
|
||||||
|
|||||||
Reference in New Issue
Block a user