Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1f1902d
should fix issue... who knows
santisq Sep 10, 2025
f176650
bump module ver
santisq Sep 10, 2025
548c87c
remove commented method, no longer needed
santisq Sep 10, 2025
09ecb57
remove unnecessary `continue`
santisq Sep 10, 2025
97be6e3
add preprocessor directives for using statements
santisq Sep 10, 2025
90c8db4
simplifies method. removes the need for SB.
santisq Sep 11, 2025
19e9f77
this commit introduces logic for re-using the TreeCache per input obj…
santisq Sep 12, 2025
dd09d8e
....
santisq Sep 12, 2025
9eac3d2
refactoring... simplifying
santisq Sep 13, 2025
aad4f00
refactoring... simplifying
santisq Sep 13, 2025
7d2f1d3
refactoring... simplifying... almost there, next same for PrincipalGr…
santisq Sep 15, 2025
ac1c14f
refactoring... simplifying... almost there, next same for PrincipalGr…
santisq Sep 15, 2025
f281c0b
abstracted most of the logic to base cmdlet
santisq Sep 17, 2025
30530a9
improves caching mechanism
santisq Sep 17, 2025
a1a6b6b
improves caching mechanism
santisq Sep 17, 2025
12b4c1c
improves caching mechanism
santisq Sep 18, 2025
42169fa
some changes to see if it works now :(
santisq Sep 18, 2025
0c7146e
for now member cmdlet seem to be working
santisq Sep 18, 2025
8c827c2
depth to get only
santisq Sep 21, 2025
09618e3
renames Exception.cs. Moves extensions to their own folder. Makes Dep…
santisq Sep 25, 2025
5b1ab7f
Merge branch '16-cross-store-reference-resolution-error' of https://g…
santisq Sep 25, 2025
cd5d310
renames Exception.cs. Moves extensions to their own folder. Makes Dep…
santisq Sep 25, 2025
cd6dac6
looks like final version. both cmdlets looking good
santisq Sep 29, 2025
3819892
adds perf tests...
santisq Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion module/PSADTree.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
}

# Version number of this module.
ModuleVersion = '1.1.5'
ModuleVersion = '1.1.6'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down
201 changes: 42 additions & 159 deletions src/PSADTree/Commands/GetADTreeGroupMemberCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Management.Automation;
using PSADTree.Extensions;

namespace PSADTree.Commands;

Expand All @@ -17,150 +19,46 @@ public sealed class GetADTreeGroupMemberCommand : PSADTreeCmdletBase
[Parameter]
public SwitchParameter Group { get; set; }

protected override void ProcessRecord()
{
Dbg.Assert(Identity is not null);
Dbg.Assert(_context is not null);
_truncatedOutput = false;

try
{
using GroupPrincipal? group = GroupPrincipal.FindByIdentity(_context, Identity);
if (group is null)
{
WriteError(Exceptions.IdentityNotFound(Identity));
return;
}

TreeObjectBase[] result = Traverse(
groupPrincipal: group,
source: group.DistinguishedName);
protected override Principal GetFirstPrincipal() => GroupPrincipal.FindByIdentity(Context, Identity);

DisplayWarningIfTruncatedOutput();
WriteObject(sendToPipeline: result, enumerateCollection: true);
}
catch (Exception _) when (_ is PipelineStoppedException or FlowControlException)
{
throw;
}
catch (MultipleMatchesException exception)
{
WriteError(exception.AmbiguousIdentity(Identity));
}
catch (Exception exception)
{
WriteError(exception.Unspecified(Identity));
}
}

private TreeObjectBase[] Traverse(
GroupPrincipal groupPrincipal,
string source)
protected override void HandleFirstPrincipal(Principal principal)
{
int depth;
Clear();
Push(groupPrincipal, new TreeGroup(source, groupPrincipal));

while (_stack.Count > 0)
if (principal is GroupPrincipal group && !ShouldExclude(principal))
{
(GroupPrincipal? current, TreeGroup treeGroup) = _stack.Pop();

try
{
depth = treeGroup.Depth + 1;

// if this node has been already processed
if (!_cache.TryAdd(treeGroup))
{
current?.Dispose();
treeGroup.Hook(_cache);
_index.Add(treeGroup);

// if it's a circular reference, go next
if (TreeCache.IsCircular(treeGroup))
{
treeGroup.SetCircularNested();
continue;
}

// else, if we want to show all nodes
if (ShowAll.IsPresent)
{
// reconstruct the output without querying AD again
EnumerateMembers(treeGroup, depth);
continue;
}

// else, just skip this reference and go next
treeGroup.SetProcessed();
continue;
}

using PrincipalSearchResult<Principal>? search = current?.GetMembers();

if (search is not null)
{
EnumerateMembers(treeGroup, search, source, depth);
}

_index.Add(treeGroup);
_index.TryAddPrincipals();
current?.Dispose();
}
catch (Exception _) when (_ is PipelineStoppedException or FlowControlException)
{
throw;
}
catch (Exception exception)
{
WriteError(exception.EnumerationFailure(current));
}
string source = group.DistinguishedName;
PushToStack(new TreeGroup(source, group), group);
}

return _index.GetTree();
}

private void EnumerateMembers(
protected override void BuildFromAD(
TreeGroup parent,
PrincipalSearchResult<Principal> searchResult,
GroupPrincipal groupPrincipal,
string source,
int depth)
{
foreach (Principal member in searchResult.GetSortedEnumerable(_comparer))
IEnumerable<Principal> members = groupPrincipal.ToSafeSortedEnumerable(
selector: group => group.GetMembers(),
cmdlet: this,
comparer: Comparer);

foreach (Principal member in members)
{
IDisposable? disposable = null;
try
{
if (member is { DistinguishedName: null })
{
disposable = member;
continue;
}

if (member.StructuralObjectClass != "group")
if (member is { DistinguishedName: null } ||
member.StructuralObjectClass != "group" && Group.IsPresent ||
ShouldExclude(member))
{
disposable = member;
if (Group.IsPresent)
{
continue;
}
}

if (ShouldExclude(member, _exclusionPatterns))
{
continue;
}

TreeObjectBase treeObject = ProcessPrincipal(
ProcessPrincipal(
principal: member,
parent: parent,
source: source,
depth: depth);

if (ShowAll.IsPresent)
{
parent.AddChild(treeObject);
}
}
finally
{
Expand All @@ -169,62 +67,47 @@ private void EnumerateMembers(
}
}

private TreeObjectBase ProcessPrincipal(
protected override void BuildFromCache(TreeGroup parent, string source, int depth)
{
foreach (TreeObjectBase member in parent.Children)
{
if (member is TreeGroup treeGroup)
{
PushToStack((TreeGroup)treeGroup.Clone(parent, source, depth));
continue;
}

if (depth <= Depth)
{
Builder.Add(member.Clone(parent, source, depth));
}
}
}

private void ProcessPrincipal(
Principal principal,
TreeGroup parent,
string source,
int depth)
{
return principal switch
TreeObjectBase treeObject = principal switch
{
UserPrincipal user => AddTreeObject(new TreeUser(source, parent, user, depth)),
ComputerPrincipal computer => AddTreeObject(new TreeComputer(source, parent, computer, depth)),
GroupPrincipal group => HandleGroup(parent, group, source, depth),
GroupPrincipal group => ProcessGroup(parent, group, source, depth),
_ => throw new ArgumentOutOfRangeException(nameof(principal)),
};

parent.AddChild(treeObject);

TreeObjectBase AddTreeObject(TreeObjectBase obj)
{
if (depth <= Depth)
{
_index.AddPrincipal(obj);
Builder.Stage(obj);
}

return obj;
}

TreeObjectBase HandleGroup(
TreeGroup parent,
GroupPrincipal group,
string source,
int depth)
{
if (_cache.TryGet(group.DistinguishedName, out TreeGroup? treeGroup))
{
Push(group, (TreeGroup)treeGroup.Clone(parent, depth));
return treeGroup;
}

treeGroup = new TreeGroup(source, parent, group, depth);
Push(group, treeGroup);
return treeGroup;
}
}

private void EnumerateMembers(TreeGroup parent, int depth)
{
foreach (TreeObjectBase member in parent.Childs)
{
if (member is TreeGroup treeGroup)
{
Push(null, (TreeGroup)treeGroup.Clone(parent, depth));
continue;
}

if (depth <= Depth)
{
_index.Add(member.Clone(parent, depth));
}
}
}
}
Loading
Loading