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.identity import CAPABILITY_IDENTITY_DIRECTORY from govoplan_core.core.encryption import CAPABILITY_ENCRYPTION_CONTENT_CIPHER from govoplan_core.core.idm import ( CAPABILITY_IDM_DIRECTORY, CAPABILITY_IDM_FUNCTION_ASSIGNMENTS, ) from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.notifications import CAPABILITY_NOTIFICATIONS_DISPATCH from govoplan_core.core.organizations import ( CAPABILITY_ORGANIZATION_DIRECTORY, CAPABILITY_ORGANIZATION_HIERARCHY_DIRECTORY, ) from govoplan_core.core.postbox import ( CAPABILITY_POSTBOX_ACCESS, CAPABILITY_POSTBOX_DELIVERY, CAPABILITY_POSTBOX_DIRECTORY, CAPABILITY_POSTBOX_EVIDENCE, CAPABILITY_POSTBOX_MESSAGES, CAPABILITY_POSTBOX_ROUTING, ) from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_postbox.backend.db import models as postbox_models MODULE_ID = "postbox" MODULE_NAME = "Postbox" MODULE_VERSION = "0.1.2" READ_SCOPE = "postbox:postbox:read" SEND_SCOPE = "postbox:message:write" REPLY_SCOPE = "postbox:message:reply" ACKNOWLEDGE_SCOPE = "postbox:message:acknowledge" DELIVERY_SCOPE = "postbox:delivery:write" BINDING_ADMIN_SCOPE = "postbox:binding:admin" TEMPLATE_ADMIN_SCOPE = "postbox:template:admin" CONFIDENTIAL_SCOPE = "postbox:classification:confidential" RESTRICTED_SCOPE = "postbox:classification:restricted" def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Postbox", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission( READ_SCOPE, "View assigned postboxes", "Discover and read postboxes for currently effective organization-function assignments.", ), _permission( SEND_SCOPE, "Send through assigned postboxes", "Create new messages in postboxes available through the current function context.", ), _permission( REPLY_SCOPE, "Reply through assigned postboxes", "Reply to messages in postboxes available through the current function context.", ), _permission( ACKNOWLEDGE_SCOPE, "Acknowledge postbox messages", "Record personal read and acknowledgement state for accessible messages.", ), _permission( DELIVERY_SCOPE, "Deliver to postboxes", "Accept idempotent deliveries from approved platform producers.", ), _permission( BINDING_ADMIN_SCOPE, "Administer postbox bindings", "Create, archive, and inspect exact organization-function postboxes and bindings.", ), _permission( TEMPLATE_ADMIN_SCOPE, "Administer postbox templates", "Create, revise, publish, and retire reusable function-scoped postbox templates.", ), _permission( CONFIDENTIAL_SCOPE, "Access confidential Postbox content", "Discover and use confidential Postboxes and messages when function access also permits it.", ), _permission( RESTRICTED_SCOPE, "Access restricted Postbox content", "Discover and use restricted Postboxes and messages when function access also permits it.", ), ) ROLE_TEMPLATES = ( RoleTemplate( slug="postbox_user", name="Postbox user", description="Use postboxes available through current function assignments.", permissions=(READ_SCOPE, SEND_SCOPE, REPLY_SCOPE, ACKNOWLEDGE_SCOPE), default_authenticated=True, ), RoleTemplate( slug="postbox_manager", name="Postbox manager", description="Administer postbox templates and concrete function-bound containers.", permissions=( READ_SCOPE, SEND_SCOPE, REPLY_SCOPE, ACKNOWLEDGE_SCOPE, DELIVERY_SCOPE, BINDING_ADMIN_SCOPE, TEMPLATE_ADMIN_SCOPE, CONFIDENTIAL_SCOPE, RESTRICTED_SCOPE, ), ), ) def _configure(context: ModuleContext): from govoplan_postbox.backend.runtime import configure_runtime, get_service configure_runtime(registry=context.registry) return get_service() def _router(context: ModuleContext): _configure(context) from govoplan_postbox.backend.router import router return router def _tenant_summary(session, tenant_id: str) -> dict[str, int]: return { "postboxes": session.query(postbox_models.Postbox) .filter( postbox_models.Postbox.tenant_id == tenant_id, postbox_models.Postbox.status == "active", ) .count(), "postbox_messages": session.query(postbox_models.PostboxMessage) .filter(postbox_models.PostboxMessage.tenant_id == tenant_id) .count(), "vacant_deliveries": session.query(postbox_models.PostboxDelivery) .filter( postbox_models.PostboxDelivery.tenant_id == tenant_id, postbox_models.PostboxDelivery.status == "accepted_vacant", ) .count(), } _OWNED_TABLES = ( postbox_models.PostboxAccessEvent, postbox_models.PostboxGroupingSource, postbox_models.PostboxGrouping, postbox_models.PostboxMessageReceipt, postbox_models.PostboxRoute, postbox_models.PostboxDelivery, postbox_models.PostboxAttachmentReference, postbox_models.PostboxParticipant, postbox_models.PostboxMessage, postbox_models.PostboxBinding, postbox_models.Postbox, postbox_models.PostboxAddress, postbox_models.PostboxTemplateRevision, postbox_models.PostboxTemplate, ) manifest = ModuleManifest( id=MODULE_ID, name=MODULE_NAME, version=MODULE_VERSION, dependencies=("identity", "organizations", "idm"), optional_dependencies=( "access", "audit", "campaigns", "encryption", "files", "mail", "notifications", "policy", "portal", "views", "workflow_engine", ), required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_IDENTITY_DIRECTORY, CAPABILITY_IDM_DIRECTORY, CAPABILITY_IDM_FUNCTION_ASSIGNMENTS, CAPABILITY_ORGANIZATION_DIRECTORY, CAPABILITY_ORGANIZATION_HIERARCHY_DIRECTORY, ), provides_interfaces=tuple( ModuleInterfaceProvider(name=name, version=MODULE_VERSION) for name in ( CAPABILITY_POSTBOX_DIRECTORY, CAPABILITY_POSTBOX_ACCESS, CAPABILITY_POSTBOX_MESSAGES, CAPABILITY_POSTBOX_DELIVERY, CAPABILITY_POSTBOX_EVIDENCE, CAPABILITY_POSTBOX_ROUTING, ) ), requires_interfaces=( ModuleInterfaceRequirement( name=CAPABILITY_IDM_FUNCTION_ASSIGNMENTS, version_min="0.1.8", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name="organizations.hierarchy_directory", version_min="0.1.0", version_max_exclusive="0.2.0", ), ModuleInterfaceRequirement( name=CAPABILITY_NOTIFICATIONS_DISPATCH, version_min="0.1.8", version_max_exclusive="0.2.0", optional=True, ), ModuleInterfaceRequirement( name=CAPABILITY_ENCRYPTION_CONTENT_CIPHER, version_min="1.0.0", version_max_exclusive="2.0.0", optional=True, ), ), permissions=PERMISSIONS, role_templates=ROLE_TEMPLATES, nav_items=( NavItem( path="/postbox", label="Postbox", icon="inbox", required_any=(READ_SCOPE,), order=58, ), ), frontend=FrontendModule( module_id=MODULE_ID, package_name="@govoplan/postbox-webui", routes=( FrontendRoute( path="/postbox", component="PostboxPage", required_any=(READ_SCOPE,), order=58, ), ), nav_items=( NavItem( path="/postbox", label="Postbox", icon="inbox", required_any=(READ_SCOPE,), order=58, ), ), view_surfaces=( ViewSurface( id="postbox.inbox.directory", module_id=MODULE_ID, kind="section", label="Postbox directory", order=10, ), ViewSurface( id="postbox.inbox.messages", module_id=MODULE_ID, kind="section", label="Postbox messages", order=20, ), ViewSurface( id="postbox.widget.inbox", module_id=MODULE_ID, kind="section", label="Postbox inbox widget", order=25, ), ViewSurface( id="postbox.admin.templates", module_id=MODULE_ID, kind="section", label="Postbox templates and bindings", order=30, ), ), ), route_factory=_router, tenant_summary_providers=(_tenant_summary,), migration_spec=MigrationSpec( module_id=MODULE_ID, metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( *_OWNED_TABLES, label="Postbox", ), retirement_notes=( "Destructive retirement removes Postbox templates, addresses, " "messages, delivery evidence, receipts, groupings, and access events " "after the installer captures a database snapshot." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( postbox_models.Postbox, postbox_models.PostboxMessage, postbox_models.PostboxDelivery, label="Postbox", ), ), capability_factories={ CAPABILITY_POSTBOX_DIRECTORY: _configure, CAPABILITY_POSTBOX_ACCESS: _configure, CAPABILITY_POSTBOX_MESSAGES: _configure, CAPABILITY_POSTBOX_DELIVERY: _configure, CAPABILITY_POSTBOX_EVIDENCE: _configure, CAPABILITY_POSTBOX_ROUTING: _configure, }, documentation=( DocumentationTopic( id="postbox.function-bound-containers", title="Function-bound Postboxes", summary=( "Durable institutional message containers whose access follows " "effective organization-function assignments." ), body=( "Postboxes belong to responsibilities, not individual accounts. " "A stable postbox remains addressable during vacancy and " "reassignment. Current access combines a generic Postbox " "permission with effective IDM assignment context. Templates " "can lazily materialize unit-specific addresses, while exact " "postboxes cover exceptional responsibilities. The first " "Plaintext Postboxes remain available without Encryption. A " "server-envelope profile stores message bodies as ciphertext and " "uses the optional Encryption capability for authorized reads. " "External ciphertext profiles retain producer-managed references " "and keys; neither profile is described as end-to-end encryption." ), layer="available", documentation_types=("admin", "user"), audience=("administrator", "user", "campaign_manager"), related_modules=( "identity", "idm", "organizations", "campaigns", "files", "notifications", ), order=35, ), ), architecture=declared_module_architecture( layer="communication_participation", kind="domain", maturity="vertical_slice", documentation_ref="docs/POSTBOX_CONCEPT.md", test_ref="tests/test_service.py", known_limits=( "Subjects, routing metadata, participants, and attachment references remain plaintext metadata.", "Server-envelope protection is server-decryptable and is not end-to-end encryption.", "External ciphertext profiles require a separately governed producer and client key-custody profile.", ), owned_concepts=("postbox", "postbox address", "postbox message", "delivery receipt", "access event"), non_owned_concepts=("identity", "function assignment", "campaign", "cryptographic key custody"), recovery_docs=("docs/POSTBOX_CONCEPT.md",), security_docs=("docs/POSTBOX_CONCEPT.md",), operations_docs=("README.md",), ), ) def get_manifest() -> ModuleManifest: return manifest