Files
govoplan-postbox/src/govoplan_postbox/backend/manifest.py
T

635 lines
24 KiB
Python

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 (
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleInterfaceRequirement,
ModuleManifest,
NavItem,
PermissionDefinition,
ProductAreaContribution,
QuickAccessTool,
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.search import SearchSourceProviderRegistration
from govoplan_core.core.tasks import WorkItemProviderRegistration
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_postbox.backend.db import models as postbox_models
from govoplan_postbox.backend.search_source import create_postbox_search_source
from govoplan_postbox.backend.permissions import (
ACKNOWLEDGE_SCOPE,
BINDING_ADMIN_SCOPE,
CONFIDENTIAL_SCOPE,
DELIVERY_SCOPE,
READ_SCOPE,
REPLY_SCOPE,
RESTRICTED_SCOPE,
SEND_SCOPE,
TEMPLATE_ADMIN_SCOPE,
)
MODULE_ID = "postbox"
MODULE_NAME = "Postbox"
MODULE_VERSION = "0.1.18"
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 _work_items(context: ModuleContext):
from govoplan_postbox.backend.work_items import PostboxWorkItemProvider
return PostboxWorkItemProvider(registry=context.registry)
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",
"search",
"tasks",
),
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,
),
ModuleInterfaceRequirement(
name="search.source",
version_min="1.0.0",
version_max_exclusive="2.0.0",
optional=True,
),
),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
search_sources=(
SearchSourceProviderRegistration(
id="postbox.messages",
factory=create_postbox_search_source,
),
),
work_item_providers=(
WorkItemProviderRegistration(
id="postbox.unread",
factory=_work_items,
order=40,
),
),
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,
),
ViewSurface(
id="postbox.quick_access.messages",
module_id=MODULE_ID,
kind="quick_access",
label="Postbox Quick Access",
order=35,
),
),
product_areas=(
ProductAreaContribution(
id="communication",
module_id=MODULE_ID,
label="i18n:govoplan-core.product_area.communication",
icon="mail",
description="i18n:govoplan-core.product_area.communication_description",
surface_ids=("postbox.nav.postbox", "postbox.route.postbox"),
order=40,
),
),
quick_access_tools=(
QuickAccessTool(
id="postbox.messages",
module_id=MODULE_ID,
category_id="messages",
label="i18n:govoplan-postbox.postbox",
description="i18n:govoplan-postbox.quick_access_description",
surface_id="postbox.quick_access.messages",
icon="inbox",
full_page_path="/postbox",
required_any=(READ_SCOPE,),
order=20,
modes=("browse", "author"),
),
),
),
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.quick-access-and-product-area",
title="Postbox in Communication and Messages",
summary="Use function-bound Postboxes in Communication and the shared Messages Quick Access drawer.",
body=(
"Postbox contributes its inbox to Communication. With Quick Access enabled, its owner-rendered unread summary "
"appears beside Mail and future chat providers inside Messages while retaining function assignment, classification, "
"read-receipt, retention, encryption, and evidence semantics in Postbox."
),
layer="configured",
documentation_types=("user", "admin"),
audience=("administrator", "user", "campaign_manager"),
related_modules=("quick_access", "views", "mail"),
translations={
"de": {
"title": "Postfach in Kommunikation und Nachrichten",
"summary": "Funktionsgebundene Postfächer in Kommunikation und der gemeinsamen Schnellzugriffseinblendung Nachrichten verwenden.",
"body": (
"Postbox ordnet seinen Eingang Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die vom Modul gerenderte "
"Zusammenfassung ungelesener Nachrichten neben Mail und künftigen Chat-Anbietern unter Nachrichten. "
"Funktionszuordnung, Klassifikation, Lesestatus, Aufbewahrung, Verschlüsselung und Nachweise verbleiben bei Postbox."
),
}
},
metadata={"kind": "reference", "help_contexts": ["postbox.quick_access.messages"]},
order=33,
),
DocumentationTopic(
id="postbox.search.messages",
title="Search authorized Postbox messages",
summary="Expose Postbox subjects and permitted plaintext content to permission-aware platform Search.",
body=(
"When Search is installed, Postbox contributes message subjects, sender labels, and plaintext "
"content only. Ciphertext and key material are never indexed. Every result is tenant-bounded and "
"rechecks current function assignment, Postbox binding, classification, acting context, and generic "
"read authority without recording a message read. Committed deliveries and message changes update "
"the derived index through the durable platform event path."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("administrator", "user", "campaign_manager"),
related_modules=("search", "idm", "encryption"),
order=34,
),
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. 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. "
"When Tasks is enabled, currently readable unread messages also appear "
"in the common work inbox and disappear when the personal read receipt is recorded."
),
layer="available",
documentation_types=("admin", "user"),
audience=("administrator", "user", "campaign_manager"),
related_modules=(
"identity",
"idm",
"organizations",
"campaigns",
"files",
"notifications",
"tasks",
),
links=(
DocumentationLink(
label="Postbox inbox",
href="/postbox",
kind="runtime",
),
DocumentationLink(
label="Postbox directory API",
href="/api/v1/postbox/directory",
kind="api",
),
),
metadata={
"kind": "guide",
"help_contexts": [
"postbox.inbox.directory",
"postbox.inbox.messages",
"postbox.widget.inbox",
"postbox.admin.templates",
"postbox.blocker.assignment",
"postbox.state.unavailable",
],
"privacy_notes": [
"Directory access is derived from current effective function assignments.",
"Unavailable message states reveal only retained audit metadata permitted to the current actor.",
"Subjects, participants, routing facts, and attachment references remain observable metadata.",
],
},
order=35,
),
DocumentationTopic(
id="postbox.reference.fields-and-consequences",
title="Postbox fields and consequences",
summary=(
"Reference for address, template, routing, message, grouping, "
"classification, retention, and lifecycle fields."
),
body=(
"A Postbox address is durable and bound to an organization function. "
"Template revisions are immutable after publication; retiring a template "
"does not remove materialized addresses. Archiving an address stops new "
"delivery while retaining messages and evidence. Unified inbox views are "
"personal projections only and never move or delete source messages. "
"Classification limits eligible delivery and hierarchy-copy targets. "
"Hierarchy copies are independent deliveries with their own evidence; "
"vacancy escalation is delayed and separately auditable. Message expiry "
"or withdrawal blocks future content access but cannot retract plaintext "
"already copied, exported, or printed. Subtree templates select one "
"explicit organization structure and optional relation types. Their "
"read-only impact preview reports generated addresses, current holders, "
"vacancy, collisions, cycles, depth limits, and ambiguous paths without "
"creating templates or Postboxes. Grouping totals include only source "
"Postboxes currently visible to the account."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("administrator", "user", "campaign_manager"),
related_modules=(
"organizations",
"idm",
"policy",
"audit",
"encryption",
),
links=(
DocumentationLink(
label="Postbox administration",
href="/admin?section=postbox",
kind="runtime",
),
DocumentationLink(
label="Postbox concept",
href="docs/POSTBOX_CONCEPT.md",
kind="source",
),
),
metadata={
"kind": "reference",
"help_contexts": [
"postbox.field.address",
"postbox.field.function",
"postbox.field.scope",
"postbox.field.classification",
"postbox.field.retention",
"postbox.field.hierarchy-routing",
"postbox.action.preview-template",
"postbox.field.recipients",
"postbox.action.archive",
"postbox.action.retire-template",
"postbox.action.delete-grouping",
],
"consequence_classes": {
"publish_template": "Freezes an immutable address and routing revision for future materialization.",
"preview_template": "Reads current organization, hierarchy, and incumbency state without materializing any address or Postbox.",
"retire_template": "Stops new revisions and materialization while retaining existing addresses.",
"archive_postbox": "Stops new delivery while retaining messages, receipts, and evidence.",
"delete_grouping": "Deletes only the personal projection; source Postboxes and messages remain unchanged.",
"route_copy": "Creates a separately retained delivery and evidence record at each bounded target.",
"withdraw_or_expire": "Blocks future content access while retaining permitted audit metadata.",
},
},
order=36,
),
),
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