Files
govoplan-access/src/govoplan_access/backend/directory.py

468 lines
19 KiB
Python

from __future__ import annotations
from collections.abc import Iterable, Mapping
from govoplan_core.core.access import (
AccessSemanticDirectory,
AccessSubjectRef,
AccountRef,
FunctionAssignmentRef,
FunctionRef,
GroupRef,
IdentityRef,
OrganizationUnitRef,
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.organizations import (
ORGANIZATIONS_MODULE_ID,
OrganizationDirectory,
OrganizationFunctionRef as DirectoryFunctionRef,
OrganizationUnitRef as DirectoryOrganizationUnitRef,
)
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:
return "active" if active else "inactive"
def _account_ref(account: Account) -> AccountRef:
return AccountRef(
id=account.id,
email=account.email,
display_name=account.display_name,
status=_status(account.is_active), # type: ignore[arg-type]
)
def _user_ref(user: User, account: Account | None = None) -> UserRef:
return UserRef(
id=user.id,
account_id=user.account_id,
tenant_id=user.tenant_id,
email=account.email if account else user.email,
display_name=user.display_name or (account.display_name if account else None),
status=_status(user.is_active), # type: ignore[arg-type]
)
def _group_ref(group: Group) -> GroupRef:
return GroupRef(
id=group.id,
tenant_id=group.tenant_id,
name=group.name,
status=_status(group.is_active), # type: ignore[arg-type]
)
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 _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:
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 _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:
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 _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:
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]
)
def _idm_function_assignment_ref(item: IdmFunctionAssignmentRef, *, account_id: str) -> FunctionAssignmentRef:
return FunctionAssignmentRef(
id=item.id,
tenant_id=item.tenant_id,
account_id=item.account_id or 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=item.status, # type: ignore[arg-type]
)
class SqlAccessDirectory(AccessSemanticDirectory):
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._identity_directory = identity_directory
self._organization_directory = organization_directory
def get_account(self, account_id: str) -> AccountRef | None:
with get_database().session() as session:
account = session.get(Account, account_id)
return _account_ref(account) if account is not None else None
def get_user(self, user_id: str) -> UserRef | None:
with get_database().session() as session:
user = session.get(User, user_id)
if user is None:
return None
account = session.get(Account, user.account_id)
return _user_ref(user, account)
def get_users(self, user_ids: Iterable[str]) -> Mapping[str, UserRef]:
ids = {str(user_id) for user_id in user_ids if user_id}
if not ids:
return {}
with get_database().session() as session:
users = session.query(User).filter(User.id.in_(ids)).all()
account_ids = {user.account_id for user in users}
accounts = {
account.id: account
for account in session.query(Account).filter(Account.id.in_(account_ids)).all()
} if account_ids else {}
return {user.id: _user_ref(user, accounts.get(user.account_id)) for user in users}
def users_for_tenant(self, tenant_id: str) -> tuple[UserRef, ...]:
with get_database().session() as session:
users = session.query(User).filter(User.tenant_id == tenant_id).order_by(User.display_name.asc(), User.email.asc()).all()
account_ids = {user.account_id for user in users}
accounts = {
account.id: account
for account in session.query(Account).filter(Account.id.in_(account_ids)).all()
} if account_ids else {}
return tuple(_user_ref(user, accounts.get(user.account_id)) for user in users)
def get_group(self, group_id: str) -> GroupRef | None:
with get_database().session() as session:
group = session.get(Group, group_id)
return _group_ref(group) if group is not None else None
def get_groups(self, group_ids: Iterable[str]) -> Mapping[str, GroupRef]:
ids = {str(group_id) for group_id in group_ids if group_id}
if not ids:
return {}
with get_database().session() as session:
groups = session.query(Group).filter(Group.id.in_(ids)).all()
return {group.id: _group_ref(group) for group in groups}
def groups_for_tenant(self, tenant_id: str) -> tuple[GroupRef, ...]:
with get_database().session() as session:
groups = session.query(Group).filter(Group.tenant_id == tenant_id).order_by(Group.name.asc()).all()
return tuple(_group_ref(group) for group in groups)
def groups_for_user(self, user_id: str, *, tenant_id: str) -> tuple[GroupRef, ...]:
with get_database().session() as session:
from govoplan_access.backend.db.models import UserGroupMembership
groups = (
session.query(Group)
.join(UserGroupMembership, UserGroupMembership.group_id == Group.id)
.filter(
UserGroupMembership.user_id == user_id,
UserGroupMembership.tenant_id == tenant_id,
Group.tenant_id == tenant_id,
)
.order_by(Group.name.asc())
.all()
)
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
if subject.kind == "user":
user = self.get_user(subject.id)
return user.display_name or user.email if user else subject.label
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:
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:
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, ...]:
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:
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:
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:
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, ...]:
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:
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:
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:
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, ...]:
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:
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)
assignments = [_function_assignment_ref(item) for item in items]
if self._idm_directory is not None:
assignments.extend(
_idm_function_assignment_ref(item, account_id=account_id)
for item in self._idm_directory.organization_function_assignments_for_account(
account_id,
tenant_id=tenant_id,
)
)
deduped = {item.id: item for item in assignments}
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()}