feat: add access module boundary migrations

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 37828fe340
commit 04681f1d75
41 changed files with 7229 additions and 738 deletions

View File

@@ -2,9 +2,30 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping
from govoplan_core.core.access import AccessDirectory, AccessSubjectRef, AccountRef, GroupRef, UserRef
from govoplan_access.backend.db.models import Account, Group, User
from govoplan_core.core.access import (
AccessSemanticDirectory,
AccessSubjectRef,
AccountRef,
FunctionAssignmentRef,
FunctionRef,
GroupRef,
IdentityRef,
OrganizationUnitRef,
UserRef,
)
from govoplan_access.backend.db.models import (
Account,
Function,
FunctionAssignment,
FunctionRoleAssignment,
Group,
Identity,
IdentityAccountLink,
OrganizationUnit,
User,
)
from govoplan_core.db.session import get_database
from govoplan_access.backend.semantic import active_function_assignments_for_account
def _status(active: bool) -> str:
@@ -40,7 +61,60 @@ def _group_ref(group: Group) -> GroupRef:
)
class SqlAccessDirectory(AccessDirectory):
def _identity_ref(identity: Identity, account_links: list[IdentityAccountLink]) -> IdentityRef:
primary_account_id = next((link.account_id for link in account_links if link.is_primary), None)
return IdentityRef(
id=identity.id,
display_name=identity.display_name,
primary_account_id=primary_account_id,
account_ids=tuple(link.account_id for link in account_links),
status=_status(identity.is_active), # type: ignore[arg-type]
)
def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
return OrganizationUnitRef(
id=item.id,
tenant_id=item.tenant_id,
name=item.name,
parent_id=item.parent_id,
status=_status(item.is_active), # type: ignore[arg-type]
)
def _function_ref(function: Function, 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=_status(function.is_active), # type: ignore[arg-type]
)
def _function_assignment_ref(item: FunctionAssignment) -> FunctionAssignmentRef:
return FunctionAssignmentRef(
id=item.id,
tenant_id=item.tenant_id,
account_id=item.account_id,
identity_id=item.identity_id,
function_id=item.function_id,
organization_unit_id=item.organization_unit_id,
applies_to_subunits=item.applies_to_subunits,
source=item.source, # type: ignore[arg-type]
delegated_from_assignment_id=item.delegated_from_assignment_id,
acting_for_account_id=item.acting_for_account_id,
valid_from=item.valid_from,
valid_until=item.valid_until,
status=_status(item.is_active), # type: ignore[arg-type]
)
class SqlAccessDirectory(AccessSemanticDirectory):
def get_account(self, account_id: str) -> AccountRef | None:
with get_database().session() as session:
account = session.get(Account, account_id)
@@ -113,6 +187,9 @@ class SqlAccessDirectory(AccessDirectory):
return tuple(_group_ref(group) for group in groups)
def display_label(self, subject: AccessSubjectRef) -> str | None:
if subject.kind == "identity":
identity = self.get_identity(subject.id)
return identity.display_name if identity else subject.label
if subject.kind == "account":
account = self.get_account(subject.id)
return account.display_name or account.email if account else subject.label
@@ -122,4 +199,117 @@ class SqlAccessDirectory(AccessDirectory):
if subject.kind == "group":
group = self.get_group(subject.id)
return group.name if group else subject.label
if subject.kind == "organization_unit":
item = self.get_organization_unit(subject.id)
return item.name if item else subject.label
if subject.kind == "function":
item = self.get_function(subject.id)
return item.name if item else subject.label
return subject.label or subject.id
def get_identity(self, identity_id: str) -> IdentityRef | None:
with get_database().session() as session:
identity = session.get(Identity, identity_id)
if identity is None:
return None
links = (
session.query(IdentityAccountLink)
.filter(IdentityAccountLink.identity_id == identity.id)
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
.all()
)
return _identity_ref(identity, links)
def accounts_for_identity(self, identity_id: str) -> tuple[AccountRef, ...]:
with get_database().session() as session:
accounts = (
session.query(Account)
.join(IdentityAccountLink, IdentityAccountLink.account_id == Account.id)
.filter(IdentityAccountLink.identity_id == identity_id)
.order_by(IdentityAccountLink.is_primary.desc(), Account.email.asc())
.all()
)
return tuple(_account_ref(account) for account in accounts)
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
with get_database().session() as session:
item = session.get(OrganizationUnit, organization_unit_id)
return _organization_unit_ref(item) if item is not None else None
def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]:
with get_database().session() as session:
items = (
session.query(OrganizationUnit)
.filter(OrganizationUnit.tenant_id == tenant_id)
.order_by(OrganizationUnit.name.asc())
.all()
)
return tuple(_organization_unit_ref(item) for item in items)
def get_function(self, function_id: str) -> FunctionRef | None:
with get_database().session() as session:
function = session.get(Function, function_id)
if function is None:
return None
role_ids = [
row[0]
for row in session.query(FunctionRoleAssignment.role_id)
.filter(FunctionRoleAssignment.function_id == function.id)
.order_by(FunctionRoleAssignment.created_at.asc())
.all()
]
return _function_ref(function, role_ids)
def functions_for_organization_unit(
self,
organization_unit_id: str,
*,
include_subunits: bool = False,
) -> tuple[FunctionRef, ...]:
with get_database().session() as session:
unit_ids = {organization_unit_id}
if include_subunits:
pending = [organization_unit_id]
while pending:
parent_id = pending.pop()
children = [
row[0]
for row in session.query(OrganizationUnit.id)
.filter(OrganizationUnit.parent_id == parent_id)
.all()
]
for child_id in children:
if child_id not in unit_ids:
unit_ids.add(child_id)
pending.append(child_id)
functions = (
session.query(Function)
.filter(Function.organization_unit_id.in_(unit_ids))
.order_by(Function.name.asc())
.all()
)
role_rows = (
session.query(FunctionRoleAssignment.function_id, FunctionRoleAssignment.role_id)
.filter(FunctionRoleAssignment.function_id.in_([item.id for item in functions]))
.all()
if functions else []
)
role_ids_by_function: dict[str, list[str]] = {}
for function_id, role_id in role_rows:
role_ids_by_function.setdefault(function_id, []).append(role_id)
return tuple(_function_ref(item, role_ids_by_function.get(item.id, [])) for item in functions)
def get_function_assignment(self, assignment_id: str) -> FunctionAssignmentRef | None:
with get_database().session() as session:
item = session.get(FunctionAssignment, assignment_id)
return _function_assignment_ref(item) if item is not None else None
def function_assignments_for_account(
self,
account_id: str,
*,
tenant_id: str | None = None,
) -> tuple[FunctionAssignmentRef, ...]:
with get_database().session() as session:
items = active_function_assignments_for_account(session, account_id, tenant_id=tenant_id)
return tuple(_function_assignment_ref(item) for item in items)