Shared/ADHelpers.psm1

# private helpers for the other NestedModules
Set-StrictMode -Version Latest
$ErrorActionPreference = [Management.Automation.ActionPreference]::Stop
. $PSScriptRoot\Variables.ps1

function Get-LdapSearcher {
    [OutputType([DirectoryServices.DirectorySearcher])]
    [CmdletBinding()]
    param (
        # Path of the OU or container to search within, in DN form.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $SearchBase,

        # Path of the OU or container to search within, in DN form but without
        # the DC components. Only used when SearchBase is not provided.
        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $DefaultRelativeBase,

        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $Server,

        [Parameter(ValueFromPipelineByPropertyName)]
        [PSCredential] $Credential
    )
    process {
        $ldapPath = if ($Server) { "LDAP://$Server" } else { "LDAP://" }
        $domainEntry = if ($Credential) {
            [DirectoryServices.DirectoryEntry]::new($ldapPath, $Credential.UserName, $Credential.GetNetworkCredential().Password)
        } else {
            [DirectoryServices.DirectoryEntry]::new($ldapPath)
        }

        if (-not $SearchBase) {
            $domainDN = $domainEntry.distinguishedName
            $SearchBase = if ($DefaultRelativeBase) {
                "$DefaultRelativeBase,$domainDN"
            } else {
                $null
            }
        }
        if ($SearchBase) {
            $ldapPath += "/$SearchBase"
        }
        Write-Verbose "Creating DirectorySearcher for LDAP path $ldapPath"
        $searchBaseEntry = if ($Credential) {
            [DirectoryServices.DirectoryEntry]::new($ldapPath, $Credential.UserName, $Credential.GetNetworkCredential().Password)
        } else {
            # output
            [DirectoryServices.DirectoryEntry]::new($ldapPath)
        }
        [DirectoryServices.DirectorySearcher]::new($searchBaseEntry)
    }
}


function Get-DistinguishedNameComponent {
    <#
    .SYNOPSIS
        Filter the components of a DistinguishedName. Assumes that DN is
        ActiveDirectory-style, meaning CNs then OUs then DCs, no O or C or
        whatever.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    [CmdletBinding()]
    param (
        [Parameter([string])]
        $DistinguishedName,

        [switch] $CommonName,

        [switch] $OrganizationalUnit,

        [switch] $DomainComponent
    )
    process
    {
        # (?<!XXX) is negative lookbehind to handle escaped commas \,
        $components = $DistinguishedName -split '(?<!\\),' 
        $result = @()
        if ($CommonName) {
            $result += $components | Where-Object -Match "^CN=.*$"
        }
        if ($OrganizationalUnit) {
            $result += $components | Where-Object -Match "^OU=.*$"
        }
        if ($DomainName) {
            $result += $components | Where-Object -Match "^DC=.*$"
        }

        # output
        $result -join ','
    }
}


function Convert-ADIdentityToFilter {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [Parameter(ValueFromPipeline, Mandatory)]
        [string] $Identity
    )
    process {
        if ($Identity -match "^\*$") {
            throw [ArgumentException]::new("'*' cannot be used for -Identity parameters", 'Identity')
        }
        if ($Identity -match "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") {
            "(objectGUID=$Identity)"
        } elseif ($Identity -match "^S-\d-\d+-(\d+-){1,14}\d+$") {
            "(objectSid=$Identity)"
        } elseif ($Identity -match "^(?:(?<cn>CN=(?<name>[^,]*)),)?(?:(?<path>(?:(?:CN|OU)=[^,]+,?)+),)?(?<domain>(?:DC=[^,]+,?)+)$") {
            # regex from https://regexr.com/3l4au
            "(distinguishedName=$Identity)"
        } else {
            "(sAMAccountName=$Identity)"
        }
    }
}


function Update-ADUserEntry {
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [DirectoryServices.DirectoryEntry] $Entry
    )
    process {
        Update-DirectoryEntryFlag $Entry userAccountControl $UserAccountControl_ACCOUNT_DISABLED -NotePropertyName Enabled -TrueValue $false -FalseValue $true
    }
}