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

@@ -6,10 +6,11 @@ administration.
The repository contains the extracted access seed implementation under The repository contains the extracted access seed implementation under
`src/govoplan_access/backend`. Session, API-key, and password helper services, `src/govoplan_access/backend`. Session, API-key, and password helper services,
interactive auth routes, and administration routers are owned here. The public interactive auth routes, and administration routers are owned here. Access
FastAPI request dependency API is exported from `govoplan_access.auth`; modules still exports `govoplan_access.auth` for compatibility, but sibling modules
must not import the backend dependency module directly. Access-side admin should import the core `govoplan_core.auth` facade so auth can become a
service helpers remain provider-neutral capability. Modules must not import the backend dependency
module directly. Access-side admin service helpers remain
here for users, groups, roles, system accounts, sessions, API keys, tenant here for users, groups, roles, system accounts, sessions, API keys, tenant
access enforcement, admin/audit lookup capabilities, tenant owner access enforcement, admin/audit lookup capabilities, tenant owner
provisioning, and governance-template materialization into access-owned groups provisioning, and governance-template materialization into access-owned groups
@@ -18,10 +19,12 @@ transitional administration WebUI route shell and
access-owned panels live under `webui/src` as `@govoplan/access-webui`. Generic access-owned panels live under `webui/src` as `@govoplan/access-webui`. Generic
system administration panels are contributed by `@govoplan/admin-webui` through system administration panels are contributed by `@govoplan/admin-webui` through
core's `admin.sections` UI capability. Live access ORM models are defined here core's `admin.sections` UI capability. Live access ORM models are defined here
with `access_*` table names; tenant records live in `govoplan-tenancy`, with `access_*` table names. Access stores current scope identifiers in
governance templates in `govoplan-admin`, audit logs in `govoplan-audit`, and `tenant_id` columns but does not hard-depend on the tenancy module package.
system settings in `govoplan-core`. The current access boundary is documented Tenancy-specific tenant administration and tenant resolver behavior live in
in: `govoplan-tenancy`; governance templates in `govoplan-admin`, audit logs in
`govoplan-audit`, and system settings in `govoplan-core`. The current access
boundary is documented in:
- `/mnt/DATA/git/govoplan-core/docs/ACCESS_RBAC_MODEL.md` - `/mnt/DATA/git/govoplan-core/docs/ACCESS_RBAC_MODEL.md`
- `/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md` - `/mnt/DATA/git/govoplan-core/docs/MODULE_ARCHITECTURE.md`
@@ -47,7 +50,8 @@ This module will own:
- permission evaluation - permission evaluation
- access administration backend routes - access administration backend routes
- access administration WebUI route contributions - access administration WebUI route contributions
- published FastAPI auth dependency API at `govoplan_access.auth` - compatibility FastAPI auth dependency API at `govoplan_access.auth`
- provider-facing auth facade consumed by sibling modules at `govoplan_core.auth`
- access administration, tenant provisioning, and governance materializer - access administration, tenant provisioning, and governance materializer
capabilities capabilities
- access-owned migrations - access-owned migrations
@@ -55,15 +59,16 @@ This module will own:
The governance-template routes under `/admin/system/governance-templates` are The governance-template routes under `/admin/system/governance-templates` are
contributed by `govoplan-admin`; access must not register those routes. contributed by `govoplan-admin`; access must not register those routes.
`govoplan-access` depends on `govoplan-tenancy`; the registry loads tenancy `govoplan-access` treats `govoplan-tenancy` as optional. Access can run in the
before access for authenticated platform composition. single-scope compatibility mode used by the core/access baseline, and tenancy
adds tenant administration plus tenant resolver behavior when installed.
## Principal Context ## Principal Context
The stable principal DTO is `govoplan_core.core.access.PrincipalRef`. Access The stable principal DTO is `govoplan_core.core.access.PrincipalRef`. Access
resolves sessions, API keys, and future service accounts into that DTO and resolves sessions, API keys, and future service accounts into that DTO and
serializes it as `principal` in auth API responses. Feature modules should use serializes it as `principal` in auth API responses. Feature modules should use
that DTO, primitive IDs, or the published `govoplan_access.auth` dependency API that DTO, primitive IDs, or the core `govoplan_core.auth` dependency facade
instead of importing access ORM models or backend dependency internals. instead of importing access ORM models or backend dependency internals.
The detailed module boundary and serialization fields are documented in The detailed module boundary and serialization fields are documented in

View File

@@ -53,9 +53,15 @@ importing access internals:
- generic security helpers that are not access-state semantics, such as - generic security helpers that are not access-state semantics, such as
secret encryption and UTC time helpers secret encryption and UTC time helpers
Feature modules should depend on these kernel contracts or the published Feature modules should depend on these kernel contracts or the core
`govoplan_access.auth` request dependency API, not on access ORM models or `govoplan_core.auth` request dependency facade, not on access ORM models or
`govoplan_access.backend.*` implementation internals. `govoplan_access.backend.*` implementation internals. The access package still
exports `govoplan_access.auth` for compatibility, but new routers should use
the core facade so auth can move behind provider-neutral capabilities.
Access declares tenancy as an optional module integration. It uses the
core-owned `core_scopes` table as the scope table, but it must not import
`govoplan_tenancy` or require the tenancy package to start.
## Principal Context Contract ## Principal Context Contract
@@ -115,7 +121,7 @@ selection are still follow-up work on top of these routes.
## Removed Compatibility Paths ## Removed Compatibility Paths
These legacy imports were removed from core. Use access-owned modules, the These legacy imports were removed from core. Use access-owned modules, the
public `govoplan_access.auth` request dependency API, or kernel capabilities core `govoplan_core.auth` request dependency facade, or kernel capabilities
instead: instead:
- `govoplan_core.security.api_keys` - `govoplan_core.security.api_keys`

View File

@@ -8,6 +8,8 @@ from sqlalchemy.orm import Session
from govoplan_core.core.access import ( from govoplan_core.core.access import (
CAPABILITY_ACCESS_PERMISSION_EVALUATOR, CAPABILITY_ACCESS_PERMISSION_EVALUATOR,
CAPABILITY_ACCESS_PRINCIPAL_RESOLVER, CAPABILITY_ACCESS_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
PermissionEvaluator, PermissionEvaluator,
PrincipalRef, PrincipalRef,
PrincipalResolver, PrincipalResolver,
@@ -16,7 +18,7 @@ from govoplan_core.core.modules import AccessDecision
from govoplan_core.core.registry import PlatformRegistry 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.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
from govoplan_core.db.session import get_database, get_session 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.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.api_keys import authenticate_api_key
from govoplan_access.backend.security.sessions import ( from govoplan_access.backend.security.sessions import (
@@ -26,7 +28,6 @@ from govoplan_access.backend.security.sessions import (
collect_user_scopes, collect_user_scopes,
verify_auth_session_csrf, 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.module_permissions import scopes_grant_compatible
from govoplan_core.security.permissions import intersect_api_key_scopes from govoplan_core.security.permissions import intersect_api_key_scopes
from govoplan_core.settings import settings 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: def _principal_resolver_from_request(request: Request) -> PrincipalResolver | None:
registry = _registry_from_request(request) registry = _registry_from_request(request)
if registry is None or not registry.has_capability(CAPABILITY_ACCESS_PRINCIPAL_RESOLVER): if registry is None:
return 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): 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 return capability
def _permission_evaluator_from_request(request: Request) -> PermissionEvaluator | None: def _permission_evaluator_from_request(request: Request) -> PermissionEvaluator | None:
registry = _registry_from_request(request) registry = _registry_from_request(request)
if registry is None or not registry.has_capability(CAPABILITY_ACCESS_PERMISSION_EVALUATOR): if registry is None:
return 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): 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 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 sqlalchemy.orm import Mapped, mapped_column, relationship
from govoplan_access.backend.db.base import AccessBase, TimestampMixin 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: 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) 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) 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) email: Mapped[str] = mapped_column(String(320), nullable=False, index=True)
display_name: Mapped[str | None] = mapped_column(String(255)) 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) 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) 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") account: Mapped[Account] = relationship(back_populates="memberships")
api_keys: Mapped[list[ApiKey]] = relationship(back_populates="user", cascade="all, delete-orphan") 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") 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"),) __table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_groups_tenant_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) 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) slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text) 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) 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) slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text) 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"),) __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) 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) 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) slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), 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"),) __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) 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) 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) slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), 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"),) __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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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"),) __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) 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) 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) 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"),) __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) 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) 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) 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"),) __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) 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) 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) 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" __tablename__ = "access_api_keys"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) 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) 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) name: Mapped[str] = mapped_column(String(255), nullable=False)
prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True) prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
@@ -309,7 +308,7 @@ class AuthSession(AccessBase, TimestampMixin):
__tablename__ = "access_auth_sessions" __tablename__ = "access_auth_sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) 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) 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) 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) 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_PRINCIPAL_RESOLVER,
CAPABILITY_ACCESS_TENANT_PROVISIONER, CAPABILITY_ACCESS_TENANT_PROVISIONER,
CAPABILITY_ACCESS_SEMANTIC_DIRECTORY, 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.module_guards import persistent_table_uninstall_guard
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
@@ -402,8 +404,7 @@ manifest = ModuleManifest(
id="access", id="access",
name="Access", name="Access",
version="0.1.6", version="0.1.6",
dependencies=("tenancy",), optional_dependencies=("identity", "organizations", "tenancy"),
optional_dependencies=("identity", "organizations"),
permissions=ACCESS_PERMISSIONS, permissions=ACCESS_PERMISSIONS,
role_templates=ACCESS_ROLE_TEMPLATES, role_templates=ACCESS_ROLE_TEMPLATES,
route_factory=_route_factory, 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),), nav_items=(NavItem(path="/admin", label="Admin", icon="admin", required_any=ADMIN_READ_SCOPES, order=900),),
), ),
capability_factories={ 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_PRINCIPAL_RESOLVER: _legacy_principal_resolver,
CAPABILITY_ACCESS_PERMISSION_EVALUATOR: _legacy_permission_evaluator, CAPABILITY_ACCESS_PERMISSION_EVALUATOR: _legacy_permission_evaluator,
CAPABILITY_ACCESS_DIRECTORY: _access_directory, 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()) 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]: def _indexes(table_name: str) -> set[str]:
return {item["name"] for item in sa.inspect(op.get_bind()).get_indexes(table_name)} 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: def upgrade() -> None:
tables = _tables() tables = _tables()
scope_fk_target = _scope_fk_target()
if "access_identities" not in tables: if "access_identities" not in tables:
op.create_table( op.create_table(
@@ -119,8 +125,8 @@ def upgrade() -> None:
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
["tenant_id"], ["tenant_id"],
["tenancy_tenants.id"], [scope_fk_target],
name=op.f("fk_access_organization_units_tenant_id_tenancy_tenants"), name=op.f("fk_access_organization_units_tenant_id_scopes"),
ondelete="CASCADE", ondelete="CASCADE",
), ),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_organization_units")), sa.PrimaryKeyConstraint("id", name=op.f("pk_access_organization_units")),
@@ -153,8 +159,8 @@ def upgrade() -> None:
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
["tenant_id"], ["tenant_id"],
["tenancy_tenants.id"], [scope_fk_target],
name=op.f("fk_access_functions_tenant_id_tenancy_tenants"), name=op.f("fk_access_functions_tenant_id_scopes"),
ondelete="CASCADE", ondelete="CASCADE",
), ),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_functions")), sa.PrimaryKeyConstraint("id", name=op.f("pk_access_functions")),
@@ -187,8 +193,8 @@ def upgrade() -> None:
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
["tenant_id"], ["tenant_id"],
["tenancy_tenants.id"], [scope_fk_target],
name=op.f("fk_access_function_role_assignments_tenant_id_tenancy_tenants"), name=op.f("fk_access_function_role_assignments_tenant_id_scopes"),
ondelete="CASCADE", ondelete="CASCADE",
), ),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_role_assignments")), sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_role_assignments")),
@@ -256,8 +262,8 @@ def upgrade() -> None:
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
["tenant_id"], ["tenant_id"],
["tenancy_tenants.id"], [scope_fk_target],
name=op.f("fk_access_function_assignments_tenant_id_tenancy_tenants"), name=op.f("fk_access_function_assignments_tenant_id_scopes"),
ondelete="CASCADE", ondelete="CASCADE",
), ),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_assignments")), sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_assignments")),
@@ -309,8 +315,8 @@ def upgrade() -> None:
), ),
sa.ForeignKeyConstraint( sa.ForeignKeyConstraint(
["tenant_id"], ["tenant_id"],
["tenancy_tenants.id"], [scope_fk_target],
name=op.f("fk_access_function_delegations_tenant_id_tenancy_tenants"), name=op.f("fk_access_function_delegations_tenant_id_scopes"),
ondelete="CASCADE", ondelete="CASCADE",
), ),
sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_delegations")), sa.PrimaryKeyConstraint("id", name=op.f("pk_access_function_delegations")),