Providers/AD/Private/Resolve-ADTestGroupMember.ps1

function Resolve-ADTestGroupMember {
    <#
    .SYNOPSIS
        Resolves the members a seeded group's rule names, inside the seed OU only

    .DESCRIPTION
        A row in ADSecurityGroups.csv with AutoAssignment set carries its membership rule in
        three columns, and this is the one place they are interpreted:

        - MemberFilter an Active Directory filter, as Get-ADUser -Filter takes it, over the
                        users in the seed OU: "Department -eq 'Sales' -and Title -like '*Manager*'".
        - MemberSource blank or User for the users the filter matches; DeviceOwner to run the
                        filter over the seeded computers instead and return their managedBy owners.
        - MemberLimit a number, to take only the first so many, ordered by account name so the
                        same people are chosen on every run.

        Every search is scoped to the seed's root OU. The rules once lived as a regex switch in
        the group step, searching the whole domain, and a rule such as "every enabled account"
        took in every enabled account the domain held: a live run put twelve real accounts,
        Administrator among them, into seeded groups. A device owner is looked up by identity,
        which cannot take a search base, so the owner's distinguished name is checked instead.

        The result is unique by distinguished name. A rule that matches the same person twice
        used to count that person as two members added.

    .PARAMETER Rule
        The CSV row: GroupName, MemberFilter, MemberSource and MemberLimit are read.

    .PARAMETER SeedRoot
        The distinguished name of the seed's root OU.

    .OUTPUTS
        Microsoft.ActiveDirectory.Management.ADUser, or nothing when the rule matches nobody.

    .EXAMPLE
        PS> Resolve-ADTestGroupMember -Rule $row -SeedRoot 'OU=ZZ-TEST-TestData,DC=contoso,DC=com'

        DESCRIPTION: Resolves one group's members
        OUTPUT: The users the row's rule names, each once
        USE CASE: Called by New-ADTestSecurityGroups for every row with AutoAssignment set

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object]$Rule,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$SeedRoot
    )

    $filter = [string]$Rule.MemberFilter
    if ([string]::IsNullOrWhiteSpace($filter)) {
        Write-Verbose "No membership rule for $($Rule.GroupName); the group is left empty"
        return
    }

    $source = if ($Rule.PSObject.Properties['MemberSource'] -and $Rule.MemberSource) { [string]$Rule.MemberSource } else { 'User' }

    $members = switch ($source) {
        'DeviceOwner' {
            # The filter picks computers; the members are the people those computers are
            # managed by. -Identity cannot be combined with -SearchBase, so an owner outside
            # the seed OU is dropped by its distinguished name instead.
            $owners = @(Get-ADComputer -Filter $filter -SearchBase $SeedRoot -Properties ManagedBy -ErrorAction Stop |
                    Where-Object { $_.ManagedBy } | ForEach-Object { $_.ManagedBy } | Sort-Object -Unique)
            foreach ($owner in $owners) {
                if ($owner -notlike "*,$SeedRoot") { continue }
                Get-ADUser -Identity $owner -ErrorAction SilentlyContinue
            }
        }
        'User' {
            Get-ADUser -Filter $filter -SearchBase $SeedRoot -ErrorAction Stop
        }
        default {
            throw "Group '$($Rule.GroupName)' names an unknown MemberSource '$source'. Use User or DeviceOwner."
        }
    }

    $members = @($members | Where-Object { $_ -and $_.DistinguishedName } | Sort-Object -Property DistinguishedName -Unique)

    if ($Rule.PSObject.Properties['MemberLimit'] -and -not [string]::IsNullOrWhiteSpace([string]$Rule.MemberLimit)) {
        $limit = [int]$Rule.MemberLimit
        # Ordered by account name first, so the limit picks the same people on every run rather
        # than whichever the directory happened to return first.
        $members = @($members | Sort-Object -Property SamAccountName | Select-Object -First $limit)
    }

    $members
}