IDENTITY GOVERNANCE · LAB
Search your directory with LDAP filters, and with their SCIM equivalents today
Plan the LDAP binds and searches your tenant will answer, then run the same searches with SCIM filters today, including a lookup that builds a filter from typed input and the escaping that fixes it.
PlannedUses 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.
Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.
- G24 LDAP directory
Setup
Open a bash shell and set the variables and helpers from the directory lab (
ISSUER,SCIM,SCIM_SCOPE,ENT,CLIENT_ID,CLIENT_SECRET,token_for,TOKEN,scim). The simulated workforce from the sources of truth lab gives the searches something to find.Look up Ava's ID:
export AVA=$(scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "[email protected]"' | jq -r '.Resources[0].id').Planned: once G24 exists, User Management > Provisioning (or a new Directory access page) turns on read-only LDAPS for the tenant and creates a service account
uid=svc-lab-reader,ou=services,dc=tenant-<id>,dc=beyondthelogin,dc=devwith a generated password shown once.
Planned walkthrough
These steps run once the tenant has an LDAP interface. They use ldapsearch from the OpenLDAP client tools. -W prompts for the service account's password, so it never appears on the command line.
Bind and read the root. A base object search on the tenant's root entry.
export LDAP_URI=ldaps://ldap.tenant-<id>.beyondthelogin.dev
export BASE=dc=tenant-<id>,dc=beyondthelogin,dc=dev
export BIND_DN=uid=svc-lab-reader,ou=services,$BASE
ldapsearch -H "$LDAP_URI" -D "$BIND_DN" -W -b "$BASE" -s base "(objectClass=*)"
The root entry comes back, and Audit records ldap.bind succeeded for the service account.
Compare scopes. Run the same filter based at
ou=people,$BASEwith-s oneand then-s sub. The single level search lists the department units; the whole subtree search also lists every person inside them.Search by department, and ask for the stable ID. Operational attributes come back only when requested, so
entryUUIDis named explicitly.
ldapsearch -H "$LDAP_URI" -D "$BIND_DN" -W -b "ou=people,$BASE" -s sub \
"(&(objectClass=inetOrgPerson)(departmentNumber=Engineering))" uid cn mail entryUUID
Each entryUUID equals that person's SCIM id. Move one simulated person to Finance with the HR Simulator: their DN changes from ou=engineering to ou=finance, and their entryUUID does not.
People with no manager.
"(&(objectClass=inetOrgPerson)(!(manager=*)))"lists the same people as the SCIM query in Do today step 1.Direct membership. Base the search on the group's DN with
-s baseand filter(member=<Ava's DN>). The group comes back only if Ava is a direct member.Binding rules. An anonymous bind (
-xwith no-D), a simple bind over plainldap://on port 389, and a bind with the service account's DN and an empty password are each refused. Audit recordsldap.bindrejected with a reason for each.Filter escaping. A lab page builds
(&(objectClass=inetOrgPerson)(uid=<typed value>))from a typed user name, with and without RFC 4515 escaping, and shows the filter it sent and the entries returned, so the lesson's*example can be seen against your own tenant.
Do today
The tenant has no LDAP listener yet, but its SCIM API answers the same questions. Every step below is a real request.
Translate the lesson's filters to SCIM. Run each SCIM filter with
scim -G "$SCIM/Users" --data-urlencode "filter=<filter>" | jq .totalResults, predicting the count first.
| LDAP filter | SCIM filter | |
|---|---|---|
([email protected]) | userName eq "[email protected]" | |
(departmentNumber=Engineering) | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department eq "Engineering" | |
(cn=Ad*) | name.givenName sw "Ad" | |
(mail=*) | emails pr | |
| `(\ | (departmentNumber=Sales)(departmentNumber=Finance))` | <ENT>:department eq "Sales" or <ENT>:department eq "Finance" |
(&(objectClass=inetOrgPerson)(!(manager=*))) | not (<ENT>:manager pr) |
Why it matters: this is "Searching". Base and scope have no SCIM equivalent, because SCIM resources are not a tree. That is also why there is no DN to change when someone moves: id plays the part of entryUUID.
Ask for only what you need. Add
--data-urlencode 'attributes=userName,emails'to any search.idandschemasalways come back as well.
Why it matters: like an LDAP search that names its attributes, a narrow request returns less personal data to every application that does not need it.
Check direct membership.
scim -G "$SCIM/Groups" --data-urlencode "filter=displayName eq \"Photo moderation\" and members[value eq \"$AVA\"]" | jq .totalResults
The answer is 1 if Ava is a member, 0 if not, the same question as the lesson's base object search on the group.
A lookup built from typed input, with a read-only client. First give the experiment a credential that can only read. In OAuth > Clients, create a confidential client named
lab-scim-readerwithclient_credentialsand only the scopescim-<id6>.read. Copy the secret once, then switch your token to it.
export READER_ID=<lab-scim-reader client_id>
read -rs READER_SECRET; export READER_SECRET
export TOKEN=$(token_for "$READER_ID" "$READER_SECRET" "$SCIM_SCOPE.read")
lookup_unsafe() { scim -G "$SCIM/Users" --data-urlencode "filter=userName eq \"$1\"" | jq .totalResults; }
lookup_unsafe '[email protected]'
lookup_unsafe 'x" or userName pr or userName eq "y'
The first lookup returns 1. The second returns every user in your tenant: the typed text closed the quoted value and added conditions the script's author never wrote. This is the lesson's filter injection in SCIM syntax, run against your own tenant with a client that cannot change anything.
Apply the fix. SCIM filter values are JSON strings, so the escaping function for this syntax is JSON string encoding.
lookup_safe() { scim -G "$SCIM/Users" --data-urlencode "filter=userName eq $(jq -rn --arg v "$1" '$v|@json')" | jq .totalResults; }
lookup_safe '[email protected]'
lookup_safe 'x" or userName pr or userName eq "y'
Ava's lookup still returns 1. The typed text now returns 0, because it is compared as one literal user name.
Why it matters: this is "LDAP and modern applications". The lesson's * and )( tricks are the same mistake in a different syntax. The fix is the escaping function for that syntax, plus a service account that can only read what the application needs.
Switch back to the provisioning client:
export TOKEN=$(token_for "$CLIENT_ID" "$CLIENT_SECRET").
Break it
Send a filter with an unclosed quote:
scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "ava' | jq .. The answer is400withscimType: invalidFilterand "A quoted value is not closed." A filter the service cannot parse is refused, not guessed at.
Check your work
This lab has no automated checks until its LDAP steps exist. In Audit, Source User directory, look for:
scim.users.searchsummaries forlab-scim-reader, one of them counting every user in the tenant (Do today step 4)scim.users.searchsummaries for the safe lookups (step 5)scim.users.searchrejected withinvalid_filter(Break it)tenant.oauth.clients.createforlab-scim-reader
Cleanup
Keep
lab-scim-readerand its secret. Later labs use it as the read-only directory client.Make sure
TOKENbelongs tolab-provisioningagain.
Missing infrastructure
G24: the tenant has no LDAP listener, DN mapping, bind accounts, LDAPS or StartTLS, and no
ldap.*audit events. Once they exist, the Planned walkthrough runs against the same users the SCIM labs created, so a learner can watch a DN change on a real mover whileentryUUIDand the SCIMidstay the same, and can see each refused bind recorded with its reason.