Skip to content

Add explicit Windows credential SSPI provider to the 2.x library - #60

Merged
potatoqualitee merged 2 commits into
mainfrom
main-sspi-port-2026-09-14
Sep 16, 2026
Merged

potatoqualitee merged 2 commits into
mainfrom
main-sspi-port-2026-09-14

Conversation

@potatoqualitee

Copy link
Copy Markdown
Member

Summary

PR #56 was cut from libmigration and squash-merged into main, which dumped the whole 3.0 tree onto main. main has been reverted to v2026.5.3 (d43805b) and the full PR #56 + #57 work now lives on libmigration (cc04650).

This PR brings the part of that work the 2.x library actually needs back onto main:

  • Microsoft.Data.SqlClient 6.1.5 -> 7.0.1, plus Azure.Identity, Microsoft.Data.SqlClient.Extensions.Azure, and Microsoft.Identity.Client as explicit references (SqlClient 7 stopped pulling them transitively)
  • NetworkCredentialSspiContextProvider (.NET 8 only), including the Principal accessor from Make Get-DbaConnectedInstance see explicit-Windows-credential connections #57
  • SqlClientCompatibilityTest covering the provider
  • ModuleVersion 2026.9.14

The C# connection-service plumbing from #56/#57 is 3.0-only (2.x main has no ConnectionService at all) and is intentionally not ported. On 2.x, Connect-DbaInstance creates and assigns the provider itself (dataplat/dbatools#10495) and degrades gracefully when the type is absent.

Verification

  • build.ps1 -CoreOnly on Linux: green; provider type present in core/lib/dbatools.dll; shipped SqlClient is 7.0.1
  • dotnet test -f net8.0 --filter Connection: the 6 SSPI tests pass. The 7 pre-existing ConnectionString/Defaults tests fail identically on untouched main in the same WSL environment (System.Management.Automation not resolvable there); not related to this change.
  • The preview release this PR's CI produces is pinned in Pin dbatools.library to 2.x SSPI dbatools#10716 to prove the Windows SSPI integration leg goes green on 2.x before merging.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PRG7aY5eVNEXTw5ahQbGW9

Port of the SSPI half of PR 56 onto the restored 2.x main: upgrade Microsoft.Data.SqlClient to 7.0.1 with the dependencies it no longer brings transitively, and add NetworkCredentialSspiContextProvider (.NET 8 only, including the Principal accessor from PR 57). The C# connection-service plumbing from PR 56/57 is 3.0-only and stays on libmigration; on 2.x, Connect-DbaInstance wires the provider itself (dataplat/dbatools#10495) and degrades gracefully when the type is absent.

Bumps ModuleVersion to 2026.9.14 so the first release cut from the restored main carries the provider.

(do Connect-DbaInstance)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRG7aY5eVNEXTw5ahQbGW9
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review

Thanks for the clear write-up on the main vs libmigration history — that context made this easy to follow. Focused review below.

⚠️ Contradicts this repo's own documented dependency ceiling

CLAUDE.md's dependency table says:

| Microsoft.Data.SqlClient | 6.x only | DacFx/SMO compiled against 6.x; 7.x causes type-load failures |

This PR bumps Microsoft.Data.SqlClient from 6.1.5 to 7.0.1 in the shared ItemGroup (project/dbatools/dbatools.csproj), i.e. for both net472 and net8.0, alongside Microsoft.SqlServer.DacFx and Microsoft.SqlServer.SqlManagementObjects unchanged. That's exactly the combination the ceiling was written to prevent.

The PR's verification section covers the new SSPI provider and Connect-DbaInstance-style connection tests, but doesn't mention exercising any DacFx/SMO code paths (e.g. Export-DbaDacPackage, SMO-based backup/restore, scripting), and I don't see MSTest coverage for DacFx/SMO type loading in dbatools.Tests either — so the specific failure mode the ceiling exists for wouldn't be caught by this PR's test run or by CI as it stands.

Two asks:

  1. Confirm DacFx/SMO actually load and function correctly against SqlClient 7.0.1 (ideally with a smoke test), not just that the assembly builds.
  2. Either way, update the CLAUDE.md ceiling table in this PR — right now it actively contradicts the change that just shipped, which will confuse the next contributor (or hook/agent) that reads it.

Thread-safety of NetworkCredentialSspiContextProvider

GenerateContext does a read-modify-write on authentication/resource with no locking:

if (authentication == null || authentication.IsAuthenticated ||
    !String.Equals(resource, authParams.Resource, StringComparison.Ordinal))
{
    if (authentication != null)
        authentication.Dispose();
    ...
}

Since the type overrides Equals/GetHashCode (per the doc comment, "for the SqlClient pool key"), it looks like it's designed to be usable as a stable identity for connection-pool grouping. If the same provider instance is ever handed to more than one SqlConnection that gets opened/used concurrently (e.g. reused across pooled connections with the same credential), two threads could race on authentication/resource, causing a disposed-object use or a corrupted handshake. Worth confirming with the consumer side (Connect-DbaInstance in dbatools 2.x) whether a single provider instance can end up shared across concurrent connections — if so, this needs a lock around the mutable state in GenerateContext.

Minor: test assertion style inconsistency

SqlClientCompatibilityTest.cs uses Assert.ThrowsExactly<ArgumentNullException>(...), but every other test in dbatools.Tests uses Assert.ThrowsException<T>(...) (confirmed via grep — 15+ call sites, all ThrowsException). Both exist in MSTest 3.11.1, so it compiles, but it's an unnecessary style divergence in an otherwise consistent test suite.

What looks good

  • LangVersion 7.3 compliance: no C# 8+ syntax, no string interpolation, using (...) blocks rather than declarations.
  • NetworkCredentialSspiContextProvider is correctly gated behind #if NET8_0_OR_GREATER for both the impl and the test, keeping net472 untouched.
  • Sensible security touches: GetHashCode() deliberately excludes the password-derived hash (only hashes the principal) to avoid exposing a password-derived value through hash codes, and Equals uses CryptographicOperations.FixedTimeEquals for the credential-identity comparison to avoid timing side channels.
  • ArgumentNullException on null credential, proper Dispose()/ResetAuthentication() cleanup of the NegotiateAuthentication context on completion, failure, and exception paths.
  • Scope is appropriately minimal — the 3.0-only ConnectionService plumbing is correctly left out of this backport, and the PR explains why.
  • File sizes are well within the 400-line limit (151 lines for the new provider).

🤖 Generated with Claude Code

The 6.x-only SqlClient ceiling predates PR 56, which validated 7.0.1 against the pinned SMO and DacFx packages (PS3 golden image, Azure matrix, three-OS package build). libmigration's table already says so; bring main's copy in line so it stops contradicting the csproj.

(do Connect-DbaInstance)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRG7aY5eVNEXTw5ahQbGW9
potatoqualitee pushed a commit to dataplat/dbatools that referenced this pull request Sep 14, 2026
Points CI at the preview built from dataplat/dbatools.library#60 (2.x main plus the SSPI provider and SqlClient 7.0.1) so the Windows SSPI integration leg can prove itself green on a real 2.x library. Once #60 merges and a release is cut from main, this pin moves to that version.

(do Connect-DbaInstance)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRG7aY5eVNEXTw5ahQbGW9
@potatoqualitee

Copy link
Copy Markdown
Member Author

Addressed:

SqlClient ceiling / CLAUDE.md — the 6.x-only row predated #56, which validated 7.0.1 with the pinned SMO and DacFx packages (PS3 golden image import, ten-slice Azure matrix, three-OS package build). libmigration's CLAUDE.md already carried the corrected table; 43e019d brings main's copy in line. The full build-release run on this branch (Windows package build, critical-file check, tests on Windows/Linux/macOS) is green, and the preview it produced is now pinned in dataplat/dbatools#10716 so the SSPI integration leg and the rest of the 2.x matrix run against it.

Thread safety — on 2.x the only consumer is Connect-DbaInstance, which does New-Object NetworkCredentialSspiContextProvider per connection and assigns it to that one SqlConnection; the provider instance is never shared across connections. The Equals/GetHashCode overrides give the pool a stable key, but each connection still owns its own instance. Same as on libmigration.

ThrowsExactly — kept as-is so the test file stays byte-identical to libmigration and future merges are conflict-free.

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review

Solid, well-scoped port — the #if NET8_0_OR_GREATER gating, the pool-key Equals/GetHashCode override, and keeping the connection-service plumbing off main are all handled sensibly. A few things worth a look:

Versioning: AssemblyVersion/FileVersion/Version weren't bumped alongside ModuleVersion

dbatools.library.psd1's ModuleVersion moved to 2026.9.14, but project/dbatools/dbatools.csproj still has AssemblyVersion/FileVersion/Version at 0.10.0.81 — unchanged from before this PR. Per the repo's own bump workflow (.claude/commands/bump.md / build/bump-version.ps1), the module version and the DLL's fourth-segment version are supposed to move together. Right now the shipped DLL's internal version won't reflect this release, which will confuse anyone diffing Get-DbaBuild/assembly metadata against the module version during support/troubleshooting. Worth running the actual bump script (or manually bumping the csproj) rather than hand-editing just the .psd1.

NetworkCredentialSspiContextProvider — mutable state isn't synchronized

authentication and resource are plain mutable fields touched in GenerateContext with no locking. That's fine under the intended "one provider instance per SqlConnection" usage the PR description implies for Connect-DbaInstance, but the type is public, and the deliberate Equals/GetHashCode override (by design) makes instances for the same credential interchangeable for pool-key purposes — which could tempt a future caller into caching/reusing a single instance across concurrent connections (e.g., to skip the SHA-256 hash recompute). If that ever happens, concurrent GenerateContext calls on the same instance will race on authentication/resource and corrupt handshakes. Not a bug today, but a doc comment noting "one instance per connection, not thread-safe for concurrent use" would head this off.

Test coverage gap

The new SqlClientCompatibilityTest tests are good guardrails against upstream SqlClient API drift, but none of them exercise GenerateContext itself — the actual SSPI negotiation logic (multi-leg continuation, the reset-on-Completed path, the ObjectDisposedException/ArgumentNullException guards) is untested. I get that a full negotiate handshake is hard to unit test without a real server, but even a fake/mock NegotiateAuthentication-free path (e.g., asserting behavior after Dispose() is called, via the existing ObjectDisposedException guard) would catch regressions the current tests can't.

Minor / nit

  • SHA256.Create().ComputeHash(...) inside a using block works fine, but since this file is already NET8_0_OR_GREATER-only, the static one-shot SHA256.HashData(byte[]) would be slightly simpler and avoids the HashAlgorithm allocation.
  • Nice touch using CryptographicOperations.FixedTimeEquals for the credential-identity comparison in Equals — avoids a timing side channel on password comparison.

Style/LangVersion compliance

Checked against CLAUDE.md's rules: no C# 8+ syntax, no string interpolation, #if gating is appropriate for the net472/net8.0 split, XML <summary> docs are present on the new public members, and the dbatools.csproj package changes correctly make the previously-transitive Azure.Identity/Microsoft.Identity.Client/Microsoft.Data.SqlClient.Extensions.Azure references explicit. All good there.

🤖 Generated with Claude Code

@potatoqualitee
potatoqualitee merged commit 56fd1f7 into main Sep 16, 2026
25 of 29 checks passed
@potatoqualitee
potatoqualitee deleted the main-sspi-port-2026-09-14 branch September 16, 2026 19:46
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.

2 participants