chore: consolidate platform split checks
This commit is contained in:
105
docs/POLICY_CONTRACTS.md
Normal file
105
docs/POLICY_CONTRACTS.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# GovOPlaN Policy Contracts
|
||||
|
||||
GovOPlaN has several policy families that are moving out of core into owning
|
||||
modules. The shared kernel contract keeps their decision and provenance shape
|
||||
consistent while each module still owns its domain rules.
|
||||
|
||||
## Current Policy Inventory
|
||||
|
||||
| Policy area | Current owner | Runtime surface | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Privacy retention | `govoplan-policy` routes with compatibility helpers in core | `/api/v1/admin/privacy-retention/policies/{scope}` and `/explain` | System, tenant, user, group, and campaign sources merge into the effective retention policy. Parent locks block lower-level widening. |
|
||||
| Mail profile policy | `govoplan-mail` | `/api/v1/mail/policies/{scope}` | Uses the same source-step path format for system, tenant, owner, and campaign provenance. |
|
||||
| RBAC/access policy | `govoplan-access` | access capabilities in `govoplan_core.core.access` | Permission decisions should use access capability contracts. Explain responses should adopt `PolicyDecision` when an API-level explanation is added. |
|
||||
| Governance defaults | `govoplan-admin` plus `govoplan-access` materializer | admin settings, governance template routes, access materialization capability | System governance can block tenant-local groups, roles, and API keys. |
|
||||
| Delegation and ownership policy | access/campaign/mail/files modules | capability checks and owner-scoped APIs | Source provenance should use this contract when policies become externally explainable. |
|
||||
|
||||
## Policy Decision
|
||||
|
||||
The shared DTO lives in `govoplan_core.core.policy.PolicyDecision`.
|
||||
|
||||
```json
|
||||
{
|
||||
"allowed": false,
|
||||
"reason": "Parent retention policy locks lower-level changes.",
|
||||
"source_path": [
|
||||
{
|
||||
"scope_type": "system",
|
||||
"scope_id": null,
|
||||
"path": "system",
|
||||
"label": "System",
|
||||
"applied_fields": ["allow_lower_level_limits"],
|
||||
"policy": {}
|
||||
}
|
||||
],
|
||||
"requirements": ["raw_campaign_json_retention_days"],
|
||||
"details": {
|
||||
"blocked_fields": ["raw_campaign_json_retention_days"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`allowed` is the effective answer for the checked action. `reason` is a stable,
|
||||
human-readable summary. `source_path` lists the policy sources that explain the
|
||||
answer. `requirements` lists machine-readable blockers or prerequisites, and
|
||||
`details` carries domain-specific structured context.
|
||||
|
||||
Every source step should be concrete enough for an operator to understand the
|
||||
decision without knowing internal merge rules. Use real scope labels such as
|
||||
`System`, `Tenant`, `Owner user`, `Group`, or a campaign/profile name. Include
|
||||
the stable `path`, the fields applied by that step, and the local policy
|
||||
fragment that caused them. This lets UIs render explanations like
|
||||
`System: Allow > Tenant: Deny without override` without additional lookups.
|
||||
If a policy family cannot expose the full local fragment for security reasons,
|
||||
it must still include a redacted structured value that identifies the applied
|
||||
field and the effective allow/deny or lock state.
|
||||
|
||||
## Source Path Format
|
||||
|
||||
Policy source paths are stable string identifiers for provenance steps:
|
||||
|
||||
- `system`
|
||||
- `<scope_type>:<url-encoded-scope-id>`
|
||||
|
||||
Supported scope types are `system`, `tenant`, `user`, `group`, and `campaign`.
|
||||
Examples:
|
||||
|
||||
- `tenant:4a45b4fe-1d86-43ce-9d10-6022333f4d4b`
|
||||
- `campaign:campaign%2Fwith%20space`
|
||||
|
||||
Use `policy_source_path()` and `parse_policy_source_path()` instead of building
|
||||
or splitting these strings manually.
|
||||
|
||||
## Retention Explain Endpoint
|
||||
|
||||
`GET /api/v1/admin/privacy-retention/policies/{scope_type}/explain` returns:
|
||||
|
||||
- `scope_type` and optional `scope_id`
|
||||
- `decision`, using the shared `PolicyDecision` shape
|
||||
- `effective_policy`
|
||||
- optional `parent_policy`
|
||||
- `effective_policy_sources`
|
||||
- `parent_policy_sources`
|
||||
- `blocked_fields`
|
||||
|
||||
The endpoint is read-only. Enforcement remains in the existing policy write
|
||||
path. For lower-level scopes, `blocked_fields` is derived from the parent
|
||||
policy's `allow_lower_level_limits`; clients can use it to disable local
|
||||
controls before attempting a write.
|
||||
|
||||
## Frontend Contract
|
||||
|
||||
Policy UIs must:
|
||||
|
||||
- render effective source provenance when `effective_policy_sources` is present
|
||||
- display a field-level path when the source data is shown next to a specific
|
||||
setting, using concrete source labels and stop at the first non-overridable
|
||||
deny/lock
|
||||
- disable local field controls when the parent policy sets that field's
|
||||
lower-level limit to `false`
|
||||
- avoid sending locked fields or re-enable attempts in save payloads
|
||||
- show inherited values separately from local overrides
|
||||
|
||||
The core WebUI helper `privacyRetentionParentAllowsField()` centralizes the
|
||||
field-lock decision used by the retention editor and its lightweight module
|
||||
tests.
|
||||
Reference in New Issue
Block a user