diff --git a/README.md b/README.md index 4f03f67..5b1a7bb 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ administration. The repository contains the extracted access seed implementation under `src/govoplan_access/backend`. Session, API-key, and password helper services, -interactive auth routes, and administration routers are owned here. The public -FastAPI request dependency API is exported from `govoplan_access.auth`; modules -must not import the backend dependency module directly. Access-side admin -service helpers remain +interactive auth routes, and administration routers are owned here. Access +still exports `govoplan_access.auth` for compatibility, but sibling modules +should import the core `govoplan_core.auth` facade so auth can become a +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 access enforcement, admin/audit lookup capabilities, tenant owner 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 system administration panels are contributed by `@govoplan/admin-webui` through core's `admin.sections` UI capability. Live access ORM models are defined here -with `access_*` table names; tenant records live 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: +with `access_*` table names. Access stores current scope identifiers in +`tenant_id` columns but does not hard-depend on the tenancy module package. +Tenancy-specific tenant administration and tenant resolver behavior live 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/MODULE_ARCHITECTURE.md` @@ -47,7 +50,8 @@ This module will own: - permission evaluation - access administration backend routes - 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 capabilities - access-owned migrations @@ -55,15 +59,16 @@ This module will own: The governance-template routes under `/admin/system/governance-templates` are contributed by `govoplan-admin`; access must not register those routes. -`govoplan-access` depends on `govoplan-tenancy`; the registry loads tenancy -before access for authenticated platform composition. +`govoplan-access` treats `govoplan-tenancy` as optional. Access can run in the +single-scope compatibility mode used by the core/access baseline, and tenancy +adds tenant administration plus tenant resolver behavior when installed. ## Principal Context The stable principal DTO is `govoplan_core.core.access.PrincipalRef`. Access resolves sessions, API keys, and future service accounts into that DTO and 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. The detailed module boundary and serialization fields are documented in diff --git a/docs/ACCESS_MODULE_BOUNDARY.md b/docs/ACCESS_MODULE_BOUNDARY.md index 63db79c..bbb47a5 100644 --- a/docs/ACCESS_MODULE_BOUNDARY.md +++ b/docs/ACCESS_MODULE_BOUNDARY.md @@ -53,9 +53,15 @@ importing access internals: - generic security helpers that are not access-state semantics, such as secret encryption and UTC time helpers -Feature modules should depend on these kernel contracts or the published -`govoplan_access.auth` request dependency API, not on access ORM models or -`govoplan_access.backend.*` implementation internals. +Feature modules should depend on these kernel contracts or the core +`govoplan_core.auth` request dependency facade, not on access ORM models or +`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 @@ -115,7 +121,7 @@ selection are still follow-up work on top of these routes. ## Removed Compatibility Paths 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: - `govoplan_core.security.api_keys` diff --git a/src/govoplan_access/backend/auth/dependencies.py b/src/govoplan_access/backend/auth/dependencies.py index 81e0bc1..64dff10 100644 --- a/src/govoplan_access/backend/auth/dependencies.py +++ b/src/govoplan_access/backend/auth/dependencies.py @@ -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 diff --git a/src/govoplan_access/backend/db/models.py b/src/govoplan_access/backend/db/models.py index 5fe53a6..3230e93 100644 --- a/src/govoplan_access/backend/db/models.py +++ b/src/govoplan_access/backend/db/models.py @@ -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) diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index 29132e0..b44f2b1 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -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, diff --git a/src/govoplan_access/backend/migrations/versions/4a5b6c7d8e9f_access_semantic_directory.py b/src/govoplan_access/backend/migrations/versions/4a5b6c7d8e9f_access_semantic_directory.py index 768b8f3..c6e2aaf 100644 --- a/src/govoplan_access/backend/migrations/versions/4a5b6c7d8e9f_access_semantic_directory.py +++ b/src/govoplan_access/backend/migrations/versions/4a5b6c7d8e9f_access_semantic_directory.py @@ -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")),