Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

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.

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.

  1. Get a token whose trace shows the deciding rules

    Recorded as oauth.token succeeded for lab-printer-app.

  2. Switch the combining approach to first applicable

    Recorded as tenant.oauth.managers.update succeeded.

  3. Run the manager's built-in test

    Recorded as tenant.oauth.managers.test succeeded.

  4. Restore deny overrides

    Recorded as tenant.oauth.managers.update succeeded.

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.

  1. Complete Permit and forbid rules in a token policy for lab-printer-app, the helpers and Mia's verified address.

  2. Create lab-tmp-policy again (JWT, default key), assign it to lab-printer-app only, and paste this policy. Use Mia's address, and set EMBARGO_UNTIL to 10 minutes from now with date -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

  1. Before the embargo, as Mia, run authorize "openid photos.read prints.create" and redeem, then btl-lab decode "$TOKEN". policy_trace shows editor-print matched and embargo matched, and prints.create is 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.

  1. As Ava, the same request: editor-print not matched, embargo matched, refused. After the embargo, as Mia again: editor-print matched, embargo not matched, prints.create granted, 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.

  1. Change the combining approach. Replace the two lines that compute forbid and permit and the if after 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.

  1. 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

  1. A rule that cannot be evaluated. Change the embargo line to const EMBARGO_UNTIL = undefined; and request as Mia. The embargo rule records error, and because an error counts as a forbid, prints.create is refused. Now change the forbid test to item.matched === true and 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_trace for each case, and one with obligations.

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-app back to the default access token manager and delete lab-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_access cannot 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.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab