diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/microsoft/ad/plugins/module_utils | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/microsoft/ad/plugins/module_utils')
-rw-r--r-- | ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 | 1130 |
1 files changed, 1130 insertions, 0 deletions
diff --git a/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 new file mode 100644 index 000000000..4a7ccf87c --- /dev/null +++ b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 @@ -0,0 +1,1130 @@ +# Copyright (c) 2023 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# FOR INTERNAL COLLECTION USE ONLY +# The interfaces in this file are meant for use within this collection +# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release. +# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686 +# Please open an issue if you have questions about this. + +#AnsibleRequires -CSharpUtil Ansible.Basic + +Function Compare-AnsibleADAttribute { + <# + .SYNOPSIS + Compares AD attribute values. + + .PARAMETER Name + The attribute name to compare. + + .PARAMETER ADObject + The AD object to compare with. + + .PARAMETER Attribute + The attribute value(s) to add/remove/set. + + .PARAMETER Action + Set to Add to add the value(s), Remove to remove the value(s), and Set to replace the value(s). + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Name, + + [Parameter()] + [AllowNull()] + [Microsoft.ActiveDirectory.Management.ADObject] + $ADObject, + + [Parameter()] + [AllowEmptyCollection()] + [object] + $Attribute, + + [ValidateSet("Add", "Remove", "Set")] + [string] + $Action + ) + + <# Gets all the known types the AD module can return + + DateTime, Guid, SecurityIdentifier are all from readonly properties + that the AD module alaises of the real LDAP attributes. + + Get-ADObject -LDAPFilter '(objectClass=*)' -Properties * | + ForEach-Object { + foreach ($name in $_.PSObject.Properties.Name) { + if ($name -in @('AddedProperties', 'ModifiedProperties', 'RemovedProperties', 'PropertyNames')) { continue } + + $v = $_.$name + if ($null -eq $v) { continue } + if ($v -isnot [System.Collections.IList] -or $v -is [System.Byte[]]) { + $v = @(, $v) + } + + foreach ($value in $v) { + $value.GetType() + } + } + } | + Sort-Object -Unique + #> + $getDiffValue = { + if ($_ -is [System.Byte[]]) { + [System.Convert]::ToBase64String($_) + } + elseif ($_ -is [System.DateTime]) { + $_.ToUniversalTime().ToString('o') + } + elseif ($_ -is [System.DirectoryServices.ActiveDirectorySecurity]) { + $_.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All) + } + else { + # Bool, Int32, Int64, String + $_ + } + } + + $existingAttributes = [System.Collections.Generic.List[object]]@() + if ($ADObject -and $null -ne $ADObject.$Name) { + $existingValues = $ADObject.$Name + if ($null -ne $existingValues) { + if ( + $existingValues -is [System.Collections.IList] -and + $existingValues -isnot [System.Byte[]] + ) { + # Wrap with @() to help pwsh unroll the property value collection + $existingAttributes.AddRange(@($existingValues)) + + } + else { + $existingAttributes.Add($existingValues) + } + } + } + + $desiredAttributes = [System.Collections.Generic.List[object]]@() + if ($null -ne $Attribute -and $Attribute -isnot [System.Collections.IList]) { + $Attribute = @($Attribute) + } + foreach ($attr in $Attribute) { + if ($attr -is [System.Collections.IDictionary]) { + if ($attr.Keys.Count -gt 2) { + $keyList = $attr.Keys -join "', '" + throw "Attribute '$Name' entry should only contain the 'type' and 'value' keys, found: '$keyList'" + } + + $type = $attr.type + $value = $attr.value + } + else { + $type = 'raw' + $value = $attr + } + + switch ($type) { + bool { + $desiredAttributes.Add([System.Boolean]$value) + } + bytes { + $desiredAttributes.Add([System.Convert]::FromBase64String($value)) + } + date_time { + $dtVal = [DateTimeOffset]::ParseExact( + $value, + [string[]]@("yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK"), + [System.Globalization.CultureInfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::AssumeUniversal) + $desiredAttributes.Add($dtVal.UtcDateTime) + } + int { + $desiredAttributes.Add([Int64]$value) + } + security_descriptor { + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($value) + $desiredAttributes.Add($sd) + } + string { + $desiredAttributes.Add($value.ToString()) + } + raw { + # If the value is an Int32 we need it to be Int64 to ensure + # the values are all the same type. + if ($value -is [int]) { + $value = [Int64]$value + } + $desiredAttributes.Add($value) + } + default { throw "Attribute type '$type' must be bytes, date_time, int, security_descriptor, or raw" } + } + } + + $diffBefore = @($existingAttributes | ForEach-Object -Process $getDiffValue) + $diffAfter = [System.Collections.Generic.List[object]]@() + $value = [System.Collections.Generic.List[object]]@() + $changed = $false + + # It's a lot easier to compare the string values + $existing = [string[]]$diffBefore + $desired = [string[]]@($desiredAttributes | ForEach-Object -Process $getDiffValue) + + if ($Action -eq 'Add') { + $diffAfter.AddRange($existingAttributes) + + for ($i = 0; $i -lt $desired.Length; $i++) { + if ($desired[$i] -cnotin $existing) { + $value.Add($desiredAttributes[$i]) + $diffAfter.Add($desiredAttributes[$i]) + $changed = $true + } + } + } + elseif ($Action -eq 'Remove') { + $diffAfter.AddRange($existingAttributes) + + for ($i = $desired.Length - 1; $i -ge 0; $i--) { + if ($desired[$i] -cin $existing) { + $value.Add($desiredAttributes[$i]) + $diffAfter.RemoveAt($i) + $changed = $true + } + } + } + else { + $diffAfter.AddRange($desiredAttributes) + + $toAdd = [string[]][System.Linq.Enumerable]::Except($desired, $existing) + $toRemove = [string[]][System.Linq.Enumerable]::Except($existing, $desired) + if ($toAdd.Length -or $toRemove.Length) { + $changed = $true + } + + if ($changed) { + $value.AddRange($desiredAttributes) + } + } + + [PSCustomObject]@{ + Name = $Name + Value = $value.ToArray() # AD cmdlets expect an array here + Changed = $changed + DiffBefore = @($diffBefore | Sort-Object) + DiffAfter = @($diffAfter | ForEach-Object -Process $getDiffValue | Sort-Object) + } +} + +Function Update-AnsibleADSetADObjectParam { + <# + .SYNOPSIS + Updates the Set-AD* parameter splat with the parameters needed to set the + attributes requested. + It will output a boolean that indicates whether a change is needed to + update the attributes. + + .PARAMETER Splat + The parameter splat to update. + + .PARAMETER Add + The attributes to add. + + .PARAMETER Remove + The attributes to remove. + + .PARAMETER Set + The attributes to set. + + .PARAMETER Diff + An optional dictionary that can be used to store the diff output value on + what was changed. + + .PARAMETER ADObject + The AD object to compare the requested attribute values with. + + .PARAMETER ForNew + This Splat is used for New-AD* and will update the OtherAttributes + parameter. + #> + [OutputType([bool])] + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.Collections.IDictionary] + $Splat, + + [Parameter()] + [AllowNull()] + [System.Collections.IDictionary] + $Add, + + [Parameter()] + [AllowNull()] + [System.Collections.IDictionary] + $Remove, + + [Parameter()] + [AllowNull()] + [System.Collections.IDictionary] + $Set, + + [Parameter()] + [System.Collections.IDictionary] + $Diff, + + [Parameter()] + [AllowNull()] + [Microsoft.ActiveDirectory.Management.ADObject] + $ADObject, + + [Parameter()] + [switch] + $ForNew + ) + + $diffBefore = @{} + $diffAfter = @{} + + $addAttributes = @{} + $removeAttributes = @{} + $replaceAttributes = @{} + $clearAttributes = [System.Collections.Generic.List[String]]@() + + if ($Add.Count) { + foreach ($kvp in $Add.GetEnumerator()) { + $val = Compare-AnsibleADAttribute -Name $kvp.Key -ADObject $ADObject -Attribute $kvp.Value -Action Add + if ($val.Changed -and $val.Value.Count) { + $addAttributes[$kvp.Key] = $val.Value + } + $diffBefore[$kvp.Key] = $val.DiffBefore + $diffAfter[$kvp.Key] = $val.DiffAfter + } + } + # remove doesn't make sense when creating a new object + if (-not $ForNew -and $Remove.Count) { + foreach ($kvp in $Remove.GetEnumerator()) { + $val = Compare-AnsibleADAttribute -Name $kvp.Key -ADObject $ADObject -Attribute $kvp.Value -Action Remove + if ($val.Changed -and $val.Value.Count) { + $removeAttributes[$kvp.Key] = $val.Value + } + $diffBefore[$kvp.Key] = $val.DiffBefore + $diffAfter[$kvp.Key] = $val.DiffAfter + } + } + if ($Set.Count) { + foreach ($kvp in $Set.GetEnumerator()) { + $val = Compare-AnsibleADAttribute -Name $kvp.Key -ADObject $ADObject -Attribute $kvp.Value -Action Set + if ($val.Changed) { + if ($val.Value.Count) { + $replaceAttributes[$kvp.Key] = $val.Value + } + else { + $clearAttributes.Add($kvp.Key) + } + } + $diffBefore[$kvp.Key] = $val.DiffBefore + $diffAfter[$kvp.Key] = $val.DiffAfter + } + } + + $changed = $false + if ($ForNew) { + $diffBefore = $null + $otherAttributes = @{} + + foreach ($kvp in $addAttributes.GetEnumerator()) { + $otherAttributes[$kvp.Key] = $kvp.Value + } + foreach ($kvp in $replaceAttributes.GetEnumerator()) { + $otherAttributes[$kvp.Key] = $kvp.Value + } + + if ($otherAttributes.Count) { + $changed = $true + $Splat.OtherAttributes = $otherAttributes + } + } + else { + if ($addAttributes.Count) { + $changed = $true + $Splat.Add = $addAttributes + } + if ($removeAttributes.Count) { + $changed = $true + $Splat.Remove = $removeAttributes + } + if ($replaceAttributes.Count) { + $changed = $true + $Splat.Replace = $replaceAttributes + } + if ($clearAttributes.Count) { + $changed = $true + $Splat.Clear = $clearAttributes + } + } + + if ($null -ne $Diff.Count) { + $Diff.after = $diffAfter + $Diff.before = $diffBefore + } + + $changed +} + + +Function Compare-AnsibleADIdempotentList { + <# + .SYNOPSIS + Common code to compare AD property values with an add/remove/set collection. + + .PARAMETER Existing + The existing values for the property. + + .PARAMETER Add + A list of values to add + + .PARAMETER Remove + A list of values to remove + + .PARAMETER Set + A list of files to set, will remove existing values if they are not in the + list and add ones that are not in the existing values. + + .PARAMETER CaseInsensitive + Whether to perform a case insensitive comparison check. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [AllowEmptyCollection()] + [AllowNull()] + [object[]] + $Existing, + + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [object[]] + $Add, + + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [object[]] + $Remove, + + [Parameter()] + [AllowNull()] + [AllowEmptyCollection()] + [object[]] + $Set, + + [Parameter()] + [switch] + $CaseInsensitive + ) + + # It's easier to compare with strings. + $existingString = [string[]]@(if ($null -ne $Existing) { $Existing | ForEach-Object ToString }) + $comparer = if ($CaseInsensitive) { + [System.StringComparer]::OrdinalIgnoreCase + } + else { + [System.StringComparer]::CurrentCulture + } + + $value = [System.Collections.Generic.List[Object]]@() + $toAdd = [System.Collections.Generic.List[Object]]@() + $toRemove = [System.Collections.Generic.List[Object]]@() + + if ($null -ne $Set) { + $setString = [string[]]@($Set | ForEach-Object ToString) + $value.AddRange($Set) + + for ($i = 0; $i -lt $setString.Length; $i++) { + $setElement = $setString[$i] + if (-not [System.Linq.Enumerable]::Contains($existingString, $setElement, $comparer)) { + $toAdd.Add($Set[$i]) + } + } + for ($i = 0; $i -lt $existingString.Length; $i++) { + $existingElement = $existingString[$i] + if (-not [System.Linq.Enumerable]::Contains($setString, $existingElement, $comparer)) { + $toRemove.Add($Existing[$i]) + } + } + } + else { + if ($Remove) { + $removeString = [string[]]@($Remove | ForEach-Object ToString) + + for ($i = 0; $i -lt $existingString.Length; $i++) { + $existingElement = $existingString[$i] + if ([System.Linq.Enumerable]::Contains($removeString, $existingElement, $comparer)) { + $toRemove.Add($Existing[$i]) + } + else { + $value.Add($Existing[$i]) + } + } + } + else { + $value.AddRange($Existing) + } + + if ($Add) { + $addString = [string[]]@($Add | ForEach-Object ToString) + + for ($i = 0; $i -lt $addString.Length; $i++) { + $addElement = $addString[$i] + if (-not [System.Linq.Enumerable]::Contains($existingString, $addElement, $comparer)) { + $toAdd.Add($Add[$i]) + $value.Add($Add[$i]) + } + } + } + } + + [PSCustomObject]@{ + Value = if ($value.Count) { $value.ToArray() } else { $null } + # Also returned if the API doesn't support explicitly setting 1 value + ToAdd = $toAdd.ToArray() + ToRemove = $toRemove.ToArray() + Changed = [bool]($toAdd.Count -or $toRemove.Count) + } +} + +Function Get-AnsibleADObject { + <# + .SYNOPSIS + The -Identity params is limited to just objectGuid and distinguishedName + on Get-ADObject. Try to preparse the value to support more common props + like sAMAccountName, objectSid, userPrincipalName. + + .PARAMETER Identity + The Identity to get. + + .PARAMETER Properties + Extra properties to request on the object + + .PARAMETER Server + The explicit domain controller to query. + + .PARAMETER Credential + Custom queries to authenticate with. + + .PARAMETER GetCommand + The Get-AD* cmdlet to use to get the AD object. Defaults to Get-ADObject. + #> + [OutputType([Microsoft.ActiveDirectory.Management.ADObject])] + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Identity, + + [Parameter()] + [AllowEmptyCollection()] + [string[]] + $Properties, + + [string] + $Server, + + [PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.CommandInfo] + $GetCommand = $null + ) + + $getParams = @{} + if ($Properties.Count) { + $getParams.Properties = $Properties + } + if ($Server) { + $getParams.Server = $Server + } + if ($Credential) { + $getParams.Credential = $Credential + } + + # The -Identity parameter is used where possible as LDAPFilter is limited + # to just the defaultNamingContext as defined by -SearchBase. + $objectGuid = [Guid]::Empty + if ([System.Guid]::TryParse($Identity, [ref]$objectGuid)) { + $getParams.Identity = $objectGuid + } + elseif ($Identity -match '^.*\@.*\..*$') { + $getParams.LDAPFilter = "(userPrincipalName=$($Matches[0]))" + } + elseif ($Identity -match '^(?:[^:*?""<>|\/\\]+\\)?(?<username>[^;:""<>|?,=\*\+\\\(\)]{1,20})$') { + $getParams.LDAPFilter = "(sAMAccountName=$($Matches.username))" + } + else { + try { + $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $Identity + $sidBytes = New-Object -TypeName System.Byte[] -ArgumentList $sid.BinaryLength + $sid.GetBinaryForm($sidBytes, 0) + + $value = @($sidBytes | ForEach-Object { + '\' + [System.BitConverter]::ToString($_).ToLowerInvariant() + }) -join '' + $getParams.LDAPFilter = "(objectSid=$value)" + } + catch [System.ArgumentException] { + # Finally fallback to DistinguishedName. + $getParams.Identity = $Identity + } + } + + if ($GetCommand) { + $null = $getParams.Remove('GetCommand') + } + else { + $GetCommand = Get-Command -Name Get-ADObject -Module ActiveDirectory + } + try { + $obj = & $GetCommand @getParams | Select-Object -First 1 + } + catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { + $obj = $null + } + + $obj +} + +Function Invoke-AnsibleADObject { + <# + .SYNOPSIS + Runs the module code for managing an AD object. + + .PARAMETER PropertyInfo + The properties to compare on the AD object and what the module supports. + Each object in this array must have the following keys set + Name - The module option name + Option - Module options to define in the arg spec + + The following keys are optional: + Attribute - The ldap attribute name to compare against + CaseInsensitive - The values are case insensitive (defaults to $false) + StateRequired - Set to 'present' or 'absent' if this needs to be defined for either state + New - Called when the option is to be set on the New-AD* cmdlet splat + Set - Called when the option is to be set on the Set-AD* cmdlet splat + + If Attribute is set then requested value will be compared with the + attribute specified. The current attribute value is added to the before + diff state for the option it is on. If New is not specified then the + value requested is added to the New-AD* splat based on the attribute name. + If Set is not specified then the value requested is added to the Set-AD* + splat based on the attribute name. + + If New is specified it is called with the current module, common AD + parameters and a splat that is called with New-AD*. It is up to the + scriptblock to set the required splat parameters or called whatever + function is needed. + + If Set is specified it is called with the current module, common AD + parameters, a splat that is called with Set-AD*, and the current AD object. + It is up to the scriptblock to set the required splat parameters or call + whatever function is needed. + + Both New and Set must set the $Module.Diff.after results accordingly and/or + mark $Module.Result.changed if it is making a change outside of adjusting + the splat hashtable passed in. + + .PARAMETER DefaultPath + A scriptblock that retrieves the default path the object is created in. + Defaults to the defaultNamingContext. This is invoked with a hashtable + containing parameters used to connect to AD, such as the Server and/or + Credential. + + .PARAMETER ModuleNoun + The module cmdlet noun that is being managed. This is used to run the + correct Get-AD*, Set-AD*, and New-AD* cmdlets when needed. + + .PARAMETER ExtraProperties + Extra properties to request when getting the AD object. + + .PARAMETER PreAction + A scriptblock that is called at the beginning to perform any tasks needed + before the module util is run. This is called with the module object, + common ad parameters, and the ad object if it was found based on the input + options. + + .PARAMETER PostAction + A scriptblock that is called at the end to perform any tasks once the + object has been configured. This is called with the module object, common + ad parameters, and the ad object (state=present) else $null (state=absent) + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [object[]] + $PropertyInfo, + + [Parameter()] + [ScriptBlock] + $DefaultPath = { param ($Module, $Params) (Get-ADRootDSE @Params -Properties defaultNamingContext).defaultNamingContext }, + + [Parameter()] + [string] + $ModuleNoun = 'ADObject', + + [Parameter()] + [string[]] + $ExtraProperties, + + [Parameter()] + [ScriptBlock] + $PreAction, + + [Parameter()] + [ScriptBlock] + $PostAction + ) + + $spec = @{ + options = @{ + attributes = @{ + default = @{} + type = 'dict' + options = @{ + add = @{ + default = @{} + type = 'dict' + } + remove = @{ + default = @{} + type = 'dict' + } + set = @{ + default = @{} + type = 'dict' + } + } + } + domain_password = @{ + no_log = $true + type = 'str' + } + domain_server = @{ + type = 'str' + } + domain_username = @{ + type = 'str' + } + identity = @{ + type = 'str' + } + name = @{ + type = 'str' + } + path = @{ + type = 'str' + } + state = @{ + choices = 'absent', 'present' + default = 'present' + type = 'str' + } + } + required_one_of = @( + , @("identity", "name") + ) + required_together = @(, @('domain_username', 'domain_password')) + supports_check_mode = $true + } + + $stateRequiredIf = @{ + present = @('name') + absent = @() + } + + $PropertyInfo = @( + $PropertyInfo + + # These 3 options are common to all AD objects. + [PSCustomObject]@{ + Name = 'description' + Option = @{ type = 'str' } + Attribute = 'description' + } + [PSCustomObject]@{ + Name = 'display_name' + Option = @{ type = 'str' } + Attribute = 'displayName' + } + [PSCustomObject]@{ + Name = 'protect_from_deletion' + Option = @{ type = 'bool' } + Attribute = 'ProtectedFromAccidentalDeletion' + } + ) + + [string[]]$requestedAttributes = @( + foreach ($propInfo in $PropertyInfo) { + $ansibleOption = $propInfo.Name + + if ($propInfo.StateRequired) { + $stateRequiredIf[$propInfo.StateRequired] += $ansibleOption + } + + $spec.options[$ansibleOption] = $propInfo.Option + + if ($propInfo.Attribute) { + $propInfo.Attribute + } + } + + $ExtraProperties + ) + + $spec.required_if = @( + foreach ($kvp in $stateRequiredIf.GetEnumerator()) { + if ($kvp.Value) { + , @("state", $kvp.Key, $kvp.Value) + } + } + ) + + $module = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + $module.Result.distinguished_name = $null + $module.Result.object_guid = $null + + $adParams = @{} + if ($module.Params.domain_server) { + $adParams.Server = $module.Params.domain_server + } + + if ($module.Params.domain_username) { + $adParams.Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $module.Params.domain_username, + (ConvertTo-SecureString -AsPlainText -Force -String $module.Params.domain_password) + ) + } + + $defaultObjectPath = & $DefaultPath $module $adParams + $getCommand = Get-Command -Name "Get-$ModuleNoun" -Module ActiveDirectory + $newCommand = Get-Command -Name "New-$ModuleNoun" -Module ActiveDirectory + $setCommand = Get-Command -Name "Set-$ModuleNoun" -Module ActiveDirectory + + $requestedAttributes = [System.Collections.Generic.HashSet[string]]@( + $requestedAttributes + 'name' + $module.Params.attributes.add.Keys + $module.Params.attributes.remove.Keys + $module.Params.attributes.set.Keys + ) | Where-Object { $_ } + + $namePrefix = 'CN' + if ($ModuleNoun -eq 'ADOrganizationalUnit' -or $Module.Params.type -eq 'organizationalUnit') { + $namePrefix = 'OU' + } + + $identity = if ($module.Params.identity) { + $module.Params.identity + } + else { + $ouPath = $defaultObjectPath + if ($module.Params.path) { + $ouPath = $module.Params.path + } + "$namePrefix=$($Module.Params.name -replace ',', '\,'),$ouPath" + } + + $getParams = @{ + GetCommand = $getCommand + Identity = $identity + Properties = $requestedAttributes + } + $adObject = Get-AnsibleADObject @getParams @adParams + if ($adObject) { + $module.Result.object_guid = $adObject.ObjectGUID + $module.Result.distinguished_name = $adObject.DistinguishedName + + $module.Diff.before = @{ + attributes = $null + name = $adObject.Name + path = @($adObject.DistinguishedName -split '[^\\],', 2)[-1] + } + + foreach ($propInfo in $PropertyInfo) { + $propValue = $module.Params[$propInfo.Name] + if ($null -eq $propValue -or -not $propInfo.Attribute) { + continue + } + + $actualValue = $adObject[$propInfo.Attribute].Value + if ($module.Option.no_log) { + $actualValue = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + } + if ($actualValue -is [System.Collections.IList]) { + $actualValue = @($actualValue | Sort-Object) + } + $module.Diff.before[$propInfo.Name] = $actualValue + } + } + else { + $module.Diff.before = $null + } + + if ($PreAction) { + $null = & $PreAction $module $adParams $adObject + } + + if ($module.Params.state -eq 'absent') { + if ($adObject) { + $removeParams = @{ + Confirm = $false + WhatIf = $module.CheckMode + } + + # Remove-ADObject -Recursive fails with access is denied, use this + # instead to remove the child objects manually + Get-ADObject -Filter * -Properties ProtectedFromAccidentalDeletion -Searchbase $adObject.DistinguishedName | + Sort-Object -Property { $_.DistinguishedName.Length } -Descending | + ForEach-Object -Process { + if ($_.ProtectedFromAccidentalDeletion) { + $_ | Set-ADObject -ProtectedFromAccidentalDeletion $false @removeParams @adParams + } + $_ | Remove-ADObject @removeParams @adParams + } + + $module.Result.changed = $true + } + + $module.Diff.after = $null + } + else { + $attributes = $module.Params.attributes + $objectDN = $null + $objectGuid = $null + + if (-not $adObject) { + $newParams = @{ + Confirm = $false + Name = $module.Params.name + WhatIf = $module.CheckMode + PassThru = $true + } + + $objectPath = $null + if ($module.Params.path) { + $objectPath = $path + $newParams.Path = $module.Params.path + } + else { + $objectPath = $defaultObjectPath + } + + $diffAttributes = @{} + $null = Update-AnsibleADSetADObjectParam @attributes -Splat $newParams -Diff $diffAttributes -ForNew + + $module.Diff.after = @{ + attributes = $diffAttributes.after + name = $module.Params.name + path = $objectPath + } + + foreach ($propInfo in $PropertyInfo) { + $propValue = $module.Params[$propInfo.Name] + if ($propValue -is [System.Collections.IDictionary]) { + if ($propValue.Count -eq 0) { + continue + } + } + elseif ([string]::IsNullOrWhiteSpace($propValue)) { + continue + } + + if ($propInfo.New) { + $null = & $propInfo.New $module $adParams $newParams + } + elseif ($propInfo.Attribute) { + if ($propValue -is [System.Collections.IDictionary]) { + $propValue = @($propValue['add']; $propValue['set']) | Select-Object -Unique + } + + $newParams[$propInfo.Attribute] = $propValue + + if ($propInfo.Option.no_log) { + $propValue = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + } + if ($propValue -is [System.Collections.IList]) { + $propValue = @($propValue | Sort-Object) + } + $module.Diff.after[$propInfo.Name] = $propValue + } + } + + try { + $adObject = & $newCommand @newParams @adParams + } + catch { + # Using FailJson means other useful debugging information + # like the diff output is returned + $module.FailJson("New-$ModuleNoun failed: $_", $_) + } + $module.Result.changed = $true + + if ($module.CheckMode) { + $objectDN = "$namePrefix=$($module.Params.name -replace ',', '\,'),$objectPath" + $objectGuid = [Guid]::Empty # Dummy value for check mode + } + else { + $objectDN = $adObject.DistinguishedName + $objectGuid = $adObject.ObjectGUID + } + } + else { + $objectDN = $adObject.DistinguishedName + $objectGuid = $adObject.ObjectGUID + $objectName = $adObject.Name + $objectPath = @($objectDN -split '[^\\],', 2)[-1] + + $commonParams = @{ + Confirm = $false + Identity = $adObject.ObjectGUID + PassThru = $true + WhatIf = $module.CheckMode + } + $setParams = @{} + + $diffAttributes = @{} + $null = Update-AnsibleADSetADObjectParam @attributes -Splat $setParams -Diff $diffAttributes -ADObject $adObject + + $module.Diff.before.attributes = $diffAttributes.before + $module.Diff.after = @{ + attributes = $diffAttributes.after + name = $objectName + path = $objectPath + } + + foreach ($propInfo in $PropertyInfo) { + $propValue = $module.Params[$propInfo.Name] + if ($null -eq $propValue) { + continue + } + + if ($propInfo.Set) { + $null = & $propInfo.Set $module $adParams $setParams $adObject + } + elseif ($propInfo.Attribute) { + $actualValue = $adObject[$propInfo.Attribute] + + $compareParams = @{ + Existing = $actualValue + CaseInsensitive = $propInfo.CaseInsensitive + } + + if ($propValue -is [System.Collections.IDictionary]) { + $compareParams.Add = $propValue['add'] + $compareParams.Remove = $propValue['remove'] + $compareParams.Set = $propValue['set'] + } + elseif ([string]::IsNullOrWhiteSpace($propValue)) { + $compareParams.Set = @() + } + else { + $compareParams.Set = @($propValue) + } + + $res = Compare-AnsibleADIdempotentList @compareParams + $newValue = $res.Value + if ($res.Changed) { + $setParams[$propInfo.Attribute] = $newValue + } + + $noLog = $propInfo.Option.no_log + if ($newValue) { + if ($res.Changed -and $noLog) { + $newValue = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER - changed' + } + elseif ($noLog) { + $newValue = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER' + } + + if ($newValue -is [System.Collections.IList]) { + $newValue = @($newValue | Sort-Object) + } + } + + $module.Diff.after[$propInfo.Name] = $newValue + } + } + + $finalADObject = $null + if ($module.Params.name -cne $objectName) { + $objectName = $module.Params.name + $module.Diff.after.name = $objectName + + $finalADObject = Rename-ADObject @commonParams -NewName $objectName + $module.Result.changed = $true + } + + if ($module.Params.path -and $module.Params.path -ne $objectPath) { + $objectPath = $module.Params.path + $module.Diff.after.path = $objectPath + + $addProtection = $false + if ($adObject.ProtectedFromAccidentalDeletion) { + $addProtection = $true + $null = Set-ADObject -ProtectedFromAccidentalDeletion $false @commonParams @adParams + } + + try { + $finalADObject = Move-ADObject @commonParams -TargetPath $objectPath + } + finally { + if ($addProtection) { + $null = Set-ADObject -ProtectedFromAccidentalDeletion $true @commonParams @adParams + } + } + + $module.Result.changed = $true + } + + if ($setParams.Count) { + try { + $finalADObject = & $setCommand @commonParams @setParams @adParams + } + catch { + # Using FailJson means other useful debugging information + # like the diff output is returned + $module.FailJson("Set-$ModuleNoun failed: $_", $_) + } + $module.Result.changed = $true + } + + # Won't be set in check mode + if ($finalADObject) { + $objectDN = $finalADObject.DistinguishedName + } + else { + $objectDN = "$namePrefix=$($objectName -replace ',', '\,'),$objectPath" + } + } + + # Explicit vars are set when running in check mode as the adObject may not + # have the desired values set at runtime + $module.Result.distinguished_name = $objectDN + $module.Result.object_guid = $objectGuid.Guid + } + + if ($PostAction) { + $null = & $PostAction $Module $adParams $adObject + } + + $module.ExitJson() +} + +$exportMembers = @{ + Function = @( + "Compare-AnsibleADIdempotentList" + "Get-AnsibleADObject" + "Invoke-AnsibleADObject" + ) +} +Export-ModuleMember @exportMembers |