291 lines
21 KiB
Python
291 lines
21 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Iterable
|
|
|
|
from govoplan_core.security.scope_aliases import LEGACY_SCOPE_ALIASES
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PermissionDefinition:
|
|
scope: str
|
|
label: str
|
|
description: str
|
|
category: str
|
|
level: str = "tenant"
|
|
|
|
|
|
TENANT_PERMISSIONS: tuple[PermissionDefinition, ...] = (
|
|
PermissionDefinition("campaign:read", "View campaigns", "Open campaign metadata, versions and permitted message summaries.", "Campaigns"),
|
|
PermissionDefinition("campaign:create", "Create campaigns", "Create new campaigns in an accessible tenant scope.", "Campaigns"),
|
|
PermissionDefinition("campaign:update", "Edit campaigns", "Edit current working campaign versions.", "Campaigns"),
|
|
PermissionDefinition("campaign:copy", "Copy campaigns", "Create a new campaign or working version from an existing campaign.", "Campaigns"),
|
|
PermissionDefinition("campaign:archive", "Archive campaigns", "Archive campaigns without destroying audit evidence.", "Campaigns"),
|
|
PermissionDefinition("campaign:delete", "Delete campaigns", "Delete draft-only campaigns where retention policy allows it.", "Campaigns"),
|
|
PermissionDefinition("campaign:share", "Share campaigns", "Grant or revoke explicit campaign access for users and groups.", "Campaigns"),
|
|
PermissionDefinition("campaign:validate", "Validate campaigns", "Run technical validation and manage validation locks.", "Campaigns"),
|
|
PermissionDefinition("campaign:build", "Build campaigns", "Build exact messages and attachment evidence.", "Campaigns"),
|
|
PermissionDefinition("campaign:review", "Approve campaign review", "Approve or reject built messages and non-blocking review conditions.", "Campaigns"),
|
|
PermissionDefinition("campaign:send_test", "Mock-send campaigns", "Use mock delivery and verification tools.", "Campaigns"),
|
|
PermissionDefinition("campaign:queue", "Queue campaigns", "Place approved executions into the delivery queue.", "Campaigns"),
|
|
PermissionDefinition("campaign:control", "Control delivery", "Pause, resume or cancel queued/sending jobs.", "Campaigns"),
|
|
PermissionDefinition("campaign:send", "Send campaigns", "Start real SMTP delivery.", "Campaigns"),
|
|
PermissionDefinition("campaign:retry", "Retry delivery", "Retry failed or unattempted delivery jobs.", "Campaigns"),
|
|
PermissionDefinition("campaign:reconcile", "Reconcile uncertain delivery", "Resolve outcome-unknown SMTP attempts after operational inspection.", "Campaigns"),
|
|
PermissionDefinition("recipients:read", "View recipients", "Read recipient lists and recipient-specific campaign data.", "Recipients"),
|
|
PermissionDefinition("recipients:write", "Edit recipients", "Create and edit recipient rows and recipient field values.", "Recipients"),
|
|
PermissionDefinition("recipients:import", "Import recipients", "Bulk-import recipient lists from files or reusable lists.", "Recipients"),
|
|
PermissionDefinition("recipients:export", "Export recipients", "Export recipient data or recipient-complete reports.", "Recipients"),
|
|
PermissionDefinition("files:read", "View files", "List, search and preview managed files.", "Files"),
|
|
PermissionDefinition("files:download", "Download files", "Download managed files and generated ZIP archives.", "Files"),
|
|
PermissionDefinition("files:upload", "Upload files", "Upload new managed file versions.", "Files"),
|
|
PermissionDefinition("files:organize", "Organize files", "Create folders, rename, move or copy managed files.", "Files"),
|
|
PermissionDefinition("files:share", "Share files", "Grant or revoke managed file shares.", "Files"),
|
|
PermissionDefinition("files:delete", "Delete files", "Delete or hide managed files and folders where retention policy allows it.", "Files"),
|
|
PermissionDefinition("files:admin", "Administer file spaces", "Access and administer all user and group file spaces in the tenant.", "Files"),
|
|
PermissionDefinition("reports:read", "View reports", "View campaign delivery reports and aggregate outcome data.", "Reports and audit"),
|
|
PermissionDefinition("reports:export", "Export reports", "Download detailed campaign reports such as CSV exports.", "Reports and audit"),
|
|
PermissionDefinition("reports:send", "Send reports", "Email campaign reports to configured recipients.", "Reports and audit"),
|
|
PermissionDefinition("audit:read", "View audit log", "Read tenant and campaign audit records.", "Reports and audit"),
|
|
PermissionDefinition("mail_servers:read", "View mail servers", "Inspect approved reusable SMTP/IMAP server profile metadata.", "Mail servers"),
|
|
PermissionDefinition("mail_servers:use", "Use mail servers", "Select an approved mail-server profile for a campaign without reading secrets.", "Mail servers"),
|
|
PermissionDefinition("mail_servers:test", "Test mail servers", "Run SMTP/IMAP connection tests.", "Mail servers"),
|
|
PermissionDefinition("mail_servers:write", "Manage mail servers", "Create and edit reusable mail-server profiles.", "Mail servers"),
|
|
PermissionDefinition("mail_servers:manage_credentials", "Manage mail credentials", "Create or replace stored SMTP/IMAP credentials.", "Mail servers"),
|
|
PermissionDefinition("admin:users:read", "View users", "List tenant users and their effective access.", "Tenant administration"),
|
|
PermissionDefinition("admin:users:create", "Create users", "Create tenant memberships for accounts.", "Tenant administration"),
|
|
PermissionDefinition("admin:users:update", "Update users", "Edit non-access membership metadata.", "Tenant administration"),
|
|
PermissionDefinition("admin:users:suspend", "Suspend users", "Activate or suspend tenant memberships.", "Tenant administration"),
|
|
PermissionDefinition("admin:groups:read", "View groups", "List tenant groups, members and assigned roles.", "Tenant administration"),
|
|
PermissionDefinition("admin:groups:write", "Manage groups", "Create or edit tenant group definitions.", "Tenant administration"),
|
|
PermissionDefinition("admin:groups:manage_members", "Manage group members", "Add and remove users from tenant groups.", "Tenant administration"),
|
|
PermissionDefinition("admin:roles:read", "View roles", "Inspect role definitions and the permission catalogue.", "Tenant administration"),
|
|
PermissionDefinition("admin:roles:write", "Define roles", "Create and edit assignable tenant role definitions.", "Tenant administration"),
|
|
PermissionDefinition("admin:roles:assign", "Assign roles", "Assign tenant roles to users or groups within delegation limits.", "Tenant administration"),
|
|
PermissionDefinition("admin:api_keys:read", "View API keys", "List tenant API keys without revealing their secret.", "Tenant administration"),
|
|
PermissionDefinition("admin:api_keys:create", "Create API keys", "Create tenant-local API keys within the owner's delegation limits.", "Tenant administration"),
|
|
PermissionDefinition("admin:api_keys:revoke", "Revoke API keys", "Revoke tenant-local API keys.", "Tenant administration"),
|
|
PermissionDefinition("admin:settings:read", "View tenant settings", "Read tenant defaults and non-policy settings.", "Tenant administration"),
|
|
PermissionDefinition("admin:settings:write", "Manage tenant settings", "Change tenant defaults and non-policy settings.", "Tenant administration"),
|
|
PermissionDefinition("admin:policies:read", "View tenant policies", "Read tenant policy and governance settings.", "Tenant administration"),
|
|
PermissionDefinition("admin:policies:write", "Manage tenant policies", "Change tenant policy and governance settings where system policy permits it.", "Tenant administration"),
|
|
)
|
|
|
|
SYSTEM_PERMISSIONS: tuple[PermissionDefinition, ...] = (
|
|
PermissionDefinition("system:tenants:read", "View all tenants", "List tenants and system-wide membership counts.", "System administration", "system"),
|
|
PermissionDefinition("system:tenants:create", "Create tenants", "Create new tenant spaces.", "System administration", "system"),
|
|
PermissionDefinition("system:tenants:update", "Update tenants", "Edit tenant metadata and governance overrides.", "System administration", "system"),
|
|
PermissionDefinition("system:tenants:suspend", "Suspend tenants", "Activate or suspend tenant spaces while preserving evidence.", "System administration", "system"),
|
|
PermissionDefinition("system:accounts:read", "View accounts", "List global login accounts and memberships.", "System administration", "system"),
|
|
PermissionDefinition("system:accounts:create", "Create accounts", "Create global login accounts.", "System administration", "system"),
|
|
PermissionDefinition("system:accounts:update", "Update accounts", "Edit global account metadata.", "System administration", "system"),
|
|
PermissionDefinition("system:accounts:suspend", "Suspend accounts", "Activate or suspend global login accounts while preserving a system owner.", "System administration", "system"),
|
|
PermissionDefinition("system:roles:read", "View system roles", "Inspect instance-wide role definitions and their permissions.", "System administration", "system"),
|
|
PermissionDefinition("system:roles:write", "Define system roles", "Create and edit instance-wide role definitions within delegation limits.", "System administration", "system"),
|
|
PermissionDefinition("system:roles:assign", "Assign system roles", "Assign instance-wide roles to accounts while preserving a system owner.", "System administration", "system"),
|
|
PermissionDefinition("system:access:read", "View system access (legacy)", "Compatibility scope for listing accounts with instance-wide roles.", "System administration", "system"),
|
|
PermissionDefinition("system:access:assign", "Assign system access (legacy)", "Compatibility scope for assigning instance-wide roles.", "System administration", "system"),
|
|
PermissionDefinition("system:audit:read", "View system audit", "Read audit records across tenants.", "System administration", "system"),
|
|
PermissionDefinition("system:settings:read", "View system settings", "Read instance defaults and tenant-governance defaults.", "System administration", "system"),
|
|
PermissionDefinition("system:settings:write", "Manage system settings", "Change instance defaults and tenant-governance defaults.", "System administration", "system"),
|
|
PermissionDefinition("system:maintenance:access", "Access during maintenance", "Use the system while maintenance mode is active.", "System administration", "system"),
|
|
PermissionDefinition("system:governance:read", "View governance templates", "Inspect centrally managed group and role definitions.", "System administration", "system"),
|
|
PermissionDefinition("system:governance:write", "Manage governance templates", "Create and assign centrally managed group and role definitions.", "System administration", "system"),
|
|
)
|
|
|
|
ALL_PERMISSIONS: tuple[PermissionDefinition, ...] = TENANT_PERMISSIONS + SYSTEM_PERMISSIONS
|
|
KNOWN_SCOPES = frozenset(item.scope for item in ALL_PERMISSIONS)
|
|
TENANT_SCOPES = frozenset(item.scope for item in TENANT_PERMISSIONS)
|
|
SYSTEM_SCOPES = frozenset(item.scope for item in SYSTEM_PERMISSIONS)
|
|
|
|
DEFAULT_TENANT_ROLES: dict[str, dict[str, object]] = {
|
|
"owner": {"name": "Owner", "description": "Full tenant access, including administration and delivery.", "permissions": ["tenant:*"], "is_builtin": True, "is_assignable": True},
|
|
"tenant_admin": {"name": "Tenant administrator", "description": "Manage tenant settings, users, groups and roles. Real delivery remains separately delegable.", "permissions": [
|
|
"campaign:read", "files:read", "reports:read", "audit:read",
|
|
"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:api_keys:read", "admin:api_keys:create", "admin:api_keys:revoke",
|
|
"admin:settings:read", "admin:settings:write", "admin:policies:read", "admin:policies:write",
|
|
], "is_builtin": True, "is_assignable": True},
|
|
"admin": {"name": "Administrator (legacy)", "description": "Legacy broad tenant role retained for upgraded installations.", "permissions": sorted(TENANT_SCOPES), "is_builtin": True, "is_assignable": True},
|
|
"access_admin": {"name": "Access administrator", "description": "Manage memberships and assignments within delegation limits.", "permissions": [
|
|
"admin:users:read", "admin:users:create", "admin:users:update", "admin:users:suspend",
|
|
"admin:groups:read", "admin:groups:manage_members", "admin:roles:read", "admin:roles:assign",
|
|
], "is_builtin": True, "is_assignable": True},
|
|
"campaign_manager": {"name": "Campaign manager", "description": "Prepare, validate and build campaigns, but do not approve or start real delivery.", "permissions": [
|
|
"campaign:read", "campaign:create", "campaign:update", "campaign:copy", "campaign:validate", "campaign:build",
|
|
"recipients:read", "recipients:write", "recipients:import", "files:read", "files:download", "files:upload", "files:organize", "reports:read",
|
|
], "is_builtin": True, "is_assignable": True},
|
|
"reviewer": {"name": "Reviewer", "description": "Inspect and approve prepared campaign messages.", "permissions": ["campaign:read", "campaign:validate", "campaign:review", "recipients:read", "files:read", "reports:read"], "is_builtin": True, "is_assignable": True},
|
|
"sender": {"name": "Sender", "description": "Queue, test, control, send and reconcile prepared campaigns.", "permissions": [
|
|
"campaign:read", "campaign:send_test", "campaign:queue", "campaign:control", "campaign:send", "campaign:retry", "campaign:reconcile",
|
|
"recipients:read", "files:read", "reports:read", "reports:send", "mail_servers:use", "mail_servers:test",
|
|
], "is_builtin": True, "is_assignable": True},
|
|
"file_manager": {"name": "File manager", "description": "Manage tenant file spaces without campaign delivery rights.", "permissions": ["files:read", "files:download", "files:upload", "files:organize", "files:share", "files:delete"], "is_builtin": True, "is_assignable": True},
|
|
"viewer": {"name": "Viewer", "description": "Read campaigns, recipient data, files and reports without changing them.", "permissions": ["campaign:read", "recipients:read", "files:read", "reports:read"], "is_builtin": True, "is_assignable": True},
|
|
"auditor": {"name": "Auditor", "description": "Read campaigns, recipient evidence, reports and audit records.", "permissions": ["campaign:read", "recipients:read", "recipients:export", "reports:read", "reports:export", "audit:read"], "is_builtin": True, "is_assignable": True},
|
|
}
|
|
|
|
DEFAULT_SYSTEM_ROLES: dict[str, dict[str, object]] = {
|
|
"system_owner": {
|
|
"name": "System owner",
|
|
"description": "Protected full instance-wide administration. At least one active account must retain this role.",
|
|
"permissions": ["system:*"],
|
|
"is_builtin": True,
|
|
"is_assignable": True,
|
|
"managed": True,
|
|
},
|
|
"system_admin": {
|
|
"name": "System administrator",
|
|
"description": "Manage tenants, accounts, system roles, settings and governance without being able to grant the protected System owner role.",
|
|
"permissions": sorted(SYSTEM_SCOPES),
|
|
"is_builtin": False,
|
|
"is_assignable": True,
|
|
"managed": False,
|
|
},
|
|
"system_auditor": {
|
|
"name": "System auditor",
|
|
"description": "Read tenant registry, accounts, system roles, settings, governance and cross-tenant audit records.",
|
|
"permissions": [
|
|
"system:tenants:read", "system:accounts:read", "system:roles:read", "system:access:read",
|
|
"system:audit:read", "system:settings:read", "system:governance:read",
|
|
],
|
|
"is_builtin": False,
|
|
"is_assignable": True,
|
|
"managed": False,
|
|
},
|
|
"maintenance_operator": {
|
|
"name": "Maintenance operator",
|
|
"description": "Access the system while maintenance mode is active.",
|
|
"permissions": ["system:settings:read", "system:maintenance:access"],
|
|
"is_builtin": False,
|
|
"is_assignable": True,
|
|
"managed": False,
|
|
},
|
|
}
|
|
|
|
|
|
def normalize_email(value: str) -> str:
|
|
return value.strip().casefold()
|
|
|
|
|
|
def scope_grants(granted: str, required: str) -> bool:
|
|
if granted == required:
|
|
return True
|
|
if required in LEGACY_SCOPE_ALIASES.get(granted, frozenset()):
|
|
return True
|
|
if granted == "*":
|
|
return True
|
|
if granted == "tenant:*":
|
|
return required == "tenant:*" or not required.startswith("system:")
|
|
if granted == "system:*":
|
|
return required.startswith("system:")
|
|
if granted.endswith(":*") and required.startswith(granted[:-1]):
|
|
return True
|
|
return any(scope_grants(alias, required) for alias in LEGACY_SCOPE_ALIASES.get(granted, frozenset()) if alias != granted)
|
|
|
|
|
|
def scopes_grant(scopes: Iterable[str], required: str) -> bool:
|
|
return any(scope_grants(scope, required) for scope in scopes)
|
|
|
|
|
|
def effective_permission_scopes(scopes: Iterable[str], *, level: str | None = None) -> set[str]:
|
|
"""Return canonical permissions granted by a possibly wildcarded scope set.
|
|
|
|
Wildcards and compatibility aliases are presentation/assignment shorthand;
|
|
counts and access summaries should describe the canonical capabilities they
|
|
grant rather than counting the shorthand token itself.
|
|
"""
|
|
if level == "tenant":
|
|
candidates = TENANT_SCOPES
|
|
elif level == "system":
|
|
candidates = SYSTEM_SCOPES
|
|
else:
|
|
candidates = KNOWN_SCOPES
|
|
granted = list(scopes)
|
|
return {scope for scope in candidates if scopes_grant(granted, scope)}
|
|
|
|
|
|
def effective_permission_count(scopes: Iterable[str], *, level: str | None = None) -> int:
|
|
return len(effective_permission_scopes(scopes, level=level))
|
|
|
|
|
|
def expand_scopes(scopes: Iterable[str], *, include_unknown: bool = True) -> list[str]:
|
|
raw = {str(scope) for scope in scopes if scope}
|
|
expanded: set[str] = set()
|
|
for scope in raw:
|
|
if scope in {"*", "tenant:*"}:
|
|
expanded.add("tenant:*")
|
|
expanded.update(TENANT_SCOPES)
|
|
continue
|
|
if scope == "system:*":
|
|
expanded.add("system:*")
|
|
expanded.update(SYSTEM_SCOPES)
|
|
continue
|
|
aliases = LEGACY_SCOPE_ALIASES.get(scope, frozenset())
|
|
expanded.update(aliases)
|
|
for alias in aliases:
|
|
if alias.endswith(":*"):
|
|
expanded.update(item for item in KNOWN_SCOPES if scope_grants(alias, item))
|
|
if scope.endswith(":*"):
|
|
expanded.update(item for item in KNOWN_SCOPES if scope_grants(scope, item))
|
|
expanded.add(scope)
|
|
elif include_unknown or scope in KNOWN_SCOPES:
|
|
expanded.add(scope)
|
|
return sorted(expanded)
|
|
|
|
|
|
def delegateable_tenant_scopes(scopes: Iterable[str]) -> set[str]:
|
|
expanded = set(expand_scopes(scopes, include_unknown=False))
|
|
if "tenant:*" in expanded:
|
|
return set(TENANT_SCOPES)
|
|
return {scope for scope in expanded if scope in TENANT_SCOPES}
|
|
|
|
|
|
def delegateable_system_scopes(scopes: Iterable[str]) -> set[str]:
|
|
expanded = set(expand_scopes(scopes, include_unknown=False))
|
|
if "system:*" in expanded:
|
|
return set(SYSTEM_SCOPES)
|
|
return {scope for scope in expanded if scope in SYSTEM_SCOPES}
|
|
|
|
|
|
def intersect_api_key_scopes(user_scopes: Iterable[str], key_scopes: Iterable[str]) -> list[str]:
|
|
user = list(user_scopes)
|
|
key = list(key_scopes)
|
|
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 validate_tenant_permissions(scopes: Iterable[str]) -> list[str]:
|
|
normalized = {str(scope) for scope in scopes if scope}
|
|
if normalized.intersection({"*", "tenant:*"}):
|
|
return ["tenant:*"]
|
|
expanded: set[str] = set()
|
|
invalid: list[str] = []
|
|
for scope in sorted(normalized):
|
|
if scope in TENANT_SCOPES:
|
|
expanded.add(scope)
|
|
continue
|
|
aliases = {item for item in LEGACY_SCOPE_ALIASES.get(scope, frozenset()) if item in TENANT_SCOPES}
|
|
if aliases:
|
|
expanded.update(aliases)
|
|
continue
|
|
invalid.append(scope)
|
|
if invalid:
|
|
raise ValueError(f"Unsupported tenant permissions: {', '.join(invalid)}")
|
|
return sorted(expanded)
|
|
|
|
|
|
def validate_system_permissions(scopes: Iterable[str]) -> list[str]:
|
|
normalized = sorted({str(scope) for scope in scopes if scope})
|
|
invalid = [scope for scope in normalized if scope not in SYSTEM_SCOPES and scope != "system:*"]
|
|
if invalid:
|
|
raise ValueError(f"Unsupported system permissions: {', '.join(invalid)}")
|
|
if "system:*" in normalized:
|
|
return ["system:*"]
|
|
return normalized
|