feat(postbox): enforce unified inbox separation
This commit is contained in:
@@ -52,6 +52,7 @@ from govoplan_postbox.backend.schemas import (
|
||||
PostboxGroupingItem,
|
||||
PostboxGroupingListResponse,
|
||||
PostboxGroupingPayload,
|
||||
PostboxGroupingPolicyUpdateRequest,
|
||||
PostboxGroupingUpdateRequest,
|
||||
PostboxMaterializeRequest,
|
||||
PostboxMessageItem,
|
||||
@@ -80,6 +81,10 @@ from govoplan_postbox.backend.schemas import (
|
||||
PostboxTemplateReviseRequest,
|
||||
)
|
||||
from govoplan_postbox.backend.service import PostboxError
|
||||
from govoplan_postbox.backend.grouping_policies import (
|
||||
grouping_policy_conflicts,
|
||||
normalize_postbox_grouping_policy,
|
||||
)
|
||||
from govoplan_postbox.backend.protection_profiles import (
|
||||
POSTBOX_PROTECTION_PROFILE_DEFINITIONS,
|
||||
POSTBOX_STANDARD_PROFILE,
|
||||
@@ -145,6 +150,7 @@ def _http_error(exc: PostboxError) -> HTTPException:
|
||||
"template_slug_exists",
|
||||
"address_collision",
|
||||
"idempotency_conflict",
|
||||
"grouping_policy_conflict",
|
||||
}:
|
||||
code = status.HTTP_409_CONFLICT
|
||||
else:
|
||||
@@ -265,6 +271,7 @@ def _template_item(template) -> PostboxTemplateItem:
|
||||
"encryption_profile": revision.encryption_profile,
|
||||
"encryption_vault_id": revision.encryption_vault_id,
|
||||
"protection_policy": dict(revision.history_policy or {}),
|
||||
"grouping_policy": dict(revision.grouping_policy or {}),
|
||||
"history_policy": dict(revision.history_policy or {}),
|
||||
"routing_policy": dict(revision.routing_policy or {}),
|
||||
"retention_policy": dict(revision.retention_policy or {}),
|
||||
@@ -290,6 +297,48 @@ def _grouping_item(
|
||||
if source.postbox_id in visible_ids
|
||||
]
|
||||
counts = counts_by_postbox or {}
|
||||
constraints = []
|
||||
policy_sources = []
|
||||
for source in grouping.sources:
|
||||
if source.postbox_id not in visible_ids:
|
||||
continue
|
||||
settings = (
|
||||
source.postbox.settings
|
||||
if isinstance(source.postbox.settings, Mapping)
|
||||
else {}
|
||||
)
|
||||
policy = normalize_postbox_grouping_policy(
|
||||
settings.get("grouping_policy")
|
||||
if isinstance(settings.get("grouping_policy"), Mapping)
|
||||
else None
|
||||
)
|
||||
policy_sources.append(
|
||||
(
|
||||
source.postbox_id,
|
||||
source.postbox.classification,
|
||||
policy,
|
||||
)
|
||||
)
|
||||
if policy["mode"] == "allow":
|
||||
continue
|
||||
constraints.append(
|
||||
{
|
||||
"code": (
|
||||
"source_requires_separation"
|
||||
if policy["mode"] == "separate"
|
||||
else "classification_separation_required"
|
||||
),
|
||||
"mode": policy["mode"],
|
||||
"postbox_id": source.postbox_id,
|
||||
"reason": policy["reason"],
|
||||
"enforced_by": "postbox_configuration",
|
||||
}
|
||||
)
|
||||
count_source_ids = (
|
||||
[]
|
||||
if grouping_policy_conflicts(policy_sources)
|
||||
else visible_source_ids
|
||||
)
|
||||
return PostboxGroupingItem(
|
||||
id=grouping.id,
|
||||
name=grouping.name,
|
||||
@@ -299,12 +348,13 @@ def _grouping_item(
|
||||
postbox_ids=visible_source_ids,
|
||||
total_count=sum(
|
||||
int(counts.get(postbox_id, {}).get("total", 0))
|
||||
for postbox_id in visible_source_ids
|
||||
for postbox_id in count_source_ids
|
||||
),
|
||||
unread_count=sum(
|
||||
int(counts.get(postbox_id, {}).get("unread", 0))
|
||||
for postbox_id in visible_source_ids
|
||||
for postbox_id in count_source_ids
|
||||
),
|
||||
constraints=constraints,
|
||||
created_at=grouping.created_at,
|
||||
updated_at=grouping.updated_at,
|
||||
)
|
||||
@@ -1033,6 +1083,52 @@ def api_update_postbox_protection_policy(
|
||||
return item
|
||||
|
||||
|
||||
@router.put(
|
||||
"/admin/postboxes/{postbox_id}/grouping-policy",
|
||||
response_model=PostboxDirectoryItem,
|
||||
)
|
||||
def api_update_postbox_grouping_policy(
|
||||
postbox_id: str,
|
||||
payload: PostboxGroupingPolicyUpdateRequest,
|
||||
response: Response,
|
||||
if_match: str | None = Header(default=None, alias="If-Match"),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> PostboxDirectoryItem:
|
||||
_require(principal, BINDING_ADMIN_SCOPE)
|
||||
_require_mutation_precondition(
|
||||
if_match,
|
||||
resource_type="postbox",
|
||||
resource_id=postbox_id,
|
||||
base_revision=payload.base_revision,
|
||||
)
|
||||
try:
|
||||
get_service().update_grouping_policy(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
postbox_id=postbox_id,
|
||||
grouping_policy=payload.grouping_policy.model_dump(),
|
||||
actor_id=principal.account_id,
|
||||
expected_revision=payload.base_revision,
|
||||
)
|
||||
except PostboxError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
except ConcurrencyError as exc:
|
||||
session.rollback()
|
||||
raise _concurrency_http_error(exc) from exc
|
||||
session.commit()
|
||||
item = _directory_item(
|
||||
get_service().resolve_postbox(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
target=PostboxTargetRef(postbox_id=postbox_id),
|
||||
)
|
||||
)
|
||||
_set_etag(response, item.etag)
|
||||
return item
|
||||
|
||||
|
||||
@router.post(
|
||||
"/admin/postboxes/{postbox_id}/protection-transitions",
|
||||
response_model=PostboxProtectionTransitionResponse,
|
||||
|
||||
Reference in New Issue
Block a user