Move access permissions to access module
This commit is contained in:
299
src/govoplan_access/backend/permissions/catalog.py
Normal file
299
src/govoplan_access/backend/permissions/catalog.py
Normal file
@@ -0,0 +1,299 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from govoplan_access.backend.permissions.evaluator import scope_grants as _scope_grants
|
||||
from govoplan_access.backend.permissions.evaluator import scopes_grant as _scopes_grant
|
||||
from govoplan_core.core.modules import PermissionDefinition, PermissionLevel, RoleTemplate
|
||||
from govoplan_core.security.module_permissions import compatible_required_scopes
|
||||
|
||||
|
||||
LEGACY_SCOPE_ALIASES: dict[str, frozenset[str]] = {
|
||||
"campaign:write": frozenset({
|
||||
"campaign:create",
|
||||
"campaign:update",
|
||||
"campaign:copy",
|
||||
"campaign:archive",
|
||||
"campaign:delete",
|
||||
"campaign:share",
|
||||
"recipients:read",
|
||||
"recipients:write",
|
||||
"recipients:import",
|
||||
}),
|
||||
"attachments:read": frozenset({"files:read", "files:download"}),
|
||||
"attachments:write": frozenset({"files:upload", "files:organize", "files:share", "files:delete"}),
|
||||
"admin:users": frozenset({
|
||||
"admin:users:read",
|
||||
"admin:users:create",
|
||||
"admin:users:update",
|
||||
"admin:users:suspend",
|
||||
"admin:groups:read",
|
||||
"admin:groups:write",
|
||||
"admin:groups:manage_members",
|
||||
"admin:roles:read",
|
||||
"admin:roles:write",
|
||||
"admin:roles:assign",
|
||||
}),
|
||||
"admin:users:write": frozenset({
|
||||
"admin:users:create",
|
||||
"admin:users:update",
|
||||
"admin:users:suspend",
|
||||
"admin:roles:assign",
|
||||
"admin:groups:manage_members",
|
||||
}),
|
||||
"admin:api_keys:write": frozenset({"admin:api_keys:create", "admin:api_keys:revoke"}),
|
||||
"admin:settings": frozenset({
|
||||
"admin:settings:read",
|
||||
"admin:settings:write",
|
||||
"admin:api_keys:read",
|
||||
"admin:api_keys:create",
|
||||
"admin:api_keys:revoke",
|
||||
}),
|
||||
"system:tenants:write": frozenset({"system:tenants:create", "system:tenants:update", "system:tenants:suspend"}),
|
||||
"system:access:write": frozenset({
|
||||
"system:access:assign",
|
||||
"system:roles:assign",
|
||||
"system:accounts:create",
|
||||
"system:accounts:update",
|
||||
"system:accounts:suspend",
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
def _permission(scope: str, label: str, description: str, category: str, level: PermissionLevel) -> PermissionDefinition:
|
||||
module_id, resource, action = scope.split(":", 2)
|
||||
return PermissionDefinition(
|
||||
scope=scope,
|
||||
label=label,
|
||||
description=description,
|
||||
category=category,
|
||||
level=level,
|
||||
module_id=module_id,
|
||||
resource=resource,
|
||||
action=action,
|
||||
deprecated=True,
|
||||
)
|
||||
|
||||
|
||||
LEGACY_PERMISSION_DEFINITIONS: tuple[PermissionDefinition, ...] = (
|
||||
_permission("admin:users:read", "View users", "List tenant users and their effective access.", "Tenant administration", "tenant"),
|
||||
_permission("admin:users:create", "Create users", "Create tenant memberships for accounts.", "Tenant administration", "tenant"),
|
||||
_permission("admin:users:update", "Update users", "Edit non-access membership metadata.", "Tenant administration", "tenant"),
|
||||
_permission("admin:users:suspend", "Suspend users", "Activate or suspend tenant memberships.", "Tenant administration", "tenant"),
|
||||
_permission("admin:groups:read", "View groups", "List tenant groups, members and assigned roles.", "Tenant administration", "tenant"),
|
||||
_permission("admin:groups:write", "Manage groups", "Create or edit tenant group definitions.", "Tenant administration", "tenant"),
|
||||
_permission("admin:groups:manage_members", "Manage group members", "Add and remove users from tenant groups.", "Tenant administration", "tenant"),
|
||||
_permission("admin:roles:read", "View roles", "Inspect role definitions and the permission catalogue.", "Tenant administration", "tenant"),
|
||||
_permission("admin:roles:write", "Define roles", "Create and edit assignable tenant role definitions.", "Tenant administration", "tenant"),
|
||||
_permission("admin:roles:assign", "Assign roles", "Assign tenant roles to users or groups within delegation limits.", "Tenant administration", "tenant"),
|
||||
_permission("admin:api_keys:read", "View API keys", "List tenant API keys without revealing their secret.", "Tenant administration", "tenant"),
|
||||
_permission("admin:api_keys:create", "Create API keys", "Create tenant-local API keys within the owner's delegation limits.", "Tenant administration", "tenant"),
|
||||
_permission("admin:api_keys:revoke", "Revoke API keys", "Revoke tenant-local API keys.", "Tenant administration", "tenant"),
|
||||
_permission("admin:settings:read", "View tenant settings", "Read tenant defaults and non-policy settings.", "Tenant administration", "tenant"),
|
||||
_permission("admin:settings:write", "Manage tenant settings", "Change tenant defaults and non-policy settings.", "Tenant administration", "tenant"),
|
||||
_permission("admin:policies:read", "View tenant policies", "Read tenant policy and governance settings.", "Tenant administration", "tenant"),
|
||||
_permission("admin:policies:write", "Manage tenant policies", "Change tenant policy and governance settings where system policy permits it.", "Tenant administration", "tenant"),
|
||||
_permission("system:tenants:read", "View all tenants", "List tenants and system-wide membership counts.", "System administration", "system"),
|
||||
_permission("system:tenants:create", "Create tenants", "Create new tenant spaces.", "System administration", "system"),
|
||||
_permission("system:tenants:update", "Update tenants", "Edit tenant metadata and governance overrides.", "System administration", "system"),
|
||||
_permission("system:tenants:suspend", "Suspend tenants", "Activate or suspend tenant spaces while preserving evidence.", "System administration", "system"),
|
||||
_permission("system:accounts:read", "View accounts", "List global login accounts and memberships.", "System administration", "system"),
|
||||
_permission("system:accounts:create", "Create accounts", "Create global login accounts.", "System administration", "system"),
|
||||
_permission("system:accounts:update", "Update accounts", "Edit global account metadata.", "System administration", "system"),
|
||||
_permission("system:accounts:suspend", "Suspend accounts", "Activate or suspend global login accounts while preserving a system owner.", "System administration", "system"),
|
||||
_permission("system:roles:read", "View system roles", "Inspect instance-wide role definitions and their permissions.", "System administration", "system"),
|
||||
_permission("system:roles:write", "Define system roles", "Create and edit instance-wide role definitions within delegation limits.", "System administration", "system"),
|
||||
_permission("system:roles:assign", "Assign system roles", "Assign instance-wide roles to accounts while preserving a system owner.", "System administration", "system"),
|
||||
_permission("system:access:read", "View system access", "Compatibility scope for listing accounts with instance-wide roles.", "System administration", "system"),
|
||||
_permission("system:access:assign", "Assign system access", "Compatibility scope for assigning instance-wide roles.", "System administration", "system"),
|
||||
_permission("system:audit:read", "View system audit", "Read audit records across tenants.", "System administration", "system"),
|
||||
_permission("system:settings:read", "View system settings", "Read instance defaults and tenant-governance defaults.", "System administration", "system"),
|
||||
_permission("system:settings:write", "Manage system settings", "Change instance defaults and tenant-governance defaults.", "System administration", "system"),
|
||||
_permission("system:maintenance:access", "Access during maintenance", "Use the system while maintenance mode is active.", "System administration", "system"),
|
||||
_permission("system:governance:read", "View governance templates", "Inspect centrally managed group and role definitions.", "System administration", "system"),
|
||||
_permission("system:governance:write", "Manage governance templates", "Create and assign centrally managed group and role definitions.", "System administration", "system"),
|
||||
)
|
||||
|
||||
|
||||
def normalize_email(value: str) -> str:
|
||||
return value.strip().casefold()
|
||||
|
||||
|
||||
def permission_catalog(*, include_legacy: bool = True) -> tuple[PermissionDefinition, ...]:
|
||||
catalog: dict[str, PermissionDefinition] = {}
|
||||
for permission in _active_permission_definitions():
|
||||
catalog[permission.scope] = permission
|
||||
if include_legacy:
|
||||
for permission in LEGACY_PERMISSION_DEFINITIONS:
|
||||
catalog.setdefault(permission.scope, permission)
|
||||
return tuple(catalog.values())
|
||||
|
||||
|
||||
def permission_map(*, include_legacy: bool = True) -> dict[str, PermissionDefinition]:
|
||||
return {permission.scope: permission for permission in permission_catalog(include_legacy=include_legacy)}
|
||||
|
||||
|
||||
def role_templates() -> tuple[RoleTemplate, ...]:
|
||||
registry = _registry()
|
||||
if registry is not None and hasattr(registry, "role_templates"):
|
||||
return tuple(registry.role_templates())
|
||||
from govoplan_access.backend.manifest import ACCESS_ROLE_TEMPLATES
|
||||
|
||||
return ACCESS_ROLE_TEMPLATES
|
||||
|
||||
|
||||
def role_templates_for_level(level: PermissionLevel) -> tuple[RoleTemplate, ...]:
|
||||
return tuple(template for template in role_templates() if template.level == level)
|
||||
|
||||
|
||||
def scope_grants(granted: str, required: str) -> bool:
|
||||
catalog = permission_map(include_legacy=True)
|
||||
if _scope_grants(granted, required, catalog=catalog):
|
||||
return True
|
||||
for alias in LEGACY_SCOPE_ALIASES.get(granted, frozenset()):
|
||||
if scope_grants(alias, required):
|
||||
return True
|
||||
return any(
|
||||
_scope_grants(granted, alias, catalog=catalog)
|
||||
for alias in compatible_required_scopes(required)
|
||||
if alias != required
|
||||
)
|
||||
|
||||
|
||||
def scopes_grant(scopes: Iterable[str], required: str) -> bool:
|
||||
return any(scope_grants(scope, required) for scope in scopes)
|
||||
|
||||
|
||||
def expand_scopes(scopes: Iterable[str], *, include_unknown: bool = True) -> list[str]:
|
||||
catalog = permission_map(include_legacy=True)
|
||||
raw = {str(scope) for scope in scopes if scope}
|
||||
expanded: set[str] = set()
|
||||
for scope in raw:
|
||||
if scope in {"*", "tenant:*", "system:*"} or scope.endswith(":*"):
|
||||
expanded.add(scope)
|
||||
for alias in LEGACY_SCOPE_ALIASES.get(scope, frozenset()):
|
||||
expanded.add(alias)
|
||||
matched = {candidate for candidate in catalog if scope_grants(scope, candidate)}
|
||||
expanded.update(matched)
|
||||
for alias in compatible_required_scopes(scope):
|
||||
if alias != scope:
|
||||
expanded.add(alias)
|
||||
if include_unknown and not matched:
|
||||
expanded.add(scope)
|
||||
return sorted(expanded)
|
||||
|
||||
|
||||
def effective_permission_scopes(scopes: Iterable[str], *, level: PermissionLevel | None = None) -> set[str]:
|
||||
candidates = _effective_permission_candidates(level=level)
|
||||
granted = list(scopes)
|
||||
return {scope for scope in candidates if scopes_grant(granted, scope)}
|
||||
|
||||
|
||||
def effective_permission_count(scopes: Iterable[str], *, level: PermissionLevel | None = None) -> int:
|
||||
return len(effective_permission_scopes(scopes, level=level))
|
||||
|
||||
|
||||
def _effective_permission_candidates(*, level: PermissionLevel | None = None) -> set[str]:
|
||||
active_catalog = permission_map(include_legacy=False)
|
||||
full_catalog = permission_map(include_legacy=True)
|
||||
active_scopes = {
|
||||
scope
|
||||
for scope, definition in active_catalog.items()
|
||||
if level is None or definition.level == level
|
||||
}
|
||||
candidates = set(active_scopes)
|
||||
for scope, definition in full_catalog.items():
|
||||
if scope in active_catalog:
|
||||
continue
|
||||
if level is not None and definition.level != level:
|
||||
continue
|
||||
if any(scope_grants(active_scope, scope) or scope_grants(scope, active_scope) for active_scope in active_scopes):
|
||||
continue
|
||||
candidates.add(scope)
|
||||
return candidates
|
||||
|
||||
|
||||
def validate_permissions(scopes: Iterable[str], *, level: PermissionLevel) -> list[str]:
|
||||
normalized = {str(scope) for scope in scopes if scope}
|
||||
wildcard = "system:*" if level == "system" else "tenant:*"
|
||||
if "*" in normalized or wildcard in normalized:
|
||||
return [wildcard]
|
||||
|
||||
catalog = permission_map(include_legacy=True)
|
||||
expanded: set[str] = set()
|
||||
invalid: list[str] = []
|
||||
for scope in sorted(normalized):
|
||||
aliases = LEGACY_SCOPE_ALIASES.get(scope, frozenset())
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
definition = catalog.get(alias)
|
||||
if definition is not None and definition.level == level:
|
||||
expanded.add(alias)
|
||||
continue
|
||||
if scope.endswith(":*"):
|
||||
matching = {candidate for candidate, definition in catalog.items() if definition.level == level and scope_grants(scope, candidate)}
|
||||
if matching:
|
||||
expanded.add(scope)
|
||||
continue
|
||||
definition = catalog.get(scope)
|
||||
if definition is not None and definition.level == level:
|
||||
expanded.add(scope)
|
||||
continue
|
||||
invalid.append(scope)
|
||||
if invalid:
|
||||
raise ValueError(f"Unsupported {level} permissions: {', '.join(invalid)}")
|
||||
return sorted(expanded)
|
||||
|
||||
|
||||
def validate_tenant_permissions(scopes: Iterable[str]) -> list[str]:
|
||||
return validate_permissions(scopes, level="tenant")
|
||||
|
||||
|
||||
def validate_system_permissions(scopes: Iterable[str]) -> list[str]:
|
||||
return validate_permissions(scopes, level="system")
|
||||
|
||||
|
||||
def delegateable_scopes(scopes: Iterable[str], *, level: PermissionLevel) -> set[str]:
|
||||
expanded = set(expand_scopes(scopes, include_unknown=False))
|
||||
wildcard = "system:*" if level == "system" else "tenant:*"
|
||||
if "*" in expanded or wildcard in expanded:
|
||||
return {scope for scope, definition in permission_map(include_legacy=True).items() if definition.level == level}
|
||||
return effective_permission_scopes(expanded, level=level)
|
||||
|
||||
|
||||
def delegateable_tenant_scopes(scopes: Iterable[str]) -> set[str]:
|
||||
return delegateable_scopes(scopes, level="tenant")
|
||||
|
||||
|
||||
def delegateable_system_scopes(scopes: Iterable[str]) -> set[str]:
|
||||
return delegateable_scopes(scopes, level="system")
|
||||
|
||||
|
||||
def intersect_api_key_scopes(user_scopes: Iterable[str], key_scopes: Iterable[str]) -> list[str]:
|
||||
user = list(user_scopes)
|
||||
key = list(key_scopes)
|
||||
tenant_scopes = {scope for scope, definition in permission_map(include_legacy=True).items() if definition.level == "tenant"}
|
||||
allowed = {scope for scope in tenant_scopes if scopes_grant(user, scope) and scopes_grant(key, scope)}
|
||||
user_raw = set(expand_scopes(user))
|
||||
key_raw = set(expand_scopes(key))
|
||||
allowed.update(
|
||||
scope
|
||||
for scope in user_raw.intersection(key_raw)
|
||||
if not scope.startswith("system:") and scope not in {"*", "tenant:*"}
|
||||
)
|
||||
return sorted(allowed)
|
||||
|
||||
|
||||
def _active_permission_definitions() -> tuple[PermissionDefinition, ...]:
|
||||
registry = _registry()
|
||||
if registry is not None and hasattr(registry, "permissions"):
|
||||
return tuple(registry.permissions())
|
||||
from govoplan_access.backend.manifest import ACCESS_PERMISSIONS
|
||||
|
||||
return ACCESS_PERMISSIONS
|
||||
|
||||
|
||||
def _registry() -> object | None:
|
||||
from govoplan_access.backend.runtime import get_registry
|
||||
|
||||
return get_registry()
|
||||
Reference in New Issue
Block a user