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

IDENTITY GOVERNANCE · LAB

Run birthright access rules against your simulated workforce

Write three access rules, evaluate them against real directory data, apply additions and removals over SCIM, recalculate after a move, hold a mass removal, and keep an exception out of the rule.

Partly readyUses your lab tenant

The lesson

Builds on: Sources of truth.

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. Apply the rules' additions over SCIM

    Recorded as scim.group.patch succeeded for lab-provisioning.

  2. The source moves a person to another department

    Recorded as scim.user.patch succeeded for lab-hr-feed.

  3. The rules recalculate group membership after the move

    Recorded as scim.group.patch succeeded for lab-provisioning.

  4. Management authority cannot ride in on directory data

    Recorded as scim.user.patch rejected (invalid_path) for lab-provisioning.

Setup

Your tenant has no rule engine yet, so you write a small one. It reads attributes the HR Simulator owns and maintains groups through lab-provisioning.

  1. Open a bash shell and set the variables and helpers from the directory lab, plus HR_ID and HR_SECRET (read -rs) and export HR_TOKEN=$(token_for "$HR_ID" "$HR_SECRET" "$SCIM_SCOPE.users $SCIM_SCOPE.groups").

  2. Find two departments that exist in your simulated workforce and name them.

scim -G "$SCIM/Users" --data-urlencode 'filter=externalId sw "HRSIM-" and active eq true' --data-urlencode count=200 \
  --data-urlencode "attributes=$ENT:department" | jq -r ".Resources[].\"$ENT\".department" | sort | uniq -c
export DEPT_A=<first department> DEPT_B=<second department>
  1. In Groups, create lab-tmp-rule-all-staff, lab-tmp-rule-a-staff and lab-tmp-rule-b-staff.

  2. Press Start on the lab page.

Walkthrough

  1. Write the rules. Each rule is a condition on attributes and the group it maintains. R1 covers every active simulated employee; R2 and R3 cover one department each.

jq -n --arg ent "$ENT" --arg a "$DEPT_A" --arg b "$DEPT_B" '[
  {id:"R1", group:"lab-tmp-rule-all-staff", filter:"active eq true and externalId sw \"HRSIM-\"", agreed:"IT service owner"},
  {id:"R2", group:"lab-tmp-rule-a-staff", filter:("active eq true and " + $ent + ":department eq \"" + $a + "\""), agreed:"Head of the department"},
  {id:"R3", group:"lab-tmp-rule-b-staff", filter:("active eq true and " + $ent + ":department eq \"" + $b + "\""), agreed:"Head of the department"}]' > rules.json
echo '[]' > exceptions.json

Why it matters: this is "Writing an access rule". Each rule records who agreed to it, so "why does this person have it?" is answered by a rule, its owners and the attributes that matched.

  1. Evaluate and apply. Define the engine. For each rule it computes who should be in the group, compares that with the members, keeps anyone covered by an unexpired exception, holds any run that would remove more than a quarter of a group, and sends one PATCH with the differences.

apply_rules() {
  local now; now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  jq -c '.[]' rules.json | while read -r rule; do
    local group filter gid want keep have adds removes total nrem ops
    group=$(jq -r .group <<<"$rule"); filter=$(jq -r .filter <<<"$rule")
    gid=$(scim -G "$SCIM/Groups" --data-urlencode "filter=displayName eq \"$group\"" | jq -r '.Resources[0].id')
    want=$(scim -G "$SCIM/Users" --data-urlencode "filter=$filter" --data-urlencode count=200 --data-urlencode attributes=id | jq -r '.Resources[].id' | sort)
    keep=$(jq -r --arg g "$group" --arg t "$now" '.[] | select(.group == $g and .ends > $t) | .user' exceptions.json | sort)
    have=$(scim "$SCIM/Groups/$gid" | jq -r '.members[]?.value' | sort)
    adds=$(comm -13 <(echo "$have") <(echo "$want") | grep .)
    removes=$(comm -23 <(echo "$have") <(printf '%s\n%s\n' "$want" "$keep" | sort -u) | grep .)
    total=$(echo "$have" | grep -c .); nrem=$(echo "$removes" | grep -c .)
    echo "$(jq -r .id <<<"$rule") $group: $(echo "$adds" | grep -c .) to add, $nrem to remove"
    if [ "$nrem" -gt 0 ] && [ $((nrem * 4)) -gt "$total" ] && [ "$1" != "--confirm" ]; then echo "  held: removing $nrem of $total members needs --confirm"; continue; fi
    ops=$(jq -n --arg a "$adds" --arg r "$removes" '[($a | split("\n") | map(select(length > 0)) | if length > 0 then {op:"add",path:"members",value:map({value:.})} else empty end),
      ($r | split("\n") | map(select(length > 0)) | .[] | {op:"remove",path:("members[value eq \"" + . + "\"]")})]')
    [ "$(jq length <<<"$ops")" -gt 0 ] && jq -n --argjson ops "$ops" '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:$ops}' \
      | scim -X PATCH "$SCIM/Groups/$gid" --data-binary @- -o /dev/null -w "  applied: %{http_code}\n"
  done
}
apply_rules

Each rule reports its additions and sends them. Audit shows scim.group.patch by lab-provisioning with the number of members added. Run apply_rules again: nothing to add or remove.

Why it matters: this is "The same start for everyone in a job". Everyone in a department gets the same baseline, whatever access a colleague happened to collect.

  1. Recalculate on a move, with a handover exception. Pick one person in $DEPT_A as P. Before HR moves them, record a two-week handover exception instead of editing R2.

export P=<id of one person in DEPT_A>
jq -n --arg u "$P" --arg c "$(scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "[email protected]"' | jq -r '.Resources[0].id')" \
  --arg end "$(date -u -d '+14 days' +%Y-%m-%dT00:00:00Z)" \
  '[{id:"EXC-1", user:$u, group:"lab-tmp-rule-a-staff", reason:"Handover after the move", owner:$c, ends:$end}]' > exceptions.json
jq -n --arg ent "$ENT" --arg b "$DEPT_B" '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:($ent+":department"),value:$b}]}' \
  | curl -s -X PATCH "$SCIM/Users/$P" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/scim+json" --data-binary @- -o /dev/null -w '%{http_code}\n'
apply_rules

R3 adds P and R2 keeps them, because the exception covers the handover. Now end the handover early: set ends in exceptions.json to yesterday and run apply_rules again. R2 removes P. On macOS, use date -u -v+14d and date -u -v-1d.

Why it matters: this is "When attributes change" and "Exceptions". The same evaluation that adds access removes it, nobody files a ticket, and R2 still describes one department and nothing else.

  1. A fragile condition. Rewrite R2 to match today's job titles instead of the department, then let HR tidy its titles.

TITLES=$(scim -G "$SCIM/Users" --data-urlencode "filter=$ENT:department eq \"$DEPT_A\"" --data-urlencode attributes=title \
  | jq -r '[.Resources[].title] | unique | map("title eq \"" + . + "\"") | join(" or ")')
jq --arg f "active eq true and ($TITLES)" 'map(if .id == "R2" then .filter = $f else . end)' rules.json > rules.tmp && mv rules.tmp rules.json
for u in $(scim -G "$SCIM/Users" --data-urlencode "filter=$ENT:department eq \"$DEPT_A\"" --data-urlencode attributes=id | jq -r '.Resources[].id'); do
  t=$(scim "$SCIM/Users/$u" | jq -r .title)
  jq -n --arg t "$t (full time)" '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"title",value:$t}]}' \
    | curl -s -X PATCH "$SCIM/Users/$u" -H "Authorization: Bearer $HR_TOKEN" -H "Content-Type: application/scim+json" --data-binary @- -o /dev/null; done
apply_rules

R2 now matches nobody, and the run prints held instead of removing the whole group.

Why it matters: like the lesson's "Registered Nurse" tidy-up, the rule depended on a value written for people to read. The guard turned a mass removal into a question for a person.

Restore: rerun the step 1 block, which puts R2 back on the department condition and empties exceptions.json. Run the title loop again with --arg t "${t% (full time)}" in place of --arg t "$t (full time)", then run apply_rules. It reports nothing to add or remove.

  1. What a rule must not grant. Look at Provisioning > Roles for provisioned users: it offers directory roles such as member, never management roles. Then try to give a user management authority through directory data.

jq -n '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"managementRoles",value:["Help desk"]}]}' \
  | scim -X PATCH "$SCIM/Users/$P" --data-binary @- | jq .

The answer is 400 with scimType: invalidPath: there is no such attribute.

Why it matters: this is "What a rule should not grant". Management authority is assigned to named people on Roles and Users, never derived from HR data that a clerk could mistype.

Planned walkthrough

Once G50, G21 and G23 exist, the engine and the exceptions move into the tenant.

  1. In Groups, make lab-tmp-rule-a-staff rule-based with the condition department eq <DEPT_A>, owners recorded on the rule. Membership is computed by the tenant, and Audit names the rule that granted each membership.

  2. Step 3's department change removes P from R2's group in the same moment, with no script run, unless an exception covers them.

  3. A change that would remove more than a set share of a rule's members is held for confirmation in the portal, and the hold is recorded.

  4. Exceptions are tenant records with person, group, reason, owner and end date, and they expire by themselves.

  5. With group claims (G21), an application reading Ava's ID token sees the rule-based groups and can grant from them.

Check your work

Press Check my progress. The checks look for, in order:

  • scim.group.patch succeeded by lab-provisioning (step 2)

  • scim.user.patch succeeded by lab-hr-feed (step 3)

  • scim.group.patch succeeded by lab-provisioning after the move (step 3)

  • scim.user.patch rejected with invalid_path (step 5)

After the Restore, apply_rules reports nothing to add or remove.

Cleanup

  1. Delete the three lab-tmp-rule- groups and rules.json. Keep the apply_rules function in mind; the access review lab writes a smaller version.

  2. Move P back to $DEPT_A as lab-hr-feed if you want the workforce as it was.

Missing infrastructure

  • G50: the tenant has no attribute-based groups. Rules that recalculate on every change, hold large removals for confirmation, and record the granting rule on each membership would replace apply_rules.

  • G21: groups are not issued as token claims, so a rule-maintained group matters to an application only if it reads the directory itself.

  • G23: exceptions with an owner, a reason and an end date are your file, not tenant records, so nothing removes access when they expire unless your script runs.

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