Skip to content

Policy HTTP API

This is the reference for the project-scoped network Policy HTTP surface. It maps each operation to its OpenAPI schema, the per-call ReBAC gate, the audit relation it stamps, the outbox event it emits, and the closed Problem.code taxonomy. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract — for the bounded-context narrative (the aggregate, the rule value objects, and the compile pipeline) see ../../contexts/policy/model.md.

A Policy is an aggregate that owns an ordered list of network rules behind a selector. Every mutation lands a new immutable revision: Create mints the head revision, Update appends a child revision and advances the head, and the revision history is read-only thereafter. A landed revision emits a policy_revision_created outbox event that drives the policy compile pipeline; Delete emits policy_deleted so the compiler purges the deleted Policy's contribution. The dry-run surface evaluates a candidate revision against the live mesh without persisting anything.

Operations

MethodPathOperation IDReBAC gateAudit relationOutbox eventBody cap
GET/v1/projects/{project_id}/policiesListPoliciesproject#read on the parent Project (top-level) + per-row policy#read filterpolicy.list(none)n/a
POST/v1/projects/{project_id}/policiesCreatePolicypolicy#manage AND project#operator on the parent Projectpolicy.createpolicy_revision_created64 KiB
GET/v1/projects/{project_id}/policies/{policy_id}GetPolicypolicy#readpolicy.read(none)n/a
PATCH/v1/projects/{project_id}/policies/{policy_id}UpdatePolicypolicy#manage AND project#operatorpolicy.updatepolicy_revision_created64 KiB
DELETE/v1/projects/{project_id}/policies/{policy_id}DeletePolicypolicy#manage AND project#operatorpolicy.deletepolicy_deletedn/a
GET/v1/projects/{project_id}/policies/{policy_id}/revisionsListPolicyRevisionspolicy#readpolicy.read(none)n/a
GET/v1/projects/{project_id}/policies/{policy_id}/revisions/{revision_id}GetPolicyRevisionpolicy#readpolicy.read(none)n/a
POST/v1/projects/{project_id}/policies/{policy_id}/dry-runDryRunPolicypolicy#editpolicy.dryrun(none)64 KiB
GET/v1/projects/{project_id}/policies/{policy_id}/diffDiffPolicyRevisionspolicy#readpolicy.diff(none)n/a
  • body_cap = 64 KiB is enforced before the JSON decoder runs on the three body-carrying operations (CreatePolicy, UpdatePolicy, DryRunPolicy); an over-cap body surfaces as 413 request_body_too_large.
  • Every handler short-circuits to 501 policies_not_provisioned until the production composition root supplies the policy application service and the ReBAC authorizer.

Authorization model

All nine operations accept the same first-success-wins credential triple as the rest of /v1: a psk_… API token Bearer, an OIDC JWT Bearer, or a plexsphere_session cookie. A missing or unresolved credential is 401 unauthenticated; the ReBAC gate then decides the 403.

  • Read paths (GetPolicy, ListPolicyRevisions, GetPolicyRevision, DiffPolicyRevisions) gate on policy#read against the addressed Policy.
  • ListPolicies gates the request on project#read against the parent Project, then applies a per-row policy#read filter so the page contains only the Policies the caller may see (a missing per-row relation drops the row, never turns the list into a 5xx).
  • Mutating paths (CreatePolicy, UpdatePolicy, DeletePolicy) run a dual gate: policy#manage AND project#operator. At create time the new Policy has no id yet, so policy#manage is checked against the parent Project.
  • DryRunPolicy gates on policy#edit — a strictly weaker permission than policy#manage, so an editor can preview a candidate revision without holding the manage relation that lets them commit it.

Every gate decision — granted or denied — and every body-shape rejection emits an audit row through the audit sink, stamped with the operation's audit relation (policy.create, policy.read, policy.list, policy.update, policy.delete, policy.dryrun, policy.diff).

Path & query parameters

OperationParameterTypeRequiredNotes
every operationproject_id (path)string (uuid)yesOwning Project. Malformed → 400 invalid_project_id.
every /{policy_id} operationpolicy_id (path)string (uuid)yesPolicy identifier. Malformed → 400 invalid_policy_id.
GetPolicyRevisionrevision_id (path)string (uuid)yesRevision identifier. Malformed → 400 invalid_revision_id.
ListPolicies / ListPolicyRevisionscursor (query)stringnoContinuation token from a prior page's next_cursor; malformed → 400 invalid_cursor.
ListPolicies / ListPolicyRevisionslimit (query)integernoPage size; out-of-range → 400 invalid_limit.
DiffPolicyRevisionsfrom_revision (query)string (uuid)yesBase revision of the diff.
DiffPolicyRevisionsto_revision (query)string (uuid)yesTarget revision of the diff.

Schemas

The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:

  • Policy: Policy (id, project_id, slug, display_name, head_revision_id, created_at, updated_at, and — on GetPolicy — an embedded head revision; list rows omit head).
  • Revision: PolicyRevision (id, policy_id, optional parent_id, optional correlation_id, created_at, created_by, selector, rules).
  • Selector: PolicySelector (source, destination) — both label-selector expressions; an empty or unparseable side is 400 selector_syntax_error.
  • Rule: PolicyRule (actionallow / deny / log; protocoltcp / udp / icmp / any; source_cidr; destination_cidr; optional ports PolicyPortRange {from, to}, omitted for icmp / any).
  • Requests: PolicyCreateRequest (slug, optional display_name, selector, rules), PolicyUpdateRequest (optional display_name, selector, rules, and an optional expected_revision_id for optimistic-concurrency control), PolicyDryRunRequest (selector, rules).
  • Responses: PolicyListResponse / PolicyRevisionListResponse (items + optional next_cursor), PolicyDiff (added, removed, modified — each modified entry a PolicyDiffPair of before / after), PolicyDryRunResponse (matched_node_ids, rule_diff, peer_pairs_affected, unreachable_node_ids).

Revisions, dry-run, and diff

  • Create / Update land revisions. CreatePolicy mints the head revision; UpdatePolicy appends a child revision (its parent_id pointing at the prior head) and advances head_revision_id. An UpdatePolicy body carrying expected_revision_id is rejected with 409 expected_revision_mismatch when a concurrent edit has already advanced the head; an UpdatePolicy that changes nothing is 400 empty_patch.
  • History is read-only. ListPolicyRevisions pages the revision chain newest-first; GetPolicyRevision fetches a single historical revision. Neither mutates.
  • Dry-run is non-persisting. DryRunPolicy evaluates a candidate selector + rules against the live mesh and returns the matched_node_ids, the rule_diff versus the current head, the peer_pairs_affected, and any unreachable_node_ids — writing no rows and emitting no outbox event.
  • Diff compares two revisions. DiffPolicyRevisions computes the canonical rule diff between from_revision and to_revision, returning added / removed / modified rules.

Error taxonomy

All error responses use the shared Problem envelope (application/problem+json); the 403 path uses the PermissionDenied shape with reason = insufficient_relation.

CodeStatusWhereMeaning
invalid_project_id400every operationMalformed Project UUID.
invalid_policy_id400every /{policy_id} operationMalformed Policy UUID.
invalid_revision_id400GetPolicyRevisionMalformed revision UUID.
invalid_cursor400ListPolicies / ListPolicyRevisionscursor is not a valid continuation token.
invalid_limit400ListPolicies / ListPolicyRevisionslimit is out of range.
invalid_body400CreatePolicy / UpdatePolicy / DryRunPolicyBody cannot be decoded as the operation's request envelope.
invalid_policy400CreatePolicyThe Policy envelope failed a top-level shape check.
empty_patch400UpdatePolicyThe patch changes nothing.
selector_syntax_error400mutating + dry-runselector source or destination is empty or unparseable.
unauthenticated401every operationMissing or unresolved credential.
insufficient_relation403every operationCaller lacks the required ReBAC relation (policy#read / policy#edit / policy#manage / project#operator / project#read).
policy_not_visible404/{policy_id} operationsPolicy not visible to the caller (post-authz; no id oracle).
revision_not_visible404GetPolicyRevision / DiffPolicyRevisionsThe named revision is not visible on the addressed Policy.
revision_conflict409UpdatePolicyA concurrent edit advanced the head before this request landed; refetch and retry.
expected_revision_mismatch409UpdatePolicyThe supplied expected_revision_id no longer matches the head.
policy_slug_taken409CreatePolicyA Policy with this slug already exists in the Project.
invalid_rule422mutating + dry-runThe rule list failed an aggregate invariant.
cidr_family_mismatch422mutating + dry-runA rule's source and destination CIDRs disagree on address family.
port_range_inverted422mutating + dry-runA rule's port range has from greater than to.
unknown_action422mutating + dry-runA rule action is not one of allow / deny / log.
unknown_protocol422mutating + dry-runA rule protocol is not one of tcp / udp / icmp / any.
rule_count_exceeded422mutating + dry-runThe rule list exceeds the per-Policy maximum.
request_body_too_large413CreatePolicy / UpdatePolicy / DryRunPolicyRequest body exceeded the 64 KiB policy envelope cap.
policies_not_provisioned501every operationThe policy application service / authorizer is not wired in this build.
internal500every operationServer-side failure path; the wire body stays generic.

Cross-references

  • ../../contexts/policy/model.md — the Policy aggregate, the rule and selector value objects, and the ubiquitous language.
  • ../../contexts/policy/compiler.md — the compile pipeline a landed revision drives.
  • ../../contexts/policy/events.md — the policy_revision_created / policy_deleted outbox events and the compile-consumer that recompiles or purges on each.
  • ./projects.md — the parent Project surface; the project#read and project#operator relations gate Policy access.
  • ./index.md — the HTTP API surface map.
  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the ListPolicies / CreatePolicy / GetPolicy / UpdatePolicy / DeletePolicy / ListPolicyRevisions / GetPolicyRevision / DryRunPolicy / DiffPolicyRevisions operations and their Policy / PolicyRevision / PolicyRule / PolicySelector / PolicyDiff / PolicyCreateRequest / PolicyUpdateRequest / PolicyDryRunRequest / PolicyListResponse / PolicyRevisionListResponse / PolicyDryRunResponse schemas.