ADObject.psm1
|
Import-Module "$PSScriptRoot\Shared\ADHelpers.psm1" -Verbose:$false Import-Module "$PSScriptRoot\ADRootDSE.psm1" -Verbose:$false Set-StrictMode -Version Latest $ErrorActionPreference = [Management.Automation.ActionPreference]::Stop function Get-ADObject { <# .SYNOPSIS Retrieves an LDAP entry. .DESCRIPTION Retrieves an LDAP entry by their identity, which can be a distinguished name, GUID, SID, or sAMAccountName. .OUTPUTS [System.PSCustomObject] # $null if not found. #> [OutputType([PSCustomObject])] [CmdletBinding(DefaultParameterSetName='Filter')] param ( # The ObjectClass to search for. [Parameter(Position=0)] [string] $Type, # The filter to search for entries. Uses normal LDAP Search syntax, *not* # PS ActiveDirectory search. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Filter')] [string] $LDAPFilter, # The identity of the entry to retrieve. Can be sAMAcountName, SID, LDAP # path, or distinguished name. [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='Identity')] [string] $Identity, # The base path to search within on the given server [Parameter()] [string] $SearchBase, [Parameter()] [scriptblock] $ObjectPropertyConverter, # The domain controller to query. [Parameter()] [string] $Server, # Credentials for the domain controller. [Parameter()] [PSCredential] $Credential ) begin { if (-not $SearchBase) { $adRoot = Get-ADRootDSE -Server $Server -Credential $Credential -Verbose:$VerbosePreference $SearchBase = $adRoot.Properties['defaultNamingContext'] } if (-not $ObjectPropertyConverter) { $ObjectPropertyConverter = ${function:Convert-ADObjectPropertyTable} } } process { if ($Identity) { $LDAPFilter = Convert-ADIdentityToFilter -Identity $Identity } if ($Type) { $LDAPFilter = "(&(objectClass=$Type)($LDAPFilter))" } else { $LDAPFilter = "($LDAPFilter)" } Write-Verbose "Searching for '$LDAPFilter' under '$SearchBase'..." $searchResult = Invoke-SearchRequest $LDAPFilter $SearchBase -Server $Server -Credential $Credential | ConvertFrom-LDAPSearchResponse -ObjectPropertyConverter $ObjectPropertyConverter if ($Identity) { $resultCount = $searchResult | Measure-Object | Select-Object -ExpandProperty Count if ($resultCount -gt 1) { throw [InvalidOperationException]::new("Request for identity value '$Identity' returned multiple values of class '$Type', which isn't supposed to be possible.") } } # output $searchResult } } function New-ADObject { <# .SYNOPSIS Creates a new LDAP entry. .DESCRIPTION Creates a new LDAP entry with the specified name. .OUTPUTS [PSCustomObject] when PassThru is enabled. #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')] param ( # The ObjectClass of the type to create. [Parameter(Mandatory, Position=0)] [string] $Type, # The type of the DistinguishedName component for this new object. # Should be CN or OU. Defaults to CN. [ValidateSet('CN', 'OU')] [Parameter(Position=1)] [string] $DistinguishedComponentType = 'CN', # The name of the new entry. [Parameter(Mandatory, Position=2, ValueFromPipeline)] [string] $Name, # LDAP displayName-based (not object property names - "mail" not # "EmailAddress") hashtable for values on the new object. [Parameter()] [hashtable] $OtherAttributes, # Path of the OU or container where the new object is created, in DN form. [Parameter()] [string] $Path, # Path of the OU or container where the new object is created, in DN # form *without* the DC components. Used if -Path is not provided. [Parameter()] [string] $DefaultRelativePath, [Parameter()] [scriptblock] $ObjectPropertyConverter, # The domain controller to query. [Parameter()] [string] $Server, # Credentials for the domain controller. [Parameter()] [PSCredential] $Credential, # Should set sAM Account Name? If not set will default to a GUID. [Switch] $DoSAMAccountName, # Returns an object representing the item with which you are working. By # default, this cmdlet does not generate any output. [Switch] $PassThru ) begin { if (-not $Path) { # if Path is not provided, fetch the Root DSE so the DefaultRelativePath can be tacked-on. $adRootDSE = Get-ADRootDSE -Server $Server -Credential $Credential $Path = $adRootDSE.Properties['defaultNamingContext'] if ($DefaultRelativePath) { $Path = "$DefaultRelativePath,$Path" } } $OtherAttributes = if ($OtherAttributes) { $OtherAttributes.Clone() } else { @{} } if (-not (Test-ADObject -Identity $Path -Server $Server -Credential $Credential)) { Write-Error "Parent container node '$(if ($Path) { $Path } else { $DefaultRelativePath })' not found." } if (-not $ObjectPropertyConverter) { $ObjectPropertyConverter = ${function:Convert-ADObjectPropertyTable} } } process { if ($PSCmdlet.ShouldProcess("$Type '$Name' in container '$Path'")) { if ($DoSAMAccountName) { $existing = Get-ADObject -LDAPFilter "sAMAccountName=$Name" -ObjectPropertyConverter $ObjectPropertyConverter -Server $Server -Credential $Credential if (($existing | Measure-Object).Count) { # objectClass contains the full class inheritance hierarchy so we only want the final, most-specific entry. $existingClass = $existing.objectClass | Select-Object -Last 1 Write-Error "There is already an existing entry '$(Get-DistinguishedName $existing)' of type '$($existingClass)'." } $OtherAttributes['sAMAccountName'] = $Name } $newDistinguishedName = "$DistinguishedComponentType=$Name,$Path" $request = [DirectoryServices.Protocols.AddRequest]::new($newDistinguishedName, $Type) foreach ($attrPair in $OtherAttributes.GetEnumerator()) { $request.Attributes.Add([DirectoryServices.Protocols.DirectoryAttribute]::new($attrPair.Key, $attrPair.Value)) | Out-Null } $ldapConnection = New-LDAPConnection $Server $Credential $ldapConnection.SendRequest($request) | Out-Null if ($PassThru) { # output Get-ADObject -Type $Type -Identity $newDistinguishedName -ObjectPropertyConverter $ObjectPropertyConverter -Server $Server -Credential $Credential } } } } function Set-ADObject { <# .SYNOPSIS Modifies an LDAP entry on the server. .DESCRIPTION Modifies an LDAP entry on the server with the specified attributes. Does not update the local client version. .OUTPUTS [System.PSCustomObject] when PassThru is enabled. #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess)] param ( # The ObjectClass to modify. [Parameter(Position=0)] [string] $Type, # The identity of the LDAP entry to modify. [Parameter(Mandatory, ValueFromPipeline, Position=1)] [string] $Identity, # ObjectPropertyConverter, mostly important when running in PassThru # mode since PassThru for this object redownloads the object. [Parameter()] [scriptblock] $ObjectPropertyConverter, # A hashtable of LDAP attributes to add on the LDAP entry. [Parameter()] [hashtable] $Add, # A hashtable of LDAP attributes to remove from the LDAP entry. [Parameter()] [hashtable] $Remove, # A hashtable of LDAP attributes to replace on the LDAP entry. [Parameter()] [hashtable] $Replace, # The domain controller to query. [Parameter()] [string] $Server, # Credentials for the domain controller. [Parameter()] [PSCredential] $Credential = $null, # Returns an object representing the item with which you are working. By # default, this cmdlet does not generate any output. [switch] $PassThru ) process { $entry = Get-ADObject $Type -Identity $Identity -ObjectPropertyConverter $ObjectPropertyConverter -Server $Server -Credential $Credential if (($entry | Measure-Object).Count -eq 1) { if ($PSCmdlet.ShouldProcess($Identity, "Modifying $Type '$(Get-DistinguishedName $entry)'")) { $attributeModifications = [Collections.ArrayList]::new() if ($Add) { foreach ($attribute in $Add.GetEnumerator()) { Add-DirectoryAttributeModification -AttributeModificationList $attributeModifications -Operation Add -Name $attribute.Key -Value $attribute.Value } } if ($Remove) { foreach ($attribute in $Remove.GetEnumerator()) { Add-DirectoryAttributeModification -AttributeModificationList $attributeModifications -Operation Delete -Name $attribute.Key -Value $attribute.Value } } if ($Replace) { foreach ($attribute in $Replace.GetEnumerator()) { Add-DirectoryAttributeModification -AttributeModificationList $attributeModifications -Operation Replace -Name $attribute.Key -Value $attribute.Value } } $modifyRequest = [DirectoryServices.Protocols.ModifyRequest]::new( (Get-DistinguishedName $entry), $attributeModifications ) $ldapConnection = New-LDAPConnection $Server $Credential $ldapConnection.SendRequest($modifyRequest) | Out-Null if ($PassThru) { # output Get-ADObject $Type -Identity $Identity -ObjectPropertyConverter $ObjectPropertyConverter -Server $Server -Credential $Credential } } } elseif (-not $entry) { Write-Error "Could not find $Type '$Identity', cannot remove." } else { Write-Error "Multiple entries of type $Type found matching identity '$Identity', cannot remove." } } } function Remove-ADObject { <# .SYNOPSIS Removes an LDAP entry. .DESCRIPTION Removes an LDAP entry by their identity. .OUTPUTS None #> [CmdletBinding(SupportsShouldProcess)] param ( # The ObjectClass of the entry to modify. [Parameter(Position=0)] [string] $Type, # The identity of the LDAP entry to remove. [Parameter(Mandatory, ValueFromPipeline, Position=1)] [string] $Identity, # The domain controller to query. [Parameter()] [string] $Server, # Credentials for the domain controller. [Parameter()] [PSCredential] $Credential ) process { $entry = Get-ADObject $Type -Identity $Identity -Server $Server -Credential $Credential if ($PSCmdlet.ShouldProcess($Identity, "Removing $Type '$(Get-DistinguishedName $entry)'")) { if (($entry | Measure-Object).Count -eq 1) { $ldapConnection = New-LDAPConnection $Server $Credential $deleteRequest = [DirectoryServices.Protocols.DeleteRequest]::new( (Get-DistinguishedName $entry) ) $ldapConnection.SendRequest($deleteRequest) | Out-Null } elseif (-not $entry) { Write-Error "Could not find $Type '$Identity', cannot remove." } else { Write-Error "Multiple entries of type $Type found matching identity '$Identity', cannot remove." } } } } function Test-ADObject { <# .SYNOPSIS Tests if an LDAP entry exists. .DESCRIPTION Tests if an LDAP entry exists by their identity. .OUTPUTS [bool] #> [OutputType([bool])] [CmdletBinding()] param ( # The ObjectClass of the entry to test. [Parameter(Position=0)] [string] $Type, # The identity of the LDAP entry to test. [Parameter(Mandatory, ValueFromPipeline, Position=1)] [string] $Identity, # The domain controller to query. [Parameter()] [string] $Server, # Credentials for the domain controller. [Parameter()] [PSCredential] $Credential = $null ) process { $entry = Get-ADObject $Type -Identity $Identity -Server $Server -Credential $Credential # output $null -ne $entry } } function Set-ADObjectEntry { <# .SYNOPSIS Merge in changes to an ADObjectEntry's LDAP Attribute table. This allows a client #> [CmdletBinding(SupportsShouldProcess)] param ( # The object whose attributes collection must be modified [Parameter(ValueFromPipeline)] [PSCustomObject] $Entry, # A hashtable of LDAP attributes to add on the LDAP entry. [Parameter()] [hashtable] $Add, # A hashtable of LDAP attributes to remove from the LDAP entry. [Parameter()] [hashtable] $Remove, # A hashtable of LDAP attributes to replace on the LDAP entry. [Parameter()] [hashtable] $Replace ) process { if ($PSCmdlet.ShouldProcess((Get-DistinguishedName $Entry))) { if ($Add) { foreach ($attrPair in $Add.GetEnumerator()) { # See https://ldap.com/the-ldap-modify-operation/ to # understand the semantics. TODO: Technically values are # supposed to be unique within the attribute but we don't # check for that. $existingVal = $Entry.Attributes[$attrPair.Key] $existingValCount = ($existingVal | Measure-Object).Count if ($existingValCount -gt 1) { $Entry.Attributes[$attrPair.Key] += $attrPair.Value } elseif ($existingValCount -eq 1) { $Entry.Attributes[$attrPair.Key] = ,$existingVal + $attrPair.Value } else { # empty $Entry.Attributes[$attrPair.Key] = $attrPair.Value } } } if ($Remove) { foreach ($attrPair in $Remove.GetEnumerator()) { # See https://ldap.com/the-ldap-modify-operation/ to # understand the semantics of the LDAP "Delete" operation, # which we call "Remove" if ($attrPair.Value) { # where the Delete modification has a Value, we treat # the Attribute as an array an filter out that value. $Entry.Attributes[$attrPair.Key] = $Entry.Attributes[$attrPair.Key] | Where-Object { $_ -ne $attrPair.Value } } else { # where the LDAP Delete modification is empty, we remove the # whole Attribute. $Entry.Attributes.Remove($attrPair.Key) } } } if ($Replace) { foreach ($attrPair in $Replace.GetEnumerator()) { $Entry.Attributes[$attrPair.Key] = $attrPair.Value } } } } } function Update-ADObjectEntry { <# .SYNOPSIS Update the ADObject's properties from its LDAP Attributes #> [Diagnostics.CodeAnalysis.SuppressMessage( 'PSShouldProcess','',Scope='Function',Justification='-WhatIf passed through to helper funcs' )] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(ValueFromPipeline)] [PSCustomObject] $Entry, [Parameter()] [scriptblock] $ObjectPropertyConverter ) begin { if (-not $ObjectPropertyConverter) { $ObjectPropertyConverter = ${function:Convert-ADObjectPropertyTable} } } process { # Convert the current LDAP Attributes hashtable into Object Properties hashtable $objectPropertyTable = Invoke-Command $ObjectPropertyConverter -ArgumentList @($Entry.Attributes) if ($PSCmdlet.ShouldProcess((Get-DistinguishedName $Entry))) { # apply the resulting Object Properties Table to the given object's properties $objectPropertyTable.Keys | ForEach-Object { if ($Entry.PSObject.Properties.Name -contains $_) { $Entry.$_ = $objectPropertyTable[$_] } } } } } function Convert-ADObjectPropertyTable { <# .SYNOPSIS Takes a table of raw LDAP attributes and converts them into a table of object properties for an ADObject. .NOTES Adapted from https://learn.microsoft.com/en-us/archive/technet-wiki/12037.active-directory-get-aduser-default-and-extended-properties #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [hashtable] $LdapAttributeTable, [hashtable] $ObjectPropertyTable ) process { if(-not $ObjectPropertyTable) { $ObjectPropertyTable = @{} } $ObjectPropertyTable['CanonicalName'] = $LdapAttributeTable['canonicalName'] $ObjectPropertyTable['CN'] = $LdapAttributeTable['cn'] $ObjectPropertyTable['Created'] = $LdapAttributeTable['createTimeStamp'] $ObjectPropertyTable['Deleted'] = $LdapAttributeTable['isDeleted'] $ObjectPropertyTable['Description'] = $LdapAttributeTable['description'] $ObjectPropertyTable['DisplayName'] = $LdapAttributeTable['displayName'] $ObjectPropertyTable['DistinguishedName'] = $LdapAttributeTable['distinguishedName'] $ObjectPropertyTable['LastKnownParent'] = $LdapAttributeTable['lastKnownParent'] $ObjectPropertyTable['Modified'] = $LdapAttributeTable['modifyTimeStamp'] $ObjectPropertyTable['Name'] = $LdapAttributeTable['name'] # (Relative Distinguished Name) $ObjectPropertyTable['ObjectCategory'] = $LdapAttributeTable['objectCategory'] $ObjectPropertyTable['ObjectClass'] = $LdapAttributeTable['objectClass'] | Select-Object -Last 1 $ObjectPropertyTable['ObjectGUID'] = [string] $LdapAttributeTable['objectGUID'] $ObjectPropertyTable['ProtectedFromAccidentalDeletion'] = $LdapAttributeTable['nTSecurityDescriptor'] #output $ObjectPropertyTable } } |