Parent: part of #109 → #100
Summary
Automating translation from Kuadrant AuthPolicy to PPE config is made significantly harder by a mismatch between the two dictionaries used to reference runtime values evaluated through the auth pipeline. AuthPolicy user-defined rules — CEL expressions, OPA/Rego policies, and native gjson pattern-matching — are written against Kuadrant Well-Known Attributes (WKA) (request.*, auth.identity.*, auth.metadata.*, source.*, …), while the PPE evaluates against its Attribute Bags (http.*, subject.*, client.*, claim.*, metadata.*, …). The names and structures differ, so rules that compile cleanly on both sides can misbehave at runtime after a naive move.
This epic tracks the design and implementation of a correct translation strategy for these embedded expressions.
Why this matters for feature parity
Consider an AuthPolicy OPA rule:
opa:
rego: 'allow { object.get(input.request.headers, "x-custom-header", "") == "foo" }'
A naive lift-and-shift into PPE config:
- opa:
module: |
package authz
import rego.v1
default allow := false
allow if { object.get(input.request.headers, "x-custom-header", "") == "foo" } # wrong at runtime
Both modules are syntactically valid and compile, but the PPE version does not behave as intended because input.request.headers does not exist in the PPE — the equivalent is input.http.request_headers. In this particular OPA case the mismatch fails silently (the lookup resolves to undefined). Other policies behave less gently: the same class of mismatch in CEL tends to surface as a more acute error, up to and including a panic, depending on the expression.
Note: this example also happens to use OPA v0 syntax (allow { … }) where the PPE requires v1 (allow if { … }). The OPA version migration is a separate concern and is out of scope for this epic, which focuses solely on the mismatching dictionaries.
The core problem: incompatible dictionaries
This affects OPA/Rego, CEL, and gjson pattern-matching alike. Naive string substitution (sed s/request.headers/http.request_headers/g) is not a solution, because the reference can be indirected:
req := input.request
allow if { object.get(req.headers, "x-custom-header", "") == "foo" }
That leaves two viable approaches:
- Option 1 — Adapter input (preferred). Feed the PPE's OPA/CEL evaluation an
input/environment shaped like the Kuadrant/Authorino object, so request.headers et al. resolve. User rules stay verbatim. This could be gated behind a "compatibility mode" feature flag. Easier and less costly to deliver; the trade-off is a compatibility shim and its runtime overhead, and that it adapts rather than converts. This is the preferred starting point.
- Option 2 — Semantic compiler. Parse each rule (Rego/CEL) into an AST and rewrite whole subtrees with proper semantics — e.g. replace an entire
input.request subtree (including indirected bindings) with input.http — likely in the control plane. Sound and more elegant (produces native PPE config), but considerably harder to achieve.
Beyond request.*
request.* ↔ http.* is the easy instance. Harder cases:
auth.identity.* ↔ subject.* / client.* / claim.*, and auth.metadata.* ↔ metadata.*, where User-Defined Types (UDT) and their inheritance make the mapping non-mechanical.
- Namespace collision: PPE's own
request.* bag denotes environment/trace metadata (request.request_id, request.trace_id, …), not the HTTP request — a source of subtle mistranslation.
Forward look (non-blocking)
This will likely intersect praxis-proxy/praxis#1232 ("small, core type system" for Praxis, @alexsnaps). A natural evolution is expanding that type system to semantic types (HttpRequest, HttpHeader, Metadata, …); the PPE's type system should then be compatible with / inherit from Praxis's, which would give the compiler in Option 2 a principled target.
References
Parent: part of #109 → #100
Summary
Automating translation from Kuadrant
AuthPolicyto PPE config is made significantly harder by a mismatch between the two dictionaries used to reference runtime values evaluated through the auth pipeline. AuthPolicy user-defined rules — CEL expressions, OPA/Rego policies, and native gjson pattern-matching — are written against Kuadrant Well-Known Attributes (WKA) (request.*,auth.identity.*,auth.metadata.*,source.*, …), while the PPE evaluates against its Attribute Bags (http.*,subject.*,client.*,claim.*,metadata.*, …). The names and structures differ, so rules that compile cleanly on both sides can misbehave at runtime after a naive move.This epic tracks the design and implementation of a correct translation strategy for these embedded expressions.
Why this matters for feature parity
Consider an AuthPolicy OPA rule:
A naive lift-and-shift into PPE config:
Both modules are syntactically valid and compile, but the PPE version does not behave as intended because
input.request.headersdoes not exist in the PPE — the equivalent isinput.http.request_headers. In this particular OPA case the mismatch fails silently (the lookup resolves to undefined). Other policies behave less gently: the same class of mismatch in CEL tends to surface as a more acute error, up to and including a panic, depending on the expression.The core problem: incompatible dictionaries
This affects OPA/Rego, CEL, and gjson pattern-matching alike. Naive string substitution (
sed s/request.headers/http.request_headers/g) is not a solution, because the reference can be indirected:That leaves two viable approaches:
input/environment shaped like the Kuadrant/Authorino object, sorequest.headerset al. resolve. User rules stay verbatim. This could be gated behind a "compatibility mode" feature flag. Easier and less costly to deliver; the trade-off is a compatibility shim and its runtime overhead, and that it adapts rather than converts. This is the preferred starting point.input.requestsubtree (including indirected bindings) withinput.http— likely in the control plane. Sound and more elegant (produces native PPE config), but considerably harder to achieve.Beyond
request.*request.*↔http.*is the easy instance. Harder cases:auth.identity.*↔subject.*/client.*/claim.*, andauth.metadata.*↔metadata.*, where User-Defined Types (UDT) and their inheritance make the mapping non-mechanical.request.*bag denotes environment/trace metadata (request.request_id,request.trace_id, …), not the HTTP request — a source of subtle mistranslation.Forward look (non-blocking)
This will likely intersect praxis-proxy/praxis#1232 ("small, core type system" for Praxis, @alexsnaps). A natural evolution is expanding that type system to semantic types (
HttpRequest,HttpHeader,Metadata, …); the PPE's type system should then be compatible with / inherit from Praxis's, which would give the compiler in Option 2 a principled target.References
well_known_attributes.go