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

Send a joiner, a mover and a leaver over raw SCIM, and read every answer

Create, find, page, patch, move between groups, deactivate and delete a user against your own tenant, and produce each SCIM error from the lesson's catalogue, including a stale write stopped by If-Match.

ReadyUses your lab tenant

The lesson

Builds on: What a directory holds.

New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.

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. Create a disabled joiner

    Recorded as scim.user.create succeeded for lab-provisioning.

  2. A second user with the same userName is refused

    Recorded as scim.user.create rejected (uniqueness) for lab-provisioning.

  3. A PATCH with one bad operation changes nothing

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

  4. Add the mover to their new group

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

  5. A write based on an old version is refused

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

  6. Delete the leaver's account

    Recorded as scim.user.delete succeeded for lab-provisioning.

Request console

Requests in this lab can be sent from this page to your tenant: open one and choose Send. Fill in the values below first. They stay in this page's memory and are gone when you leave; secrets are never stored or sent anywhere except the request you send.

Setup

The lesson follows Sam Reyes from joiner to leaver. Here a temporary user, lab-tmp-sam, makes the same journey through your tenant.

  1. Open a bash shell and set the variables and helpers from the directory lab, plus READER_ID and READER_SECRET for lab-scim-reader.

  2. Look up the people and group the steps use.

export CORA=$(scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "[email protected]"' | jq -r '.Resources[0].id')
export MOD=$(scim -G "$SCIM/Groups" --data-urlencode 'filter=displayName eq "Photo moderation"' | jq -r '.Resources[0].id')
  1. Press Start on the lab page.

Walkthrough

  1. Create a disabled user. Send the joiner with active false, so the account exists before its start date.

POST$ISSUER/scim/v2/Users Open in console
POST $ISSUER/scim/v2/Users HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: application/scim+json

{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
 "externalId":"E07731","userName":"lab-tmp-sam","name":{"givenName":"Sam","familyName":"Reyes"},"displayName":"Sam Reyes",
 "emails":[{"value":"[email protected]","type":"work","primary":true}],"active":false,
 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"employeeNumber":"E07731","department":"Print support"}}

The answer is 201 Created with a Location ending in the new id, an ETag, and the stored user. Run export SAM=<id>.

Why it matters: this is "Creating a user". The client sends its own key in externalId, the service assigns id, and the client stores that id for every later request.

  1. Uniqueness. Send the same body again with a different externalId. The answer is 409 with scimType: uniqueness. Then find what holds the name: scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "lab-tmp-sam"' --data-urlencode 'attributes=userName,externalId,active' | jq .Resources.

Why it matters: a conflict means an account you did not create may exist. Search for it and let a person decide; never add a digit to the name.

  1. Filter operators. Predict each count, then run it with scim -G "$SCIM/Users" --data-urlencode "filter=<filter>" | jq .totalResults:

    • externalId eq "E07731"

    • displayName co "Reyes"

    • userName sw "lab-tmp-"

    • emails.value ew "@example.com"

    • externalId pr

    • meta.lastModified gt "<today>T00:00:00Z"

    • active eq true and not (externalId pr)

    • emails[type eq "work" and value ew "@example.com"]

Why it matters: this is "Finding users". A search that matches nothing is not an error, and active eq true and not (externalId pr) finds the accounts no client has claimed.

  1. Paging, and searching in the body.

scim -G "$SCIM/Users" --data-urlencode count=5 --data-urlencode startIndex=1 --data-urlencode sortBy=meta.created | jq '{totalResults, startIndex, itemsPerPage}'
scim -G "$SCIM/Users" --data-urlencode count=5 --data-urlencode startIndex=6 --data-urlencode sortBy=meta.created | jq '{totalResults, startIndex, itemsPerPage}'
jq -n '{schemas:["urn:ietf:params:scim:api:messages:2.0:SearchRequest"],filter:"userName eq \"lab-tmp-sam\"",count:5}' \
  | scim -X POST "$SCIM/Users/.search" --data-binary @- | jq .totalResults

Keep raising startIndex until you have seen totalResults.

Why it matters: pages are not a snapshot, so a client keeps going until it has seen every match. /Users/.search keeps filter values out of URLs, which tend to end up in logs.

  1. Move the user with PATCH. Enable Sam and move him to moderation in one atomic request, then replace only his work email.

jq -n --arg ent "$ENT" --arg cora "$CORA" '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[
  {op:"replace",path:"active",value:true},{op:"replace",path:($ent+":department"),value:"Photo moderation"},
  {op:"replace",path:($ent+":manager"),value:{value:$cora}}]}' | scim -i -X PATCH "$SCIM/Users/$SAM" --data-binary @- | grep -i -E '^(HTTP|etag)'
jq -n '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"emails[type eq \"work\"].value",value:"[email protected]"}]}' \
  | scim -X PATCH "$SCIM/Users/$SAM" --data-binary @- | jq '.emails'

The first answer is 200 with a new ETag. The second changes only the work address.

Why it matters: this is "Changing a user". PATCH changes only what it names, so attributes another team added survive. The enterprise path starts with the extension's URN, and a filter in square brackets picks one email out of a list.

  1. Atomic means all or nothing. Send two operations where the second has the misspelled path departmnet.

jq -n '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"title",value:"Moderator"},{op:"replace",path:"departmnet",value:"x"}]}' \
  | scim -X PATCH "$SCIM/Users/$SAM" --data-binary @- | jq .
scim "$SCIM/Users/$SAM" | jq .title

The answer is 400 with scimType: invalidPath, and Sam's title is unchanged: the first operation was not applied either.

  1. Group membership. Add Sam to Photo moderation, send the same add again, then remove him with a filter.

add() { jq -n --arg u "$SAM" '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"add",path:"members",value:[{value:$u}]}]}'; }
add | scim -X PATCH "$SCIM/Groups/$MOD" --data-binary @- -o /dev/null -w '%{http_code}\n'
add | scim -X PATCH "$SCIM/Groups/$MOD" --data-binary @- -o /dev/null -w '%{http_code}\n'
scim "$SCIM/Groups/$MOD" | jq --arg u "$SAM" '[.members[] | select(.value == $u)] | length'

Both adds succeed and Sam is listed once. Remove him with {"op":"remove","path":"members[value eq \"<SAM>\"]"}.

Why it matters: this is "Changing group membership". Adding is safe to repeat, and a filtered remove changes only the named member, never anyone the client does not know about.

  1. Optimistic concurrency. Read Sam and keep his version, let someone else change him, then send a write based on the old version.

export E1=$(scim -i "$SCIM/Users/$SAM" | awk 'tolower($1)=="etag:" {print $2}' | tr -d '\r')

In the portal, change Sam's first name to Samuel. Now replace his title with If-Match: $E1:

jq -n '{schemas:["urn:ietf:params:scim:api:messages:2.0:PatchOp"],Operations:[{op:"replace",path:"title",value:"Moderator"}]}' \
  | scim -X PATCH "$SCIM/Users/$SAM" -H "If-Match: $E1" --data-binary @- | jq .

The answer is 412 with "The resource changed since the version in If-Match. Read it again before retrying." and no scimType. Read Sam again, take the new ETag, and retry: 200.

Why it matters: this is "Conflicts and errors". The right response to 412 is to read again and reapply the intended change, never to drop the header.

  1. The error catalogue. Produce each error and note its status and scimType:

    • invalidFilter: a filter with an unclosed quote

    • invalidPath: a PATCH path with a misspelled attribute

    • noTarget: {"op":"replace","path":"emails[type eq \"home\"].value","value":"[email protected]"} on Sam, who has no home email

    • invalidValue: {"op":"replace","path":"active","value":"no"}

    • mutability: {"op":"replace","path":"id","value":"x"}

    • 401: any request with no token

    • 403: a create with a lab-scim-reader token

  2. Deactivate, then delete. Replace Sam's active with false (200, and Audit records scim.user.lock). Then delete him and read him back.

scim -X DELETE "$SCIM/Users/$SAM" -o /dev/null -w '%{http_code}\n'
scim "$SCIM/Users/$SAM" | jq .

The delete answers 204, and the read answers 404 with "User not found."

Why it matters: this is "Deactivating and deleting". A leaver process sends both, in that order: active false at departure, the delete after the retention period.

Break it

  1. See what If-Match prevents. Create lab-tmp-kim (Kim Sato) the same way as step 1 with a new externalId, read it with -i, and save the body without meta as kim.json. In the portal, change Kim's first name. Now send a PUT built from the stale copy with no If-Match: scim -X PUT "$SCIM/Users/<kim id>" --data-binary @kim.json. It succeeds, and the portal's change is gone without anyone deciding to discard it.

Restore: delete lab-tmp-kim and kim.json.

Check your work

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

  • scim.user.create succeeded by lab-provisioning (step 1)

  • scim.user.create rejected with uniqueness (step 2)

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

  • scim.group.patch succeeded (step 7)

  • scim.user.patch rejected with version_mismatch (step 8)

  • scim.user.delete succeeded (step 10)

In Audit, also find scim.user.unlock and scim.user.lock for Sam, and the rejected entries from step 9.

Cleanup

  1. Confirm lab-tmp-sam and lab-tmp-kim are deleted.

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