Skip to content

feat: add OneLogin as the seventh provider - #56

Merged
fadwen merged 2 commits into
mainfrom
feat/onelogin-provider
Sep 29, 2026
Merged

fadwen merged 2 commits into
mainfrom
feat/onelogin-provider

Conversation

@fadwen

@fadwen fadwen commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds OneLogin as the module's seventh provider. It seeds a OneLogin account through the OneLogin API (v2, with v1 where v2 has no equivalent), authenticating as an API credential. It reports on the seeded objects, verifies them against the seed data, repairs drift, and tears everything down, proving ownership of each object before deleting it. It plugs into the shared dispatchers, so Connect-TestEnvironment -Provider OneLogin, New-TestEnvironment, Get-TestEnvironmentReport, Test-TestEnvironment, Repair-TestEnvironment, Compare-TestEnvironment and Remove-TestEnvironment all work unchanged.

The provider is designed to run against an account real people sign in to, not only a lab. Nothing it seeds can act on a real object, and no real object is drawn into the seed. None of the properties that guarantee this can be switched off by a parameter.

What is seeded

Object Count Notes
Custom user fields 4 zztest_seed_tag carries the ownership tag on every seeded person
Users 321 21 hand-designed + 300 generated; every lifecycle status and state OneLogin retains; one locked; managers for 317
Licensed users 10 All other people are Unlicensed or Rejected on purpose, so a seed never uses more than ten licences
Roles 4 14 explicit memberships, plus one added by the enabled mapping
Groups 5 By office; 319 people placed
User security policies 2 Password and lockout settings on three of the groups; the other two use the account default
Apps 5 OIDC web (Basic and Post auth), public client (PKCE), native client, SAML; 5 role grants
App rules 2 One enabled, one disabled; each sets a seeded app's groups claim based on a seeded role
API authorization servers 2 4 scopes (one granted to no client), 3 claims, 3 seeded apps as clients
User mappings 2 One enabled, one disabled; both gated to seeded people
Smart Hook 1 Pre-authentication, disabled, gated on a seeded role
Self-registration profile 1 Disabled, moderated, lab domain only
MFA factors 3 Pre-enrolled and verified, only where the account already offers the factor

Every person also gets the directory identifiers a directory-synchronised account would have: sAMAccountName, user principal name, distinguished name, member_of, external id and phone. The people shared with other providers carry exactly the names in Core/Data/SeedPeople.csv, including the non-Latin writing systems and the decomposed name.

Ownership and teardown

Most OneLogin objects carry nothing but a name, so Get-OneLoginSeededObject proves each type by its contents:

  • Person: the tag in zztest_seed_tag and the prefix on the username.
  • App, API authorization server, self-registration profile: the tag in the description or help text and the prefix on the name.
  • Role: the prefix, no administrators, at least one member, and nothing but proved seeded people and apps.
  • Group: the prefix, no administrators, at least one member, only proved seeded people, and no policy other than a prefixed, non-default one.
  • Policy: used only by proved seeded groups, and not the default.
  • Mapping: the seed-tag condition with match all, and add-role actions only.
  • App rule: sits on a proved seeded app.
  • Smart Hook: the marker line // Seeded by TestEnvironment. Safe to delete. [ZZ-TEST-seed] first in its code, since a hook has no name or description.
  • Custom field: declared in the provider's data and prefixed zztest_.

A mapping, app rule or hook may name a seeded role, or a role that no longer exists (so a run interrupted after roles were deleted can still be proved). It may never name a role that exists and is not seeded.

Teardown proves every object before deleting any, because the proofs depend on each other, then deletes in dependency order. An object that fails its proof is listed with the reason and left alone. -Keep also keeps whatever the kept type is proved by (the transitive closure of $script:OneLoginProofDependency) and reports what it added. As on every provider, -WhatIf wins over -Force.

Safety properties with no parameter

  • Every mapping carries a condition requiring the seed tag, with match all, so an enabled mapping can act on seeded people only.
  • The Smart Hook is always created disabled and gated on a seeded role; a re-run disables it again.
  • The self-registration profile is always disabled, moderated, restricted to the lab email domain (example.com by default) and has no default role or group; a re-run restores that state.
  • A policy is never made the account default and is attached only to seeded groups.
  • Only seeded apps are clients of seeded API servers.
  • An app rule is placed only on a seeded app and names only seeded roles. Its action is fixed: the groups claim from member_of.
  • No MFA factor is enabled account-wide. Factors are enrolled, already verified, on seeded people only.
  • Every directory identifier sits under the prefix or the lab domain, and phone numbers stay within 555-0100 to 555-0199, so no identifier can match a real account.
  • Nobody who is not seeded is added to a role or group, given a manager, or made one.

New-OneLoginStep.Tests.ps1 asserts each of these, and also that no step exposes a parameter that could loosen them.

Saved app secrets (opt-in)

OneLogin returns an app's client secret only in the response to its creation; later reads of the app return the client id alone. By default the secret is discarded. With New-TestEnvironment -SaveAppSecret:

  • The secrets of the two confidential clients created in that run (Expenses Web, Payroll Console) are written through the shared Export-TestCredentialRecord, one record per app id: DPAPI-protected under ~/.testenvironment/<subdomain>.onelogin-app.<id>.json, or in a SecretStore vault with -UseSecretStore.
  • Get-OneLoginAppCredential returns them as PSCredential objects (client id as user name). The secret never reaches the pipeline as plain text.
  • If an app already existed, its secret can't be recovered. The step names the app and warns rather than leaving a gap unexplained.
  • Teardown deletes each record, including its vault entry, together with its app. It also deletes any record whose app is no longer in the account, checked against a listing of every app. It never deletes a record while its app exists, never touches another account's records or a file whose content disagrees with its name, and deletes nothing under -WhatIf. Under -Keep Apps it doesn't read the records at all.
  • The environment report shows which apps have a saved secret.

The seeded apps' redirect URLs are under the lab email domain, so a test relying party must be reachable there or have its redirect URL added in the portal. The client credentials grant isn't enabled on the seeded apps; token requests using it return HTTP 500.

OneLogin behaviour the provider accounts for

Each item below was observed against a live trial account. None of it produces an error.

Behaviour Handling
A person is approved only while a licence is free; beyond that they are made Unlicensed, although the create response says Approved Ten people are Approved; the users step reads them back and names anyone left unlicensed
A role grant is accepted for anyone but kept only for an Approved person with status 1–5 Seed data gives roles only to such people; SeedData.Tests.ps1 enforces it
A role grant becomes visible seconds to minutes after the response, and occasionally only after it is re-sent The users step waits up to four minutes and re-sends missing grants
Unactivated and Unapproved revert; a Locked status sent on create or update does not hold Those two states are not seeded; the locked person is created Active and then locked for a year through the v1 lock_user call
User listings omit custom fields, roles, manager and directory fields unless fields= names them Every listing names $script:OneLoginUserFields
Mappings and app rules list only enabled entries unless enabled=false is requested Both are requested
Smart Hook listings omit code, and group listings omit administrators Each is read in detail before it is proved
A Smart Hook DELETE returns 202 with a plain-text body Invoke-OneLoginRequest returns non-JSON success bodies as strings
A one-element array piped to ConvertTo-Json serialises as a bare number JSON arrays of ids are pre-serialised

A trial account allows 5 roles (including Default), 5 apps and 12 user licences (including the owner). The seed data is sized to fit.

Not seeded: devices (OneLogin has no API to create one; a device appears when an agent enrols it), risk rules (they apply account-wide and cannot be scoped to seeded people), and branding, privileges, trusted identity providers and directories (unavailable on a trial, and each is account-wide).

Commands

Exported, with PlatyPS help under docs/TestEnvironment/ and rebuilt MAML: New-OneLoginCustomAttribute, New-OneLoginRole, New-OneLoginGroup, New-OneLoginPolicy, New-OneLoginApp, New-OneLoginAppRule, New-OneLoginApiAuthorization, New-OneLoginMapping, New-OneLoginSmartHook, New-OneLoginSelfRegistration, New-OneLoginUser, New-OneLoginMfaFactor, Get-OneLoginAppCredential.

The connect, seed, report, verify and teardown commands are reached through the shared dispatchers and keep full comment-based help. Each is a <Verb>-OneLoginEnvironment command: Connect, Disconnect, New, Get-...Report, Test and Remove. Connect-TestEnvironment -Provider OneLogin -Subdomain <name> accepts a bare name, host or portal URL. -SaveSecret stores the API credential once the connection is proved, and -UseStoredCredential reads it back. Disconnect-TestEnvironment revokes the token, which otherwise lives ten hours.

Also in this change

  • Test-OneLoginEnvironment: the verifier originally counted one-row seed files with .Count directly. On Windows PowerShell 5.1 a single PSCustomObject has no .Count, so the Smart Hook check would have failed there. Every seed read in it is now wrapped in @(), and the matching reads in the unit fixtures are too.
  • The module help page (docs/TestEnvironment/TestEnvironment.md) lists the PingOne commands, which were missing.
  • CLAUDE.md, README.md, docs/Architecture.md, Tests/README.md, Verify/README.md, CHANGELOG.md (Unreleased) and about_TestEnvironment are updated for the new provider.

Release

This PR also prepares 1.5.0 (commit release: 1.5.0):

  • ModuleVersion is 1.5.0, and the manifest's ReleaseNotes open with a 1.5.0 entry.
  • CHANGELOG.md moves the Unreleased section under ## [1.5.0] - 2026-09-29 and leaves Unreleased as "Nothing yet."
  • The version is a minor bump: a new provider and thirteen new exported commands, with no change to any existing command's parameters or output.
  • The UTF-8 byte order mark is restored on TestEnvironment.psd1 and TestEnvironment.psm1. It had been dropped on this branch; both files are ASCII, so nothing read them differently.

./Build/Publish-Module.ps1 -WhatIf stages and verifies the tree as TestEnvironment 1.5.0 -> PSGallery, and Windows PowerShell 5.1 reads the manifest as 1.5.0.

After merge, the release is tag-driven:

git checkout main
git pull
git tag v1.5.0
git push origin v1.5.0

.github/workflows/release.yml re-runs the quality gates, checks the tag against ModuleVersion, stages, imports the staged tree in a fresh process, and publishes.

Verification

  • Unit suite: 3049 passed / 0 failed on PowerShell 7.6; 3050 passed / 0 failed on Windows PowerShell 5.1. No test reaches an account.
  • PSScriptAnalyzer (Error, Warning): 0 findings.
  • ./Build/Build-Help.ps1: all help gates pass (100 commands); a rebuild matches the committed MAML byte for byte.
  • ./Build/Publish-Module.ps1 -WhatIf: the staged module imports, exports the declared commands, discovers all seven providers and serves MAML help for every command.
  • Live full cycle against a OneLogin trial, via Verify/Invoke-LiveCycle.ps1 -Provider OneLogin:
    • Windows PowerShell 5.1: seed in 1 min 47 s; all 23 verification checks passed; 347 objects removed; nothing left behind.
    • PowerShell 7.6: seed in 5 min 49 s (OneLogin was slower that run to show role grants); all 23 checks passed; 347 objects removed; nothing left behind.
  • Live -SaveAppSecret check on both editions:
    • Two records written, DPAPI-protected, with no plaintext secret on disk.
    • Both read back by Get-OneLoginAppCredential.
    • The report flags the two apps.
    • A -WhatIf teardown removed nothing.
    • The teardown removed both records plus a planted orphan record, leaving none.

After every live run the account was confirmed back at its baseline: the owner, the Default role and the Default policy only.

Seeds a OneLogin account through its API as an API credential, reports on
it, verifies it against the seed data, repairs it and tears it down, proving
ownership of every object before deleting it.

Seeded: four custom user fields, 321 people in every lifecycle state OneLogin
keeps (one of them locked) with managers and directory identifiers, four
roles, five office groups, two user security policies, five OIDC and SAML
apps with two app rules, two API authorization servers with scopes, claims
and seeded clients, two gated user mappings, a disabled Smart Hook, a
disabled self-registration profile, and MFA factors where the account
already offers them.

Built to be safe in an account real people sign in to. A role, group, policy,
mapping, app rule and hook are proved by what they hold or name, since most
carry nothing but a name. Nothing seeded can reach a real object and nothing
real is pulled into the seed, and none of those properties has a parameter.

New-OneLoginApp -SaveAppSecret optionally keeps the two confidential apps'
client secrets through the shared credential record writer (DPAPI or the
SecretStore); Get-OneLoginAppCredential reads them back, and teardown deletes
each with its app and any whose app is gone.

Adds 13 exported commands with PlatyPS help, and the provider README.
Cuts the Unreleased section as 1.5.0: OneLogin joins as the seventh
provider, with ownership proved by what each object holds, safety properties
that have no parameter, and opt-in saved app secrets that teardown removes
with their apps.

Minor rather than patch: a new provider and thirteen new exported commands,
with no change to any existing command's parameters or output.

Also restores the UTF-8 byte order mark on TestEnvironment.psd1 and
TestEnvironment.psm1, which an editing pass on this branch had dropped.
Both files are ASCII today, so nothing read them differently, but main has
always carried the mark and Windows PowerShell 5.1 needs it the moment
either file holds a non-ASCII character.
@fadwen
fadwen merged commit 04c22da into main Sep 29, 2026
4 checks passed
@fadwen
fadwen deleted the feat/onelogin-provider branch September 29, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant