Skip to content

Release v1.3.2 - surface AD secret/config errors instead of silent UNKNOWN_USER - #19

Merged
luigilink merged 4 commits into
mainfrom
release/1.3.2
Jul 9, 2026
Merged

Release v1.3.2 - surface AD secret/config errors instead of silent UNKNOWN_USER#19
luigilink merged 4 commits into
mainfrom
release/1.3.2

Conversation

@luigilink

Copy link
Copy Markdown
Owner

Summary

Hardening release (v1.3.2) that makes a broken or mis-deployed secrets.psd1 fail fast and loud instead of silently degrading the result. Follows a field case (~100k-user multi-forest farm) where a secrets.psd1 that could not be decoded on the User Profile master server (DPAPI SecureStrings are bound to the machine + account that created them) silently skipped thousands of real users as UNKNOWN_USER, the only trace being one Event-Log line per user.

This PR fixes the following issues:

Root cause

Get-SPSADUser caught every exception — including an undecodable DPAPI secret — wrote it to the Event Log only, and returned $null, exactly like a genuinely absent user. Test-SPSADUser is ($null -ne (Get-SPSADUser ...)), so a secret failure returned $falseSPSyncUserProfile.ps1 labelled the user UNKNOWN_USER and skipped it. On the InfoList side the same swallow produced empty FirstName/LastName for a whole forest, filling the Not Added list. Same silent-failure family as #16 (v1.3.1).

Changes

Config/secret error = fatal, connectivity error = non-fatal (the key distinction)

  • Get-SPSADConnection / Get-SPSADUser throw a terminating SPSADConfigError on a build-time misconfiguration (missing LdapPath/CredentialKey, undecodable/missing secret). A genuine lookup miss still returns $null.
  • A connectivity error (LDAP server not operational / referral — e.g. an external directory reachable from an app farm but not from the UPA master) stays non-fatal: logged and left unresolved so the run continues. A flaky forest never nukes a 100k-user run.

Fail loud where it matters

  • Get-SPSADConnectionError (new public) — query-free per-forest secret pre-flight.
  • SPSyncUserProfile.ps1 pre-flights every credential-mode forest in the input JSON and Exit 1s with an actionable message when a secret cannot be decoded on this server — no more silent UNKNOWN_USER en masse.
  • SPSyncUserInfoList.ps1 (parallel) fails the run naming the affected forest(s) on a config/secret error, instead of shipping an empty-name forest.
  • Test-SPSADUser propagates config errors (still $true/$false for found/not-found).

Docs

  • Troubleshooting: the Failed to decode SecureString entry now documents the fail-fast behaviour; a new entry covers server not operational / referral returned LDAP connectivity errors.

Validation

  • Pester 79/79 (68 → +11 new: config-vs-connectivity, Test-SPSADUser propagation, Get-SPSADConnectionError pre-flight).
  • PSScriptAnalyzer on ./src: clean. Manifest valid (1.3.2, 17 functions).
  • No behaviour change for a correctly-deployed farm; JSON and profile updates unchanged.

Scope note

The connectivity/RGA semantic (whether the profile step should trust the JSON and create a profile when AD is unreachable, vs skip-and-retry) is intentionally deferred to a later release, pending field confirmation. This PR does not change that path beyond keeping it non-fatal.

Upgrade notes

Drop-in replacement for 1.3.1. Run Test-SPSUserSyncReadiness.ps1 on each server (application farms and the UPA master), signed in as the service account, to confirm every forest's secret decodes and binds before enabling the scheduled tasks.

A broken/mis-deployed secrets.psd1 (a DPAPI SecureString generated on another
machine or account) was caught by Get-SPSADUser, logged only to the Event Log,
and returned as $null - indistinguishable from a genuinely absent user. On a
~100k-user multi-forest farm this silently produced empty-name JSON records for
a whole forest and downgraded thousands of real users to UNKNOWN_USER in
SPSyncUserProfile, one Event-Log line each.

- Get-SPSADConnection / Get-SPSADUser now distinguish a build-time configuration
  error (missing LdapPath/CredentialKey, undecodable/missing secret) - which
  throws a terminating SPSADConfigError - from a genuine lookup miss ($null).
- A connectivity error (server not operational / referral, e.g. an external
  RGA-style directory unreachable from the UPA master) stays NON-fatal: the
  login is logged and left unresolved so the run continues.
- Get-SPSADConnectionError (new public) - query-free per-forest secret pre-flight.
- SPSyncUserProfile pre-flights every credential-mode forest in the input and
  Exit 1s with an actionable message when a secret cannot be decoded here,
  instead of silently skipping users as UNKNOWN_USER.
- SPSyncUserInfoList (parallel) fails the run loudly on a config/secret error,
  naming the affected forest(s), rather than shipping an empty-name forest.
- Test-SPSADUser propagates config errors (still true/false for found/not-found).
- Docs: troubleshooting entries for the decode failure (now fail-fast) and for
  the 'server not operational' / 'referral returned' connectivity errors.
- Pester: Get-SPSADUser config-vs-connectivity, Test-SPSADUser propagation,
  Get-SPSADConnectionError pre-flight.

Fixes #18
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Pester Test Results

81 tests   81 ✅  3s ⏱️
21 suites   0 💤
 1 files     0 ❌

Results for commit 1cbd8ab.

♻️ This comment has been updated with latest results.

luigilink added 2 commits July 9, 2026 10:14
…irectories

Some deployments resolve users against a non-Active-Directory LDAP directory
(a partner / group-wide identity store) that needs a simple bind and a custom
uid filter. Get-SPSADConnection already read an AuthenticationType key but it was
undocumented, only applied to Credential-mode domains, and resolved via the
static accessor [AuthenticationTypes]::$name - so a typo silently yielded $null
(the same silent-config-error family as #16/#18).

- Validate AuthenticationType with [Enum]::TryParse (case-insensitive, supports
  flag combinations); an unknown value throws with the list of valid names
  instead of silently falling back to $null. Default stays 'Secure'.
- Honour AuthenticationType on Default-mode domains too, while keeping the exact
  original 1-arg DirectoryEntry construction when the key is absent (zero
  behaviour change for existing AD forests).
- Document AuthenticationType in ad-domains.example.psd1 with a non-AD directory
  example (None simple bind / SecureSocketsLayer, uid filter, host:port/baseDN).
- Genericize the non-AD directory references in the module help.
- Pester: invalid AuthenticationType throws with valid names; a valid type is
  not rejected at validation time.

Fixes #20
Bump module to 1.3.2, date the CHANGELOG 1.3.2 section, rewrite RELEASE-NOTES,
and document the AD secret/config hardening (#18) and the AuthenticationType
support (#20) in the changelog and the Usage troubleshooting wiki.
… 5.1 compat)

Test-SPSUserSyncReadiness / Get-SPSADConnection crashed on every domain under
Windows PowerShell 5.1 with 'Cannot find an overload for TryParse and the
argument count: 4'. The non-generic [Enum]::TryParse(Type, string, bool, [ref])
overload only exists on .NET Core / .NET 5+; the .NET Framework that hosts
Windows PowerShell 5.1 (which runs the scripts in production, for the SharePoint
snap-in) does not have it. The AuthenticationType parse runs for every domain, so
it broke all AD connections.

- Replace it with [Enum]::Parse(Type, string, ignoreCase) in a try/catch (present
  since .NET Framework 1.1; same case-insensitive + flag-combination behaviour,
  and an invalid value still throws the actionable 'invalid AuthenticationType'
  error). Applied the same fix to the companion Test-LdapConnectivity diagnostic.
- Add a Windows PowerShell 5.1 Pester job to CI. The suite previously ran only
  under pwsh (PowerShell 7), whose newer .NET BCL has the extra overload, so the
  pwsh-only pass went green and missed this. The existing AuthenticationType
  tests now also assert no 'overload'/'TryParse' method-resolution error, so this
  regression is caught under 5.1.

Relates to #20
@luigilink
luigilink merged commit 397feac into main Jul 9, 2026
4 of 7 checks passed
@luigilink
luigilink deleted the release/1.3.2 branch July 9, 2026 13:19
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.

SyncUserProfile: surface AD secret/config errors instead of mislabeling users as UNKNOWN_USER

1 participant