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

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.

Setup

  1. 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.

  2. Look up Ava's ID: export AVA=$(scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "[email protected]"' | jq -r '.Resources[0].id').

  3. 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=dev with 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.

  1. 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.

  1. Compare scopes. Run the same filter based at ou=people,$BASE with -s one and then -s sub. The single level search lists the department units; the whole subtree search also lists every person inside them.

  2. Search by department, and ask for the stable ID. Operational attributes come back only when requested, so entryUUID is 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.

  1. People with no manager. "(&(objectClass=inetOrgPerson)(!(manager=*)))" lists the same people as the SCIM query in Do today step 1.

  2. Direct membership. Base the search on the group's DN with -s base and filter (member=<Ava's DN>). The group comes back only if Ava is a direct member.

  3. Binding rules. An anonymous bind (-x with no -D), a simple bind over plain ldap:// on port 389, and a bind with the service account's DN and an empty password are each refused. Audit records ldap.bind rejected with a reason for each.

  4. 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.

  1. 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 filterSCIM 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.

  1. Ask for only what you need. Add --data-urlencode 'attributes=userName,emails' to any search. id and schemas always 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.

  1. 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.

  1. 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-reader with client_credentials and only the scope scim-<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.

  1. 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.

  1. Switch back to the provisioning client: export TOKEN=$(token_for "$CLIENT_ID" "$CLIENT_SECRET").

Break it

  1. Send a filter with an unclosed quote: scim -G "$SCIM/Users" --data-urlencode 'filter=userName eq "ava' | jq .. The answer is 400 with scimType: invalidFilter and "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.search summaries for lab-scim-reader, one of them counting every user in the tenant (Do today step 4)

  • scim.users.search summaries for the safe lookups (step 5)

  • scim.users.search rejected with invalid_filter (Break it)

  • tenant.oauth.clients.create for lab-scim-reader

Cleanup

  1. Keep lab-scim-reader and its secret. Later labs use it as the read-only directory client.

  2. Make sure TOKEN belongs to lab-provisioning again.

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 while entryUUID and the SCIM id stay the same, and can see each refused bind recorded with its reason.

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