From 9a6b6c7e8e77d65100be4b2e478d250a462bda65 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 10 Jul 2026 17:33:52 +0200 Subject: [PATCH] Use capability-based module boundaries --- README.md | 5 ++++- src/govoplan_campaign/backend/db/models.py | 16 +++++++--------- src/govoplan_campaign/backend/manifest.py | 3 ++- ...4e5f7081_recipient_import_mapping_profiles.py | 8 +++++++- src/govoplan_campaign/backend/router.py | 2 +- src/govoplan_campaign/backend/schema_router.py | 2 +- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 28b69de..9108a46 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ This repository owns: - WebUI package `@govoplan/campaign-webui` - route contributions for `/campaigns`, `/campaigns/:campaignId/*`, `/operator`, `/reports`, `/address-book`, and `/templates` -Core owns auth, tenants, RBAC evaluation, database/session primitives, CSRF/API helpers, shell layout, and route rendering. Files and mail own their respective storage and transport capabilities. +Core owns the auth facade, RBAC/capability contracts, database/session +primitives, CSRF/API helpers, shell layout, and route rendering. Tenancy is an +optional platform module for tenant administration and tenant resolver behavior. +Files and mail own their respective storage and transport capabilities. ## Dependencies diff --git a/src/govoplan_campaign/backend/db/models.py b/src/govoplan_campaign/backend/db/models.py index 3d4b6e5..9c2e4ab 100644 --- a/src/govoplan_campaign/backend/db/models.py +++ b/src/govoplan_campaign/backend/db/models.py @@ -108,7 +108,7 @@ class Campaign(Base, TimestampMixin): __table_args__ = (UniqueConstraint("tenant_id", "external_id", name="uq_campaigns_tenant_external_id"),) 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) created_by_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True) owner_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True) owner_group_id: Mapped[str | None] = mapped_column(ForeignKey("access_groups.id", ondelete="SET NULL"), nullable=True, index=True) @@ -120,7 +120,6 @@ class Campaign(Base, 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("Tenant") versions: Mapped[list[CampaignVersion]] = relationship(back_populates="campaign", cascade="all, delete-orphan") jobs: Mapped[list[CampaignJob]] = relationship(back_populates="campaign", cascade="all, delete-orphan") @@ -130,7 +129,7 @@ class CampaignShare(Base, TimestampMixin): __table_args__ = (UniqueConstraint("campaign_id", "target_type", "target_id", name="uq_campaign_share_target"),) 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) campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True) target_type: Mapped[str] = mapped_column(String(20), nullable=False, index=True) target_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True) @@ -148,7 +147,7 @@ class RecipientImportMappingProfile(Base, 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) owner_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) column_count: Mapped[int] = mapped_column(Integer, nullable=False) @@ -162,7 +161,6 @@ class RecipientImportMappingProfile(Base, TimestampMixin): value_separators: Mapped[str] = mapped_column(String(50), default=",;|", nullable=False) mappings: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list, nullable=False) - tenant: Mapped["Tenant"] = relationship("Tenant") owner: Mapped["User"] = relationship("User") @@ -223,7 +221,7 @@ class CampaignJob(Base, TimestampMixin): __table_args__ = (UniqueConstraint("campaign_version_id", "entry_index", name="uq_campaign_jobs_version_entry"),) 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) campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True) campaign_version_id: Mapped[str] = mapped_column(ForeignKey("campaign_versions.id", ondelete="CASCADE"), nullable=False, index=True) entry_index: Mapped[int] = mapped_column(Integer, nullable=False) @@ -263,7 +261,7 @@ class CampaignIssue(Base, TimestampMixin): __tablename__ = "campaign_issues" 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) campaign_id: Mapped[str] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True) campaign_version_id: Mapped[str | None] = mapped_column(ForeignKey("campaign_versions.id", ondelete="CASCADE"), nullable=True, index=True) job_id: Mapped[str | None] = mapped_column(ForeignKey("campaign_jobs.id", ondelete="CASCADE"), nullable=True, index=True) @@ -279,7 +277,7 @@ class AttachmentBlob(Base, TimestampMixin): __table_args__ = (UniqueConstraint("tenant_id", "sha256", name="uq_attachment_blobs_tenant_sha256"),) 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) sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) mime_type: Mapped[str | None] = mapped_column(String(255)) @@ -291,7 +289,7 @@ class AttachmentInstance(Base, TimestampMixin): __tablename__ = "attachment_instances" 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) owner_user_id: Mapped[str | None] = mapped_column(ForeignKey("access_users.id", ondelete="SET NULL"), nullable=True, index=True) campaign_id: Mapped[str | None] = mapped_column(ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=True, index=True) blob_id: Mapped[str] = mapped_column(ForeignKey("attachment_blobs.id", ondelete="CASCADE"), nullable=False, index=True) diff --git a/src/govoplan_campaign/backend/manifest.py b/src/govoplan_campaign/backend/manifest.py index b155e93..e94b2e9 100644 --- a/src/govoplan_campaign/backend/manifest.py +++ b/src/govoplan_campaign/backend/manifest.py @@ -2,6 +2,7 @@ from __future__ import annotations from pathlib import Path +from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.campaigns import ( CAPABILITY_CAMPAIGNS_ACCESS, CAPABILITY_CAMPAIGNS_DELIVERY_TASKS, @@ -132,7 +133,7 @@ manifest = ModuleManifest( id="campaigns", name="Campaigns", version="0.1.6", - dependencies=("access",), + required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("files", "mail"), permissions=PERMISSIONS, route_factory=_campaigns_router, diff --git a/src/govoplan_campaign/backend/migrations/versions/2c3d4e5f7081_recipient_import_mapping_profiles.py b/src/govoplan_campaign/backend/migrations/versions/2c3d4e5f7081_recipient_import_mapping_profiles.py index 7afeed5..402aefa 100644 --- a/src/govoplan_campaign/backend/migrations/versions/2c3d4e5f7081_recipient_import_mapping_profiles.py +++ b/src/govoplan_campaign/backend/migrations/versions/2c3d4e5f7081_recipient_import_mapping_profiles.py @@ -16,10 +16,16 @@ branch_labels = None depends_on = None +def _scope_fk_target(inspector) -> str: + tables = set(inspector.get_table_names()) + return "core_scopes.id" if "core_scopes" in tables else "tenancy_tenants.id" + + def upgrade() -> None: inspector = sa.inspect(op.get_bind()) if "campaign_recipient_import_mapping_profiles" in inspector.get_table_names(): return + scope_fk_target = _scope_fk_target(inspector) op.create_table( "campaign_recipient_import_mapping_profiles", sa.Column("id", sa.String(length=36), nullable=False), @@ -39,7 +45,7 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(["owner_user_id"], ["access_users.id"], name=op.f("fk_campaign_recipient_import_mapping_profiles_owner_user_id_users"), ondelete="CASCADE"), - sa.ForeignKeyConstraint(["tenant_id"], ["tenancy_tenants.id"], name=op.f("fk_campaign_recipient_import_mapping_profiles_tenant_id_tenants"), ondelete="CASCADE"), + sa.ForeignKeyConstraint(["tenant_id"], [scope_fk_target], name=op.f("fk_campaign_recipient_import_mapping_profiles_tenant_id_scopes"), ondelete="CASCADE"), sa.PrimaryKeyConstraint("id", name=op.f("pk_campaign_recipient_import_mapping_profiles")), ) op.create_index(op.f("ix_campaign_recipient_import_mapping_profiles_tenant_id"), "campaign_recipient_import_mapping_profiles", ["tenant_id"], unique=False) diff --git a/src/govoplan_campaign/backend/router.py b/src/govoplan_campaign/backend/router.py index 029d9b8..5c780b1 100644 --- a/src/govoplan_campaign/backend/router.py +++ b/src/govoplan_campaign/backend/router.py @@ -44,7 +44,7 @@ from govoplan_campaign.backend.schemas import ( ReportEmailRequest, ReportEmailResponse, ) -from govoplan_access.auth import ApiPrincipal, has_scope, require_scope +from govoplan_core.auth import ApiPrincipal, has_scope, require_scope from govoplan_core.audit.logging import audit_from_principal from govoplan_core.core.access import CAPABILITY_ACCESS_DIRECTORY, AccessDirectory from govoplan_core.core.change_sequence import ( diff --git a/src/govoplan_campaign/backend/schema_router.py b/src/govoplan_campaign/backend/schema_router.py index 7a4bd5e..241b166 100644 --- a/src/govoplan_campaign/backend/schema_router.py +++ b/src/govoplan_campaign/backend/schema_router.py @@ -4,7 +4,7 @@ from typing import Any from fastapi import APIRouter, Depends -from govoplan_access.auth import ApiPrincipal, require_any_scope +from govoplan_core.auth import ApiPrincipal, require_any_scope from govoplan_campaign.backend.campaign.loader import load_campaign_schema, load_campaign_schema_ui