diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-26 06:22:20 +0000 |
commit | 18bd2207b6c1977e99a93673a7be099e23f0f547 (patch) | |
tree | 40fd9e5913462a88be6ba24be6953383c5b39874 /ansible_collections/microsoft/ad/plugins/modules | |
parent | Releasing progress-linux version 10.0.1+dfsg-1~progress7.99u1. (diff) | |
download | ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.tar.xz ansible-18bd2207b6c1977e99a93673a7be099e23f0f547.zip |
Merging upstream version 10.1.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/microsoft/ad/plugins/modules')
15 files changed, 666 insertions, 307 deletions
diff --git a/ansible_collections/microsoft/ad/plugins/modules/computer.ps1 b/ansible_collections/microsoft/ad/plugins/modules/computer.ps1 index b97bb1062..9010c103d 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/computer.ps1 +++ b/ansible_collections/microsoft/ad/plugins/modules/computer.ps1 @@ -12,15 +12,10 @@ $setParams = @{ Name = 'delegates' Option = @{ aliases = 'principals_allowed_to_delegate' - type = 'dict' - options = @{ - add = @{ type = 'list'; elements = 'str' } - remove = @{ type = 'list'; elements = 'str' } - set = @{ type = 'list'; elements = 'str' } - } + type = 'add_remove_set' } Attribute = 'PrincipalsAllowedToDelegateToAccount' - CaseInsensitive = $true + DNLookup = $true } [PSCustomObject]@{ Name = 'dns_hostname' @@ -35,24 +30,8 @@ $setParams = @{ [PSCustomObject]@{ Name = 'kerberos_encryption_types' Option = @{ - type = 'dict' - options = @{ - add = @{ - choices = 'aes128', 'aes256', 'des', 'rc4' - type = 'list' - elements = 'str' - } - remove = @{ - choices = 'aes128', 'aes256', 'des', 'rc4' - type = 'list' - elements = 'str' - } - set = @{ - choices = 'aes128', 'aes256', 'des', 'rc4' - type = 'list' - elements = 'str' - } - } + type = 'add_remove_set' + choices = 'aes128', 'aes256', 'des', 'rc4' } Attribute = 'KerberosEncryptionType' CaseInsensitive = $true @@ -107,8 +86,9 @@ $setParams = @{ } [PSCustomObject]@{ Name = 'managed_by' - Option = @{ type = 'str' } + Option = @{ type = 'raw' } Attribute = 'ManagedBy' + DNLookup = $true } [PSCustomObject]@{ Name = 'sam_account_name' @@ -119,45 +99,11 @@ $setParams = @{ Name = 'spn' Option = @{ aliases = 'spns' - type = 'dict' - options = @{ - add = @{ type = 'list'; elements = 'str' } - remove = @{ type = 'list'; elements = 'str' } - set = @{ type = 'list'; elements = 'str' } - } - } - Attribute = 'ServicePrincipalNames' - New = { - param($Module, $ADParams, $NewParams) - - $spns = @( - $Module.Params.spn.add - $Module.Params.spn.set - ) | Select-Object -Unique - - $NewParams.ServicePrincipalNames = $spns - $Module.Diff.after.spn = $spns - } - Set = { - param($Module, $ADParams, $SetParams, $ADObject) - - $desired = $Module.Params.spn - $compareParams = @{ - Existing = $ADObject.ServicePrincipalNames - CaseInsensitive = $true - } - $res = Compare-AnsibleADIdempotentList @compareParams @desired - if ($res.Changed) { - $SetParams.ServicePrincipalNames = @{} - if ($res.ToAdd) { - $SetParams.ServicePrincipalNames.Add = $res.ToAdd - } - if ($res.ToRemove) { - $SetParams.ServicePrincipalNames.Remove = $res.ToRemove - } - } - $module.Diff.after.kerberos_encryption_types = @($res.Value | Sort-Object) + type = 'add_remove_set' } + Attribute = 'servicePrincipalName' + CaseInsensitive = $true + IsRawAttribute = $true } [PSCustomObject]@{ Name = 'trusted_for_delegation' diff --git a/ansible_collections/microsoft/ad/plugins/modules/computer.py b/ansible_collections/microsoft/ad/plugins/modules/computer.py index ab336d6b4..cf160256a 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/computer.py +++ b/ansible_collections/microsoft/ad/plugins/modules/computer.py @@ -15,14 +15,19 @@ options: description: - The principal objects that the current AD object can trust for delegation to either add, remove or set. - - The values for each sub option must be specified as a distinguished name - C(CN=shenetworks,CN=Users,DC=ansible,DC=test) + - Each subkey value is a list of values in the form of a + C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName), + or C(userPrincipalName) string or a dictionary with the I(name) and + optional I(server) key. - This is the value set on the C(msDS-AllowedToActOnBehalfOfOtherIdentity) LDAP attribute. - This is a highly sensitive attribute as it allows the principals specified to impersonate any account when authenticating with the AD computer object being managed. - To clear all principals, use I(set) with an empty list. + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. - See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values) for more information on how to add/remove/set list options. aliases: @@ -31,29 +36,35 @@ options: suboptions: add: description: - - The AD objects by their C(DistinguishedName) to add as a principal - allowed to delegate. + - Adds the principals specified as principals allowed to delegate to. - Any existing principals not specified by I(add) will be untouched unless specified by I(remove) or not in I(set). type: list - elements: str + elements: raw + lookup_failure_action: + description: + - Control the action to take when the lookup fails to find the DN. + - C(fail) will cause the task to fail. + - C(ignore) will ignore the value and continue. + - C(warn) will ignore the value and display a warning. + choices: ['fail', 'ignore', 'warn'] + default: fail + type: str remove: description: - - The AD objects by their C(DistinguishedName) to remove as a principal - allowed to delegate. + - Removes the principals specified as principals allowed to delegate to. - Any existing pricipals not specified by I(remove) will be untouched unless I(set) is defined. type: list - elements: str + elements: raw set: description: - - The AD objects by their C(DistinguishedName) to set as the only - principals allowed to delegate. + - Sets the principals specified as principals allowed to delegate to. - This will remove any existing principals if not specified in this list. - Specify an empty list to remove all principals allowed to delegate. type: list - elements: str + elements: raw dns_hostname: description: - Specifies the fully qualified domain name (FQDN) of the computer. @@ -124,9 +135,13 @@ options: description: - The user or group that manages the object. - The value can be in the form of a C(distinguishedName), C(objectGUID), - C(objectSid), or sAMAccountName). + C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a + dictionary with the I(name) and optional I(server) key. - This is the value set on the C(managedBy) LDAP attribute. - type: str + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. + type: raw sam_account_name: description: - The C(sAMAccountName) value to set for the group. @@ -220,7 +235,7 @@ EXAMPLES = r""" dns_hostname: one_linux_server.my_org.local path: OU=servers,DC=my_org,DC=local description: Example of linux server - enabled: yes + enabled: true state: present - name: Remove linux computer from Active Directory using a windows machine @@ -233,26 +248,26 @@ EXAMPLES = r""" identity: TheComputer spn: add: - - HOST/TheComputer - - HOST/TheComputer.domain.test - - HOST/TheComputer.domain.test:1234 + - HOST/TheComputer + - HOST/TheComputer.domain.test + - HOST/TheComputer.domain.test:1234 - name: Remove SPNs on the computer microsoft.ad.computer: identity: TheComputer spn: remove: - - HOST/TheComputer - - HOST/TheComputer.domain.test - - HOST/TheComputer.domain.test:1234 + - HOST/TheComputer + - HOST/TheComputer.domain.test + - HOST/TheComputer.domain.test:1234 - name: Set the principals the computer trusts for delegation from microsoft.ad.computer: identity: TheComputer delegates: set: - - CN=FileShare,OU=Computers,DC=domain,DC=test - - CN=DC,OU=Domain Controllers,DC=domain,DC=test + - CN=FileShare,OU=Computers,DC=domain,DC=test + - OtherServer$ # Lookup by sAMAaccountName """ RETURN = r""" diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain.py b/ansible_collections/microsoft/ad/plugins/modules/domain.py index 15578f7fd..0d9359242 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/domain.py +++ b/ansible_collections/microsoft/ad/plugins/modules/domain.py @@ -99,6 +99,7 @@ attributes: bypass_host_loop: support: none seealso: +- module: microsoft.ad.domain_child - module: microsoft.ad.domain_controller - module: microsoft.ad.group - module: microsoft.ad.membership diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1 b/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1 new file mode 100644 index 000000000..85fe3053d --- /dev/null +++ b/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1 @@ -0,0 +1,242 @@ +#!powershell + +# Copyright (c) 2024 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + create_dns_delegation = @{ + type = 'bool' + } + database_path = @{ + type = 'path' + } + dns_domain_name = @{ + type = 'str' + } + domain_admin_password = @{ + type = 'str' + required = $true + no_log = $true + } + domain_admin_user = @{ + type = 'str' + required = $true + } + domain_mode = @{ + type = 'str' + } + domain_type = @{ + choices = 'child', 'tree' + default = 'child' + type = 'str' + } + install_dns = @{ + type = 'bool' + } + log_path = @{ + type = 'path' + } + parent_domain_name = @{ + type = 'str' + } + reboot = @{ + default = $false + type = 'bool' + } + safe_mode_password = @{ + type = 'str' + required = $true + no_log = $true + } + site_name = @{ + type = 'str' + } + sysvol_path = @{ + type = 'path' + } + } + required_if = @( + , @('domain_type', 'tree', @('parent_domain_name')) + ) + required_together = @( + , @("domain_admin_user", "domain_admin_password") + ) + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$module.Result.reboot_required = $false +$module.Result._do_action_reboot = $false # Used by action plugin + +$createDnsDelegation = $module.Params.create_dns_delegation +$databasePath = $module.Params.database_path +$dnsDomainName = $module.Params.dns_domain_name +$domainMode = $module.Params.domain_mode +$domainType = $module.Params.domain_type +$installDns = $module.Params.install_dns +$logPath = $module.Params.log_path +$parentDomainName = $module.Params.parent_domain_name +$safeModePassword = $module.Params.safe_mode_password +$siteName = $module.Params.site_name +$sysvolPath = $module.Params.sysvol_path + +$domainCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $module.Params.domain_admin_user, + (ConvertTo-SecureString -AsPlainText -Force -String $module.Params.domain_admin_password) +) + +if ($domainType -eq 'child' -and $parentDomainName) { + $module.FailJson("parent_domain_name must not be set when domain_type=child") +} + +$requiredFeatures = @("AD-Domain-Services", "RSAT-ADDS") +$features = Get-WindowsFeature -Name $requiredFeatures +$unavailableFeatures = Compare-Object -ReferenceObject $requiredFeatures -DifferenceObject $features.Name -PassThru + +if ($unavailableFeatures) { + $module.FailJson("The following features required for a domain child are unavailable: $($unavailableFeatures -join ',')") +} + +$missingFeatures = $features | Where-Object InstallState -NE Installed +if ($missingFeatures) { + $res = Install-WindowsFeature -Name $missingFeatures -WhatIf:$module.CheckMode + $module.Result.changed = $true + $module.Result.reboot_required = [bool]$res.RestartNeeded + + # When in check mode and the prereq was "installed" we need to exit early as + # the AD cmdlets weren't really installed + if ($module.CheckMode) { + $module.ExitJson() + } +} + +# Check that we got a valid domain_mode +$validDomainModes = [Enum]::GetNames((Get-Command -Name Install-ADDSDomain).Parameters.DomainMode.ParameterType) +if (($null -ne $domainMode) -and -not ($domainMode -in $validDomainModes)) { + $validModes = $validDomainModes -join ", " + $module.FailJson("The parameter 'domain_mode' does not accept '$domainMode', please use one of: $validModes") +} + +$systemRole = Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, DomainRole +if ($systemRole.DomainRole -in @(4, 5)) { + if ($systemRole.Domain -ne $dnsDomainName) { + $module.FailJson("Host is already a domain controller in another domain $($systemRole.Domain)") + } + $module.ExitJson() +} + +$installParams = @{ + Confirm = $false + Credential = $domainCredential + Force = $true + NoRebootOnCompletion = $true + SafeModeAdministratorPassword = (ConvertTo-SecureString $safeModePassword -AsPlainText -Force) + SkipPreChecks = $true + WhatIf = $module.CheckMode +} + +if ($domainType -eq 'child') { + $newDomainName, $parentDomainName = $dnsDomainName.Split([char[]]".", 2) + $installParams.DomainType = 'ChildDomain' + $installParams.NewDomainName = $newDomainName + $installParams.ParentDomainName = $parentDomainName +} +else { + $installParams.DomainType = 'TreeDomain' + $installParams.NewDomainName = $dnsDomainName + $installParams.ParentDomainName = $parentDomainName +} + +if ($null -ne $createDnsDelegation) { + $installParams.CreateDnsDelegation = $createDnsDelegation +} +if ($databasePath) { + $installParams.DatabasePath = $databasePath +} +if ($domainMode) { + $installParams.DomainMode = $domainMode +} +if ($null -ne $installDns) { + $installParams.InstallDns = $installDns +} +if ($logPath) { + $installParams.LogPath = $logPath +} +if ($siteName) { + $installParams.SiteName = $siteName +} +if ($sysvolPath) { + $installParams.SysvolPath = $sysvolPath +} + +try { + $null = Install-ADDSDomain @installParams +} +catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] { + # ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.' + # DCPromo exit codes details can be found at + # https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment + if ($_.Exception.ExitCode -in @(15, 19)) { + $module.Result.reboot_required = $true + $module.Result._do_action_reboot = $true + } + + $module.FailJson("Failed to install ADDSDomain, DCPromo exited with $($_.Exception.ExitCode)", $_) +} +finally { + # The Netlogon service is set to auto start but is not started. This is + # required for Ansible to connect back to the host and reboot in a + # later task. Even if this fails Ansible can still connect but only + # with ansible_winrm_transport=basic so we just display a warning if + # this fails. + if (-not $module.CheckMode) { + try { + Start-Service -Name Netlogon + } + catch { + $msg = -join @( + "Failed to start the Netlogon service after promoting the host, " + "Ansible may be unable to connect until the host is manually rebooted: $($_.Exception.Message)" + ) + $module.Warn($msg) + } + } +} + +$module.Result.changed = $true +$module.Result.reboot_required = $true + +if ($module.Result.reboot_required -and $module.Params.reboot -and -not $module.CheckMode) { + # Promoting or depromoting puts the server in a very funky state and it may + # not be possible for Ansible to connect back without a reboot is done. If + # the user requested the action plugin to perform the reboot then start it + # here and get the action plugin to continue where this left off. + + $lastBootTime = (Get-CimInstance -ClassName Win32_OperatingSystem -Property LastBootUpTime).LastBootUpTime.ToFileTime() + $module.Result._previous_boot_time = $lastBootTime + + $shutdownRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\AutoLogonChecked' + Remove-Item -LiteralPath $shutdownRegPath -Force -ErrorAction SilentlyContinue + + $comment = 'Reboot initiated by Ansible' + $stdout = $null + $stderr = . { shutdown.exe /r /t 10 /c $comment | Set-Variable stdout } 2>&1 | ForEach-Object ToString + if ($LASTEXITCODE -eq 1190) { + # A reboot was already scheduled, abort it and try again + shutdown.exe /a + $stdout = $null + $stderr = . { shutdown.exe /r /t 10 /c $comment | Set-Variable stdout } 2>&1 | ForEach-Object ToString + } + + if ($LASTEXITCODE) { + $module.Result.rc = $LASTEXITCODE + $module.Result.stdout = $stdout + $module.Result.stderr = $stderr + $module.FailJson("Failed to initiate reboot, see rc, stdout, stderr for more information") + } +} + +$module.ExitJson() diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml b/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml new file mode 100644 index 000000000..0f3308098 --- /dev/null +++ b/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml @@ -0,0 +1,184 @@ +# Copyright (c) 2024 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION: + module: domain_child + short_description: Manage domain children in an existing Active Directory forest. + description: + - Ensure that a Windows Server host is configured as a domain controller as + a new domain in an existing forest. + - This module may require subsequent use of the + M(ansible.windows.win_reboot) action if changes are made. + - This module will only check if the domain specified by I(dns_domain_name) + exists or not. If the domain already exists under the same name, no other + options, other than the domain name will be checked during the run. + options: + create_dns_delegation: + description: + - Whether to create a DNS delegation that references the new DNS + server that was installed. + - Valid for Active Directory-integrated DNS only. + - The default is computed automatically based on the environment. + type: bool + database_path: + description: + - The path to a directory on a fixed disk of the Windows host where the + domain database will be created.. + - If not set then the default path is C(%SYSTEMROOT%\NTDS). + type: path + dns_domain_name: + description: + - The full DNS name of the domain to create. + - When I(domain_type=child), the parent DNS domain name is derived + from this value. + type: str + domain_admin_password: + description: + - Password for the specified I(domain_admin_user). + type: str + required: true + domain_admin_user: + description: + - Username of a domain admin for the parent domain. + type: str + required: true + domain_mode: + description: + - Specifies the domain functional level of child/tree. + - The domain functional level cannot be lower than the forest + functional level, but it can be higher. + - The default is automatically computed and set. + - Current known modes are C(Win2003), C(Win2008), C(Win2008R2), + C(Win2012), C(Win2012R2), or C(WinThreshold). + type: str + domain_type: + description: + - Specifies the type of domain to create. + - Set to C(child) to create a child of an existing domain as specified + by I(dns_domain_name). + - Set to C(tree) to create a new domain tree in an existing forest as + specified by I(parent_domain_name). The I(dns_domain_name) must be + the full domain name of the new domain tree to create. + choices: + - child + - tree + default: child + type: str + install_dns: + description: + - Whether to install the DNS service when creating the domain + controller. + - If not specified then the C(-InstallDns) option is not supplied to + the C(Install-ADDSDomain) command, see + L(Install-ADDSDomain,https://learn.microsoft.com/en-us/powershell/module/addsdeployment/install-addsdomain#-installdns) + for more information. + type: bool + log_path: + description: + - Specified the fully qualified, non-UNC path to a directory on a fixed + disk of the local computer that will contain the domain log files. + type: path + parent_domain_name: + description: + - The fully qualified domain name of an existing parent domain to + create a new domain tree in. + - This can only be set when I(domain_type=tree). + type: str + reboot: + description: + - If C(true), this will reboot the host if a reboot was create the + domain. + - If C(false), this will not reboot the host if a reboot was required + and instead sets the I(reboot_required) return value to C(true). + - Multiple reboots may occur if the host required a reboot before the + domain promotion. + - This cannot be used with async mode. + type: bool + default: false + safe_mode_password: + description: + - Safe mode password for the domain controller. + required: true + type: str + site_name: + description: + - Specifies the name of an existing site where you can place the new + domain controller. + type: str + sysvol_path: + description: + - The path to a directory on a fixed disk of the Windows host where the + Sysvol folder will be created. + - If not set then the default path is C(%SYSTEMROOT%\SYSVOL). + type: path + notes: + - It is highly recommended to set I(reboot=true) to have Ansible manage the + host reboot phase as the actions done by this module puts the host in a + state where it may not be possible for Ansible to reconnect in a + subsequent task without a reboot. + - This module must be run on a Windows target host. + extends_documentation_fragment: + - ansible.builtin.action_common_attributes + - ansible.builtin.action_common_attributes.flow + attributes: + check_mode: + support: full + diff_mode: + support: none + platform: + platforms: + - windows + action: + support: full + async: + support: partial + details: Supported for all scenarios except with I(reboot=True). + bypass_host_loop: + support: none + seealso: + - module: microsoft.ad.domain + - module: microsoft.ad.domain_controller + author: + - Jordan Borean (@jborean93) + +EXAMPLES: | + - name: Create a child domain foo.example.com with parent example.com + microsoft.ad.domain_child: + dns_domain_name: foo.example.com + domain_admin_user: testguy@example.com + domain_admin_password: password123! + safe_mode_password: password123! + reboot: true + + - name: Create a domain tree foo.example.com with parent bar.example.com + microsoft.ad.domain_child: + dns_domain_name: foo.example.com + parent_domain_name: bar.example.com + domain_type: tree + domain_admin_user: testguy@bar.example.com + domain_admin_password: password123! + local_admin_password: password123! + reboot: true + + # This scenario is not recommended, use reboot: true when possible + - name: Promote server with custom paths with manual reboot task + microsoft.ad.domain_child: + dns_domain_name: foo.ansible.vagrant + domain_admin_user: testguy@ansible.vagrant + domain_admin_password: password123! + safe_mode_password: password123! + sysvol_path: D:\SYSVOL + database_path: D:\NTDS + log_path: D:\NTDS + register: dc_promotion + + - name: Reboot after promotion + microsoft.ad.win_reboot: + when: dc_promotion.reboot_required + +RETURNS: + reboot_required: + description: True if changes were made that require a reboot. + returned: always + type: bool + sample: true diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py index df4641741..69971243b 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py +++ b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py @@ -114,6 +114,7 @@ attributes: seealso: - module: microsoft.ad.computer - module: microsoft.ad.domain +- module: microsoft.ad.domain_child - module: microsoft.ad.group - module: microsoft.ad.membership - module: microsoft.ad.user @@ -150,7 +151,7 @@ EXAMPLES = r""" domain_admin_password: password123! safe_mode_password: password123! state: domain_controller - read_only: yes + read_only: true site_name: London reboot: true @@ -168,7 +169,7 @@ EXAMPLES = r""" register: dc_promotion - name: Reboot after promotion - microsoft.ad.win_reboot: + ansible.windows.win_reboot: when: dc_promotion.reboot_required """ diff --git a/ansible_collections/microsoft/ad/plugins/modules/group.ps1 b/ansible_collections/microsoft/ad/plugins/modules/group.ps1 index bbb3aa8d7..ed4a52164 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/group.ps1 +++ b/ansible_collections/microsoft/ad/plugins/modules/group.ps1 @@ -26,141 +26,14 @@ $setParams = @{ Name = 'managed_by' Option = @{ type = 'str' } Attribute = 'ManagedBy' + DNLookup = $true } [PSCustomObject]@{ Name = 'members' - Option = @{ - type = 'dict' - options = @{ - add = @{ - type = 'list' - elements = 'str' - } - remove = @{ - type = 'list' - elements = 'str' - } - set = @{ - type = 'list' - elements = 'str' - } - } - } + Option = @{ type = 'add_remove_set' } Attribute = 'member' - New = { - param($Module, $ADParams, $NewParams) - - $newMembers = @( - foreach ($actionKvp in $Module.Params.members.GetEnumerator()) { - if ($null -eq $actionKvp.Value -or $actionKvp.Key -eq 'remove') { continue } - - $invalidMembers = [System.Collections.Generic.List[string]]@() - - foreach ($m in $actionKvp.Value) { - $obj = Get-AnsibleADObject -Identity $m @ADParams | - Select-Object -ExpandProperty DistinguishedName - if ($obj) { - $obj - } - else { - $invalidMembers.Add($m) - } - } - - if ($invalidMembers) { - $module.FailJson("Failed to find the following ad objects for group members: '$($invalidMembers -join "', '")'") - } - } - ) - - if ($newMembers) { - if (-not $NewParams.ContainsKey('OtherAttributes')) { - $NewParams.OtherAttributes = @{} - } - # The AD cmdlets don't like explicitly casted arrays, use - # ForEach-Object to get back a vanilla object[] to set. - $NewParams.OtherAttributes.member = $newMembers | ForEach-Object { "$_" } - } - $Module.Diff.after.members = @($newMembers | Sort-Object) - } - Set = { - param($Module, $ADParams, $SetParams, $ADObject) - - [string[]]$existingMembers = $ADObject.member - - $desiredState = @{} - foreach ($actionKvp in $Module.Params.members.GetEnumerator()) { - if ($null -eq $actionKvp.Value) { continue } - - $invalidMembers = [System.Collections.Generic.List[string]]@() - - $dns = foreach ($m in $actionKvp.Value) { - $obj = Get-AnsibleADObject -Identity $m @ADParams | - Select-Object -ExpandProperty DistinguishedName - if ($obj) { - $obj - } - else { - $invalidMembers.Add($m) - } - } - - if ($invalidMembers) { - $module.FailJson("Failed to find the following ad objects for group members: '$($invalidMembers -join "', '")'") - } - - $desiredState[$actionKvp.Key] = @($dns) - } - - $ignoreCase = [System.StringComparer]::OrdinalIgnoreCase - [string[]]$diffAfter = @() - if ($desiredState.ContainsKey('set')) { - [string[]]$desiredMembers = $desiredState.set - $diffAfter = $desiredMembers - - $toAdd = [string[]][System.Linq.Enumerable]::Except($desiredMembers, $existingMembers, $ignoreCase) - $toRemove = [string[]][System.Linq.Enumerable]::Except($existingMembers, $desiredMembers, $ignoreCase) - - if ($toAdd -or $toRemove) { - if (-not $SetParams.ContainsKey('Replace')) { - $SetParams.Replace = @{} - } - $SetParams.Replace.member = $desiredMembers - } - } - else { - [string[]]$toAdd = @() - [string[]]$toRemove = @() - $diffAfter = $existingMembers - - if ($desiredState.ContainsKey('add') -and $desiredState.add) { - [string[]]$desiredMembers = $desiredState.add - $toAdd = [string[]][System.Linq.Enumerable]::Except($desiredMembers, $existingMembers, $ignoreCase) - $diffAfter = [System.Linq.Enumerable]::Union($desiredMembers, $diffAfter, $ignoreCase) - } - if ($desiredState.ContainsKey('remove') -and $desiredState.remove) { - - [string[]]$desiredMembers = $desiredState.remove - $toRemove = [string[]][System.Linq.Enumerable]::Intersect($desiredMembers, $existingMembers, $ignoreCase) - $diffAfter = [System.Linq.Enumerable]::Except($diffAfter, $desiredMembers, $ignoreCase) - } - - if ($toAdd) { - if (-not $SetParams.ContainsKey('Add')) { - $SetParams.Add = @{} - } - $SetParams.Add.member = $toAdd - } - if ($toRemove) { - if (-not $SetParams.ContainsKey('Remove')) { - $SetParams.Remove = @{} - } - $SetParams.Remove.member = $toRemove - } - } - - $Module.Diff.after.members = ($diffAfter | Sort-Object) - } + DNLookup = $true + IsRawAttribute = $true } [PSCustomObject]@{ Name = 'sam_account_name' diff --git a/ansible_collections/microsoft/ad/plugins/modules/group.py b/ansible_collections/microsoft/ad/plugins/modules/group.py index 9fb28e819..df2c70440 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/group.py +++ b/ansible_collections/microsoft/ad/plugins/modules/group.py @@ -32,19 +32,29 @@ options: description: - The user or group that manages the group. - The value can be in the form of a C(distinguishedName), C(objectGUID), - C(objectSid), or C(sAMAccountName). + C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a + dictionary with the I(name) and optional I(server) key. - This is the value set on the C(managedBy) LDAP attribute. - type: str + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. + type: raw members: description: - The members of the group to set. - The value is a dictionary that contains 3 keys, I(add), I(remove), and I(set). - - Each subkey is set to a list of AD principal objects to add, remove or - set as the members of this AD group respectively. A principal can be in - the form of a C(distinguishedName), C(objectGUID), C(objectSid), or - C(sAMAccountName). - - The module will fail if it cannot find any of the members referenced. + - Each subkey value is a list of values in the form of a + C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName), + or C(userPrincipalName) string or a dictionary with the I(name) and + optional I(server) key. + - The value for each subkey can either be specified as a string or a + dictionary with the I(name) and optional I(server) key. The I(name) is + the identity to lookup and I(server) is an optional key to override what + AD server to lookup the identity on. + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information. type: dict suboptions: add: @@ -52,13 +62,22 @@ options: - Adds the principals specified as members of the group, keeping the existing membership if they are not specified. type: list - elements: str + elements: raw + lookup_failure_action: + description: + - Control the action to take when the lookup fails to find the DN. + - C(fail) will cause the task to fail. + - C(ignore) will ignore the value and continue. + - C(warn) will ignore the value and display a warning. + choices: ['fail', 'ignore', 'warn'] + default: fail + type: str remove: description: - Removes the principals specified as members of the group, keeping the existing membership if they are not specified. type: list - elements: str + elements: raw set: description: - Sets only the principals specified as members of the group. @@ -66,7 +85,7 @@ options: if not specified in this list. - Set this to an empty list to remove all members from a group. type: list - elements: str + elements: raw sam_account_name: description: - The C(sAMAccountName) value to set for the group. @@ -179,8 +198,8 @@ EXAMPLES = r""" scope: domainlocal members: add: - - Domain Admins - - Domain Users + - Domain Admins + - Domain Users - name: Remove members from the group, preserving existing membership microsoft.ad.group: @@ -188,8 +207,8 @@ EXAMPLES = r""" scope: domainlocal members: remove: - - Domain Admins - - Domain Users + - Domain Admins + - Domain Users - name: Replace entire membership of group microsoft.ad.group: @@ -197,8 +216,14 @@ EXAMPLES = r""" scope: domainlocal members: set: - - Domain Admins - - Domain Users + - Domain Admins + - Domain Users + - name: UserInOtherDomain + server: OtherDomain + domain_credentials: + - name: OtherDomain + username: OtherDomainUser + password: '{{ other_domain_password }}' """ RETURN = r""" diff --git a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 index d2be34e9f..963733a97 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 +++ b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 @@ -143,7 +143,7 @@ Function Get-CurrentState { } [PSCustomObject]@{ - HostName = $env:COMPUTERNAME + HostName = [System.Net.Dns]::GetHostName() PartOfDomain = $cs.PartOfDomain DnsDomainName = $domainName WorkgroupName = $cs.Workgroup diff --git a/ansible_collections/microsoft/ad/plugins/modules/object.py b/ansible_collections/microsoft/ad/plugins/modules/object.py index c6396619a..6b305afa2 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/object.py +++ b/ansible_collections/microsoft/ad/plugins/modules/object.py @@ -128,8 +128,8 @@ EXAMPLES = r""" attributes: add: extensionName: - - value 1 - - value 2 + - value 1 + - value 2 type: container state: present @@ -139,8 +139,8 @@ EXAMPLES = r""" attributes: remove: extensionName: - - value 1 - - value 3 + - value 1 + - value 3 type: container state: present """ diff --git a/ansible_collections/microsoft/ad/plugins/modules/object_info.py b/ansible_collections/microsoft/ad/plugins/modules/object_info.py index 0cdcf06a7..88460979b 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/object_info.py +++ b/ansible_collections/microsoft/ad/plugins/modules/object_info.py @@ -130,13 +130,13 @@ EXAMPLES = r""" microsoft.ad.object_info: filter: ObjectClass -eq 'user' -and objectCategory -eq 'Person' properties: - - objectSid + - objectSid - name: Get the SID for all user accounts as a LDAP filter microsoft.ad.object_info: ldap_filter: (&(objectClass=user)(objectCategory=Person)) properties: - - objectSid + - objectSid - name: Search all computer accounts in a specific path that were added after February 1st microsoft.ad.object_info: diff --git a/ansible_collections/microsoft/ad/plugins/modules/ou.ps1 b/ansible_collections/microsoft/ad/plugins/modules/ou.ps1 index 6af68b5ae..909b13cd9 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/ou.ps1 +++ b/ansible_collections/microsoft/ad/plugins/modules/ou.ps1 @@ -22,6 +22,7 @@ $setParams = @{ Name = 'managed_by' Option = @{ type = 'str' } Attribute = 'ManagedBy' + DNLookup = $true } [PSCustomObject]@{ Name = 'postal_code' diff --git a/ansible_collections/microsoft/ad/plugins/modules/ou.py b/ansible_collections/microsoft/ad/plugins/modules/ou.py index 5d1d60503..1e31cc890 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/ou.py +++ b/ansible_collections/microsoft/ad/plugins/modules/ou.py @@ -26,9 +26,13 @@ options: description: - The user or group that manages the object. - The value can be in the form of a C(distinguishedName), C(objectGUID), - C(objectSid), or sAMAccountName). + C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a + dictionary with the I(name) and optional I(server) key. - This is the value set on the C(managedBy) LDAP attribute. - type: str + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. + type: raw postal_code: description: - Configures the user's postal code / zip code. @@ -116,6 +120,13 @@ EXAMPLES = r""" attributes: set: comment: A comment for the OU + +- name: Set managedBy using an identity from another DC + microsoft.ad.ou: + name: MyOU + managed_by: + name: manager-user + server: OtherDC """ RETURN = r""" diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.ps1 b/ansible_collections/microsoft/ad/plugins/modules/user.ps1 index 267c77627..8eef49635 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/user.ps1 +++ b/ansible_collections/microsoft/ad/plugins/modules/user.ps1 @@ -66,6 +66,9 @@ $setParams = @{ type = 'bool' } Attribute = 'LockedOut' + # We cannot lock a user and creating a user that is unlocked + # requires no action. + New = {} Set = { param($Module, $ADParams, $SetParams, $ADObject) @@ -100,15 +103,10 @@ $setParams = @{ Name = 'delegates' Option = @{ aliases = 'principals_allowed_to_delegate' - type = 'dict' - options = @{ - add = @{ type = 'list'; elements = 'str' } - remove = @{ type = 'list'; elements = 'str' } - set = @{ type = 'list'; elements = 'str' } - } + type = 'add_remove_set' } Attribute = 'PrincipalsAllowedToDelegateToAccount' - CaseInsensitive = $true + DNLookup = $true } [PSCustomObject]@{ @@ -134,10 +132,11 @@ $setParams = @{ Option = @{ type = 'dict' options = @{ - add = @{ type = 'list'; elements = 'str' } - remove = @{ type = 'list'; elements = 'str' } - set = @{ type = 'list'; elements = 'str' } - missing_behaviour = @{ + add = @{ type = 'list'; elements = 'raw' } + remove = @{ type = 'list'; elements = 'raw' } + set = @{ type = 'list'; elements = 'raw' } + lookup_failure_action = @{ + aliases = @('missing_behaviour') choices = 'fail', 'ignore', 'warn' default = 'fail' type = 'str' @@ -367,27 +366,19 @@ $setParams = @{ return } - $groupMissingBehaviour = $Module.Params.groups.missing_behaviour - $lookupGroup = { - try { - (Get-ADGroup -Identity $args[0] @ADParams).DistinguishedName - } - catch { - if ($groupMissingBehaviour -eq "fail") { - $module.FailJson("Failed to locate group $($args[0]): $($_.Exception.Message)", $_) - } - elseif ($groupMissingBehaviour -eq "warn") { - $module.Warn("Failed to locate group $($args[0]) but continuing on: $($_.Exception.Message)") - } - } - } - [string[]]$existingGroups = @( # In check mode the ADObject won't be given if ($ADObject) { try { - Get-ADPrincipalGroupMembership -Identity $ADObject.ObjectGUID @ADParams -ErrorAction Stop | - Select-Object -ExpandProperty DistinguishedName + # Get-ADPrincipalGroupMembership doesn't work well with + # cross domain membership. It also gets the primary group + # so this code reflects that using Get-ADUser instead. + $userMembership = Get-ADUser -Identity $ADObject.ObjectGUID @ADParams -Properties @( + 'MemberOf', + 'PrimaryGroup' + ) -ErrorAction Stop + $userMembership.memberOf + $userMembership.PrimaryGroup } catch { $module.Warn("Failed to enumerate user groups but continuing on: $($_.Exception.Message)") @@ -403,14 +394,42 @@ $setParams = @{ CaseInsensitive = $true Existing = $existingGroups } - 'add', 'remove', 'set' | ForEach-Object -Process { - if ($null -ne $Module.Params.groups[$_]) { - $compareParams[$_] = @( - foreach ($group in $Module.Params.groups[$_]) { - & $lookupGroup $group + $dnServerParams = @{} + foreach ($actionKvp in $Module.Params.groups.GetEnumerator()) { + if ($null -eq $actionKvp.Value -or $actionKvp.Key -in @('lookup_failure_action', 'missing_behaviour')) { + continue + } + + $convertParams = @{ + Module = $Module + Context = "groups.$($actionKvp.Key)" + FailureAction = $Module.Params.groups.lookup_failure_action + } + $dns = foreach ($lookupId in $actionKvp.Value) { + $dn = $lookupId | ConvertTo-AnsibleADDistinguishedName @ADParams @convertParams + if (-not $dn) { + continue # Warning was written + } + + # As membership is done on the group server, we need to store + # correct server and credentials that was used for the lookup. + if ($lookupId -is [System.Collections.IDictionary] -and $lookupId.server) { + $dnServerParams[$dn] = @{ + Server = $lookupId.server + } + + if ($Module.ServerCredentials.ContainsKey($lookupId.server)) { + $dnServerParams[$dn].Credential = $Module.ServerCredentials[$lookupId.server] } - ) + } + else { + $dnServerParams[$dn] = $ADParams + } + + $dn } + + $compareParams[$actionKvp.Key] = @($dns) } $res = Compare-AnsibleADIdempotentList @compareParams @@ -422,15 +441,32 @@ $setParams = @{ WhatIf = $Module.CheckMode } foreach ($member in $res.ToAdd) { + $lookupParams = if ($dnServerParams.ContainsKey($member)) { + $dnServerParams[$member] + } + else { + $ADParams + } if ($ADObject) { - Add-ADGroupMember -Identity $member -Members $ADObject.ObjectGUID @ADParams @commonParams + Set-ADObject -Identity $member -Add @{ + member = $ADObject.DistinguishedName + } @lookupParams @commonParams + } $Module.Result.changed = $true } foreach ($member in $res.ToRemove) { + $lookupParams = if ($dnServerParams.ContainsKey($member)) { + $dnServerParams[$member] + } + else { + $ADParams + } if ($ADObject) { try { - Remove-ADGroupMember -Identity $member -Members $ADObject.ObjectGUID @ADParams @commonParams + Set-ADObject -Identity $member -Remove @{ + member = $ADObject.DistinguishedName + } @lookupParams @commonParams } catch [Microsoft.ActiveDirectory.Management.ADException] { if ($_.Exception.ErrorCode -eq 0x0000055E) { diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.py b/ansible_collections/microsoft/ad/plugins/modules/user.py index a3e7d1ecb..81a48b41d 100644 --- a/ansible_collections/microsoft/ad/plugins/modules/user.py +++ b/ansible_collections/microsoft/ad/plugins/modules/user.py @@ -40,14 +40,19 @@ options: description: - The principal objects that the current AD object can trust for delegation to either add, remove or set. - - The values for each sub option must be specified as a distinguished name - C(CN=shenetworks,CN=Users,DC=ansible,DC=test) + - Each subkey value is a list of values in the form of a + C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName), + or C(userPrincipalName) string or a dictionary with the I(name) and + optional I(server) key. - This is the value set on the C(msDS-AllowedToActOnBehalfOfOtherIdentity) LDAP attribute. - This is a highly sensitive attribute as it allows the principals specified to impersonate any account when authenticating with the AD computer object being managed. - To clear all principals, use I(set) with an empty list. + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. - See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values) for more information on how to add/remove/set list options. aliases: @@ -56,29 +61,36 @@ options: suboptions: add: description: - - The AD objects by their C(DistinguishedName) to add as a principal - allowed to delegate. + - Adds the principals specified as principals allowed to delegate to. - Any existing principals not specified by I(add) will be untouched unless specified by I(remove) or not in I(set). type: list - elements: str + elements: raw + lookup_failure_action: + description: + - Control the action to take when the lookup fails to find the DN. + - C(fail) will cause the task to fail. + - C(ignore) will ignore the value and continue. + - C(warn) will ignore the value and display a warning. + choices: ['fail', 'ignore', 'warn'] + default: fail + type: str remove: description: - - The AD objects by their C(DistinguishedName) to remove as a principal - allowed to delegate. + - Removes the principals specified as principals allowed to delegate to. - Any existing principals not specified by I(remove) will be untouched unless I(set) is defined. type: list - elements: str + elements: raw set: description: - - The AD objects by their C(DistinguishedName) to set as the only + - Sets the principals specified as principals allowed to delegate to. principals allowed to delegate. - This will remove any existing principals if not specified in this list. - Specify an empty list to remove all principals allowed to delegate. type: list - elements: str + elements: raw email: description: - Configures the user's email address. @@ -104,10 +116,20 @@ options: - To clear all group memberships, use I(set) with an empty list. - Note that users cannot be removed from their principal group (for example, "Domain Users"). Attempting to do so will display a warning. + - Adding and removing a user from a group is done on the group AD object. + If the group is an object in a different domain, then it may require + explicit I(server) and I(domain_credentials) for it to work. - Each subkey is set to a list of groups objects to add, remove or set as the membership of this AD user respectively. A group can be in the form of a C(distinguishedName), C(objectGUID), C(objectSid), or C(sAMAccountName). + - Each subkey value is a list of group objects in the form of a + C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName), + or C(userPrincipalName) string or a dictionary with the I(name) and + optional I(server) key. + - See + R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes) + for more information on how DN lookups work. - See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values) for more information on how to add/remove/set list options. type: dict @@ -116,20 +138,20 @@ options: description: - The groups to add the user to. type: list - elements: str + elements: raw remove: description: - The groups to remove the user from. type: list - elements: str + elements: raw set: description: - The only groups the user is a member of. - This will clear out any existing groups if not in the specified list. - Set to an empty list to clear all group membership of the user. type: list - elements: str - missing_behaviour: + elements: raw + lookup_failure_action: description: - Controls what happens when a group specified by C(groups) is an invalid group name. @@ -138,6 +160,8 @@ options: - C(ignore) will ignore any groups that does not exist. - C(warn) will display a warning for any groups that do not exist but will continue without failing. + aliases: + - missing_behaviour choices: - fail - ignore @@ -287,7 +311,7 @@ EXAMPLES = r""" state: present groups: set: - - Domain Admins + - Domain Admins street: 123 4th St. city: Sometown state_province: IN @@ -316,8 +340,8 @@ EXAMPLES = r""" path: ou=test,dc=domain,dc=local groups: set: - - Domain Admins - - Domain Users + - Domain Admins + - Domain Users - name: Ensure user bob is absent microsoft.ad.user: @@ -329,15 +353,15 @@ EXAMPLES = r""" identity: liz.kenyon spn: set: - - MSSQLSvc/us99db-svr95:1433 - - MSSQLSvc/us99db-svr95.vmware.com:1433 + - MSSQLSvc/us99db-svr95:1433 + - MSSQLSvc/us99db-svr95.vmware.com:1433 - name: Ensure user has spn added microsoft.ad.user: identity: liz.kenyon spn: add: - - MSSQLSvc/us99db-svr95:2433 + - MSSQLSvc/us99db-svr95:2433 - name: Ensure user is created with delegates and spn's defined microsoft.ad.user: @@ -346,17 +370,17 @@ EXAMPLES = r""" state: present groups: set: - - Domain Admins - - Domain Users - - Enterprise Admins + - Domain Admins + - Domain Users + - Enterprise Admins delegates: set: - - CN=shenetworks,CN=Users,DC=ansible,DC=test - - CN=mk.ai,CN=Users,DC=ansible,DC=test - - CN=jessiedotjs,CN=Users,DC=ansible,DC=test + - CN=shenetworks,CN=Users,DC=ansible,DC=test + - CN=mk.ai,CN=Users,DC=ansible,DC=test + - CN=jessiedotjs,CN=Users,DC=ansible,DC=test spn: set: - - MSSQLSvc/us99db-svr95:2433 + - MSSQLSvc/us99db-svr95:2433 # The name option is the name of the AD object as seen in dsa.msc and not the # sAMAccountName. For example, this will change the sAMAccountName of the user |