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

393 lines
12 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.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.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",
"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,
),
),
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 "
"implementation persists plaintext but reserves explicit "
"ciphertext, signed-manifest, and key-epoch fields; it does not "
"claim end-to-end encryption until a trusted policy profile is selected."
),
layer="available",
documentation_types=("admin", "user"),
audience=("administrator", "user", "campaign_manager"),
related_modules=(
"identity",
"idm",
"organizations",
"campaigns",
"files",
"notifications",
),
order=35,
),
),
)
def get_manifest() -> ModuleManifest:
return manifest