Add hierarchical policy simulation helpers
This commit is contained in:
@@ -23,11 +23,16 @@ from govoplan_core.core.access import (
|
||||
AuditRecordRef,
|
||||
AuditRetentionProvider,
|
||||
)
|
||||
from govoplan_core.core.policy import policy_source_step
|
||||
from govoplan_core.core.policy import PolicySourceStep, policy_source_step
|
||||
from govoplan_core.core.runtime import get_registry
|
||||
from govoplan_core.admin.models import SystemSettings
|
||||
from govoplan_core.settings import settings
|
||||
from govoplan_core.tenancy.scope import Tenant
|
||||
from govoplan_policy.backend.hierarchy import (
|
||||
PolicyRestrictionRule,
|
||||
simulate_hierarchical_policy_change,
|
||||
validate_hierarchical_policy_patch,
|
||||
)
|
||||
|
||||
PRIVACY_POLICY_SETTINGS_KEY = "privacy_retention_policy"
|
||||
RETENTION_DAY_KEYS = (
|
||||
@@ -277,26 +282,46 @@ def _merge_privacy_policy(parent: PrivacyRetentionPolicy, patch: dict[str, Any])
|
||||
return PrivacyRetentionPolicy.model_validate(payload)
|
||||
|
||||
|
||||
def _parent_allow_lower_level_limits(parent_payload: dict[str, Any]) -> dict[str, bool]:
|
||||
return {**default_allow_lower_level_limits(), **(parent_payload.get("allow_lower_level_limits") or {})}
|
||||
|
||||
|
||||
def _privacy_restriction_rules() -> tuple[PolicyRestrictionRule, ...]:
|
||||
return (
|
||||
PolicyRestrictionRule(
|
||||
field="store_raw_campaign_json",
|
||||
is_more_restrictive_or_equal=lambda parent_value, requested_value: not (requested_value is True and parent_value is False),
|
||||
less_restrictive_message="Raw campaign JSON storage cannot be re-enabled below a parent policy that disables it.",
|
||||
),
|
||||
*(
|
||||
PolicyRestrictionRule(
|
||||
field=key,
|
||||
is_more_restrictive_or_equal=lambda parent_value, requested_value: parent_value is None or int(requested_value) <= int(parent_value),
|
||||
less_restrictive_message=f"{key} cannot be less restrictive than the parent retention policy.",
|
||||
)
|
||||
for key in RETENTION_DAY_KEYS
|
||||
),
|
||||
PolicyRestrictionRule(
|
||||
field="audit_detail_level",
|
||||
is_more_restrictive_or_equal=lambda parent_value, requested_value: AUDIT_DETAIL_LEVEL_ORDER[str(requested_value)] >= AUDIT_DETAIL_LEVEL_ORDER[str(parent_value or "full")],
|
||||
less_restrictive_message="Audit detail level cannot be less restrictive than the parent retention policy.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _validate_privacy_patch_against_parent(parent: PrivacyRetentionPolicy, patch: dict[str, Any]) -> None:
|
||||
parent_payload = parent.model_dump(mode="json")
|
||||
parent_allow = {**default_allow_lower_level_limits(), **(parent_payload.get("allow_lower_level_limits") or {})}
|
||||
for key in RETENTION_POLICY_FIELD_KEYS:
|
||||
if key in patch and patch.get(key) is not None and not parent_allow[key]:
|
||||
raise PrivacyPolicyError(f"{key} is locked by the parent retention policy.")
|
||||
patch_allow = patch.get("allow_lower_level_limits") or {}
|
||||
for key, allowed in patch_allow.items():
|
||||
if allowed and not parent_allow[key]:
|
||||
raise PrivacyPolicyError(f"{key} limiting cannot be re-enabled below a parent retention policy lock.")
|
||||
if patch.get("store_raw_campaign_json") is True and not parent.store_raw_campaign_json:
|
||||
raise PrivacyPolicyError("Raw campaign JSON storage cannot be re-enabled below a parent policy that disables it.")
|
||||
for key in RETENTION_DAY_KEYS:
|
||||
value = patch.get(key)
|
||||
parent_value = parent_payload.get(key)
|
||||
if value is not None and parent_value is not None and int(value) > int(parent_value):
|
||||
raise PrivacyPolicyError(f"{key} cannot be less restrictive than the parent retention policy.")
|
||||
detail_level = patch.get("audit_detail_level")
|
||||
if detail_level and AUDIT_DETAIL_LEVEL_ORDER[detail_level] < AUDIT_DETAIL_LEVEL_ORDER[parent.audit_detail_level]:
|
||||
raise PrivacyPolicyError("Audit detail level cannot be less restrictive than the parent retention policy.")
|
||||
issues = validate_hierarchical_policy_patch(
|
||||
parent_policy=parent_payload,
|
||||
patch=patch,
|
||||
field_keys=RETENTION_POLICY_FIELD_KEYS,
|
||||
parent_allow_lower_level_limits=_parent_allow_lower_level_limits(parent_payload),
|
||||
restriction_rules=_privacy_restriction_rules(),
|
||||
locked_field_message=lambda key: f"{key} is locked by the parent retention policy.",
|
||||
relock_message=lambda key: f"{key} limiting cannot be re-enabled below a parent retention policy lock.",
|
||||
)
|
||||
if issues:
|
||||
raise PrivacyPolicyError(issues[0].message)
|
||||
|
||||
|
||||
def _set_settings_privacy_policy(settings_payload: dict[str, Any] | None, policy: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -542,6 +567,51 @@ def set_privacy_policy_for_scope(
|
||||
raise PrivacyPolicyError("Privacy policy scope must be system, tenant, user, group or campaign")
|
||||
|
||||
|
||||
def simulate_privacy_policy_change(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
scope_type: str,
|
||||
scope_id: str | None = None,
|
||||
policy: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
clean_scope = scope_type.strip().casefold()
|
||||
patch = PrivacyRetentionPolicyPatch.model_validate(policy or {}).model_dump(mode="json", exclude_none=True)
|
||||
current = get_privacy_policy_for_scope(session, tenant_id=tenant_id, scope_type=clean_scope, scope_id=scope_id)
|
||||
if clean_scope == "system":
|
||||
parent_payload = PrivacyRetentionPolicy.model_validate(current).model_dump(mode="json")
|
||||
parent_allow = default_allow_lower_level_limits()
|
||||
parent_sources = effective_privacy_policy_sources(session)
|
||||
else:
|
||||
parent = parent_privacy_policy(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
scope_type=clean_scope,
|
||||
scope_id=scope_id or (tenant_id if clean_scope == "tenant" else None),
|
||||
)
|
||||
parent_payload = parent.model_dump(mode="json")
|
||||
parent_allow = _parent_allow_lower_level_limits(parent_payload)
|
||||
parent_sources = parent_privacy_policy_sources(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
scope_type=clean_scope,
|
||||
scope_id=scope_id or (tenant_id if clean_scope == "tenant" else None),
|
||||
)
|
||||
|
||||
simulation = simulate_hierarchical_policy_change(
|
||||
parent_policy=parent_payload,
|
||||
current_policy=current,
|
||||
patch=patch,
|
||||
field_keys=RETENTION_POLICY_FIELD_KEYS,
|
||||
parent_allow_lower_level_limits=parent_allow,
|
||||
restriction_rules=() if clean_scope == "system" else _privacy_restriction_rules(),
|
||||
source_path=[PolicySourceStep.from_mapping(source) for source in parent_sources],
|
||||
locked_field_message=lambda key: f"{key} is locked by the parent retention policy.",
|
||||
relock_message=lambda key: f"{key} limiting cannot be re-enabled below a parent retention policy lock.",
|
||||
)
|
||||
return simulation.to_dict()
|
||||
|
||||
|
||||
def sanitize_audit_details_for_policy(session: Session, details: dict[str, Any]) -> dict[str, Any]:
|
||||
policy = privacy_policy_from_session(session)
|
||||
if policy.audit_detail_level == "full":
|
||||
@@ -723,6 +793,9 @@ class SqlPrivacyRetentionService:
|
||||
def set_privacy_policy_for_scope(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return set_privacy_policy_for_scope(*args, **kwargs)
|
||||
|
||||
def simulate_privacy_policy_change(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return simulate_privacy_policy_change(*args, **kwargs)
|
||||
|
||||
def sanitize_audit_details_for_policy(self, *args: Any, **kwargs: Any) -> Any:
|
||||
return sanitize_audit_details_for_policy(*args, **kwargs)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user