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.
- G50 Access rules engine (attribute-based groups that recalculate, with a mass-removal guard)
- G21 Group and custom-attribute token claims; directory roles fixed to `member`
- G23 Access requests and approvals, access reviews, JIT or temporary access
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.
Apply the rules' additions over SCIM
Recorded as
scim.group.patchsucceeded forlab-provisioning.The source moves a person to another department
Recorded as
scim.user.patchsucceeded forlab-hr-feed.The rules recalculate group membership after the move
Recorded as
scim.group.patchsucceeded forlab-provisioning.Management authority cannot ride in on directory data
Recorded as
scim.user.patchrejected (invalid_path) forlab-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.
Open a bash shell and set the variables and helpers from the directory lab, plus
HR_IDandHR_SECRET(read -rs) andexport HR_TOKEN=$(token_for "$HR_ID" "$HR_SECRET" "$SCIM_SCOPE.users $SCIM_SCOPE.groups").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>
In Groups, create
lab-tmp-rule-all-staff,lab-tmp-rule-a-staffandlab-tmp-rule-b-staff.Press Start on the lab page.
Walkthrough
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.
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.
Recalculate on a move, with a handover exception. Pick one person in
$DEPT_AasP. 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.
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.
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.
In Groups, make
lab-tmp-rule-a-staffrule-based with the conditiondepartment eq <DEPT_A>, owners recorded on the rule. Membership is computed by the tenant, and Audit names the rule that granted each membership.Step 3's department change removes
Pfrom R2's group in the same moment, with no script run, unless an exception covers them.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.
Exceptions are tenant records with person, group, reason, owner and end date, and they expire by themselves.
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.patchsucceeded bylab-provisioning(step 2)scim.user.patchsucceeded bylab-hr-feed(step 3)scim.group.patchsucceeded bylab-provisioningafter the move (step 3)scim.user.patchrejected withinvalid_path(step 5)
After the Restore, apply_rules reports nothing to add or remove.
Cleanup
Delete the three
lab-tmp-rule-groups andrules.json. Keep theapply_rulesfunction in mind; the access review lab writes a smaller version.Move
Pback to$DEPT_Aaslab-hr-feedif 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.