Skip to content

feat: comprehensive replacement for GetDomain with fallbacks#299

Open
rvazarkar wants to merge 20 commits intoldap_beta_fixesfrom
getdomain_replacement
Open

feat: comprehensive replacement for GetDomain with fallbacks#299
rvazarkar wants to merge 20 commits intoldap_beta_fixesfrom
getdomain_replacement

Conversation

@rvazarkar
Copy link
Copy Markdown
Contributor

@rvazarkar rvazarkar commented Apr 23, 2026

Description

Addresses one of the main offenders of uncontrolled LDAP calls in the codebase. Also fixes several gaps in netonly scenarios, and drastically improves domain info resolution with several fallbacks. Adds an optional flag to the LDAPConfig which allows

Motivation and Context

This PR addresses: [GitHub issue or Jira ticket number]

How Has This Been Tested?

Unit tests for many of the changes, real testing to follow.

Screenshots (if appropriate):

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

Summary by CodeRabbit

  • New Features

    • Added DomainInfo model, async domain-info resolution APIs, and LDAP config options to control fallback and current-user domain.
  • Refactor

    • Domain discovery and LDAP queries moved to async, cached pipelines with richer domain metadata and improved connection/search base resolution.
  • Bug Fixes

    • Made multiple caches and exclusion lookups case-insensitive and tightened null/empty input handling.
  • Tests

    • Expanded unit tests for LDAP behavior, caching, domain-resolution selection, and utility string handling.

…of the biggest ADSI gaps, as well as several improvements to the logic
@rvazarkar rvazarkar self-assigned this Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 993f8517-1da4-4b3f-8f5d-eafb60c09685

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces an asynchronous domain-resolution pipeline and new DomainInfo model; replaces synchronous GetDomain/ADSI lookups with GetDomainInfoAsync calls across LDAP utilities, connection pooling, and processors; adds LdapConfig flags to gate opt-in uncontrolled fallback and introduces case-insensitive caching adjustments.

Changes

Cohort / File(s) Summary
Domain Info model
src/CommonLib/DomainInfo.cs
Added sealed DomainInfo DTO with DNS name, distinguished name, forest, optional DomainSid, NetBIOS name, PDC host, and controller hostnames.
LDAP utilities & caching (core)
src/CommonLib/LdapUtils.cs, src/CommonLib/Cache.cs, src/CommonLib/Enums/LDAPProperties.cs
Implemented GetDomainInfoAsync multi-tier resolution and caching; replaced legacy GetDomain/ADSI resolution paths; added FSMORoleOwner/NCName constants; made several internal caches case-insensitive and added normalization after deserialization.
Interface & config
src/CommonLib/ILdapUtils.cs, src/CommonLib/LdapConfig.cs
Added async interface methods GetDomainInfoAsync()/GetDomainInfoAsync(string); added AllowFallbackToUncontrolledLdap and CurrentUserDomain properties and updated ToString().
Connection pooling & manager
src/CommonLib/LdapConnectionPool.cs, src/CommonLib/ConnectionPoolManager.cs
Switched search-request creation to async, derive search bases from DomainInfo.DistinguishedName, use GetDomainInfoStaticAsync, and update connection strategies to use DomainInfo.PrimaryDomainController/DomainControllers; removed legacy ADSI fallback in ConnectionPoolManager domain-SID path.
Processors & cache usage
src/CommonLib/Processors/GPOLocalGroupProcessor.cs
Replaced synchronous GetDomain(out ...) calls with awaited _utils.GetDomainInfoAsync(); switched caches to case-insensitive comparers.
Tests & mocks
test/unit/Facades/MockLdapUtils.cs, test/unit/GPOLocalGroupProcessorTest.cs, test/unit/LDAPUtilsTest.cs, test/unit/CacheTest.cs, test/unit/...
Added async GetDomainInfoAsync stubs and updated tests to exercise new domain-info paths, caching, normalization after deserialization, LdapConfig behaviors, and many regression/unit cases for domain-info selection and enrichment.
Misc — ConnectionPoolManager change
src/CommonLib/ConnectionPoolManager.cs
Removed secondary ADSI-based SID resolution and replaced with synchronous wait on LdapUtils.GetDomainInfoStaticAsync(...).GetAwaiter().GetResult() and caching of info.DomainSid when available; removed unused DirectoryServices/compiler imports.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller/Processor
    participant LdapUtils as LdapUtils
    participant PoolMgr as ConnectionPoolManager/Pool
    participant DirectLDAP as Direct LDAP Bind
    participant ADSI as ADSI DirectoryEntry
    participant Uncontrolled as Domain.GetDomain (opt-in)

    Caller->>LdapUtils: GetDomainInfoAsync(domain?)
    alt Pooled controlled LDAP available
        LdapUtils->>PoolMgr: Use pooled async search
        PoolMgr-->>LdapUtils: (Success, DomainInfo)
    else try direct LDAP bind
        LdapUtils->>DirectLDAP: One-shot async LDAP bind
        DirectLDAP-->>LdapUtils: (Success, DomainInfo)
    else try ADSI fallback
        LdapUtils->>ADSI: DirectoryEntry lookup
        ADSI-->>LdapUtils: (Success, DomainInfo)
    else uncontrolled fallback allowed
        LdapUtils->>Uncontrolled: Domain.GetDomain()
        Uncontrolled-->>LdapUtils: (Success, DomainInfo)
    end
    LdapUtils-->>Caller: (Success, DomainInfo)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hop through DN and SID with cheer,
Async whispers make lookups clear,
Cached DomainInfo snug and bright,
PDCs and DCs leap into sight,
A rabbit’s async dance — rejoice with a carrot near! 🍵

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is incomplete. It lacks specific details about the optional flag added to LDAPConfig (sentence cuts off mid-word: 'which allows'), has minimal testing details ('real testing to follow'), and is missing complete motivation context about why the change was needed. Complete the description by: (1) finishing the LDAPConfig flag explanation, (2) providing the GitHub issue number if available, (3) detailing the test coverage more thoroughly, and (4) clarifying the gaps addressed in netonly scenarios.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing GetDomain with a new comprehensive solution that includes fallback mechanisms for domain resolution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch getdomain_replacement

Comment @coderabbitai help to get the list of available commands and usage tips.

@rvazarkar
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/unit/Facades/MockLdapUtils.cs (1)

706-712: Consider returning a baseline DomainInfo from the test double.

This mock already models a default TESTLAB.LOCAL environment via GetForest, GetDomainSidFromDomainName, and the identity resolvers. Returning (false, null) here makes the new API the only domain-resolution path that fails by default, which can turn unrelated tests red as production code migrates from GetDomain* to GetDomainInfoAsync(). A minimal happy-path DomainInfo would keep the base fixture behavior consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/Facades/MockLdapUtils.cs` around lines 706 - 712, The mock's
GetDomainInfoAsync overloads return (false, null) which breaks the established
TESTLAB.LOCAL baseline used by GetForest and GetDomainSidFromDomainName; update
both GetDomainInfoAsync(string) and parameterless GetDomainInfoAsync() to return
Task.FromResult((true, baselineDomainInfo)) where baselineDomainInfo is a
minimal DomainInfo instance that matches the existing test fixture (e.g.,
DomainName "TESTLAB.LOCAL", the same ForestName and DomainSid used by
GetForest/GetDomainSidFromDomainName and any identity resolvers) so tests
continue to see a successful, consistent domain resolution path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CommonLib/DomainInfo.cs`:
- Around line 12-33: DomainInfo instances are mutable and are being returned
from a static cache in LdapUtils.cs, allowing callers to mutate shared cached
objects; make DomainInfo immutable (remove public setters on Name,
DistinguishedName, ForestName, DomainSid, NetBiosName, PrimaryDomainController
and DomainControllers) and provide a single constructor (or init-only
properties) that fully initializes all properties (keeping DomainControllers as
an IReadOnlyList<string> defaulting to Array.Empty<string>()), then update
LdapUtils.cs to only store and return immutable DomainInfo objects (or to
clone/create a new DomainInfo instance at the cache boundary when
reading/writing) so cached state cannot be mutated by consumers.

In `@src/CommonLib/LdapUtils.cs`:
- Around line 35-36: The static cache _domainInfoCache currently keys only by
domain and can return entries produced by
TryGetDomainInfoViaUncontrolledFallback even when
LdapConfig.AllowFallbackToUncontrolledLdap is false; fix by encoding fallback
provenance in the cache key or marking cached DomainInfo entries with a boolean
(e.g., DomainInfo.IsFallback) and, at every cache read site (places that read
_domainInfoCache and return cached values, and where
TryGetDomainInfoViaUncontrolledFallback writes into the cache), revalidate
against LdapConfig.AllowFallbackToUncontrolledLdap—only return a cached
IsFallback entry when AllowFallbackToUncontrolledLdap is true, otherwise bypass
the cached fallback entry and proceed to controlled lookup or refresh the cache
entry appropriately.
- Around line 1456-1457: The XML doc comments in LdapUtils.cs reference a
non-existent member DomainInfo.FromFallback which causes unresolved cref
warnings; fix by either adding a boolean member/property named FromFallback to
the DomainInfo class (and document it) or update the XML docs to remove or
replace the cref with an existing member (e.g., an actual property like
DomainInfo.IsFallback or simply remove the cref). Update all occurrences that
reference DomainInfo.FromFallback so the cref targets a real symbol or is
removed to eliminate the unresolved-reference warnings.

---

Nitpick comments:
In `@test/unit/Facades/MockLdapUtils.cs`:
- Around line 706-712: The mock's GetDomainInfoAsync overloads return (false,
null) which breaks the established TESTLAB.LOCAL baseline used by GetForest and
GetDomainSidFromDomainName; update both GetDomainInfoAsync(string) and
parameterless GetDomainInfoAsync() to return Task.FromResult((true,
baselineDomainInfo)) where baselineDomainInfo is a minimal DomainInfo instance
that matches the existing test fixture (e.g., DomainName "TESTLAB.LOCAL", the
same ForestName and DomainSid used by GetForest/GetDomainSidFromDomainName and
any identity resolvers) so tests continue to see a successful, consistent domain
resolution path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21e5791e-c2db-494d-afb2-12809aeda7a6

📥 Commits

Reviewing files that changed from the base of the PR and between d54aebf and 7db7e42.

📒 Files selected for processing (11)
  • src/CommonLib/ConnectionPoolManager.cs
  • src/CommonLib/DomainInfo.cs
  • src/CommonLib/Enums/LDAPProperties.cs
  • src/CommonLib/ILdapUtils.cs
  • src/CommonLib/LdapConfig.cs
  • src/CommonLib/LdapConnectionPool.cs
  • src/CommonLib/LdapUtils.cs
  • src/CommonLib/Processors/GPOLocalGroupProcessor.cs
  • test/unit/Facades/MockLdapUtils.cs
  • test/unit/GPOLocalGroupProcessorTest.cs
  • test/unit/LDAPUtilsTest.cs

Comment thread src/CommonLib/DomainInfo.cs Outdated
Comment thread src/CommonLib/LdapUtils.cs
Comment thread src/CommonLib/LdapUtils.cs Outdated
Comment on lines +1456 to +1457
/// <see cref="DomainInfo"/> with <see cref="DomainInfo.FromFallback"/> = false. A failure
/// to acquire the connection or to read the default NC is fatal and returns <c>(false, null)</c>.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e csproj -e props -e targets | xargs -r rg -n "GenerateDocumentationFile|TreatWarningsAsErrors|WarningsAsErrors"
rg -n "DomainInfo\\.FromFallback|<see cref=\"DomainInfo\\.FromFallback\""

Repository: SpecterOps/SharpHoundCommon

Length of output: 332


🏁 Script executed:

fd -e cs -i "*domaininfo*" | head -20

Repository: SpecterOps/SharpHoundCommon

Length of output: 362


🏁 Script executed:

cat -n src/CommonLib/DomainInfo.cs | head -100

Repository: SpecterOps/SharpHoundCommon

Length of output: 1916


🏁 Script executed:

rg -n "FromFallback" src/CommonLib/

Repository: SpecterOps/SharpHoundCommon

Length of output: 332


The XML docs reference a member that doesn't exist.

Both comments at lines 1456 and 1630 point to DomainInfo.FromFallback, but the DomainInfo class has no such member. These broken crefs will raise unresolved-reference warnings when XML docs are generated. Either add the missing member or remove the cref references.

Also applies to: 1629-1630

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/LdapUtils.cs` around lines 1456 - 1457, The XML doc comments in
LdapUtils.cs reference a non-existent member DomainInfo.FromFallback which
causes unresolved cref warnings; fix by either adding a boolean member/property
named FromFallback to the DomainInfo class (and document it) or update the XML
docs to remove or replace the cref with an existing member (e.g., an actual
property like DomainInfo.IsFallback or simply remove the cref). Update all
occurrences that reference DomainInfo.FromFallback so the cref targets a real
symbol or is removed to eliminate the unresolved-reference warnings.

Comment thread src/CommonLib/LdapUtils.cs
@rvazarkar
Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
test/unit/GPOLocalGroupProcessorTest.cs (1)

326-327: Add coverage for the new failure path.

This only locks in the (true, DomainInfo) branch. The new early return in src/CommonLib/Processors/GPOLocalGroupProcessor.cs at Line 70 is still untested when GetDomainInfoAsync() fails or returns an empty Name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/GPOLocalGroupProcessorTest.cs` around lines 326 - 327, Add a unit
test in GPOLocalGroupProcessorTest that exercises the new early-return failure
path in GPOLocalGroupProcessor: mock LDAPUtils.GetDomainInfoAsync() to return
failure (e.g., (false, null)) and a separate case where it returns (true, new
DomainInfo{name = ""}) or DomainInfo with an empty Name, then call the
GPOLocalGroupProcessor.Process* method (the method under test) and assert the
processor returns early/does not proceed (verify no further LDAP calls, no
changes, or expected return value). Use the same mock setup pattern as the
existing test (mockLDAPUtils.Setup(x =>
x.GetDomainInfoAsync()).ReturnsAsync(...)) and assert expectations on the
processor behavior to cover both failure and empty-name branches referenced by
GPOLocalGroupProcessor and DomainInfo.
src/CommonLib/ILdapUtils.cs (1)

92-109: Clarify the caching semantics in these XML docs.

The wording here reads like disabling AllowFallbackToUncontrolledLdap guarantees no fallback-derived DomainInfo can be observed. In this PR, cached entries populated earlier can still be returned with the flag off because that path performs no network I/O, so the docs should call that out explicitly.

Based on learnings, LdapConfig.AllowFallbackToUncontrolledLdap is intentionally a gate on network activity only; cached _domainInfoCache entries may still be returned regardless of the current flag setting because serving cache performs no network I/O.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/ILdapUtils.cs` around lines 92 - 109, Update the XML
documentation on both GetDomainInfoAsync(string) and GetDomainInfoAsync() to
explicitly state that LdapConfig.AllowFallbackToUncontrolledLdap only controls
whether network-based fallback
(System.DirectoryServices.ActiveDirectory.Domain.GetDomain) is attempted, and
does not prevent returning previously cached DomainInfo from the internal
_domainInfoCache; clarify that cache hits may be returned even when the flag is
false because serving from cache performs no network I/O. Mention both the flag
name (LdapConfig.AllowFallbackToUncontrolledLdap) and the cache field name
(_domainInfoCache) so readers can locate the related logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CommonLib/ConnectionPoolManager.cs`:
- Around line 147-159: The legacy ADSI bind (Helpers.CreateDirectoryEntry) is
still invoked before using the new helper; update the flow so you call
LdapUtils.GetDomainInfoStaticAsync(domainName, _ldapConfig, _log) and inspect
the returned info (DomainSid or availability) before calling
Helpers.CreateDirectoryEntry($"LDAP://{domainName}", _ldapConfig). If
GetDomainInfoStaticAsync returns useful info (infoOk &&
!string.IsNullOrEmpty(info?.DomainSid)) short-circuit and use
Cache.AddDomainSidMapping/return, otherwise fall back to the existing
Helpers.CreateDirectoryEntry path; this prevents uncontrolled/serverless ADSI
lookups from running prior to
ResolveIdentifier/GetPool/GetLdapConnectionForServer logic.

In `@src/CommonLib/ILdapUtils.cs`:
- Around line 92-109: The added methods GetDomainInfoAsync(string) and
GetDomainInfoAsync() on ILdapUtils are source-breaking; instead of modifying
ILdapUtils, create a new interface (e.g. IDomainInfoResolver or
ILdapDomainResolver) that declares Task<(bool Success, DomainInfo DomainInfo)>
GetDomainInfoAsync(string) and Task<(bool Success, DomainInfo DomainInfo)>
GetDomainInfoAsync(), implement that new interface in the classes that provide
domain resolution, and update callers that need this functionality to depend on
the new interface rather than changing the existing ILdapUtils contract.

In `@src/CommonLib/LdapConnectionPool.cs`:
- Around line 917-925: The foreach over info.DomainControllers in
LdapConnectionPool (strategy 6) can throw when GetDomainInfoStaticAsync returns
a partial DomainInfo without DomainControllers; guard this path by checking that
info.DomainControllers is not null and has any entries before iterating, and if
empty/null skip strategy 6 (log a debug/info message) instead of allowing the
exception to bubble; update the block around CreateLDAPConnectionWithPortCheck
and the return path accordingly so only valid controllers are attempted.

In `@src/CommonLib/LdapUtils.cs`:
- Around line 1410-1425: The Domain instance returned by Domain.GetDomain
(stored in the local variable domain) is not disposed; update
TryResolveHintViaUncontrolledGetDomain so that after you read domain.Name you
dispose the Domain object (e.g., wrap Domain.GetDomain and the access of
domain.Name in a using or ensure domain.Dispose() in a finally) before
returning; preserve setting _uncontrolledGetDomainHint and domainName from name
and keep the existing catch/log behavior for exceptions.
- Around line 1670-1760: The Domain returned by Domain.GetDomain(context) and
the DirectoryEntry returned by domain.GetDirectoryEntry() are IDisposable and
must be disposed to avoid native handle leaks; wrap the Domain usage (the
variable domain) in a using (or try/finally with domain?.Dispose()) around all
accesses (forest, PdcRoleOwner, DomainControllers, GetDirectoryEntry) and
similarly ensure the rawEntry (and any DirectoryEntry-derived object from
rawEntry.ToDirectoryObject()) is disposed after extracting domainSid (use using
or finally on rawEntry and the converted entry); preserve the existing try/catch
behavior and the order of operations but add disposal for Domain and
DirectoryEntry objects (referencing Domain.GetDomain(context),
domain.GetDirectoryEntry(), rawEntry and the entry produced by
ToDirectoryObject()).

---

Nitpick comments:
In `@src/CommonLib/ILdapUtils.cs`:
- Around line 92-109: Update the XML documentation on both
GetDomainInfoAsync(string) and GetDomainInfoAsync() to explicitly state that
LdapConfig.AllowFallbackToUncontrolledLdap only controls whether network-based
fallback (System.DirectoryServices.ActiveDirectory.Domain.GetDomain) is
attempted, and does not prevent returning previously cached DomainInfo from the
internal _domainInfoCache; clarify that cache hits may be returned even when the
flag is false because serving from cache performs no network I/O. Mention both
the flag name (LdapConfig.AllowFallbackToUncontrolledLdap) and the cache field
name (_domainInfoCache) so readers can locate the related logic.

In `@test/unit/GPOLocalGroupProcessorTest.cs`:
- Around line 326-327: Add a unit test in GPOLocalGroupProcessorTest that
exercises the new early-return failure path in GPOLocalGroupProcessor: mock
LDAPUtils.GetDomainInfoAsync() to return failure (e.g., (false, null)) and a
separate case where it returns (true, new DomainInfo{name = ""}) or DomainInfo
with an empty Name, then call the GPOLocalGroupProcessor.Process* method (the
method under test) and assert the processor returns early/does not proceed
(verify no further LDAP calls, no changes, or expected return value). Use the
same mock setup pattern as the existing test (mockLDAPUtils.Setup(x =>
x.GetDomainInfoAsync()).ReturnsAsync(...)) and assert expectations on the
processor behavior to cover both failure and empty-name branches referenced by
GPOLocalGroupProcessor and DomainInfo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae63858a-1177-481c-815f-e4e52182c227

📥 Commits

Reviewing files that changed from the base of the PR and between d54aebf and 78ddc21.

📒 Files selected for processing (11)
  • src/CommonLib/ConnectionPoolManager.cs
  • src/CommonLib/DomainInfo.cs
  • src/CommonLib/Enums/LDAPProperties.cs
  • src/CommonLib/ILdapUtils.cs
  • src/CommonLib/LdapConfig.cs
  • src/CommonLib/LdapConnectionPool.cs
  • src/CommonLib/LdapUtils.cs
  • src/CommonLib/Processors/GPOLocalGroupProcessor.cs
  • test/unit/Facades/MockLdapUtils.cs
  • test/unit/GPOLocalGroupProcessorTest.cs
  • test/unit/LDAPUtilsTest.cs

Comment thread src/CommonLib/ConnectionPoolManager.cs
Comment on lines +92 to +109
/// <summary>
/// Resolves a <see cref="DomainInfo"/> for the specified domain using controlled LDAP queries
/// that honor the configured <see cref="LdapConfig"/> (server, port, SSL, auth, signing, cert verification).
/// Falls back to <c>System.DirectoryServices.ActiveDirectory.Domain.GetDomain</c> only when
/// <see cref="LdapConfig.AllowFallbackToUncontrolledLdap"/> is enabled.
/// </summary>
/// <param name="domainName">The domain name to resolve</param>
/// <returns>A tuple containing success state as well as the populated DomainInfo if successful</returns>
Task<(bool Success, DomainInfo DomainInfo)> GetDomainInfoAsync(string domainName);

/// <summary>
/// Resolves a <see cref="DomainInfo"/> for the user's current domain using controlled LDAP queries
/// that honor the configured <see cref="LdapConfig"/>. Falls back to
/// <c>System.DirectoryServices.ActiveDirectory.Domain.GetDomain</c> only when
/// <see cref="LdapConfig.AllowFallbackToUncontrolledLdap"/> is enabled.
/// </summary>
/// <returns>A tuple containing success state as well as the populated DomainInfo if successful</returns>
Task<(bool Success, DomainInfo DomainInfo)> GetDomainInfoAsync();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This public interface change is source-breaking.

Adding two required members to ILdapUtils will break any downstream implementation at compile time, so this cannot be treated as a non-breaking feature. If compatibility matters, introduce a new interface or adapter for domain-info resolution instead of extending the existing public contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/ILdapUtils.cs` around lines 92 - 109, The added methods
GetDomainInfoAsync(string) and GetDomainInfoAsync() on ILdapUtils are
source-breaking; instead of modifying ILdapUtils, create a new interface (e.g.
IDomainInfoResolver or ILdapDomainResolver) that declares Task<(bool Success,
DomainInfo DomainInfo)> GetDomainInfoAsync(string) and Task<(bool Success,
DomainInfo DomainInfo)> GetDomainInfoAsync(), implement that new interface in
the classes that provide domain resolution, and update callers that need this
functionality to depend on the new interface rather than changing the existing
ILdapUtils contract.

Comment thread src/CommonLib/LdapConnectionPool.cs
Comment thread src/CommonLib/LdapUtils.cs Outdated
Comment thread src/CommonLib/LdapUtils.cs
fix: ensure all paths respect the ldapconfig.server configuration
…d to cache misses

fix: domain/sid mapping properly caches in both directions
fix: set null cache key before writes to ensure conflicts are resolved properly
fix: invalid cached path
fix:  DCInfoCache case sensitivity
fix: a race condition for domain info resolution that could result in multiple callers attempting to resolve the same domain, use lazy loading instead
@rvazarkar
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CommonLib/Cache.cs`:
- Around line 175-192: NormalizeCaseInsensitiveCaches() will throw if the
persisted source dictionaries contain keys that collide under
StringComparer.OrdinalIgnoreCase; instead of calling the ConcurrentDictionary
constructor with the entire source, create new ConcurrentDictionary instances
for SIDToDomainCache, GlobalCatalogCache, and ValueToIdCache with
StringComparer.OrdinalIgnoreCase and copy entries into them entry-by-entry using
TryAdd (or TryAdd-equivalent) so duplicate keys differing only by case are
ignored and the cache is "healed" rather than crashing; update the code paths
that assign CacheInstance.SIDToDomainCache, CacheInstance.GlobalCatalogCache,
and CacheInstance.ValueToIdCache to perform this safe per-entry copy.

In `@src/CommonLib/LdapUtils.cs`:
- Around line 44-49: The domain hint caches (_uncontrolledGetDomainHint and
_currentDomain) are not invalidated when LDAP credentials/config change; update
SetLdapConfig() to clear or re-key those caches whenever CurrentUserDomain,
Username, Password or other auth-related config is set (in addition to
rebuilding _connectionPool), so the methods ResolveEffectiveDomainHint and any
callers of _currentDomain won't return stale values; alternatively implement
cache entries keyed by the current config (e.g., include a config fingerprint)
and ensure ResetUtils() still clears them.
- Around line 36-37: The static cache _domainInfoCache is poisoned when
TryResolveDomainInfoViaDirectLdapAsync returns a DomainInfo whose Name differs
from the requested domainName (because config.Server overrides bind target);
update caching so entries are keyed by the effective bind target/config or by
the resolved canonical name instead of blindly caching under the requested
domainName: modify TryResolveDomainInfoViaDirectLdapAsync, GetDomainInfoAsync
and GetDomainInfoStaticAsync to compute a cache key that includes either the
resolved DomainInfo.Name or the effective LDAP server/credential identity (e.g.,
combine config.Server or resolved.Name with domainName), and only insert into
_domainInfoCache under that computed key — if resolved.Name != requested
domainName, do not cache under the original request key (or also add a separate
entry keyed by resolved.Name) so callers without overrides cannot read incorrect
SID/DN/forest from cache.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f10704a7-60ce-4e57-bdd6-4c4cec85dfc2

📥 Commits

Reviewing files that changed from the base of the PR and between 78ddc21 and a90d8a3.

📒 Files selected for processing (7)
  • src/CommonLib/Cache.cs
  • src/CommonLib/ConnectionPoolManager.cs
  • src/CommonLib/LdapConnectionPool.cs
  • src/CommonLib/LdapUtils.cs
  • src/CommonLib/Processors/GPOLocalGroupProcessor.cs
  • test/unit/CacheTest.cs
  • test/unit/LDAPUtilsTest.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/CommonLib/Processors/GPOLocalGroupProcessor.cs

Comment thread src/CommonLib/Cache.cs
Comment on lines +36 to +37
private static ConcurrentDictionary<string, DomainInfo> _domainInfoCache =
new(StringComparer.OrdinalIgnoreCase);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

_domainInfoCache can be poisoned by LdapConfig.Server overrides.

TryResolveDomainInfoViaDirectLdapAsync() explicitly allows the resolved DomainInfo.Name to differ from the requested domainName when config.Server points at a DC in another domain, but both GetDomainInfoAsync() and GetDomainInfoStaticAsync() still cache that record under the requested key. Because _domainInfoCache is static and keyed only by domain string, a later caller without that override can read the wrong SID / distinguished name / forest from cache. Either scope the cache by effective bind target/config, or skip caching under the request key when the canonical name disagrees.

Also applies to: 1313-1448, 2147-2152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CommonLib/LdapUtils.cs` around lines 36 - 37, The static cache
_domainInfoCache is poisoned when TryResolveDomainInfoViaDirectLdapAsync returns
a DomainInfo whose Name differs from the requested domainName (because
config.Server overrides bind target); update caching so entries are keyed by the
effective bind target/config or by the resolved canonical name instead of
blindly caching under the requested domainName: modify
TryResolveDomainInfoViaDirectLdapAsync, GetDomainInfoAsync and
GetDomainInfoStaticAsync to compute a cache key that includes either the
resolved DomainInfo.Name or the effective LDAP server/credential identity (e.g.,
combine config.Server or resolved.Name with domainName), and only insert into
_domainInfoCache under that computed key — if resolved.Name != requested
domainName, do not cache under the original request key (or also add a separate
entry keyed by resolved.Name) so callers without overrides cannot read incorrect
SID/DN/forest from cache.

Comment thread src/CommonLib/LdapUtils.cs Outdated
fix: reset currentDomain when changing LDAPConfig
… the logic

fix: reset DCInfoCache when utils are reset
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