perf: batch authorization and activity updates
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user