perf: batch authorization and activity updates

This commit is contained in:
2026-07-29 14:16:28 +02:00
parent 3df4fc5bff
commit 984a015704
9 changed files with 613 additions and 85 deletions

View File

@@ -57,6 +57,7 @@ from govoplan_access.backend.api.v1.admin_common import (
_set_system_memberships,
_system_role_assignment_counts,
_system_account_item,
_system_account_items,
_tenant_role_assignment_counts,
_user_item,
)
@@ -484,14 +485,42 @@ def _validate_parent_organization_unit(
raise AdminValidationError("Parent organization unit does not belong to the tenant.")
def _function_item(session: Session, item: Function) -> FunctionAdminItem:
role_ids = [
row[0]
for row in session.query(FunctionRoleAssignment.role_id)
.filter(FunctionRoleAssignment.function_id == item.id)
.order_by(FunctionRoleAssignment.created_at.asc())
def _function_role_ids_by_function_id(
session: Session,
function_ids: Iterable[str],
) -> dict[str, list[str]]:
requested = tuple(dict.fromkeys(function_ids))
result: dict[str, list[str]] = {function_id: [] for function_id in requested}
if not requested:
return result
rows = (
session.query(
FunctionRoleAssignment.function_id,
FunctionRoleAssignment.role_id,
)
.filter(FunctionRoleAssignment.function_id.in_(requested))
.order_by(
FunctionRoleAssignment.function_id.asc(),
FunctionRoleAssignment.created_at.asc(),
)
.all()
]
)
for function_id, role_id in rows:
result.setdefault(function_id, []).append(role_id)
return result
def _function_item(
session: Session,
item: Function,
*,
role_ids_by_function_id: dict[str, list[str]] | None = None,
) -> FunctionAdminItem:
if role_ids_by_function_id is None:
role_ids_by_function_id = _function_role_ids_by_function_id(
session,
(item.id,),
)
return FunctionAdminItem(
id=item.id,
tenant_id=item.tenant_id,
@@ -499,7 +528,7 @@ def _function_item(session: Session, item: Function) -> FunctionAdminItem:
slug=item.slug,
name=item.name,
description=item.description,
role_ids=role_ids,
role_ids=role_ids_by_function_id.get(item.id, []),
delegable=item.delegable,
act_in_place_allowed=item.act_in_place_allowed,
is_active=item.is_active,
@@ -1266,17 +1295,26 @@ def deactivate_identity(
@router.get("/organization-units", response_model=OrganizationUnitListResponse)
def list_organization_units(
tenant_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
):
tenant = _resolve_tenant(session, principal, tenant_id)
items = (
query = (
session.query(OrganizationUnit)
.filter(OrganizationUnit.tenant_id == tenant.id)
.order_by(OrganizationUnit.name.asc())
.all()
)
return OrganizationUnitListResponse(organization_units=[_organization_unit_item(item) for item in items])
items, pagination = _page_query(
query,
page=page,
page_size=page_size,
)
return OrganizationUnitListResponse(
organization_units=[_organization_unit_item(item) for item in items],
**pagination,
)
@router.post("/organization-units", response_model=OrganizationUnitItem, status_code=status.HTTP_201_CREATED)
@@ -1394,6 +1432,8 @@ def deactivate_organization_unit(
def list_functions(
tenant_id: str | None = None,
organization_unit_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
):
@@ -1401,8 +1441,26 @@ def list_functions(
query = session.query(Function).filter(Function.tenant_id == tenant.id)
if organization_unit_id:
query = query.filter(Function.organization_unit_id == organization_unit_id)
functions = query.order_by(Function.name.asc()).all()
return FunctionListResponse(functions=[_function_item(session, item) for item in functions])
functions, pagination = _page_query(
query.order_by(Function.name.asc()),
page=page,
page_size=page_size,
)
role_ids_by_function_id = _function_role_ids_by_function_id(
session,
(item.id for item in functions),
)
return FunctionListResponse(
functions=[
_function_item(
session,
item,
role_ids_by_function_id=role_ids_by_function_id,
)
for item in functions
],
**pagination,
)
@router.post("/functions", response_model=FunctionAdminItem, status_code=status.HTTP_201_CREATED)
@@ -1535,6 +1593,8 @@ def list_external_function_role_mappings(
tenant_id: str | None = None,
source_module: str | None = None,
function_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
):
@@ -1544,23 +1604,57 @@ def list_external_function_role_mappings(
query = query.filter(ExternalFunctionRoleAssignment.source_module == source_module.strip())
if function_id:
query = query.filter(ExternalFunctionRoleAssignment.function_id == function_id.strip())
items = query.order_by(ExternalFunctionRoleAssignment.source_module.asc(), ExternalFunctionRoleAssignment.function_id.asc()).all()
return ExternalFunctionRoleMappingListResponse(mappings=[_external_function_role_mapping_item(item) for item in items])
items, pagination = _page_query(
query.order_by(
ExternalFunctionRoleAssignment.source_module.asc(),
ExternalFunctionRoleAssignment.function_id.asc(),
),
page=page,
page_size=page_size,
)
return ExternalFunctionRoleMappingListResponse(
mappings=[_external_function_role_mapping_item(item) for item in items],
**pagination,
)
def _full_external_function_role_mappings_delta_response(session: Session, tenant: Tenant) -> ExternalFunctionRoleMappingListDeltaResponse:
items = (
def _full_external_function_role_mappings_delta_response(
session: Session,
tenant: Tenant,
*,
cursor: tuple[int, int] | None = None,
limit: int = 500,
) -> ExternalFunctionRoleMappingListDeltaResponse:
snapshot_sequence = (
cursor[1]
if cursor is not None
else max_sequence_id(
session,
tenant_id=tenant.id,
module_id=ACCESS_MODULE_ID,
collections=(ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,),
)
)
page = cursor[0] if cursor is not None else 1
query = (
session.query(ExternalFunctionRoleAssignment)
.filter(ExternalFunctionRoleAssignment.tenant_id == tenant.id)
.order_by(ExternalFunctionRoleAssignment.source_module.asc(), ExternalFunctionRoleAssignment.function_id.asc())
.all()
)
items, pagination, watermark, has_more = _full_delta_page(
query,
page=page,
page_size=limit,
scope="external-function-role-mappings",
snapshot_sequence=snapshot_sequence,
)
return ExternalFunctionRoleMappingListDeltaResponse(
mappings=[_external_function_role_mapping_item(item) for item in items],
deleted=[],
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,)),
has_more=False,
watermark=watermark,
has_more=has_more,
full=True,
**pagination,
)
@@ -1620,8 +1714,17 @@ def list_external_function_role_mappings_delta(
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
):
tenant = _resolve_tenant(session, principal, tenant_id)
if since is None:
return _full_external_function_role_mappings_delta_response(session, tenant)
full_cursor = _decode_full_delta_cursor(
since,
scope="external-function-role-mappings",
)
if since is None or full_cursor is not None:
return _full_external_function_role_mappings_delta_response(
session,
tenant,
cursor=full_cursor,
limit=limit,
)
return _external_function_role_mappings_delta_response(session, tenant, since=since, limit=limit)
@@ -1735,6 +1838,8 @@ def list_function_assignments(
tenant_id: str | None = None,
account_id: str | None = None,
function_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:function:assign")),
):
@@ -1744,8 +1849,15 @@ def list_function_assignments(
query = query.filter(FunctionAssignment.account_id == account_id)
if function_id:
query = query.filter(FunctionAssignment.function_id == function_id)
assignments = query.order_by(FunctionAssignment.created_at.desc()).all()
return FunctionAssignmentListResponse(assignments=[_function_assignment_item(item) for item in assignments])
assignments, pagination = _page_query(
query.order_by(FunctionAssignment.created_at.desc()),
page=page,
page_size=page_size,
)
return FunctionAssignmentListResponse(
assignments=[_function_assignment_item(item) for item in assignments],
**pagination,
)
@router.post("/function-assignments", response_model=FunctionAssignmentAdminItem, status_code=status.HTTP_201_CREATED)
@@ -1888,6 +2000,8 @@ def deactivate_function_assignment(
def list_function_delegations(
tenant_id: str | None = None,
account_id: str | None = None,
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:function:delegate")),
):
@@ -1895,8 +2009,15 @@ def list_function_delegations(
query = session.query(FunctionDelegation).filter(FunctionDelegation.tenant_id == tenant.id)
if account_id:
query = query.filter((FunctionDelegation.delegator_account_id == account_id) | (FunctionDelegation.delegate_account_id == account_id))
delegations = query.order_by(FunctionDelegation.created_at.desc()).all()
return FunctionDelegationListResponse(delegations=[_function_delegation_item(item) for item in delegations])
delegations, pagination = _page_query(
query.order_by(FunctionDelegation.created_at.desc()),
page=page,
page_size=page_size,
)
return FunctionDelegationListResponse(
delegations=[_function_delegation_item(item) for item in delegations],
**pagination,
)
@router.post("/function-delegations", response_model=FunctionDelegationAdminItem, status_code=status.HTTP_201_CREATED)
@@ -2981,6 +3102,25 @@ def _system_role_summaries_for_response(session: Session, roles: list[Role]) ->
return [_role_summary(session, role, system_role_assignment_counts=system_role_counts) for role in roles]
def _system_roles_for_account_catalog(session: Session) -> list[Role]:
roles = (
session.query(Role)
.filter(Role.tenant_id.is_(None))
.order_by(Role.name.asc(), Role.id.asc())
.limit(1001)
.all()
)
if len(roles) > 1000:
raise HTTPException(
status_code=status.HTTP_413_CONTENT_TOO_LARGE,
detail=(
"The system role catalog exceeds 1000 entries. "
"Use the paginated system roles endpoint."
),
)
return roles
def _full_system_roles_delta_response(session: Session, *, cursor: tuple[int, int] | None = None, limit: int = 500) -> RoleListDeltaResponse:
ensure_default_roles(session, None)
session.commit()
@@ -3178,13 +3318,13 @@ _SYSTEM_ACCOUNTS_DELTA_COLLECTIONS = (ACCESS_SYSTEM_ACCOUNTS_COLLECTION, ACCESS_
def _full_system_accounts_delta_response(session: Session, *, cursor: tuple[int, int] | None = None, limit: int = 500) -> SystemAccountListDeltaResponse:
ensure_default_roles(session, None)
session.commit()
system_roles = session.query(Role).filter(Role.tenant_id.is_(None)).order_by(Role.name.asc()).all()
system_roles = _system_roles_for_account_catalog(session)
snapshot_sequence = cursor[1] if cursor is not None else max_sequence_id(session, tenant_id=None, module_id=ACCESS_MODULE_ID, collections=_SYSTEM_ACCOUNTS_DELTA_COLLECTIONS)
page = cursor[0] if cursor is not None else 1
query = session.query(Account).order_by(Account.email.asc(), Account.id.asc())
accounts, pagination, watermark, has_more = _full_delta_page(query, page=page, page_size=limit, scope="system-accounts", snapshot_sequence=snapshot_sequence)
return SystemAccountListDeltaResponse(
accounts=[_system_account_item(session, account) for account in accounts],
accounts=_system_account_items(session, accounts),
roles=_system_role_summaries_for_response(session, system_roles),
deleted=[],
watermark=watermark,
@@ -3223,7 +3363,7 @@ def _system_accounts_delta_response(session: Session, *, since: str, limit: int)
)
]
return SystemAccountListDeltaResponse(
accounts=[_system_account_item(session, account) for account in visible_accounts.values()],
accounts=_system_account_items(session, list(visible_accounts.values())),
roles=_system_role_summaries_for_response(session, list(visible_roles.values())),
deleted=deleted,
watermark=_access_delta_response_watermark(session, tenant_id=None, collections=_SYSTEM_ACCOUNTS_DELTA_COLLECTIONS, entries=entries, has_more=has_more),
@@ -3255,11 +3395,11 @@ def list_system_accounts(
):
ensure_default_roles(session, None)
session.commit()
system_roles = session.query(Role).filter(Role.tenant_id.is_(None)).order_by(Role.name.asc()).all()
system_roles = _system_roles_for_account_catalog(session)
query = session.query(Account).order_by(Account.email.asc())
accounts, pagination = _page_query(query, page=page, page_size=page_size)
return SystemAccountListResponse(
accounts=[_system_account_item(session, account) for account in accounts],
accounts=_system_account_items(session, accounts),
roles=_system_role_summaries_for_response(session, system_roles),
**pagination,
)