diff --git a/CHANGELOG.md b/CHANGELOG.md index a952b0f..5825b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.6.1 - TBD + Remove length check for `sAMAccountName` when used in the `-Identity` parameter ++ Fix the search base and scope when using `Get-OpenAD*` with the `-Identity` parameter, it should now be possible to find objects not in the `defaultNamingContext` ## v0.6.0 - 2025-03-12 diff --git a/src/PSOpenAD.Module/Commands/GetOpenAD.cs b/src/PSOpenAD.Module/Commands/GetOpenAD.cs index 0bbba2e..93436a3 100644 --- a/src/PSOpenAD.Module/Commands/GetOpenAD.cs +++ b/src/PSOpenAD.Module/Commands/GetOpenAD.cs @@ -113,6 +113,8 @@ public abstract class GetOpenADOperation : OpenADSessionCmdletBase protected override void ProcessRecordWithSession(OpenADSession session) { + string searchBase = SearchBase ?? session.DefaultNamingContext; + SearchScope searchScope = SearchScope; LDAPFilter finalFilter; if (!string.IsNullOrWhiteSpace(LDAPFilter)) { @@ -153,7 +155,17 @@ protected override void ProcessRecordWithSession(OpenADSession session) } else if (Identity != null) { - finalFilter = new FilterAnd(new[] { FilteredClass, Identity.LDAPFilter }); + if (string.IsNullOrEmpty(Identity.DistinguishedName)) + { + finalFilter = new FilterAnd(new[] { FilteredClass, Identity.LDAPFilter }); + } + else + { + // If searching by DN we want to match just that object. + finalFilter = FilteredClass; + searchBase = Identity.DistinguishedName; + searchScope = SearchScope.Base; + } } else { @@ -216,18 +228,17 @@ protected override void ProcessRecordWithSession(OpenADSession session) return; } - string searchBase = SearchBase ?? session.DefaultNamingContext; - bool outputResult = false; - HashSet finalObjectProperties = requestedProperties .Where(v => v != "*" && (showAll || explicitProperties.Contains(v, _caseInsensitiveComparer))) .ToHashSet(); - foreach (SearchResultEntry result in SearchRequest(session, searchBase, finalFilter, - requestedProperties.ToArray(), serverControls)) + bool noSuchObject = true; + foreach (SearchResultEntry result in SearchRequest(session, searchBase, searchScope, finalFilter, + requestedProperties.ToArray(), serverControls, (r) => r.ResultCode == LDAPResultCode.NoSuchObject)) { + noSuchObject = false; OpenADEntity adObj = CreateOutputObject( session, result, @@ -236,13 +247,12 @@ protected override void ProcessRecordWithSession(OpenADSession session) this ); ProcessOutputObject(PSObject.AsPSObject(adObj)); - outputResult = true; WriteObject(adObj); } - if (ParameterSetName.EndsWith("Identity") && !outputResult) + if (noSuchObject && ParameterSetName.EndsWith("Identity")) { - string msg = $"Cannot find an object with identity filter: '{finalFilter}' under: '{searchBase}'"; + string msg = $"Cannot find an object with identity filter '{finalFilter}' under search base '{searchBase}' with scope {searchScope}"; ErrorRecord rec = new(new ItemNotFoundException(msg), "IdentityNotFound", ErrorCategory.ObjectNotFound, finalFilter.ToString()); WriteError(rec); @@ -252,13 +262,14 @@ protected override void ProcessRecordWithSession(OpenADSession session) internal virtual IEnumerable SearchRequest( OpenADSession session, string searchBase, + SearchScope searchScope, LDAPFilter filter, string[] attributes, - IList? serverControls - ) + IList? serverControls, + Func errorHandler) { - return Operations.LdapSearchRequest(session.Connection, searchBase, SearchScope, 0, session.OperationTimeout, - filter, attributes, serverControls, CancelToken, this, false); + return Operations.LdapSearchRequest(session.Connection, searchBase, searchScope, 0, session.OperationTimeout, + filter, attributes, serverControls, CancelToken, this, false, errorHandler); } internal virtual void ProcessOutputObject(PSObject obj) { } diff --git a/src/PSOpenAD.Module/Commands/OpenADGroupMember.cs b/src/PSOpenAD.Module/Commands/OpenADGroupMember.cs index 1d47002..71666b1 100644 --- a/src/PSOpenAD.Module/Commands/OpenADGroupMember.cs +++ b/src/PSOpenAD.Module/Commands/OpenADGroupMember.cs @@ -1,4 +1,5 @@ using PSOpenAD.LDAP; +using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; @@ -28,14 +29,15 @@ internal override OpenADObject CreateADObject(Dictionary SearchRequest( OpenADSession session, string searchBase, + SearchScope searchScope, LDAPFilter filter, string[] attributes, - IList? serverControls - ) + IList? serverControls, + Func errorHandler) { foreach (SearchResultEntry group in Operations.LdapSearchRequest(session.Connection, searchBase, - SearchScope, 1, session.OperationTimeout, filter, new[] { "primaryGroupToken" }, serverControls, - CancelToken, this, false)) + searchScope, 1, session.OperationTimeout, filter, new[] { "primaryGroupToken" }, serverControls, + CancelToken, this, false, errorHandler)) { // use memberOf rather than member to make recursive search easier & avoid paging LDAPFilter memberOfFilter; @@ -77,8 +79,8 @@ internal override IEnumerable SearchRequest( _currentGroupDN = group.ObjectName; try { - foreach (SearchResultEntry result in Operations.LdapSearchRequest(session.Connection, searchBase, - SearchScope, 0, session.OperationTimeout, memberOfFilter, attributes, serverControls, CancelToken, + foreach (SearchResultEntry result in Operations.LdapSearchRequest(session.Connection, session.DefaultNamingContext, + SearchScope.Subtree, 0, session.OperationTimeout, memberOfFilter, attributes, serverControls, CancelToken, this, false)) { yield return result; diff --git a/src/PSOpenAD.Module/Commands/OpenADPrincipalGroupMembership.cs b/src/PSOpenAD.Module/Commands/OpenADPrincipalGroupMembership.cs index b4a1dbd..5c98e12 100644 --- a/src/PSOpenAD.Module/Commands/OpenADPrincipalGroupMembership.cs +++ b/src/PSOpenAD.Module/Commands/OpenADPrincipalGroupMembership.cs @@ -1,5 +1,6 @@ using PSOpenAD.LDAP; using PSOpenAD.Security; +using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; @@ -28,14 +29,15 @@ internal override OpenADObject CreateADObject(Dictionary SearchRequest( OpenADSession session, string searchBase, + SearchScope searchScope, LDAPFilter filter, string[] attributes, - IList? serverControls - ) + IList? serverControls, + Func errorHandler) { foreach (SearchResultEntry principal in Operations.LdapSearchRequest(session.Connection, searchBase, - SearchScope, 1, session.OperationTimeout, filter, new[] { "memberOf", "objectSid", "primaryGroupID" }, - serverControls, CancelToken, this, false)) + searchScope, 1, session.OperationTimeout, filter, new[] { "memberOf", "objectSid", "primaryGroupID" }, + serverControls, CancelToken, this, false, errorHandler)) { FilterEquality? primaryGroupFilter = null; LDAPFilter groupMembershipFilter; @@ -80,8 +82,8 @@ internal override IEnumerable SearchRequest( try { - foreach (SearchResultEntry result in Operations.LdapSearchRequest(session.Connection, searchBase, - SearchScope, 0, session.OperationTimeout, groupMembershipFilter, attributes, serverControls, + foreach (SearchResultEntry result in Operations.LdapSearchRequest(session.Connection, session.DefaultNamingContext, + SearchScope.Subtree, 0, session.OperationTimeout, groupMembershipFilter, attributes, serverControls, CancelToken, this, false)) { yield return result; diff --git a/src/PSOpenAD/ADIdentity.cs b/src/PSOpenAD/ADIdentity.cs index e4ca6f7..67f2b84 100644 --- a/src/PSOpenAD/ADIdentity.cs +++ b/src/PSOpenAD/ADIdentity.cs @@ -21,7 +21,7 @@ public ADObjectIdentity(string value) } else { - LDAPFilter = DistinguishedNameFilter(value); + LDAPFilter = new FilterPresent("objectClass"); DistinguishedName = value; } @@ -53,11 +53,6 @@ internal static LDAPFilter ObjectGUIDFilter(Guid objectGuid) { return new FilterEquality("objectGUID", objectGuid.ToByteArray()); } - - internal static LDAPFilter DistinguishedNameFilter(string dn) - { - return new FilterEquality("distinguishedName", LDAPFilter.EncodeSimpleFilterValue(dn)); - } } public class ADPrincipalIdentity : ADObjectIdentity @@ -76,7 +71,7 @@ public ADPrincipalIdentity(string value) LDAPFilter = filter; else { - LDAPFilter = DistinguishedNameFilter(value); + LDAPFilter = new FilterPresent("objectClass"); DistinguishedName = value; } } diff --git a/src/PSOpenAD/Operations.cs b/src/PSOpenAD/Operations.cs index 0d1db0b..60f6c55 100644 --- a/src/PSOpenAD/Operations.cs +++ b/src/PSOpenAD/Operations.cs @@ -171,6 +171,7 @@ public static ModifyDNResponse LdapModifyDNRequest( /// Token to cancel any network IO waits /// The PSCmdlet that is running the operation. /// Ignore errors and do not write to the error stream. + /// Optional search state to track if errors occurred during the search. /// Yields each returned result containing the attributes requested from the search request. public static IEnumerable LdapSearchRequest( IADConnection connection, @@ -183,8 +184,8 @@ public static IEnumerable LdapSearchRequest( IList? controls, CancellationToken cancelToken, PSCmdlet? cmdlet, - bool ignoreErrors - ) + bool ignoreErrors, + Func? errorHandler = null) { cmdlet?.WriteVerbose($"Starting LDAP search request at '{searchBase}' for {scope} - {filter}"); @@ -236,6 +237,10 @@ bool ignoreErrors error.ErrorDetails.RecommendedAction = "Perform request on one of the referral URIs"; cmdlet?.WriteError(error); } + else if (errorHandler is not null && errorHandler(resultDone.Result)) + { + break; + } else if (!ignoreErrors && resultDone.Result.ResultCode != LDAPResultCode.Success) { ErrorRecord error = new( diff --git a/tests/Get-OpenADObject.Tests.ps1 b/tests/Get-OpenADObject.Tests.ps1 index f25f702..51496e8 100644 --- a/tests/Get-OpenADObject.Tests.ps1 +++ b/tests/Get-OpenADObject.Tests.ps1 @@ -48,10 +48,10 @@ Describe "Get-OpenADObject cmdlets" -Skip:(-not $PSOpenADSettings.Server) { } It "Fails to find entry with identity" { - $actual = Get-OpenADObject -Session $session -Identity invalid-id -ErrorAction SilentlyContinue -ErrorVariable err + $actual = Get-OpenADObject -Session $session -Identity CN=invalid-id -ErrorAction SilentlyContinue -ErrorVariable err $actual | Should -BeNullOrEmpty $err.Count | Should -Be 1 - $err[0].Exception.Message | Should -BeLike "Cannot find an object with identity filter: '(&(objectClass=*)(distinguishedName=invalid-id))' under: *" + $err[0].Exception.Message | Should -BeLike "Cannot find an object with identity filter '(objectClass=*)' under search base 'CN=invalid-id' with scope Base" } It "Requests non-default property with case insensitive name" { @@ -63,6 +63,12 @@ Describe "Get-OpenADObject cmdlets" -Skip:(-not $PSOpenADSettings.Server) { $actual = Get-OpenADObject -Session $session -LDAPFilter '(objectClass=some-invalid-class)' $actual | Should -BeNullOrEmpty } + + It "Finds by identity in non default naming context" { + $dse = Get-OpenADRootDSE -Session $session + $actual = Get-OpenADObject -Session $session -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$($dse.ConfigurationNamingContext)" -Properties sPNMappings + $actual.sPNMappings | Should -Not -BeNullOrEmpty + } } Context "Get-OpenADComputer" { @@ -285,7 +291,7 @@ Describe "Get-OpenADObject cmdlets" -Skip:(-not $PSOpenADSettings.Server) { $actual = Get-OpenADUser -Session $session -Identity MyTestContact -ErrorAction SilentlyContinue -ErrorVariable err $actual | Should -BeNullOrEmpty $err.Count | Should -Be 1 - $err[0].Exception.Message | Should -BeLike "Cannot find an object with identity filter: '(&(&(objectCategory=person)(objectClass=user))(sAMAccountName=MyTestContact))' under: *" + $err[0].Exception.Message | Should -BeLike "Cannot find an object with identity filter '(&(&(objectCategory=person)(objectClass=user))(sAMAccountName=MyTestContact))' under search base * with scope Subtree" } finally { $contact | Remove-OpenADObject -Session $session