Skip to main content

Directory Integrations

In plain English

A directory integration connects nexusID to a system that already holds your users — LDAP, Active Directory, or a SCIM/database source — so accounts are created and kept in sync automatically instead of by hand.

📺 Watch the Provisioning (SCIM) Tour →

Console View​

Directory Integrations Console

Directory Integrations (/admin/directory-integrations) let nexusID synchronize users and groups with external directories — inbound (LDAP/AD/SCIM/DB → nexusID) and, for AD/LDAP, outbound (nexusID → AD/LDAP) as well.

Supported Directories​

  • LDAP / Active Directory: Connect via LDAP(S), either Direct (the broker binds itself) or Agent (the on-prem Sync Agent drains a write queue — air-gapped networks, no inbound firewall hole).
  • SCIM 2.0: Act as a SCIM client to pull users from SCIM-compliant providers.
  • Database: Import users from legacy SQL databases via JDBC.

Sync direction​

  • One-way Import (LDAP → nexusID): read-only. Never writes back, and is exempt from create-on-missing (an import-only source is never authoritative for what should exist).
  • One-way Export (nexusID → LDAP): outbound writes only.
  • Two-way Sync: reads inbound and writes outbound. Required for Deprovision on directory deletion (below).

Sync Features​

  • Scheduled Sync: an auto-sync interval (autoSyncEnabled + syncIntervalMinutes), run by a ticker, plus on-demand Sync now.
  • Attribute Mapping: map external directory attributes to nexusID profile fields, and — for DIRECT outbound writers — map nexusID fields onto native AD/LDAP attributes.
  • Computed (transform) outbound attributes: compose a native attribute from identity fields with a small expression language (below) — for values no straight 1:1 mapping can produce.
  • Just-in-Time (JIT): create users on the fly during login (alternative to full sync).
  • OU routing, correlation rules, forests: per-identity container placement, aggregated-account matching, and multi-domain forest reads/writes for DIRECT AD connectors.

Import Safeguards​

TWO_WAY (or DIRECT create-on-missing) directories can act on what an inbound read tells them is absent — either side of the joiner/leaver relationship:

SettingDirectionEffect when an owned account is absent from the read
Deprovision on directory deletion (deprovisionOnDeletion)TWO_WAY onlySoft-deactivates the nexusID account (30-day reversible window) + enqueues a DISABLE lifecycle schedule. Okta/Entra-style "the directory deleted them, so we should too."
Create account if missing in directory (createOnMissing)DIRECT, any direction except One-way Import(Re-)provisions the account: enqueues PROVISION then ENABLE. The mirror case — "the directory lost an account this source still says should exist."

Both are off by default, scoped to accounts this integration owns (provenance-tagged to it — never the whole HR population), and guarded by the same two safeguards, framed after Okta Import Safeguards / Entra's deletion threshold:

  1. Empty-read abort — a read that returns zero users is treated as a partial/failed read, never as "everyone was deleted." Nothing is deactivated or (re)created; the sync log records deactivated=0(skipped:empty-read) / provisioned=0(skipped:empty-read).
  2. Per-sync threshold — deletionThreshold (delete side) / createThreshold (create side, new), default 10 each. If a single sync would act on more owned accounts than the threshold, the run HALTS and writes nothing for that side — surfaced as DirectorySyncStatus.PARTIAL_SUCCESS on the sync log with a BLOCKED(deletion-threshold: would deactivate N > threshold) / BLOCKED(create-threshold: would provision N > threshold) summary fragment. The read itself still succeeded — only the risky write action was withheld — which is why it's PARTIAL_SUCCESS, not FAILED.

Admin console: on the Directory Integrations edit form,

  • Two-way Sync direction exposes Deprovision on directory deletion → Deletion safeguard threshold (max per sync).
  • Direct connection mode exposes Create account if missing in directory → Create safeguard threshold (max per sync).

Enable at most one per integration — they express opposite intents about which side of a disappearance is authoritative.

First-write gate and blast-radius preview​

An integration that has never written to its directory is the riskiest one in the system — nothing has yet proven the configuration is right. Turning on Require a preview before the first write (requirePreviewBeforeFirstWrite) holds that integration's very first outbound write — create, enable, disable, and attribute update all go through the same gate — until an admin has run a preview and explicitly accepted it. Off by default so installs already past their first write are never silently gated.

  1. Admin → Directory Integrations → ⋯ → Preview. POST /{id}/preview-sync runs a read-only walk over the population (DEFAULT_CAP 2,000 identities per run; per-account detail capped at 5,000 rows) and aggregates it — the same shape as a bulk update, so a bad default is unmissable in aggregate even when no individual preview would raise it:

    telephoneNumber   351 accounts   → "0001"
    department 12 accounts → various
  2. The result reports, per attribute, the accounts affected and whether every one would land the same value — the "uniform value" signal that flags a placeholder/bad-default rewrite — plus separate counts for existing objects that would be rewritten, identities that would be created (no directory object yet — shown with their full create payload, not an empty diff), and anything a provisioning policy suppresses (routes elsewhere) rather than counting as a real create/update.

  3. Once the run finishes, Accept and allow writes clears the gate (POST /{id}/acknowledge-first-write, audited with the acting admin and timestamp). It applies once — after the first acknowledgement the integration writes normally.

  4. Re-arming: if a later edit broadens what the integration can write (adds to syncAttributes / attributeMapping / computedAttributes, or changes syncDirection) after the gate was already cleared, the acknowledgement is cleared too and a fresh preview is required — logged as DIRECTORY_WRITE_AUTHORITY_CHANGED with firstWriteGateReArmed. Narrowing what it writes does not re-arm it.

Computed (transform) outbound attributes​

For a native attribute that isn't a straight 1:1 copy of one identity field, compose it from several fields plus literals with a small expression language (shared between per-integration computed attributes and Provisioning Rules's create-time fields):

$jobTitle + " - " + $department + " - " + $manager.displayName

$jobTitle + " - " + case($company, "Public Broadcasting Service", $department,
"PBSD", "PBS Distribution") + " - " + $manager.displayName

$firstName + " " + optional($middleName:initial + ". ") + $lastName
→ "John M. Smith" (or "John Smith" when there's no middle name)
  • Terms are joined with +: a $variable (optionally piped through functions, $middleName:initial), a "quoted literal", a case(...) lookup, or an optional(...) group.
  • Functions: initial, upper, lower, trim, clean (letters+digits only), first3. Chainable ($middleName:initial:upper); an unrecognized function name passes the value through unchanged rather than eating data.
  • case(selector, match1, result1, match2, result2[, default]) is a lookup table, not a general conditional — matches compare case-insensitively, and only the selected branch is resolved, so an unresolvable value on a branch a given identity never takes (e.g. $department on a population that always hits the "PBS Distribution" literal) doesn't blank the whole value for everyone else.
  • optional(...) emits its composed content only when every variable inside it resolves non-blank, and the empty string otherwise — so an absent middle name drops just its own group instead of voiding the composed value.
  • If any referenced variable is unresolved, the entire computed value is skipped for that write (never a half-formed "Engineer - - ") — logged, and visible in the preview before anything is written.
  • A short blocklist of AD attributes can never be targeted this way regardless of mapping — credentials (unicodePwd, userPassword, …), userAccountControl (owned by lifecycle), security posture (nTSecurityDescriptor, sIDHistory, objectSid, adminCount, servicePrincipalName), group membership, and structural attributes (objectClass, distinguishedName, …). sAMAccountName, userPrincipalName, mail, and cn are not blocked.
  • Computed attributes are resolved after 1:1 attribute mapping and before per-field transforms, and now appear identically on both the create preview and the update preview — a computed value used to show correctly on update-preview but silently disappear from create-preview; the create preview now reuses the exact payload a real create would write.
  • Provisioning Rules — data-defined generation of the create-time username / email / UPN / display name, keyed by source.
  • Reconciliation — verify a target still matches nexusID after the fact.
  • Sync Agent — the on-prem AD/DB connector for AGENT-mode integrations.
  • HR Sources — the joiner/mover/leaver feeds that drive provisioning to a directory integration.