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

Every way into your tenant's directory, and two subjects in one request

Inventory every path that can change a user, prove each one enforces the same rule, and refuse a token to one client for a user who is allowed through another.

Partly readyUses your lab tenant

The lesson

Builds on: Checking access to each object, 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. Ben's edit of a role holder is refused in the portal

    Recorded as tenant.users.update rejected about [email protected].

  2. Ava gets prints through the printing service

    Recorded as oauth.token succeeded for lab-printer-app about [email protected].

  3. The policy refuses prints for the collage app

    Recorded as oauth.token rejected (policy_denied) for lab-collage about [email protected].

  4. A client that never consults the policy gets prints

    Recorded as oauth.token succeeded for lab-collage about [email protected].

Setup

The tenant's directory has several real entry points, and each enforces authorization where the change happens. A gateway in front of a photo API, row filters and partial evaluation for application data are G3 and G22.

  1. Complete Object and field checks in the tenant's own APIs (Cora holds Auditor, Ben holds Help desk with tenant.users.update, and the SCIM clients exist) and Permit and forbid rules in a token policy.

  2. Make sure lab-collage exists, as in Freshness, step-up for role holders, and SSO across two applications, and assign it the scope prints.create for this lab.

  3. Create lab-tmp-policy (JWT, default key), assign it to both lab-printer-app and lab-collage, and paste this policy with the printer's client ID:

// The tenant's decision about the client, not the person: only the printing service may place print orders.
const PRINTER = '<lab-printer-app client ID>';
if (context.client_id !== PRINTER && context.scopes.includes('prints.create')) return { allow: false, claims: {} };
return { allow: true, claims: {} };
  1. Define helpers for both clients. start prints an authorization URL for a client and waits for the callback; finish redeems the code, sending lab-collage's secret only for lab-collage.

PRINTER_ID="<lab-printer-app client ID>"; COLLAGE_ID="<lab-collage client ID>"
read -rs COLLAGE_SECRET
start() {   # $1 = client ID
  eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
  echo "$ISSUER/oauth/authorize?response_type=code&client_id=$1&redirect_uri=http://127.0.0.1:8765/callback&scope=openid%20photos.read%20prints.create&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
  btl-lab callback
}
finish() {   # $1 = client ID
  read -rsp "Code: " CODE; echo
  if [ "$1" = "$COLLAGE_ID" ]; then AUTH=(-u "$COLLAGE_ID:$COLLAGE_SECRET"); else AUTH=(-d "client_id=$1"); fi
  curl -s "${AUTH[@]}" "$ISSUER/oauth/token" -d grant_type=authorization_code -d "code=$CODE" \
    -d redirect_uri=http://127.0.0.1:8765/callback -d "code_verifier=$VERIFIER" | jq '{scope, error, error_description}'
}

Walkthrough

  1. Inventory every way a user record can change in your tenant, and the identity each path acts as: the portal's Users page (you, a BTL user), $ISSUER/manage (a tenant user with a management role), SCIM /Users with PUT, PATCH and DELETE, SCIM /Bulk (an OAuth client), the HR simulator (its own client, lab-hr-feed, through SCIM), and $ISSUER/account (the user, for their own name only).

Why it matters: a policy protects only the paths that consult it. The lesson's archive job was a path nobody listed.

  1. The same protection on each path. In Ben's window at $ISSUER/manage, edit Cora's name: refused, although Ben holds tenant.users.update. Through SCIM as lab-provisioning, PATCH Cora's active to false: 403 with protected_user. Put the same PATCH inside a /Bulk request: that operation fails with the same error.

Why it matters: the rule lives in the one place every change goes through, so a client or a batch cannot skip it, the structural fix the lesson recommends.

  1. A background job with its own identity. Open Audit and find events from an HR simulator run, or run its lifecycle template briefly. The actor is the lab-hr-feed client. Its SCIM scopes grant user and group changes and nothing else: no role assignment, no Audit, no Logs.

Why it matters: the archive job in the lesson needed its own identity with only what an archive job needs. Rules that forbid everyone apply to it unchanged.

  1. Two subjects. As Ava, run start "$PRINTER_ID", approve in Ava's window, and finish "$PRINTER_ID": prints.create is granted. Then start "$COLLAGE_ID" and finish "$COLLAGE_ID": refused with invalid_grant and "The tenant's access token policy refused to issue this token."

Why it matters: the request is limited by the scope, by Ava, and by the policy for that client. Ava is the same person both times; only the client changed, and the client comes from the registered client ID, never from anything the request claims.

  1. Coarse checks at the edge, fine checks inside. Get a lab-scim-reader token, which has only the read scope, and send a PATCH for Ava, who holds no role. It is refused with 403 for insufficient scope before any user is looked at. Compare it with the protected_user refusal in step 2.

Why it matters: the scope is the early, cheap check; the role-holder rule is the fine check inside. Both run, and neither replaces the other.

Break it

  1. A path that never asks. Unassign lab-tmp-policy from lab-collage, so it falls back to the default access token manager. As Ava, run start "$COLLAGE_ID" and finish "$COLLAGE_ID" again: prints.create is granted.

Why it matters: the rule was right; it was simply never consulted, exactly like the nightly archive job. Policy has to be attached to every path, and an inventory is how you notice one that is not.

Restore: assign lab-tmp-policy to lab-collage again and confirm the request is refused.

Check your work

Press Check my progress. The checks follow Ben's refused edit, the printer's token, the refusal for the collage app and the unprotected path in Break it.

Also confirm by hand:

  • Your inventory table names the identity for every path.

  • SCIM refused Cora's change with protected_user on both /Users and /Bulk, and the read-only client's PATCH with insufficient scope.

Cleanup

  • Remove prints.create from lab-collage.

  • Assign lab-printer-app and lab-collage back to the default access token manager and delete lab-tmp-policy.

Missing infrastructure

  • G3 and G22, enforcement inside a photo API. A gateway in front of the photo API that checks token and scope, a service that makes every object decision itself, a data layer that offers only scoped queries, and a /downloads list driven by a partially evaluated plan with a filter and per-photo checks, so each page is filled with 50 photos the caller may really download.

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