feat(access): derive authenticated baseline roles

This commit is contained in:
2026-07-21 20:48:21 +02:00
parent 21f77ffc50
commit 4075ac4d73
4 changed files with 218 additions and 7 deletions

View File

@@ -20,7 +20,7 @@ from govoplan_access.backend.db.models import (
UserRoleAssignment,
)
from govoplan_access.backend.semantic import collect_function_authorization_context, collect_function_roles
from govoplan_access.backend.permissions.catalog import expand_scopes
from govoplan_access.backend.permissions.catalog import expand_scopes, role_templates_for_level
from govoplan_core.security.time import ensure_aware_utc, utc_now
SESSION_RANDOM_BYTES = 32
@@ -183,6 +183,8 @@ def collect_user_roles(session: Session, user: User) -> list[Role]:
roles_by_id[role.id] = role
for role in collect_function_roles(session, user):
roles_by_id[role.id] = role
for role in _materialized_default_authenticated_roles(session, user):
roles_by_id[role.id] = role
return list(roles_by_id.values())
@@ -242,14 +244,24 @@ def collect_user_authorization_context(
roles_by_id[role.id] = role
for role in extra_roles:
roles_by_id[role.id] = role
for role in _materialized_default_authenticated_roles(session, user):
roles_by_id[role.id] = role
tenant_roles = list(roles_by_id.values())
system_roles = collect_system_roles(session, account) if include_system and account is not None else []
default_slugs = _default_authenticated_slugs()
scopes = {
scope
for role in [*tenant_roles, *system_roles]
for role in tenant_roles
if role.slug not in default_slugs
for scope in (role.permissions or [])
}
scopes.update(
scope
for role in system_roles
for scope in (role.permissions or [])
)
scopes.update(_default_authenticated_scopes())
return UserAuthorizationContext(
tenant_roles=tenant_roles,
system_roles=system_roles,
@@ -261,15 +273,54 @@ def collect_user_authorization_context(
def collect_user_scopes(session: Session, user: User, *, include_system: bool = True) -> list[str]:
scopes: set[str] = set()
scopes = _default_authenticated_scopes()
default_slugs = _default_authenticated_slugs()
for role in collect_user_roles(session, user):
scopes.update(role.permissions or [])
if role.slug not in default_slugs:
scopes.update(role.permissions or [])
if include_system and user.account:
for role in collect_system_roles(session, user.account):
scopes.update(role.permissions or [])
return expand_scopes(scopes)
def _default_authenticated_slugs() -> set[str]:
return {
template.slug
for template in role_templates_for_level("tenant")
if template.default_authenticated
}
def _default_authenticated_scopes() -> set[str]:
return {
scope
for template in role_templates_for_level("tenant")
if template.default_authenticated
for scope in template.permissions
}
def _materialized_default_authenticated_roles(
session: Session,
user: User,
) -> list[Role]:
"""Return the optional database projection without mutating auth reads."""
default_slugs = _default_authenticated_slugs()
if not default_slugs:
return []
return (
session.query(Role)
.filter(
Role.tenant_id == user.tenant_id,
Role.slug.in_(default_slugs),
)
.order_by(Role.name.asc(), Role.id.asc())
.all()
)
def collect_tenant_memberships(session: Session, account: Account) -> list[tuple[User, Tenant]]:
return (
session.query(User, Tenant)