feat(pdp-opa): embedded OPA/Rego policy decision point via regorus - #142
Open
araujof wants to merge 7 commits into
Open
feat(pdp-opa): embedded OPA/Rego policy decision point via regorus#142araujof wants to merge 7 commits into
araujof wants to merge 7 commits into
Conversation
…ping Add the cpex-pdp-opa crate backed by the pure-Rust regorus interpreter and wire it into the workspace. regorus is pinned with default-features disabled and an explicit builtin allow-list that excludes http, net, opa-runtime, and jsonschema, keeping the embedded policy surface free of network egress and runtime introspection. Implements the AttributeBag -> Rego input mapping (flat dotted keys to a nested JSON document, StringSet to sorted array, whole-number floats to integers, namespace-wins on leaf/namespace collisions), plus a contract test pinning the regorus API the resolver depends on.
Add config parsing that prepares a base regorus engine once from global Rego modules and data documents, failing at load on malformed policy or config. The resolver clones that base per request, sets the request input, and evaluates the configured query. The decision contract maps a boolean, a decision object (allow/deny read from a configurable field, with reason and violations flowing into the deny), or a set/array (deny-set idiom: empty allows, non-empty denies) to an allow/deny decision. An undefined result is a clean deny independent of on_error; genuine eval errors and non-decision values route through on_error (default deny); malformed Rego always denies. Inline modules are compiled once into a bounded, never-evicting cache. Register the opa PDP behind an optional feature in cpex-builtins, on by default alongside cedar and cel.
Add an integration test that drives config through the apl-cpex visitor: an opa PDP declared in YAML with Rego modules and data, queried from a route step, producing real allow/deny decisions via the dispatcher. Covers the boolean allow/deny path, the deny-set idiom, external data lookup, load-time rejection of malformed config, and a missing-query author bug surfacing as a clean deny. Extract a merge_data helper so inline data and data files share one JSON normalization and error-mapping path.
- Reject a non-string on_error/decision_field at load instead of silently defaulting, matching the strictness already applied to unknown keys and non-sequence modules. - Document OPA as a shipped default PDP builtin in the dialect table and README (it previously read as recognized-but-unimplemented). - Explain why the cache/coverage regorus features are enabled, matching the rationale given for the excluded network builtins. - Add coverage: a disabled network builtin cannot allow (pins the feature gate), module_files/data_files load-and-evaluate plus a missing data file, the dialect override, and the decision-object errors-key / id-fallback / allow-wins-over-violations paths.
…eval offload Address code-review findings on the embedded policy path: - Reject a route-step inline module whose Rego package collides with a global module's package (fail-closed). Inline modules may add new packages but can no longer merge into and override operator policy — closing an escalation path when route config is authored by a less-trusted party than global policy. - Treat a cache-full inline-module rejection as an always-deny condition rather than routing it through on_error, so a resource limit cannot fail open under on_error: allow. - Run set_input + eval_rule on a blocking thread so a CPU-heavy Rego evaluation cannot monopolize an async worker; a task panic fails closed. Adds a tokio runtime dependency (documented divergence from the inline-eval CEL resolver). - Document the inline-module trust boundary in the crate docs.
…ego PDP Capture the brainstorm requirements doc and the implementation plan (marked completed) that drove this feature.
A spawn_blocking JoinError means the eval task panicked or aborted, which is an abnormal termination rather than a policy outcome. Route it through the always-deny path instead of on_error so a panic cannot fail open under on_error: allow.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Operators standardized on OPA/Rego previously had no in-process option in CPEX: their only choices were Cedar, CEL, or an external OPA server over HTTP. This adds
cpex-pdp-opa, an embedded Rego policy decision point backed by the pure-Rust regorus interpreter, so existing Rego policy runs in-process with no sidecar and no network hop, alongside thecedarandcelbuiltins. It fills thePdpDialect::Opadialect that already existed in apl-core, and ships enabled by default.Closes #137.
Decision contract
A route's
opa: { query: "data.authz.allow" }step evaluates the query against the requestAttributeBag(mapped to the Regoinputdocument). The query result maps to allow/deny:true/falseallow); on deny,reason/messageandviolations/errorsflow into the CPEX violationdeny[msg]idiom)default)This covers the dominant Rego authoring styles (boolean rules, decision objects, and deny-sets) so existing policy ports over without a rewrite.
Design decisions
on_error: deny|allow(default deny) governs genuine eval errors and non-decision values. An undefined result is a clean deny independent ofon_error(matching OPA's undefined-is-not-granted semantics, soon_error: allowcannot flip an ordinary non-match to allow). Rego parse errors, inline/global package collisions, and cache-full rejections always deny regardless ofon_error— an author bug, a trust-boundary violation, and a resource limit must never fail open.set_input/eval_*all take&mut self, so there is no shared-immutable eval path. A baseEngineholds the compiled global modules anddata; because thearcfeatureArc-shares that state, each request clones the base cheaply, sets its owninput, and evaluates — no lock on the hot path. Eval runs onspawn_blockingso a CPU-heavy Rego evaluation cannot monopolize an async worker.default-features = falsewith an explicit allow-list that excludeshttp,net,opa-runtime, andjsonschema, keeping network egress and runtime introspection out of reach of policy authors. A regression test pins thathttp.sendis unavailable.module, but one whose Rego package collides with a global-module package is rejected fail-closed — inline modules may add new packages but cannot merge into and override operator policy.spawn_blocking); apl-cpex/cpex-core are dev-deps for the e2e test. Wiring is a one-feature-one-crate addition tocpex-builtins.Notes
Deny diagnostics serialize the full policy-authored decision object with no size cap. It is latent (no consumer reads
PdpDecision.diagnosticsoutside the PDP crates today) and worth bounding before an audit sink consumes it.Tests
57 unit tests (bag→input mapping, config parsing and strictness, the full decision contract,
on_errorboth ways, compile-vs-runtime asymmetry, inline-module isolation, cache cap, concurrency) plus 6 end-to-end tests driving config through the apl-cpex visitor (allow/deny, deny-set idiom, external data, load-time rejection, missing-query).cargo deny checkpasses with no license/source/ban changes.