Skip to main content
Version: 2.0.0

ADR 044: Backend-Side Authorization Rule Enforcement

Status​

Date: 2026-05-20 Accepted

Decision Makers: Architecture Board

Context​

CIVITAS/CORE runs an OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. in front of the API gateway today (the Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization.) — it decides "may this caller invoke this endpoint?" from request metadata and the authenticated user.

A new kind of rule cannot be evaluated by this gateway-side flow. The driving example is Pipeline-Write Datapool-Scope enforcement: a save must be rejected if any DataSource referenced by the Pipeline is not allowed in the DatasetDatasetA data-related element that contains processed data and makes it available for consumption. A Dataset is populated via Pipelines and carries Metadata and access permissions.'s Datapool. The decision needs the IDs the backend will actually persist — for partial updates that is the merge of the request with existing saved state — neither of which is available to the gateway. The check therefore runs at the backend, after entity resolution. Pipeline-Write Datapool-Scope is the first such rule; a small handful more are anticipated near-term. (Evaluating them at the gateway was rejected — see Alternatives.)

The question is where the check is performed at the backend:

  • Option A — One OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization., two callers. The existing Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. is also queried by the backend for these rules.
  • Option B — Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. + Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization.. Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. unchanged; a separate Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. runs alongside the backend and hosts these rules.
  • Option C — No OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization.; enforcement in the backend's service layer. Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. unchanged; the rules are checked directly in Java, in the same service code that performs the save.

Checked Architecture Principles​

  • [partial] Model-centric data flow — all three options decide on the final to-be-saved state.
  • [full] Distributed architecture with unified user experience
  • [full] Modular design
  • [full] Integration capability through defined interfaces
  • [full] Open source as the default
  • [full] Cloud-native architecture
  • [full] Prefer standard solutions over custom development
  • [full] Self-contained deployment
  • [full] Technological consistency
  • [none] Multi-tenancy — out of scope.
  • [partial] Security by design — see per-option analysis.

Option A — One OPA, two callers​

A single OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. instance is queried by both the gateway and the backend. Both calls are mechanically identical — same protocol the gateway uses today, just from a different caller, to a different policy path, with a different payload. The backend's call is a regular OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. call whose input is the final to-be-saved set instead of request metadata.

Implications​

  • Shared failure domain. Endpoint-level rules and the new rules share one process. A regression in either affects both.
  • Backend → OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. crosses the cluster network. The channel needs encryption + network rules to stay private.
  • One additional caller, no additional deployment. The OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. instance is the one that exists today.
  • Shared resources. Saturation in one workload can starve the other.

Option B — Gateway-OPA + Backend-OPA​

Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. unchanged. A separate Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. runs alongside the backend and is consulted by the backend once it knows the final to-be-saved set. The new rules live only in the Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization..

Implications​

  • Independent failure domains. A regression in the new rules cannot take down endpoint-level authz, and vice versa.
  • Backend → OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. channel stays inside the backend's deployment unit. Private by where the Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. runs, not by configuration.
  • Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. starts and stops with the backend. No version drift, atomic rollout.
  • Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. scales with the backend. Per-instance helper.
  • Two OPAsOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. to operate. In practice deployed together with the backend.

Option C — No OPA; enforcement in the backend's service layer​

No additional OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. call. The rule is checked directly in the backend's Java service layer, as part of the same save lifecycle that resolves the entities and validates their status. The Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. continues to handle endpoint-level rules unchanged.

Implications​

  • No additional moving parts. No OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. instance, no policy bundle, no RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. pipeline, no operational footprint beyond the backend itself.
  • Rule lives where the domain model lives. Datapool-Scope is a business invariant on the entities being touched; the check is part of the same service code that loads and saves those entities.
  • Rule changes follow the backend release cadence. No separate policy distribution.
  • No policy-as-code property. The rule is reviewed as Java service code, not as a separate RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. module that an auditor can read in isolation.
  • Reversible. The rule can be ported to RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. and Options A or B adopted later if the surface grows or audit / policy-as-code becomes a hard requirement. The entity boundary the rule reads from does not change.

Comparison​

Decision factorA — One OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization., two callersB — Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. + Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization.C — Service-layer check
Check runs in…Existing Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. processNew Backend-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. processThe backend itself (in-process)
Failure isolation between rule setsNone — shared processYes — separate processesYes — the new rules do not depend on any OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization.
Latency of the checkOne cluster-network hopLoopback inside the PodIn-process, no hop
Operational footprintOne additional caller on an existing instancePer-backend-instance helperNone beyond the backend
Policy-as-code (separate, centrally reviewable)Yes (RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first.)Yes (RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first.)No (Java service code)
Rule release cadenceIndependent of backend deployCo-deployed with backendCo-deployed with backend

Decision​

Option C — enforcement in the backend's service layer, no OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. is adopted for the first such rule (Pipeline-Write Datapool-Scope) and the small handful of similar rules anticipated near-term.

Reasoning​

  • Operational simplicity. No additional OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. instance to deploy, version, or monitor; no policy bundle distribution; no RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. pipeline in the build; no separate decision-log stream to integrate. The rule changes follow the backend release cadence like the rest of its business logic.
  • Rule count too small to justify policy-as-code infrastructure. One rule today, a small handful anticipated near-term. The fixed costs of policy-as-code (RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. authoring, bundle build + distribution, OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. operations, testing tooling) exceed their return at this scale. The rule is small enough to be reviewed in Java alongside the entities it constrains.

Migration option​

The decision is reversible. If the surface of these rules grows substantially, or audit / policy-as-code becomes a hard requirement (e.g. compliance, central policy review), the rules can be ported to RegoRegoOPA's declarative policy language, used to express authorization rules such as checking role assignments at the most specific scope first. and either Option A or Option B adopted later. The data the rules depend on (Dataset.datapool, DataSource.datapoolScope, …) is part of the domain model regardless of where the check runs, so the entity boundary does not change — only the call-site moves.

Alternatives considered​

Beyond the three options above, these wider approaches were considered and rejected:

  • Evaluating the rule at the gateway (either by extending the Gateway-OPAOpen Policy AgentThe platform's authorization service: it evaluates 'who may do what' against the CIVITAS/CORE authorization model. OPA is the platform's central Policy Decision Point (PDP) for API authorization. with body parsing, or by configuring the gateway to forward the body to an external authorization helper). The gateway does not forward the request body today, and the rule needs the post-merge set, which depends on existing saved state the gateway has no access to. Custom plugin work or body-forwarding is non-standard — the gateway vendor's own documentation marks request-body forwarding as not recommended. Body buffering at the gateway would also affect every API request, not only writes.

See also​

  • ADRArchitecture Decision RecordA document capturing an architecture decision. Each ADR has a stable identifier and short title and is managed through four lifecycle states: Proposed, Accepted, Deprecated and Superseded. 040: Separation of Data APIs and Management APIs via Subdomains in CIVITAS/CORE