Module 5: Delegating decisions (PDP)#
You are in the CPEX tutorial. This module needs the IdP.
Goal: hand an authorization decision to a policy engine (a PDP) instead of writing it as a list of require() predicates, then let CPEX enforce the engine’s verdict.
The problem#
Some rules mix several inputs at once: “engineers may search internal repos only; security may search anything.” Expressing that as separate require() lines is awkward and easy to get wrong. A rule engine expression states it directly.
Build it#
Declare a PDP once, then reference it from a step. From policies/m05.yaml:
global:
apl:
pdp:
- kind: cel
routes:
- tool: search_repos
authentication:
- keycloak
authorization:
pre_invocation:
- "require(authenticated)"
- cel:
expr: "(has(role.engineer) && role.engineer && args.visibility == 'internal') || (has(role.security) && role.security)"
on_deny:
- "deny('engineers read internal only; security reads any', 'repo.policy_denied')"The cel: step evaluates a boolean expression over the same attributes predicates see: roles and request arguments. true allows, false runs on_deny. Guard optional attributes with has(...), because referencing an unset attribute is an error, and CEL errors fail closed. Cedar is available too (kind: cedar-direct) when you want policy as data rather than an expression.
Run it#
cargo run -p cpex-tutorial --example m05_pdp▸ evan (engineer) → search_repos internal (CEL allows)
✓ ALLOWED { ... internal repos ... }
▸ evan (engineer) → search_repos public (CEL denies: engineers internal-only)
✗ DENIED [repo.policy_denied] engineers read internal only; security reads any
▸ sam (security) → search_repos public (CEL allows: security reads any)
✓ ALLOWED { ... public repos ... }One expression captured a rule that mixes role and argument. The PDP decided, and CPEX enforced.
Try it#
- Loosen the engineer rule. Change the expression to also allow engineers to read public repos. Re-run and confirm evan’s public search now allows.
- Why the guards matter. Remove the
has(role.engineer)guard from the expression and re-run. Expect: sam is still allowed.role.securityis true, and CEL’s||yields true even though the other side now errors on the unsetrole.engineer(CEL absorbs an error when the other operand is true). The guard exists so the expression stays well-defined for every caller: a caller who is neither engineer nor security (anhruser like alice) would hit that evaluation error with nothing to absorb it and be denied fail-closed. - Change the deny code. Edit the
on_denycode and confirm the reason code changes.
Checkpoint#
Who makes the decision, CPEX or the PDP?
Why did an unguarded expression fail closed?
Go deeper#
- PDP Integration for CEL, Cedar, and external engines.
Next#
Module 6: Scoped credentials: mint a downstream-scoped token with a real token exchange.