Every provider before this one had somewhere to write a marker. Active Directory has adminDescription. Entra has a directory extension. Okta and Authentik have profile fields. FreeIPA has userclass. PingOne had nothing on a user, so the provider created a custom attribute and wrote the tag there.

OneLogin gives a role a name. That is the whole object as far as identifying it goes. A group, the same. A security policy, the same. A Smart Hook does not even have a name.

Which would be a curiosity if the account were always a lab. It is not. OneLogin sits in a lot of small and mid-sized organisations as the thing everybody signs in through, and the account somebody points a seeding tool at is as likely to be production as a sandbox. A tool that deletes things has to be certain, and “the name starts with ZZ-TEST-” is not certainty.

So the seventh provider proves ownership a different way: by what each object holds.

This is part eight of the series, after the six provider posts that ended with PingOne, which I described at the time as the last one. It was the last one…for almost 24 hours…

What lands in the account

ObjectCount
Custom user fields4, one carrying the seed tag on every seeded person
Users321, of which 21 are hand-designed and 300 generated
Licensed users10, and everyone else Unlicensed or Rejected on purpose
Roles / groups4 / 5, with 14 explicit role memberships and 319 people placed in groups
Security policies2, on three of the five groups
Apps / grants to roles5 / 5
App rules2, one enabled and one disabled
API authorizations2, with 4 scopes, 3 claims and 3 seeded apps allowed to ask
Mappings2, one enabled and one disabled, both gated
Smart Hooks / sign-up profiles1 disabled / 1 disabled
MFA factors3, only where the account already offers them

A seed runs in about two minutes, occasionally up to six, and a teardown in about two. The spread is not load: it is how long OneLogin takes on a given day to show role grants the users step waits for, which has a section of its own below.

The ten licensed users are a deliberate ceiling, not a sample. A trial has twelve user licences with the account owner holding one, so a seed that approved everybody would exhaust the account. Ten Approved people, and the other 311 Unlicensed or Rejected on purpose, means a seed costs at most ten licences whatever the plan.

A role is proved by the people in it

Each type is proved by whatever OneLogin gives it, and for half of them that means contents:

  • A user needs the tag in the custom field the seed created, and the prefix on the username. The account is asked server-side for users whose field holds the tag, and each is checked again locally, ordinally, so a server that matched loosely still cannot hand teardown somebody else.
  • An app, an API authorization server and a sign-up profile need the tag in their description or help text, and the prefix on the name. Either alone is not proof: a tag can be pasted into a real object’s description, and a prefix alone is name matching.
  • A role needs the prefix, no administrators, at least one user or app, and every user it holds to be a proved seeded user and every app a proved seeded app.
  • A group is proved the same way by its members, plus either no security policy or a prefixed one that is not the account’s default.
  • A policy is proved by the groups that use it: prefixed, not the default, used by at least one group and by proved seeded groups alone.
  • A mapping needs the prefix, match all, the seed-tag condition, and only add-role actions naming proved roles.
  • A Smart Hook, which has no name at all, is proved by a marker line the seed writes as the first line of its code, and by conditions naming proved seeded roles, at least one of them.

Two consequences of that look like bugs and are not.

A prefixed role holding one real person is refused, and so is an empty one. Nothing about an empty role says who made it, so the seed never creates a role or group that nobody being seeded will hold. A type that cannot be proved when empty must never be allowed to become empty.

And because roles, groups, policies, mappings, rules and the hook are proved by what they hold or name, teardown has to prove everything before it deletes anything. Delete the people first and every role becomes unprovable in the same instant.

The other side of the proof gets reported, never swallowed. Anything carrying the prefix that failed its check is listed with the reason and left alone, so a teardown that skipped something says what and why.

-Keep follows the same logic and says so out loud: keeping people keeps the field that proves them, keeping roles keeps their people and apps, keeping policies keeps their groups, keeping mappings or the hook keeps the roles they name. A keep that removed the thing its kept object is proved by would leave an object nothing could ever claim again.

The twenty-one

The hand-designed people cover every lifecycle status OneLogin keeps and holds: Active, Suspended, Locked, PasswordExpired, PasswordPending and AwaitingPasswordReset, against the Approved, Unlicensed and Rejected states. The combinations are what make them useful. A suspended sales director who still holds every role, a person whose password has expired while every grant stays in place, a partner sitting at PasswordPending who never completed anything.

There is a service account among them with no manager and no human name, because a directory report that assumes every account belongs to a person gets one that does not. Three hundred and seventeen of the 321 have a manager, so the chains resolve several levels deep and the four without one are deliberate.

Nine are written in Han with an ideographic space, a surname above the basic multilingual plane, Cyrillic, Greek, Arabic, Devanagari, a decomposed José beside the precomposed one, a Turkish dotless i and an eszett. Same nine as the other six providers, from the same shared file, every username plain ASCII.

Each person also carries the directory identifiers a synchronised OneLogin account has: a sAMAccountName, an external id, a user principal name and a distinguished name. A report reconciling OneLogin against Active Directory reads exactly those, so seeding them gives the reconciliation something to match.

The gate the data cannot remove

A mapping in OneLogin is a rule that acts on users as they are created and updated. An enabled one acts on everybody it matches, and in a real account that is everybody.

Every mapping the seed writes therefore carries one condition the data has no say over:

1
2
3
4
5
# The gate. Written on every mapping whatever the data says, with match 'all', so the mapping
# fires only for a user whose seed tag field holds the tag - which nobody but this module
# writes. An enabled mapping in a production account therefore touches seeded people and no
# one else. There is no parameter to leave it out, and a test asserts there is none.
$gate = @{ source = ('custom_attribute_{0}' -f $script:OneLoginSeedAttribute); operator = '='; value = $marker.Tag }

match is all, so every condition has to hold, and one of them is a field nobody but this module writes. The safety is the absence of a parameter, the same shape as the Entra provider’s Conditional Access rule and the Authentik provider’s flows, because a switch is something a hurried afternoon can reach for and a missing switch is not.

A mapping that already exists under a seeded name and does not carry the gate is left alone with a message saying it belongs to somebody else. Reusing it would mean adopting a rule this module cannot vouch for.

The Smart Hook and the sign-up profile are handled the same way. The hook is always disabled and always gated on a seeded role, and a re-run disables it again. The profile is always disabled, moderated, open to the lab email domain only and given no default role or group, and a re-run puts all of that back. Neither state is reachable from the data.

Licences decide who gets approved, and the API does not say so

OneLogin approves a person only while a licence is free. Past that it makes them Unlicensed. The create call answers as though it had approved them either way.

That is the kind of behaviour that produces a seed which reports success and an account that does not match its own data, so the users step reads its people back afterwards and names anyone who came back unlicensed.

Role grants have a related rule that took a live account to pin down. OneLogin accepts a role grant for anybody and keeps it only for an Approved person whose status is Active, Suspended, Locked, PasswordExpired or AwaitingPasswordReset. Grant a role to a Rejected person and the call succeeds and the membership is not there afterwards. The seed data gives roles to those people only, and a test holds it to that, since the data cannot be trusted to stay correct on its own.

A Rejected person is also kept out of every group, not only every role, which is why the rejected partner in the seed is in nothing at all.

Answered, and not applied

The behaviour that shaped the users step more than any other:

1
2
3
4
5
6
7
# OneLogin shows a role grant a few seconds after answering it - ten, measured on a trial. On some
# runs, verified live, it answers 200 and applies nothing: twelve of thirteen grants were still
# absent two minutes later, and on another run grants that had sat unapplied for minutes appeared
# within seconds of the next grant being sent. A role is proved at teardown by the seeded people
# it holds, so an unapplied grant leaves an empty role that teardown, rightly, refuses to claim.
# So the step waits until every grant it sent is visible, sending the missing ones again every
# thirty seconds - adding somebody already in a role changes nothing - for up to four minutes.

Read that against the ownership design and the two halves lock together. A role is proved by its members. A grant that was answered and not applied leaves a role with no members. Teardown then refuses to claim it, correctly, and the account keeps a prefixed role that nothing will ever delete.

The wait is counted in attempts and not against the clock, so it takes the same real time live and runs deterministically under a test that mocks the sleep. Four minutes, because a live seed once waited more than two.

Re-sending a grant is safe, since adding somebody already in a role changes nothing, and the odd detail in that comment is that re-sending appears to prompt the ones already pending. Grants that had sat unapplied for minutes turned up within seconds of the next request going out.

There is a related trap in the same step. roles/{id}/users wants a list, and a one-element array piped through ConvertTo-Json becomes a bare number. The body is written as JSON by hand for that reason.

States OneLogin does not keep

Three lifecycle values in the seed data are not what you would write if you trusted the API to store what you sent. Each was checked against a live account.

Unactivated reads back as PasswordPending moments after creation. Unapproved reads back as Approved. Neither is a state OneLogin keeps, so neither is seeded, because a column that is written and silently ignored is worse than no column.

Locked does not hold when it is sent as a status either. The locked person is therefore created Active and then locked through OneLogin’s lock call, for a year, which holds on a licensed person. A re-run locks them again when less than a month is left, so a long-lived lab does not quietly unlock the one person seeded to be locked.

The verifier checks that person is locked for at least another day, and not a boolean, because the interesting failure is a lock that expired and not one that never happened.

Nothing in it can reach a real person

The seed is built for an account real people sign in to, so identifiers are chosen so that nothing it creates can be mistaken for, or joined to, anything real.

Every sAMAccountName and external id starts with the prefix. Every user principal name and distinguished name is in the lab domain. Every phone number is in the 555-0100 to 555-0199 range reserved for fiction. A tool that joins a directory export on any of those finds the seed and nothing else.

The directory identifiers are there because a synchronised OneLogin account carries them, and a report that reconciles OneLogin against Active Directory reads exactly those fields. Seeding them means the reconciliation has something to match. Prefixing all of them means it can never match the wrong thing.

MFA factors go the same way: enrolled on seeded people only, only as already verified so nothing is ever sent anywhere, and only where the account already offers the factor. A trial offers none, so a trial seed enrols none, and the verifier counts how many people hold a factor without judging the number.

Asking for what the API will not volunteer

A proof is only as good as what you asked for, and OneLogin’s list endpoints leave out most of what these proofs need.

A user listing omits custom fields, roles, manager and directory fields unless they are named. A mapping listing returns only the enabled ones unless the disabled ones are asked for, and the same is true of app rules. Smart Hooks come back without their code, which is where the hook’s only marker lives. Groups come back without their administrators, and “no administrators” is part of what proves a group.

Every one of those is a place where a proof would quietly pass or quietly fail on absent data, judging the listing and not the object. Every user listing therefore names its fields, both mapping queries are made, and hooks and groups are read in detail before anything is proved about them.

Two more from the same family. An enabled mapping runs as each person is created, so mappings are created before people, which is why one seeded role holds a member the data never lists. And role membership settles a few seconds after it is written, so a role proved in the same breath as the seed that filled it may read as empty, and is refused where a guess would do.

App secrets, which OneLogin shows once

OneLogin returns an app’s client secret in the answer to the create and never again. By default the provider keeps the id and drops the secret, because a lab tool holding credentials it does not need is a liability.

1
2
New-TestEnvironment -SaveAppSecret
Get-OneLoginAppCredential -Key expenses

When asked, the two confidential apps’ secrets are written through the same record writer as the connection’s own credential: DPAPI-protected on Windows, or held in a SecretStore vault. The public and native clients and the SAML app have none to keep.

Teardown deletes each saved secret with its app, and any saved secret whose app has left the account, so they do not accumulate. -WhatIf lists them and deletes nothing. -Keep Apps leaves them alone.

An app that already existed has no secret to save, since OneLogin will not show it again, and the seed names that app and warns instead of writing a record that cannot work.

Checking it afterwards

Test-TestEnvironment reads the account through the same ownership proofs teardown uses, which means the verifier and the teardown agree by construction rather than by two implementations happening to match.

It checks every name by codepoint, every lifecycle status and state, every manager, every directory identifier, every group placement, every policy on its groups and every setting inside those policies, every API scope, claim and client, both app rules, the hook, the sign-up profile, and every role membership and app grant the data lists. The locked person is checked to be locked for at least another day.

Two checks are deliberately lopsided. Role memberships are judged on what is missing only, because the enabled mapping adds a person the data never lists there, and reporting the mapping working as a fault would be wrong. And how many people hold an MFA factor is counted without being judged, since the seed enrols one only where the account already offers the factor and a trial offers none.

Repair-TestEnvironment re-runs the steps that own any failed check. Compare-TestEnvironment matches these people against any other connected provider’s by their shared login key, so the same person in this account and in the lab domain can be checked against each other.

Running it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$secret = Read-Host 'Client secret' -AsSecureString
Connect-TestEnvironment -Provider OneLogin -Subdomain contoso `
    -ClientId <client id> -ClientSecret $secret -SaveSecret

New-TestEnvironment -WhatIf
New-TestEnvironment -ShowProgress
Get-TestEnvironmentReport
Test-TestEnvironment
Remove-TestEnvironment -Force

# every run after the first
Connect-TestEnvironment -Provider OneLogin -Subdomain contoso -UseStoredCredential

The credential needs the Manage All scope. Manage Users covers people only, and the seed also creates roles, apps, mappings, policies, hooks and custom fields. Every call goes to the account’s own host, which serves the token and the API in every region, so there is no region to name and -Subdomain takes the bare name, the host or the portal URL. Disconnect-TestEnvironment revokes the token, and an unrevoked one lives ten hours.

Room on the plan is tighter here than elsewhere: four roles, five apps and ten licensed people. A trial has five roles with the Default role among them, five apps, and twelve licences. OneLogin also allows one Smart Hook of each type, so an account that already has a pre-authentication hook gets a reported refusal, and -Skip Hooks leaves the step out.

The provider’s page, with the full inventory, every ownership rule and the behaviours above in longer form, is at Providers/OneLogin/README.md.

Seven providers now, seeding the same people into seven directories. The thing this one added to the module is not a feature: it is the idea that an object with no description can still be proved, by what it holds, and that a type which cannot be proved when empty must never be allowed to become empty. That rule would have improved two of the six providers that came before it.