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

@@ -56,7 +56,7 @@ def ensure_default_roles(session: Session, tenant: Tenant | None = None) -> dict
query = query.filter(Role.tenant_id == tenant.id) if tenant is not None else query.filter(Role.tenant_id.is_(None))
role = query.one_or_none()
is_builtin = _template_is_builtin(template_managed=template.managed, protected=template.protected, tenant_role=tenant is not None)
is_assignable = True
is_assignable = not template.default_authenticated
if role is None:
role = Role(
tenant_id=tenant.id if tenant is not None else None,
@@ -200,7 +200,24 @@ def set_user_groups(session: Session, *, user: User, group_ids: Iterable[str]) -
def set_user_roles(session: Session, *, user: User, role_ids: Iterable[str]) -> None:
ids = sorted(set(role_ids))
default_slugs = {
template.slug
for template in role_templates_for_level("tenant")
if template.default_authenticated
}
default_roles = (
session.query(Role)
.filter(
Role.tenant_id == user.tenant_id,
Role.slug.in_(default_slugs),
)
.all()
if default_slugs
else []
)
default_role_ids = {role.id for role in default_roles}
requested_ids = set(role_ids) - default_role_ids
ids = sorted(requested_ids)
roles = (
session.query(Role)
.filter(Role.tenant_id == user.tenant_id, Role.id.in_(ids), Role.is_assignable.is_(True))

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)