Module 3: Shaping data#
You are in the CPEX tutorial. Runs without the IdP (redaction fires for anonymous callers). The full contrast needs it.
Goal: return a different view of the same backend record per caller, by transforming the result on the way out with redact and mask, gated by permission.
The problem#
An HR analyst with clearance should see an employee’s SSN. One without should get the record with the SSN removed. Not a different endpoint, not a second query: the same call with a field stripped. The backend returns the full record, so policy must shape it before it leaves the boundary.
Build it#
The route allows the call, then a result: field pipeline transforms the response. From policies/m03.yaml:
routes:
- tool: get_compensation
authorization:
pre_invocation: []
result:
ssn: "str | redact(!perm.view_ssn)"
salary: "int | redact(!role.hr)"
employee_id: "str | mask(4)"Each entry reads <field>: "<type> | <op>(<when>)". The op runs only when its predicate holds. redact(!perm.view_ssn) means redact when the caller does not have view_ssn. mask(4) always keeps the last four characters. This runs in the Post phase, after the backend returns and before the caller sees the response.
Run it#
Without a token, no permissions are set, so both redactions fire:
cargo run -p cpex-tutorial --example m03_shaping▸ anonymous → get_compensation (result pipeline redacts ssn + salary, masks id)
✓ ALLOWED {"employee_id":"**1001","name":"Alice Okafor","title":"Staff Engineer","salary":"[REDACTED]","ssn":"[REDACTED]"}The call is allowed, so the record still comes back, but ssn and salary are redacted and employee_id is masked. The backend returned all of it. Policy shaped it.
Try it#
- Redact another field. In
examples/tutorial/policies/m03.yaml, add a line underresult:such asname: "str | redact(!perm.view_ssn)", then re-run. Expect:namenow comes back[REDACTED]too. The pipeline transforms exactly the fields you name. - See the full record with identity. The default run is anonymous, so every redaction fires (no caller has
view_ssn). To see the per-permission contrast, give the route an identity, the same way module 2 does. This is a two-file change:- In
examples/tutorial/policies/m03.yaml, add thekeycloakidentity plugin and reference it from the route. Copy the top-levelplugins:block and theauthentication:line frompolicies/m02.yaml(module 3’s policy has no identity plugin on its own, which is why minting a token alone changes nothing). - In
examples/tutorial/examples/m03_shaping.rs, adduse cpex_tutorial::idp;near the other imports and replacelet caller = Caller::anonymous();with:let caller = match idp::mint_token("alice", "alice").await { Ok(t) => Caller::with_token(t), Err(e) => { eprintln!("{e}"); std::process::exit(1); } }; - Start the IdP and re-run. Expect: alice (
view_ssn) now sees the fullssnandsalary, withemployee_idstill masked. Swap"alice"for"dana"andssnis redacted again (she lacksview_ssn) whilesalarystays visible. Same policy, different caller. This is the contrast the capstone runs end to end.
- In
- Change the mask width. Set
employee_id: "str | mask(2)"and re-run. Expect: only the last two characters survive.
Checkpoint#
Was the call allowed or denied?
Where does the redaction happen, before or after the backend?
Go deeper#
Next#
Module 4: Effects & sequencing: compose multiple effects in order, add auditing, and write your own denial codes.