Use core auth and scope contracts

This commit is contained in:
2026-07-10 17:33:43 +02:00
parent 04681f1d75
commit c71de86a1f
6 changed files with 85 additions and 51 deletions

View File

@@ -8,6 +8,8 @@ from sqlalchemy.orm import Session
from govoplan_core.core.access import (
CAPABILITY_ACCESS_PERMISSION_EVALUATOR,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
PermissionEvaluator,
PrincipalRef,
PrincipalResolver,
@@ -16,7 +18,7 @@ from govoplan_core.core.modules import AccessDecision
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
from govoplan_core.db.session import get_database, get_session
from govoplan_access.backend.db.models import Account, ApiKey, AuthSession, User
from govoplan_access.backend.db.models import Account, ApiKey, AuthSession, Tenant, User
from govoplan_access.backend.semantic import collect_function_assignment_ids, collect_function_delegation_ids, identity_id_for_account
from govoplan_access.backend.security.api_keys import authenticate_api_key
from govoplan_access.backend.security.sessions import (
@@ -26,7 +28,6 @@ from govoplan_access.backend.security.sessions import (
collect_user_scopes,
verify_auth_session_csrf,
)
from govoplan_tenancy.backend.db.models import Tenant
from govoplan_core.security.module_permissions import scopes_grant_compatible
from govoplan_core.security.permissions import intersect_api_key_scopes
from govoplan_core.settings import settings
@@ -278,21 +279,35 @@ def _registry_from_request(request: Request) -> PlatformRegistry | None:
def _principal_resolver_from_request(request: Request) -> PrincipalResolver | None:
registry = _registry_from_request(request)
if registry is None or not registry.has_capability(CAPABILITY_ACCESS_PRINCIPAL_RESOLVER):
if registry is None:
return None
capability = registry.require_capability(CAPABILITY_ACCESS_PRINCIPAL_RESOLVER)
capability_name = (
CAPABILITY_AUTH_PRINCIPAL_RESOLVER
if registry.has_capability(CAPABILITY_AUTH_PRINCIPAL_RESOLVER)
else CAPABILITY_ACCESS_PRINCIPAL_RESOLVER
)
if not registry.has_capability(capability_name):
return None
capability = registry.require_capability(capability_name)
if not isinstance(capability, PrincipalResolver):
raise HTTPException(status_code=500, detail=f"Invalid capability: {CAPABILITY_ACCESS_PRINCIPAL_RESOLVER}")
raise HTTPException(status_code=500, detail=f"Invalid capability: {capability_name}")
return capability
def _permission_evaluator_from_request(request: Request) -> PermissionEvaluator | None:
registry = _registry_from_request(request)
if registry is None or not registry.has_capability(CAPABILITY_ACCESS_PERMISSION_EVALUATOR):
if registry is None:
return None
capability = registry.require_capability(CAPABILITY_ACCESS_PERMISSION_EVALUATOR)
capability_name = (
CAPABILITY_AUTH_PERMISSION_EVALUATOR
if registry.has_capability(CAPABILITY_AUTH_PERMISSION_EVALUATOR)
else CAPABILITY_ACCESS_PERMISSION_EVALUATOR
)
if not registry.has_capability(capability_name):
return None
capability = registry.require_capability(capability_name)
if not isinstance(capability, PermissionEvaluator):
raise HTTPException(status_code=500, detail=f"Invalid capability: {CAPABILITY_ACCESS_PERMISSION_EVALUATOR}")
raise HTTPException(status_code=500, detail=f"Invalid capability: {capability_name}")
return capability

View File

@@ -8,7 +8,7 @@ from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, Uniqu
from sqlalchemy.orm import Mapped, mapped_column, relationship
from govoplan_access.backend.db.base import AccessBase, TimestampMixin
from govoplan_tenancy.backend.db.models import Tenant
from govoplan_core.tenancy.scope import Tenant
def new_uuid() -> str:
@@ -93,7 +93,7 @@ class User(AccessBase, TimestampMixin):
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
email: Mapped[str] = mapped_column(String(320), nullable=False, index=True)
display_name: Mapped[str | None] = mapped_column(String(255))
@@ -105,7 +105,6 @@ class User(AccessBase, TimestampMixin):
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
tenant: Mapped[Tenant] = relationship(back_populates="users")
account: Mapped[Account] = relationship(back_populates="memberships")
api_keys: Mapped[list[ApiKey]] = relationship(back_populates="user", cascade="all, delete-orphan")
auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="user", cascade="all, delete-orphan")
@@ -116,7 +115,7 @@ class Group(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_groups_tenant_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
@@ -141,7 +140,7 @@ class Role(AccessBase, TimestampMixin):
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=True, index=True)
tenant_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
@@ -157,7 +156,7 @@ class OrganizationUnit(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_organization_units_tenant_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
parent_id: Mapped[str | None] = mapped_column(ForeignKey("access_organization_units.id", ondelete="SET NULL"), nullable=True, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
@@ -171,7 +170,7 @@ class Function(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "organization_unit_id", "slug", name="uq_functions_tenant_ou_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
organization_unit_id: Mapped[str] = mapped_column(ForeignKey("access_organization_units.id", ondelete="CASCADE"), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
@@ -187,7 +186,7 @@ class FunctionRoleAssignment(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "function_id", "role_id", name="uq_function_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
function_id: Mapped[str] = mapped_column(ForeignKey("access_functions.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -205,7 +204,7 @@ class FunctionAssignment(AccessBase, TimestampMixin):
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
identity_id: Mapped[str | None] = mapped_column(ForeignKey("access_identities.id", ondelete="SET NULL"), nullable=True, index=True)
function_id: Mapped[str] = mapped_column(ForeignKey("access_functions.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -233,7 +232,7 @@ class FunctionDelegation(AccessBase, TimestampMixin):
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
function_assignment_id: Mapped[str] = mapped_column(ForeignKey("access_function_assignments.id", ondelete="CASCADE"), nullable=False, index=True)
delegator_account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
delegate_account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -263,7 +262,7 @@ class UserGroupMembership(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "group_id", name="uq_user_group_memberships"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(ForeignKey("access_groups.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -273,7 +272,7 @@ class UserRoleAssignment(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "role_id", name="uq_user_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -283,7 +282,7 @@ class GroupRoleAssignment(AccessBase, TimestampMixin):
__table_args__ = (UniqueConstraint("tenant_id", "group_id", "role_id", name="uq_group_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(ForeignKey("access_groups.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True)
@@ -292,7 +291,7 @@ class ApiKey(AccessBase, TimestampMixin):
__tablename__ = "access_api_keys"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
@@ -309,7 +308,7 @@ class AuthSession(AccessBase, TimestampMixin):
__tablename__ = "access_auth_sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenancy_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("access_users.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)

View File

@@ -13,6 +13,8 @@ from govoplan_core.core.access import (
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import (
@@ -402,8 +404,7 @@ manifest = ModuleManifest(
id="access",
name="Access",
version="0.1.6",
dependencies=("tenancy",),
optional_dependencies=("identity", "organizations"),
optional_dependencies=("identity", "organizations", "tenancy"),
permissions=ACCESS_PERMISSIONS,
role_templates=ACCESS_ROLE_TEMPLATES,
route_factory=_route_factory,
@@ -442,6 +443,8 @@ manifest = ModuleManifest(
nav_items=(NavItem(path="/admin", label="Admin", icon="admin", required_any=ADMIN_READ_SCOPES, order=900),),
),
capability_factories={
CAPABILITY_AUTH_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_AUTH_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_ACCESS_DIRECTORY: _access_directory,

View File

@@ -20,6 +20,11 @@ def _tables() -> set[str]:
return set(sa.inspect(op.get_bind()).get_table_names())
def _scope_fk_target() -> str:
tables = _tables()
return "core_scopes.id" if "core_scopes" in tables else "tenancy_tenants.id"
def _indexes(table_name: str) -> set[str]:
return {item["name"] for item in sa.inspect(op.get_bind()).get_indexes(table_name)}
@@ -36,6 +41,7 @@ def _drop_index_if_exists(name: str, table_name: str) -> None:
def upgrade() -> None:
tables = _tables()
scope_fk_target = _scope_fk_target()
if "access_identities" not in tables:
op.create_table(
@@ -119,8 +125,8 @@ def upgrade() -> None:
),
sa.ForeignKeyConstraint(
["tenant_id"],
["tenancy_tenants.id"],
name=op.f("fk_access_organization_units_tenant_id_tenancy_tenants"),
[scope_fk_target],
name=op.f("fk_access_organization_units_tenant_id_scopes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_organization_units")),
@@ -153,8 +159,8 @@ def upgrade() -> None:
),
sa.ForeignKeyConstraint(
["tenant_id"],
["tenancy_tenants.id"],
name=op.f("fk_access_functions_tenant_id_tenancy_tenants"),
[scope_fk_target],
name=op.f("fk_access_functions_tenant_id_scopes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_functions")),
@@ -187,8 +193,8 @@ def upgrade() -> None:
),
sa.ForeignKeyConstraint(
["tenant_id"],
["tenancy_tenants.id"],
name=op.f("fk_access_function_role_assignments_tenant_id_tenancy_tenants"),
[scope_fk_target],
name=op.f("fk_access_function_role_assignments_tenant_id_scopes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_role_assignments")),
@@ -256,8 +262,8 @@ def upgrade() -> None:
),
sa.ForeignKeyConstraint(
["tenant_id"],
["tenancy_tenants.id"],
name=op.f("fk_access_function_assignments_tenant_id_tenancy_tenants"),
[scope_fk_target],
name=op.f("fk_access_function_assignments_tenant_id_scopes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_assignments")),
@@ -309,8 +315,8 @@ def upgrade() -> None:
),
sa.ForeignKeyConstraint(
["tenant_id"],
["tenancy_tenants.id"],
name=op.f("fk_access_function_delegations_tenant_id_tenancy_tenants"),
[scope_fk_target],
name=op.f("fk_access_function_delegations_tenant_id_scopes"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_delegations")),