AUTHORIZATION AND POLICY · LAB
Rule targets, combining and traces in a token policy
Write named rules with targets, conditions and effects, return a trace with every token, compare deny overrides with first applicable, and see why an unevaluable rule must not count as false.
Partly readyUses your lab tenant
The lesson
Builds on: Writing rules with attributes.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G22 End-user authorization engine (PDP, RBAC/ABAC/ReBAC for application data)
- G49 Token policy test cases with custom contexts, version history and rollback
Your progress
Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.
Sign in to start this lab and check your progress. Log in or create an account.
Get a token whose trace shows the deciding rules
Recorded as
oauth.tokensucceeded forlab-printer-app.Switch the combining approach to first applicable
Recorded as
tenant.oauth.managers.updatesucceeded.Run the manager's built-in test
Recorded as
tenant.oauth.managers.testsucceeded.Restore deny overrides
Recorded as
tenant.oauth.managers.updatesucceeded.
Setup
You can write a small rule engine in the real token policy and read its trace in each token. A policy language with analysis, and an application that carries out obligations, are G22; test cases with contexts you choose are G49.
Complete Permit and forbid rules in a token policy for
lab-printer-app, the helpers and Mia's verified address.Create
lab-tmp-policyagain (JWT, default key), assign it tolab-printer-apponly, and paste this policy. Use Mia's address, and setEMBARGO_UNTILto 10 minutes from now withdate -d '+10 min' +%s.
const POLICY_VERSION = 14;
const now = context.claims.iat;
const EMBARGO_UNTIL = 1790000000;
const EDITORS = ['[email protected]'];
const editor = context.subject.email_verified === true && EDITORS.includes(context.subject.email);
// Each rule has a name, a target scope, an effect, and a condition that is true, false, or null when it cannot be evaluated.
const rules = [
{ name: 'editor-print', target: 'prints.create', effect: 'permit', when: () => editor },
{ name: 'embargo', target: 'prints.create', effect: 'forbid', when: () => typeof EMBARGO_UNTIL === 'number' ? now < EMBARGO_UNTIL : null },
{ name: 'reader', target: 'photos.read', effect: 'permit', when: () => true },
];
const trace = [], granted = [];
for (const scope of context.scopes) {
const applicable = rules.filter(rule => rule.target === scope);
if (!applicable.length) { granted.push(scope); continue; } // scopes this policy does not govern
const results = applicable.map(rule => ({ rule: rule.name, effect: rule.effect, matched: rule.when() }));
results.forEach(item => trace.push(`${scope} ${item.rule} ${item.effect} ${item.matched === null ? 'error' : item.matched ? 'matched' : 'not_matched'}`));
const forbid = results.some(item => item.effect === 'forbid' && item.matched !== false); // an error counts as forbid
const permit = results.some(item => item.effect === 'permit' && item.matched === true);
if (permit && !forbid) granted.push(scope); // deny overrides, default deny
}
return { allow: true, scopes: granted, claims: { policy_version: POLICY_VERSION, policy_trace: trace,
obligations: granted.includes('prints.create') ? ['record_access'] : [] } };
Walkthrough
Before the embargo, as Mia, run
authorize "openid photos.read prints.create"andredeem, thenbtl-lab decode "$TOKEN".policy_traceshowseditor-printmatched andembargomatched, andprints.createis absent.
Why it matters: each rule has a target, conditions and an effect, and the trace shows what decided and what did not cause the refusal. Mia's role was fine; the clock was the problem.
As Ava, the same request:
editor-printnot matched,embargomatched, refused. After the embargo, as Mia again:editor-printmatched,embargonot matched,prints.creategranted, and the token carries"obligations": ["record_access"].
Why it matters: a decision can carry instructions. Something has to carry them out, and an enforcement point that cannot record the access must refuse rather than ignore the obligation.
Change the combining approach. Replace the two lines that compute
forbidandpermitand theifafter them with a first-applicable version, and save:
const first = results.find(item => item.matched === true);
if (first && first.effect === 'permit') granted.push(scope); // first applicable, default deny
Set EMBARGO_UNTIL 10 minutes ahead again and request as Mia: prints.create is granted before the embargo. Now swap the order of editor-print and embargo in rules, save, and request again: refused.
Why it matters: under first applicable, the order of the rules is part of their meaning, so moving a rule changes decisions in a way a reviewer reading one rule will not see.
Open Test in the manager editor and run it. It evaluates the policy against a fixed sample context, not one you choose, so your rules see a request they do not govern. Then restore the deny-overrides lines from the setup and save.
Why it matters: a policy reviewed as code needs tests with contexts you choose, including the requests it should refuse. The built-in test cannot do that yet (G49).
Break it
A rule that cannot be evaluated. Change the embargo line to
const EMBARGO_UNTIL = undefined;and request as Mia. The embargo rule recordserror, and because an error counts as a forbid,prints.createis refused. Now change the forbid test toitem.matched === trueand request again: granted.
Restore: put back item.matched !== false and a numeric EMBARGO_UNTIL, and save.
Why it matters: treating "cannot evaluate" as "does not match" released the print while the embargo time was unknown, the lesson's rights database that did not answer.
Check your work
Press Check my progress. The checks follow a traced token, the change to first applicable, the built-in test and the restored deny overrides.
Also confirm by hand:
Tokens with
policy_tracefor each case, and one withobligations.
Cleanup
The trace and obligations reveal the policy's structure to every token holder, which the lesson keeps in decision records instead. Assign
lab-printer-appback to the default access token manager and deletelab-tmp-policy.
Missing infrastructure
G22, decisions returned to the enforcement point. A decision endpoint that returns permit or deny with the deciding rules, obligations and advice to the photo API, which would refuse a download when
record_accesscannot be carried out, instead of embedding decisions in tokens.G49, token policy test cases and versions. Saved test cases with chosen contexts and expected results, run on every save, and version history with a diff and rollback.