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/tests/integration/targets | |
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/tests/integration/targets')
62 files changed, 6682 insertions, 0 deletions
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/computer/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/computer/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/main.yml new file mode 100644 index 000000000..1e015c027 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/main.yml @@ -0,0 +1,14 @@ +- name: remove temp computer + computer: + name: MyComputer + state: absent + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp computer + computer: + name: MyComputer + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml new file mode 100644 index 000000000..fb4eee366 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml @@ -0,0 +1,425 @@ +- name: create computer - check + computer: + name: MyComputer + state: present + register: create_comp_check + check_mode: true + +- name: get result of create computer - check + object_info: + identity: '{{ create_comp_check.distinguished_name }}' + register: create_comp_check_actual + +- name: assert create computer - check + assert: + that: + - create_comp_check is changed + - create_comp_check.distinguished_name == 'CN=MyComputer,CN=Computers,' ~ setup_domain_info.output[0].defaultNamingContext + - create_comp_check.object_guid == '00000000-0000-0000-0000-000000000000' + - create_comp_check.sid == 'S-1-5-0000' + - create_comp_check_actual.objects == [] + +- name: create computer + computer: + name: MyComputer + state: present + register: create_comp + +- set_fact: + object_identity: '{{ create_comp.object_guid }}' + +- name: get result of create computer + object_info: + identity: '{{ object_identity }}' + properties: + - dnsHostName + - objectSid + - sAMAccountName + - userAccountControl + register: create_comp_actual + +- name: assert create computer + assert: + that: + - create_comp is changed + - create_comp_actual.objects | length == 1 + - create_comp.distinguished_name == 'CN=MyComputer,CN=Computers,' ~ setup_domain_info.output[0].defaultNamingContext + - create_comp.object_guid == create_comp_actual.objects[0].ObjectGUID + - create_comp.sid == create_comp_actual.objects[0].objectSid.Sid + - create_comp_actual.objects[0].DistinguishedName == create_comp.distinguished_name + - create_comp_actual.objects[0].Name == 'MyComputer' + - create_comp_actual.objects[0].dnsHostName == None + - create_comp_actual.objects[0].sAMAccountName == 'MyComputer$' + - create_comp_actual.objects[0].ObjectClass == 'computer' + - '"ADS_UF_ACCOUNTDISABLE" not in create_comp_actual.objects[0].userAccountControl_AnsibleFlags' + +- name: remove computer - check + computer: + name: MyComputer + state: absent + register: remove_comp_check + check_mode: true + +- name: get result of remove computer - check + object_info: + identity: '{{ object_identity }}' + register: remove_comp_check_actual + +- name: assert remove computer - check + assert: + that: + - remove_comp_check is changed + - remove_comp_check_actual.objects | length == 1 + +- name: remove computer + computer: + name: MyComputer + state: absent + register: remove_comp + +- name: get result of remove computer + object_info: + identity: '{{ object_identity }}' + register: remove_comp_actual + +- name: assert remove computer + assert: + that: + - remove_comp is changed + - remove_comp_actual.objects == [] + +- name: remove computer - idempotent + computer: + name: MyComputer + state: absent + register: remove_comp_again + +- name: assert remove computer - idempotent + assert: + that: + - not remove_comp_again is changed + +- name: create computer with custom options + computer: + name: MyComputer + state: present + delegates: + set: + - CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + kerberos_encryption_types: + set: + - aes128 + - aes256 + location: Comp Location + dns_hostname: MyComputer.domain.com + enabled: false + managed_by: Domain Admins + sam_account_name: SamMyComputer + spn: + set: + - HTTP/MyComputer + trusted_for_delegation: true + upn: MyComputer@{{ domain_realm }} + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + register: custom_comp + +- set_fact: + object_identity: '{{ custom_comp.object_guid }}' + +- name: get result of create computer with custom options + object_info: + identity: '{{ object_identity }}' + properties: + - dnsHostName + - location + - managedBy + - msDS-AllowedToActOnBehalfOfOtherIdentity + - msDS-SupportedEncryptionTypes + - objectSid + - sAMAccountName + - servicePrincipalName + - userAccountControl + - userPrincipalName + register: custom_comp_actual + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ custom_comp_actual.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: custom_comp_delegates + +- name: assert create computer with custom options + assert: + that: + - custom_comp is changed + - custom_comp.distinguished_name == 'CN=MyComputer,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - custom_comp.object_guid == custom_comp_actual.objects[0].ObjectGUID + - custom_comp.sid == custom_comp_actual.objects[0].objectSid.Sid + - custom_comp_actual.objects[0].DistinguishedName == custom_comp.distinguished_name + - custom_comp_actual.objects[0].Name == 'MyComputer' + - custom_comp_actual.objects[0].dnsHostName == 'MyComputer.domain.com' + - custom_comp_actual.objects[0].location == 'Comp Location' + - custom_comp_actual.objects[0].managedBy == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - custom_comp_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 24 + - custom_comp_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["AES128_CTS_HMAC_SHA1_96", "AES256_CTS_HMAC_SHA1_96"] + - custom_comp_actual.objects[0].sAMAccountName == 'SamMyComputer$' + - custom_comp_actual.objects[0].ObjectClass == 'computer' + - custom_comp_actual.objects[0].servicePrincipalName == 'HTTP/MyComputer' + - custom_comp_actual.objects[0].userPrincipalName == 'MyComputer@' ~ domain_realm + - '"ADS_UF_ACCOUNTDISABLE" in custom_comp_actual.objects[0].userAccountControl_AnsibleFlags' + - '"ADS_UF_TRUSTED_FOR_DELEGATION" in custom_comp_actual.objects[0].userAccountControl_AnsibleFlags' + - custom_comp_delegates.output == ["administrator", "krbtgt"] + +- name: change computer with custom options + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + delegates: + set: + - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + dns_hostname: other.domain.com + kerberos_encryption_types: + set: + - aes256 + - rc4 + location: comp location + enabled: true + sam_account_name: MyComputer2$ + trusted_for_delegation: false + upn: mycomputer@{{ domain_realm }} + register: change_comp + +- name: get result of change computer with custom options + object_info: + identity: '{{ object_identity }}' + properties: + - dnsHostName + - location + - msDS-AllowedToActOnBehalfOfOtherIdentity + - msDS-SupportedEncryptionTypes + - sAMAccountName + - userAccountControl + - userPrincipalName + register: change_comp_actual + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ change_comp_actual.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: change_comp_delegates + +- name: assert change computer with custom options + assert: + that: + - change_comp is changed + - change_comp_actual.objects[0].dnsHostName == 'other.domain.com' + - change_comp_actual.objects[0].location == 'comp location' + - change_comp_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 20 + - change_comp_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["RC4_HMAC", "AES256_CTS_HMAC_SHA1_96"] + - change_comp_actual.objects[0].sAMAccountName == 'MyComputer2$' + - change_comp_actual.objects[0].userPrincipalName == 'mycomputer@' ~ domain_realm + - '"ADS_UF_ACCOUNTDISABLE" not in change_comp_actual.objects[0].userAccountControl_AnsibleFlags' + - '"ADS_UF_TRUSTED_FOR_DELEGATION" not in change_comp_actual.objects[0].userAccountControl_AnsibleFlags' + - change_comp_delegates.output == ["krbtgt"] + +- name: add and remove list options + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + delegates: + add: + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + remove: + - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }} + kerberos_encryption_types: + add: + - aes128 + - aes256 + remove: + - rc4 + register: add_remove_list + +- name: get result of add and remove list options + object_info: + identity: '{{ object_identity }}' + properties: + - msDS-AllowedToActOnBehalfOfOtherIdentity + - msDS-SupportedEncryptionTypes + register: add_remove_list_actual + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ add_remove_list_actual.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: add_remove_list_delegation + +- name: assert add and remove list options + assert: + that: + - add_remove_list is changed + - add_remove_list_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 24 + - add_remove_list_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["AES128_CTS_HMAC_SHA1_96", "AES256_CTS_HMAC_SHA1_96"] + - add_remove_list_delegation.output == ["administrator"] + +- name: add and remove list options - idempotent + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + delegates: + add: + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + remove: + - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }} + kerberos_encryption_types: + add: + - aes128 + - aes256 + remove: + - rc4 + register: add_remove_list_again + +- name: assert add and remove list options - idempotent + assert: + that: + - not add_remove_list_again is changed + +- name: unset list options + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + delegates: + set: [] + kerberos_encryption_types: + set: [] + register: unset_list_options + +- name: get result of unset list options + object_info: + identity: '{{ object_identity }}' + properties: + - msDS-AllowedToActOnBehalfOfOtherIdentity + - msDS-SupportedEncryptionTypes + register: unset_list_options_actual + +- name: assert unset list options + assert: + that: + - unset_list_options is changed + - unset_list_options_actual.objects[0]['msDS-AllowedToActOnBehalfOfOtherIdentity'] == None + - unset_list_options_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 0 + - unset_list_options_actual.objects[0]['msDS-SupportedEncryptionTypes_AnsibleFlags'] == ["None"] + +- name: unset list options - idempotent + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + delegates: + set: [] + kerberos_encryption_types: + set: [] + register: unset_list_options_again + +- name: assert unset list options - idempotent + assert: + that: + - not unset_list_options_again is changed + +- name: set spns + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + spn: + set: + - HTTP/host + - HTTP/host.domain + - HTTP/host.domain:8080 + register: spn_set + +- name: get result of set spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_set_actual + +- name: assert set spns + assert: + that: + - spn_set is changed + - spn_set_actual.objects[0].servicePrincipalName == ['HTTP/host.domain:8080', 'HTTP/host.domain', 'HTTP/host'] + +- name: remove spns + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + spn: + remove: + - HTTP/fake + - HTTP/Host.domain + register: spn_remove + +- name: get result of remove spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_remove_actual + +- name: assert remove spns + assert: + that: + - spn_remove is changed + - spn_remove_actual.objects[0].servicePrincipalName == ['HTTP/host.domain:8080', 'HTTP/host'] + +- name: add spns + computer: + name: MyComputer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + spns: + add: + - HTTP/Host.domain:8080 + - HTTP/fake + register: spn_add + +- name: get result of add spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_add_actual + +- name: assert add spns + assert: + that: + - spn_add is changed + - spn_add_actual.objects[0].servicePrincipalName == ['HTTP/fake', 'HTTP/host.domain:8080', 'HTTP/host'] diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/tasks/main.yml new file mode 100644 index 000000000..59a6d6d90 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/tasks/main.yml @@ -0,0 +1,17 @@ +# It's hard to ensure there's a common test environment with the same module +# versions so just test that the plugin runs and has the base level return +# values. + +- name: make sure module can run + debug_ldap_client: + register: res + +- name: assert return values + assert: + that: + - res.dns is defined + - res.kerberos is defined + - res.packages.dnspython is defined + - res.packages.krb5 is defined + - res.packages.pyspnego is defined + - res.packages.sansldap is defined diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/domain/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain/meta/main.yml new file mode 100644 index 000000000..025c08e43 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain/meta/main.yml @@ -0,0 +1,4 @@ +dependencies: +- name: setup_domain + vars: + run_domain_test: true diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/main.yml new file mode 100644 index 000000000..c74df4f8d --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/main.yml @@ -0,0 +1,66 @@ +- name: create domain - idempotent + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + register: domain_again + +- name: assert create domain - idempotent + assert: + that: + - not domain_again is changed + - domain_again.reboot_required == False + +- name: fail when reboot and async is used + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + async: 60 + register: fail_reboot_async + failed_when: + - fail_reboot_async.msg != "async is not supported for this task when reboot=true" + +- name: fail when domain_netbios_name is greater than 15 character + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + domain_netbios_name: AReallyLongName1 + register: fail_long_netbios_name + failed_when: + - fail_long_netbios_name.msg != "The parameter 'domain_netbios_name' should not exceed 15 characters in length" + +- name: get OS version + ansible.windows.win_powershell: + script: | + $Ansible.Changed = $false + + $osVersion = [System.Environment]::OSVersion.Version + '{0}.{1}' -f $osVersion.Major, $osVersion.Minor + register: os_version + +- set_fact: + known_modes: + '6.2': Win2003, Win2008, Win2008R2, Win2012, Default + '6.3': Win2008, Win2008R2, Win2012, Win2012R2, Default + default: Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold, Default + +- set_fact: + expected_modes: '{{ known_modes[os_version.output[0]] | default(known_modes["default"]) }}' + +- name: fail when domain_mode is invalid + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + domain_mode: fail + register: fail_domain_mode + failed_when: + - 'fail_domain_mode.msg != "The parameter ''domain_mode'' does not accept ''fail'', please use one of: " ~ expected_modes' + +- name: fail when forest_mode is invalid + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + forest_mode: fail + register: fail_forest_mode + failed_when: + - 'fail_forest_mode.msg != "The parameter ''forest_mode'' does not accept ''fail'', please use one of: " ~ expected_modes' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/test.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/test.yml new file mode 100644 index 000000000..d443d2556 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/test.yml @@ -0,0 +1,95 @@ +- name: setup domain with no prereqs - check mode + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + check_mode: true + register: domain_no_prereqs_check + +- name: assert setup domain with no prereqs - check mode + assert: + that: + - domain_no_prereqs_check is changed + - domain_no_prereqs_check.reboot_required == False + +- name: install feature pre-requisites + ansible.windows.win_feature: + name: + - AD-Domain-Services + - RSAT-ADDS + state: present + +- name: setup domain without reboot - check mode + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + check_mode: true + register: domain_check_no_reboot + +- name: assert setup domain without reboot - check mode + assert: + that: + - domain_check_no_reboot is changed + - domain_check_no_reboot.reboot_required == True + +# While not needed it puts the host in a state where it needs to reboot +# before it can create the domain. This is testing an edge case. +- name: rename host + ansible.windows.win_hostname: + name: ansible-ad-test + +- name: setup domain without reboot after reboot pending - check mode + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + check_mode: true + register: domain_check_no_reboot_pending + +- name: assert setup domain without reboot after reboot pending - check mode + assert: + that: + - domain_check_no_reboot_pending is changed + - domain_check_no_reboot_pending.reboot_required == True + +- name: setup domain with reboot - check mode + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + check_mode: true + register: domain_check_reboot + +- name: assert setup domain with reboot - check mode + assert: + that: + - domain_check_reboot is changed + - domain_check_reboot.reboot_required == False + +- name: setup domain without reboot + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + register: domain_no_reboot + ignore_errors: true + +- name: assert setup domain without reboot + assert: + that: + - not domain_no_reboot is changed + - domain_no_reboot is failed + - '"Failed to install ADDSForest" in domain_no_reboot.msg' + - '"A reboot is required" in domain_no_reboot.msg' + - domain_no_reboot.reboot_required == True + +- name: setup domain with reboot + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + register: domain_reboot + +- name: assert setup domain with reboot + assert: + that: + - domain_reboot is changed + - domain_reboot.reboot_required == False diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/README.md b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/README.md new file mode 100644 index 000000000..0628d5335 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/README.md @@ -0,0 +1,34 @@ +# microsoft.ad.domain_controller tests + +As this cannot be run in CI this is a brief guide on how to run these tests locally. +Run the following: + +```bash +vagrant up + +ansible-playbook setup.yml +``` + +It is a good idea to create a snapshot of both hosts before running the tests. +This allows you to reset the host back to a blank starting state if the tests need to be rerun. +To create a snaphost do the following: + +```bash +virsh snapshot-create-as --domain "domain_controller_DC" --name "pretest" +virsh snapshot-create-as --domain "domain_controller_TEST" --name "pretest" +``` + +To restore these snapshots run the following: + +```bash +virsh snapshot-revert --domain "domain_controller_DC" --snapshotname "pretest" --running +virsh snapshot-revert --domain "domain_controller_TEST" --snapshotname "pretest" --running +``` + +Once you are ready to run the tests run the following: + +```bash +ansible-playbook test.yml +``` + +Run `vagrant destroy` to remove the test VMs. diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/Vagrantfile b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/Vagrantfile new file mode 100644 index 000000000..5341185c3 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/Vagrantfile @@ -0,0 +1,27 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +require 'yaml' + +inventory = YAML.load_file('inventory.yml') + +Vagrant.configure("2") do |config| + inventory['all']['children'].each do |group,details| + details['hosts'].each do |server,host_details| + config.vm.define server do |srv| + srv.vm.box = host_details['vagrant_box'] + srv.vm.hostname = server + srv.vm.network :private_network, + :ip => host_details['ansible_host'], + :libvirt__network_name => 'microsoft.ad', + :libvirt__domain_name => inventory['all']['vars']['domain_realm'] + + srv.vm.provider :libvirt do |l| + l.memory = 4096 + l.cpus = 2 + end + end + end + end +end + diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/aliases new file mode 100644 index 000000000..435ff207d --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/aliases @@ -0,0 +1,2 @@ +windows +unsupported # can never run in CI, see README.md diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/ansible.cfg b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/ansible.cfg new file mode 100644 index 000000000..3a986973e --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory.yml +retry_files_enabled = False diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/inventory.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/inventory.yml new file mode 100644 index 000000000..3daa807df --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/inventory.yml @@ -0,0 +1,21 @@ +all: + children: + windows: + hosts: + DC: + ansible_host: 192.168.11.10 + vagrant_box: jborean93/WindowsServer2022 + TEST: + ansible_host: 192.168.11.11 + vagrant_box: jborean93/WindowsServer2022 + vars: + ansible_port: 5985 + ansible_connection: psrp + + vars: + ansible_user: vagrant + ansible_password: vagrant + domain_username: vagrant-domain + domain_user_upn: '{{ domain_username }}@{{ domain_realm | upper }}' + domain_password: VagrantPass1 + domain_realm: ad.test diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/setup.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/setup.yml new file mode 100644 index 000000000..5fcc09dfd --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/setup.yml @@ -0,0 +1,82 @@ +- name: create domain controller + hosts: DC + gather_facts: no + + tasks: + - name: get network connection names + ansible.windows.win_powershell: + parameters: + IPAddress: '{{ ansible_host }}' + script: | + param ($IPAddress) + + $Ansible.Changed = $false + + Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" | + ForEach-Object -Process { + $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'" + if ($config.IPAddress -contains $IPAddress) { + $_.NetConnectionID + } + } + register: connection_name + + - name: set the DNS for the internal adapters to localhost + ansible.windows.win_dns_client: + adapter_names: + - '{{ connection_name.output[0] }}' + dns_servers: + - 127.0.0.1 + + - name: ensure domain exists and DC is promoted as a domain controller + microsoft.ad.domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + + - ansible.windows.win_feature: + name: RSAT-AD-PowerShell + state: present + + - name: create domain username + microsoft.ad.user: + name: '{{ domain_username }}' + upn: '{{ domain_user_upn }}' + description: '{{ domain_username }} Domain Account' + password: '{{ domain_password }}' + password_never_expires: yes + update_password: when_changed + groups: + add: + - Domain Admins + state: present + +- name: setup test host + hosts: TEST + gather_facts: no + + tasks: + - name: get network connection names + ansible.windows.win_powershell: + parameters: + IPAddress: '{{ ansible_host }}' + script: | + param ($IPAddress) + + $Ansible.Changed = $false + + Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" | + ForEach-Object -Process { + $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'" + if ($config.IPAddress -contains $IPAddress) { + $_.NetConnectionID + } + } + register: connection_name + + - name: set DNS for the private adapter to point to the DC + ansible.windows.win_dns_client: + adapter_names: + - '{{ connection_name.output[0] }}' + dns_servers: + - '{{ hostvars["DC"]["ansible_host"] }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/tasks/main.yml new file mode 100644 index 000000000..b1288d093 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/tasks/main.yml @@ -0,0 +1,232 @@ +- set_fact: + get_role_script: | + $Ansible.Changed = $false + Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, DomainRole, PartOfDomain | + Select-Object -Property @{ + N = 'Domain' + E = { + if ($_.PartOfDomain) { + $_.Domain + } + else { + $null + } + } + }, @{ + N = 'DomainRole' + E = { + switch ($_.DomainRole) { + 0 { "StandaloneWorkstation" } + 1 { "MemberWorkstation" } + 2 { "StandaloneServer" } + 3 { "MemberServer" } + 4 { "BackupDC" } + 5 { "PrimaryDC" } + } + } + }, @{ + N = 'HostName' + E = { $env:COMPUTERNAME } + } + +- name: test no change when not a DC + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + local_admin_password: '{{ domain_password }}' + state: member_server + reboot: true + register: not_dc_no_change + +- name: assert test no change when not a DC + assert: + that: + - not not_dc_no_change is changed + - not_dc_no_change.reboot_required == False + +- name: promote to DC - check mode + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: to_dc_check + check_mode: true + +- name: get result of promote to DC - check mode + ansible.windows.win_powershell: + script: '{{ get_role_script }}' + register: to_dc_check_actual + +- name: assert promote to DC - check mode + assert: + that: + - to_dc_check is changed + - to_dc_check_actual.output[0]["Domain"] == None + - to_dc_check_actual.output[0]["DomainRole"] == "StandaloneServer" + +- name: change hostname to have a pending change before promotion + ansible.windows.win_hostname: + name: FOO + +- name: promote to DC with pending reboot + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: to_dc + +- name: get result of promote to DC with pending reboot + ansible.windows.win_powershell: + script: '{{ get_role_script }}' + register: to_dc_actual + +- name: assert promote to DC with pending reboot + assert: + that: + - to_dc is changed + - to_dc.reboot_required == False + - to_dc_actual.output[0]["Domain"] == domain_realm + - to_dc_actual.output[0]["DomainRole"] == "BackupDC" + - to_dc_actual.output[0]["HostName"] == "FOO" + +- name: promote to DC - idempotent + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: to_dc_again + +- name: assert promote to DC - idempotent + assert: + that: + - not to_dc_again is changed + - to_dc_again.reboot_required == False + +# The following operations will run with the domain admin account now that the +# host is joined to the domain +- name: change connection user and password to domain account + set_fact: + ansible_user: '{{ domain_user_upn }}' + ansible_password: '{{ domain_password }}' + +- name: fail to change domain of DC + domain_controller: + dns_domain_name: bogus.local + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: change_domain_fail + failed_when: + - change_domain_fail.msg != "The host FOO is a domain controller for the domain " ~ domain_realm ~ "; changing DC domains is not implemented" + +- name: fail with invalid username format + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_username }}' # Must be a UPN or Netbios Name + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: invalid_user_fail + failed_when: + - invalid_user_fail.msg != "domain_admin_user must be in domain\\user or user@domain.com format" + +- name: set DC as member server - check mode + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + local_admin_password: '{{ domain_password }}' + state: member_server + reboot: true + register: member_server_check + check_mode: true + +- name: get result of set DC as member server - check mode + ansible.windows.win_powershell: + script: '{{ get_role_script }}' + register: member_server_check_actual + +- name: assert set DC as member server - check mode + assert: + that: + - member_server_check is changed + - member_server_check.reboot_required == False + - member_server_check_actual.output[0]["Domain"] == domain_realm + - member_server_check_actual.output[0]["DomainRole"] == "BackupDC" + +- name: set DC as member server + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + local_admin_password: '{{ domain_password }}' + state: member_server + reboot: true + register: member_server + +- name: get result of set DC as member server + ansible.windows.win_powershell: + script: '{{ get_role_script }}' + register: member_server_actual + +- name: assert set DC as member server + assert: + that: + - member_server is changed + - member_server.reboot_required == False + - member_server_actual.output[0]["Domain"] == domain_realm + - member_server_actual.output[0]["DomainRole"] == "MemberServer" + +- name: set DC as member server - idempotent + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + local_admin_password: '{{ domain_password }}' + state: member_server + reboot: true + register: member_server_again + +- name: assert set DC as member server - idempotent + assert: + that: + - not member_server_again is changed + - member_server_again.reboot_required == False + +# Promote it once more to a DC to test an edge case where Ansible is unable to +# connect back until a reboot has occurred. +- name: promote to DC again + domain_controller: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + safe_mode_password: '{{ domain_password }}' + state: domain_controller + reboot: true + register: to_dc_manual_reboot + +- name: get result of promote to DC with manual reboot + ansible.windows.win_powershell: + script: '{{ get_role_script }}' + register: to_dc_manual_reboot_actual + +- name: assert promote to DC with manual reboot + assert: + that: + - to_dc_manual_reboot is changed + - to_dc_manual_reboot.reboot_required == False + - to_dc_manual_reboot_actual.output[0]["Domain"] == domain_realm + - to_dc_manual_reboot_actual.output[0]["DomainRole"] == "BackupDC" diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/test.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/test.yml new file mode 100644 index 000000000..69034d12f --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/test.yml @@ -0,0 +1,30 @@ +- name: ensure time is in sync + hosts: windows + gather_facts: no + tasks: + - name: get current host datetime + command: date +%s + changed_when: False + delegate_to: localhost + run_once: True + register: local_time + + - name: set datetime on Windows + ansible.windows.win_powershell: + parameters: + SecondsSinceEpoch: '{{ local_time.stdout | trim }}' + script: | + param($SecondsSinceEpoch) + + $utc = [System.DateTimeKind]::Utc + $epoch = New-Object -TypeName System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0, $utc + $date = $epoch.AddSeconds($SecondsSinceEpoch) + + Set-Date -Date $date + +- name: run microsoft.ad.domain_controller tests + hosts: TEST + gather_facts: no + + tasks: + - import_tasks: tasks/main.yml diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/group/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/group/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/main.yml new file mode 100644 index 000000000..b5291e36c --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/main.yml @@ -0,0 +1,14 @@ +- name: remove temp group + group: + name: MyGroup + state: absent + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp group + group: + name: MyGroup + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml new file mode 100644 index 000000000..bdb1b95b7 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml @@ -0,0 +1,516 @@ +- name: fail to create group without scope + group: + name: MyGroup + state: present + register: fail_no_scope + failed_when: fail_no_scope.msg != "scope must be set when state=present and the group does not exist" + +- name: create group - check + group: + name: MyGroup + state: present + scope: global + register: create_group_check + check_mode: true + +- name: get result of create group - check + object_info: + identity: '{{ create_group_check.distinguished_name }}' + register: create_group_check_actual + +- name: assert create group - check + assert: + that: + - create_group_check is changed + - create_group_check.distinguished_name == 'CN=MyGroup,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_group_check.object_guid == '00000000-0000-0000-0000-000000000000' + - create_group_check.sid == 'S-1-5-0000' + - create_group_check_actual.objects == [] + +- name: create group + group: + name: MyGroup + state: present + scope: global + register: create_group + +- set_fact: + object_identity: '{{ create_group.object_guid }}' + +- name: get result of create group + object_info: + identity: '{{ object_identity }}' + properties: + - groupType + - objectSid + register: create_group_actual + +- name: assert create group + assert: + that: + - create_group is changed + - create_group.distinguished_name == 'CN=MyGroup,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_group_actual.objects | length == 1 + - create_group.object_guid == create_group_actual.objects[0].ObjectGUID + - create_group.sid == create_group_actual.objects[0].objectSid.Sid + - create_group_actual.objects[0].groupType_AnsibleFlags == ["GROUP_TYPE_ACCOUNT_GROUP", "GROUP_TYPE_SECURITY_ENABLED"] + +- name: create group - idempotent + group: + name: MyGroup + state: present + scope: global + register: create_group_again + +- name: assert create group - idempotent + assert: + that: + - not create_group_again is changed + +- name: create ou to store group members + ou: + name: MyOU + state: present + register: ou_info + +- block: + - name: create test users + user: + name: My User {{ item }} + sam_account_name: my_user_{{ item }} + upn: user_{{ item }}@{{ domain_realm }} + state: present + path: '{{ ou_info.distinguished_name }}' + register: test_users + loop: + - 1 + - 2 + - 3 + - 4 + + - name: fail to find members to add to a group + group: + name: MyGroup + state: present + members: + add: + - my_user_1 + - fake-user + - my_user_2 + - another-user + register: fail_invalid_members + failed_when: 'fail_invalid_members.msg != "Failed to find the following ad objects for group members: ''fake-user'', ''another-user''"' + + - name: add members to a group - check + group: + name: MyGroup + state: present + members: + add: + - my_user_1 + - '{{ test_users.results[2].sid }}' + register: add_member_check + check_mode: true + + - name: get result of add members to a group - check + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: add_member_check_actual + + - name: assert add members to a group - check + assert: + that: + - add_member_check is changed + - add_member_check_actual.objects[0].member == None + + - name: add members to a group + group: + name: MyGroup + state: present + members: + add: + - my_user_1 + - '{{ test_users.results[2].sid }}' + register: add_member + + - name: get result of add members to a group + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: add_member_actual + + - name: assert add members to a group + assert: + that: + - add_member is changed + - add_member_actual.objects[0].member | length == 2 + - test_users.results[0].distinguished_name in add_member_actual.objects[0].member + - test_users.results[2].distinguished_name in add_member_actual.objects[0].member + + - name: add members to a group - idempotent + group: + name: MyGroup + state: present + members: + add: + - user_1@{{ domain_realm }} + - '{{ test_users.results[2].object_guid }}' + register: add_member_again + + - name: assert add members to a group - idempotent + assert: + that: + - not add_member_again is changed + + - name: remove member from a group + group: + name: MyGroup + state: present + members: + remove: + - '{{ test_users.results[0].distinguished_name | upper }}' + - my_user_2 + register: remove_member + + - name: get result of remove member from a group + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: remove_member_actual + + - name: assert remove member from a group + assert: + that: + - remove_member is changed + - remove_member_actual.objects[0].member == test_users.results[2].distinguished_name + + - name: remove member from a group - idempotent + group: + name: MyGroup + state: present + members: + remove: + - '{{ test_users.results[0].object_guid }}' + register: remove_member_again + + - name: assert remove member from a group - idempotent + assert: + that: + - not remove_member_again is changed + + - name: add and remove members from a group + group: + name: MyGroup + state: present + members: + add: + - my_user_1 + - user_2@{{ domain_realm }} + remove: + - my_user_3 + - my_user_4 + register: add_remove_member + + - name: get result of add and remove members from a group + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: add_remove_member_actual + + - name: assert add and remove members from a group + assert: + that: + - add_remove_member is changed + - add_remove_member_actual.objects[0].member | length == 2 + - test_users.results[0].distinguished_name in add_remove_member_actual.objects[0].member + - test_users.results[1].distinguished_name in add_remove_member_actual.objects[0].member + + - name: set members + group: + name: MyGroup + state: present + members: + set: + - my_user_1 + - my_user_3 + register: set_member + + - name: get result of set members + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: set_member_actual + + - name: assert set members + assert: + that: + - set_member is changed + - set_member_actual.objects[0].member | length == 2 + - test_users.results[0].distinguished_name in set_member_actual.objects[0].member + - test_users.results[2].distinguished_name in set_member_actual.objects[0].member + + - name: set members - idempotent + group: + name: MyGroup + state: present + members: + set: + - My_user_1 + - '{{ test_users.results[2].sid }}' + register: set_member_again + + - name: assert set members - idempotent + assert: + that: + - not set_member_again is changed + + - name: unset all members + group: + name: MyGroup + state: present + members: + set: [] + register: unset_member + + - name: get result of unset all members + object_info: + identity: '{{ object_identity }}' + properties: + - member + register: unset_member_actual + + - name: assert unset all members + assert: + that: + - unset_member is changed + - unset_member_actual.objects[0].member == None + + - name: unset all members - idempotent + group: + name: MyGroup + state: present + members: + set: [] + register: unset_member_again + + - name: assert unset all members - idempotent + assert: + that: + - not unset_member_again is changed + + - name: remove group - check + group: + name: MyGroup + state: absent + register: remove_group_check + check_mode: true + + - name: get result of remove group - check + object_info: + identity: '{{ object_identity }}' + register: remove_group_check_actual + + - name: assert remove group - check + assert: + that: + - remove_group_check is changed + - remove_group_check_actual.objects | length == 1 + + - name: remove group + group: + name: MyGroup + state: absent + register: remove_group + + - name: get result of remove group + object_info: + identity: '{{ object_identity }}' + register: remove_group_actual + + - name: assert remove group + assert: + that: + - remove_group is changed + - remove_group_actual.objects == [] + + - name: remove group - idempotent + group: + name: MyGroup + state: absent + register: remove_group_again + + - name: assert remove group - idempotent + assert: + that: + - not remove_group_again is changed + + - name: fail to create group with invalid members + group: + name: MyGroup + state: present + scope: domainlocal + members: + add: + - my_user_1 + - fake-user + - my_user_2 + - another-user + register: fail_invalid_members + failed_when: 'fail_invalid_members.msg != "Failed to find the following ad objects for group members: ''fake-user'', ''another-user''"' + + - name: create group with custom options + group: + name: MyGroup + state: present + path: '{{ ou_info.distinguished_name }}' + display_name: My Display Name + description: My Description + scope: domainlocal + category: distribution + homepage: www.ansible.com + managed_by: Domain Admins + members: + add: + - my_user_1 + - '{{ test_users.results[1].object_guid }}' + set: + - '{{ test_users.results[2].sid }}' + sam_account_name: GroupSAM + register: group_custom + + - set_fact: + object_identity: '{{ group_custom.object_guid }}' + + - name: get result of create group with custom options + object_info: + identity: '{{ object_identity }}' + properties: + - Description + - DisplayName + - groupType + - managedBy + - member + - objectSid + - wWWHomePage + - sAMAccountName + register: group_custom_actual + + - name: assert create group with custom options + assert: + that: + - group_custom is changed + - group_custom.distinguished_name == "CN=MyGroup," ~ ou_info.distinguished_name + - group_custom_actual.objects[0].DistinguishedName == group_custom.distinguished_name + - group_custom_actual.objects[0].ObjectGUID == group_custom.object_guid + - group_custom_actual.objects[0].objectSid.Sid == group_custom.sid + - group_custom_actual.objects[0].Description == 'My Description' + - group_custom_actual.objects[0].DisplayName == 'My Display Name' + - group_custom_actual.objects[0].Name == 'MyGroup' + - group_custom_actual.objects[0].groupType_AnsibleFlags == ["GROUP_TYPE_RESOURCE_GROUP"] + - group_custom_actual.objects[0].managedBy == "CN=Domain Admins,CN=Users," ~ setup_domain_info.output[0].defaultNamingContext + - group_custom_actual.objects[0].member | length == 3 + - test_users.results[0].distinguished_name in group_custom_actual.objects[0].member + - test_users.results[1].distinguished_name in group_custom_actual.objects[0].member + - test_users.results[2].distinguished_name in group_custom_actual.objects[0].member + - group_custom_actual.objects[0].sAMAccountName == "GroupSAM" + - group_custom_actual.objects[0].wWWHomePage == "www.ansible.com" + + - name: create group with custom options - idempotent + group: + name: MyGroup + state: present + path: '{{ ou_info.distinguished_name }}' + display_name: My Display Name + description: My Description + scope: domainlocal + category: distribution + homepage: www.ansible.com + managed_by: CN=Domain Admins,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + members: + add: + - my_user_1 + - '{{ test_users.results[1].object_guid }}' + - '{{ test_users.results[2].sid }}' + sam_account_name: GroupSAM + register: group_custom_again + + - name: assert create group with custom options - idempotent + assert: + that: + - group_custom_again is not changed + + - name: edit group + group: + name: MyGroup + state: present + path: '{{ ou_info.distinguished_name }}' + display_name: my display name + description: '' + homepage: www.Ansible.com + members: + set: [] + sam_account_name: MyGroup + register: group_edit + + - name: get result of edit group + object_info: + identity: '{{ object_identity }}' + properties: + - Description + - DisplayName + - groupType + - member + - objectSid + - wWWHomePage + - sAMAccountName + register: group_edit_actual + + - name: assert edit group + assert: + that: + - group_edit is changed + - group_edit_actual.objects[0].DistinguishedName == group_edit.distinguished_name + - group_edit_actual.objects[0].ObjectGUID == group_edit.object_guid + - group_edit_actual.objects[0].objectSid.Sid == group_edit.sid + - group_edit_actual.objects[0].Description == None + - group_edit_actual.objects[0].DisplayName == 'my display name' + - group_edit_actual.objects[0].Name == 'MyGroup' + - group_edit_actual.objects[0].groupType_AnsibleFlags == ["GROUP_TYPE_RESOURCE_GROUP"] + - group_edit_actual.objects[0].member == None + - group_edit_actual.objects[0].sAMAccountName == "MyGroup" + - group_edit_actual.objects[0].wWWHomePage == "www.Ansible.com" + + - name: edit group scope and category + group: + name: MyGroup + state: present + path: '{{ ou_info.distinguished_name }}' + scope: universal + category: security + register: edit_scope + + - name: get result of edit group scope and category + object_info: + identity: '{{ object_identity }}' + properties: + - groupType + register: edit_scope_actual + + - name: assert edit group scope and category + assert: + that: + - edit_scope is changed + - edit_scope_actual.objects[0].groupType_AnsibleFlags == ["GROUP_TYPE_UNIVERSAL_GROUP", "GROUP_TYPE_SECURITY_ENABLED"] + + always: + - name: remove test ou + ou: + name: MyOU + state: absent + identity: '{{ ou_info.object_guid }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/aliases new file mode 100644 index 000000000..b92c22d5a --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/aliases @@ -0,0 +1,3 @@ +windows +shippable/windows/group1 +needs/target/setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/main.yml new file mode 100644 index 000000000..14b4ae33a --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/main.yml @@ -0,0 +1,25 @@ +- name: run microsoft.ad.ldap tests + hosts: windows + gather_facts: false + + tasks: + - name: setup domain controller + import_role: + name: ../../setup_domain + + - name: setup domain certificates + import_role: + name: setup_certificate + vars: + dc_name: '{{ setup_domain_info.output[0].dnsHostName }}' + cert_path: /tmp/microsoft.ad-{{ inventory_hostname }} + + - name: run tests + import_role: + name: test + vars: + ldap_server: '{{ ansible_host | default(inventory_hostname) }}' + ldap_user: ldap-test@{{ domain_realm }} + ldap_pass: '{{ domain_password }}' + ldap_user_cert: /tmp/microsoft.ad-{{ inventory_hostname }}/user.pfx + ldap_ca_cert: /tmp/microsoft.ad-{{ inventory_hostname }}/ca.pem diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/files/generate_cert.sh b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/files/generate_cert.sh new file mode 100644 index 000000000..365757307 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/files/generate_cert.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -o pipefail -eux + +TARGET="${1}" +PASSWORD="${2}" + +generate () { + NAME="${1}" + SUBJECT="${2}" + KEY="${3}" + CA_NAME="${4}" + CA_OPTIONS=("-CA" "${CA_NAME}.pem" "-CAkey" "${CA_NAME}.key" "-CAcreateserial") + + cat > openssl.conf << EOL +distinguished_name = req_distinguished_name + +[req_distinguished_name] + +[req] +basicConstraints = CA:FALSE +keyUsage = digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = DNS:${SUBJECT} +EOL + + echo "Generating ${NAME} signed cert" + openssl req \ + -new \ + "-${KEY}" \ + -subj "/CN=${SUBJECT}" \ + -newkey rsa:2048 \ + -keyout "${NAME}.key" \ + -out "${NAME}.csr" \ + -config openssl.conf \ + -reqexts req \ + -passin pass:"${PASSWORD}" \ + -passout pass:"${PASSWORD}" + + openssl x509 \ + -req \ + -in "${NAME}.csr" \ + "-${KEY}" \ + -out "${NAME}.pem" \ + -days 365 \ + -extfile openssl.conf \ + -extensions req \ + -passin pass:"${PASSWORD}" \ + "${CA_OPTIONS[@]}" + + # PBE-SHA1-3DES/nomac is used for compatibility with Server 2016 and older + openssl pkcs12 \ + -export \ + -out "${NAME}.pfx" \ + -inkey "${NAME}.key" \ + -in "${NAME}.pem" \ + -keypbe PBE-SHA1-3DES \ + -certpbe PBE-SHA1-3DES \ + -nomac \ + -passin pass:"${PASSWORD}" \ + -passout pass:"${PASSWORD}" + + rm openssl.conf +} + +echo "Generating CA certificate" +openssl genrsa \ + -aes256 \ + -out ca.key \ + -passout pass:"${PASSWORD}" + +openssl req \ + -new \ + -x509 \ + -days 365 \ + -key ca.key \ + -out ca.pem \ + -subj "/CN=microsoft.ad root" \ + -passin pass:"${PASSWORD}" + +echo "Generating ${TARGET} LDAPS certificate" +generate ldaps "${TARGET}" sha256 ca diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/handlers/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/handlers/main.yml new file mode 100644 index 000000000..471b5f24b --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/handlers/main.yml @@ -0,0 +1,10 @@ +- name: remove test user + microsoft.ad.user: + name: ldap-test + state: absent + +- name: remove test user cert + ansible.windows.win_file: + path: C:\Windows\TEMP\user.pfx + state: absent +
\ No newline at end of file diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/tasks/main.yml new file mode 100644 index 000000000..8858e20cc --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/tasks/main.yml @@ -0,0 +1,196 @@ +- name: install Active Directory Certificate Services + ansible.windows.win_feature: + name: AD-Certificate + state: present + register: adcs_setup_res + +- name: reboot after ADCS install + ansible.windows.win_reboot: + when: adcs_setup_res.reboot_required + +- name: configure ADCS certification authority + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $Ansible.Changed = $false + + $caParams = @{ + CAType = 'EnterpriseRootCa' + CryptoProviderName = 'RSA#Microsoft Software Key Storage Provider' + KeyLength = 2048 + HashAlgorithmName = 'SHA256' + Force = $true + } + try { + Install-AdcsCertificationAuthority @caParams + $Ansible.Changed = $true + } + catch [Microsoft.CertificateServices.Deployment.Common.CertificateServicesBaseSetupException] { + if ($_.Exception.Message -like 'The Certification Authority is already installed.*') { + return + } + throw + } + become: true + become_method: runas + become_user: SYSTEM + +- name: name ensure local cert dir exists + ansible.builtin.file: + path: '{{ cert_path }}' + state: directory + delegate_to: localhost + +- name: check if certificates have been generated + ansible.windows.win_stat: + path: C:\Windows\TEMP\ca.pem + register: cert_info + +- name: fetch CA cert from remote + ansible.builtin.fetch: + src: C:\Windows\TEMP\ca.pem + dest: '{{ cert_path }}/ca.pem' + flat: true + when: cert_info.stat.exists + +- name: generate CA and LDAPS certs + when: not cert_info.stat.exists + block: + - name: generate TLS certificates + ansible.builtin.script: + cmd: generate_cert.sh {{ dc_name | quote }} password + creates: '{{ cert_path }}/ca.pem' + chdir: '{{ cert_path }}' + delegate_to: localhost + + - name: copy across CA and LDAPS cert to remote target + ansible.windows.win_copy: + src: '{{ cert_path }}/{{ item }}' + dest: C:\Windows\TEMP\{{ item }} + loop: + - ca.pem + - ldaps.pfx + +- name: import CA certificate to trusted root CA + ansible.windows.win_certificate_store: + path: C:\Windows\TEMP\ca.pem + state: present + store_location: LocalMachine + store_name: Root + +- name: add custom CA to Forest NTAuthStore + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $Ansible.Changed = $false + + $caCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList 'C:\Windows\TEMP\ca.pem' + $configRoot = (Get-ADRootDSE).configurationNamingContext + + $dn = "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,$configRoot" + $obj = Get-ADObject -Identity $dn -Properties cACertificate + + $found = $false + foreach ($certBytes in $obj.cACertificate) { + $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$certBytes) + if ($cert.Thumbprint -eq $caCert.Thumbprint) { + $found = $true + break + } + } + + if (-not $found) { + certutil.exe -dspublish C:\Windows\TEMP\ca.pem NTAuthCA + $Ansible.Changed = $true + } + become: true + become_method: runas + become_user: SYSTEM + +- name: create domain user + microsoft.ad.user: + name: ldap-test + upn: ldap-test@{{ domain_realm }} + description: ldap-test Domain Account + password: '{{ domain_password }}' + password_never_expires: true + update_password: when_changed + groups: + set: + - Domain Admins + - Domain Users + - Enterprise Admins + state: present + notify: remove test user + +- name: request User certificate + ansible.windows.win_powershell: + parameters: + Path: C:\Windows\TEMP\user.pfx + CertPass: '{{ domain_password }}' + script: | + [CmdletBinding()] + param ( + [string] + $Path, + + [string] + $CertPass + ) + $ErrorActionPreference = 'Stop' + $Ansible.Changed = $false + + if (Test-Path -LiteralPath $Path) { + return + } + + Push-Location Cert:\CurrentUser\My + $result = Get-Certificate -Template User -Url ldap: + Pop-Location + + if ($result.Status -ne "Issued") { + throw "Failed to request User certificate: $($result.Status)" + } + $Ansible.Changed = $true + + $cert = $result.Certificate + $certBytes = $result.Certificate.Export("Pfx", $CertPass) + [System.IO.File]::WriteAllBytes($Path, $certBytes) + notify: remove test user cert + vars: + ansible_become: true + ansible_become_method: runas + ansible_become_user: ldap-test@{{ domain_realm }} + ansible_become_pass: '{{ domain_password }}' + +- name: fetch certificate for user cert authentication + ansible.builtin.fetch: + src: C:\Windows\TEMP\user.pfx + dest: '{{ cert_path }}/user.pfx' + flat: true + +- name: import LDAPS certificate + ansible.windows.win_certificate_store: + path: C:\Windows\TEMP\ldaps.pfx + password: password + key_exportable: false + key_storage: machine + state: present + store_type: service + store_location: NTDS + store_name: My + register: ldaps_cert_info + +- name: register LDAPS certificate + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $dse = [adsi]'LDAP://localhost/rootDSE' + [void]$dse.Properties['renewServerCertificate'].Add(1) + $dse.CommitChanges() + when: ldaps_cert_info is changed + vars: + ansible_become: true + ansible_become_method: runas + ansible_become_user: ldap-test@{{ domain_realm }} + ansible_become_pass: '{{ domain_password }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/invoke.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/invoke.yml new file mode 100644 index 000000000..722effb22 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/invoke.yml @@ -0,0 +1,14 @@ +- name: '{{ scenario }} - create temp inventory file' + ansible.builtin.copy: + content: '{{ inventory | to_nice_yaml(sort_keys=False) }}' + dest: /tmp/tmp-microsoft.ad.ldap.yml + delegate_to: localhost + +- name: '{{ scenario }} - run ansible-inventory' + ansible.builtin.command: ansible-inventory -i /tmp/tmp-microsoft.ad.ldap.yml --list + register: inventory_out_raw + delegate_to: localhost + +- name: '{{ scenario }} - get ansible-inventory output' + set_fact: + inventory_out: '{{ inventory_out_raw.stdout | from_json }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml new file mode 100644 index 000000000..86b6d75e9 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml @@ -0,0 +1,527 @@ +- name: get domain controller info + microsoft.ad.object_info: + ldap_filter: '(objectClass=computer)' + properties: + - dNSHostName + register: dc_info_raw + +- name: make sure only 1 computer is present for start of tests + assert: + that: + - dc_info_raw.objects | length == 1 + +- set_fact: + dc_info: '{{ dc_info_raw.objects[0] }}' + +- import_tasks: invoke.yml + vars: + scenario: Failure connection invalid hostname + inventory: + plugin: microsoft.ad.ldap + server: failed + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert failure connection invalid hostname + assert: + that: + - inventory_out._meta.hostvars == {} + - '"Failed to connect to failed:389" in inventory_out_raw.stderr' + +- import_tasks: invoke.yml + vars: + scenario: Failure connection blocked port + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + port: 1234 + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert failure connection blocked port + assert: + that: + - inventory_out._meta.hostvars == {} + - '"Failed to connect to " ~ ldap_server ~ ":1234" in inventory_out_raw.stderr' + +- import_tasks: invoke.yml + vars: + scenario: Failure connection invalid port + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + port: 5985 + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert failure connection invalid port + assert: + that: + - inventory_out._meta.hostvars == {} + - '"Received invalid data from the peer" in inventory_out_raw.stderr' + +- import_tasks: invoke.yml + vars: + scenario: LDAP + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert LDAP inventory + assert: + that: &default-assertion + - inventory_out._meta.hostvars | length == 1 + - (inventory_out._meta.hostvars.keys() | list) == [dc_info.Name] + - (inventory_out._meta.hostvars[dc_info.Name].keys() | list) == ['ansible_host', 'microsoft_ad_distinguished_name'] + - inventory_out._meta.hostvars[dc_info.Name]['ansible_host'] == dc_info.dNSHostName + - inventory_out._meta.hostvars[dc_info.Name]['microsoft_ad_distinguished_name'] == dc_info.DistinguishedName + - inventory_out.ungrouped.hosts == [dc_info.Name] + +- import_tasks: invoke.yml + vars: + scenario: LDAP through environment variables + inventory: + plugin: microsoft.ad.ldap + environment: + MICROSOFT_AD_LDAP_SERVER: '{{ ldap_server }}' + MICROSOFT_AD_LDAP_USERNAME: '{{ ldap_user }}' + MICROSOFT_AD_LDAP_PASSWORD: '{{ ldap_pass }}' + +- name: assert LDAP inventory through environment variables + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: LDAPS + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + tls_mode: ldaps + ca_cert: '{{ ldap_ca_cert }}' + cert_validation: ignore_hostname + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert LDAPS inventory + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: LDAPS through environment variables + inventory: + plugin: microsoft.ad.ldap + environment: + MICROSOFT_AD_LDAP_SERVER: '{{ ldap_server }}' + MICROSOFT_AD_LDAP_TLS_MODE: ldaps + MICROSOFT_AD_LDAP_CA_CERT: '{{ ldap_ca_cert }}' + MICROSOFT_AD_LDAP_CERT_VALIDATION: ignore_hostname + MICROSOFT_AD_LDAP_USERNAME: '{{ ldap_user }}' + MICROSOFT_AD_LDAP_PASSWORD: '{{ ldap_pass }}' + +- name: assert LDAPS inventory through environment variables + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: StartTLS + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + tls_mode: start_tls + ca_cert: '{{ ldap_ca_cert }}' + cert_validation: ignore_hostname + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert StartTLS inventory + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: Simple auth + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + tls_mode: ldaps + ca_cert: '{{ lookup("file", ldap_ca_cert) }}' + cert_validation: ignore_hostname + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + auth_protocol: simple + +- name: assert Simple auth inventory + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: Simple auth fails over LDAP + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + auth_protocol: simple + +- name: assert simple auth failure over LDAP + assert: + that: + - inventory_out._meta.hostvars == {} + - '"Cannot use simple auth with encryption" in inventory_out_raw.stderr' + +- import_tasks: invoke.yml + vars: + scenario: Simple auth over LDAP with no encryption + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + auth_protocol: simple + encrypt: false + +- name: assert Simple auth over LDAP with no encryption + assert: + that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: Certificate auth with LDAPS + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + tls_mode: ldaps + ca_cert: '{{ ldap_ca_cert }}' + cert_validation: ignore_hostname + certificate: '{{ ldap_user_cert }}' + certificate_password: '{{ ldap_pass }}' + +- name: assert Certificate auth inventory with LDAPS + assert: + that: *default-assertion + +# Recent Windows Update seems to have broken this. Fails with: +# Received LDAPResult error bind failed - INVALID_CREDENTIALS - 80090317: LdapErr: DSID-0C090635, comment: The server did not receive any credentials via TLS, data 0, v4563 +# I cannot figure out why so just disabling the test for now. + +# - import_tasks: invoke.yml +# vars: +# scenario: Certificate auth with StartTLS +# inventory: +# plugin: microsoft.ad.ldap +# server: '{{ ldap_server }}' +# tls_mode: start_tls +# ca_cert: '{{ ldap_ca_cert }}' +# cert_validation: ignore_hostname +# certificate: '{{ ldap_user_cert }}' +# certificate_password: '{{ ldap_pass }}' + +# - name: assert Certificate auth inventory with StartTLS +# assert: +# that: *default-assertion + +- import_tasks: invoke.yml + vars: + scenario: TLS ignoring cert validation + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + tls_mode: ldaps + cert_validation: ignore + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + +- name: assert TLS ignoring cert validation + assert: + that: *default-assertion + +- block: + - name: setup custom server data + ansible.windows.win_powershell: + depth: 3 + script: | + $ErrorActionPreference = 'Stop' + + $ou = New-ADOrganizationalUnit -Name '<My OU, !test''>' -PassThru + $adParams = @{ + Path = $ou.DistinguishedName + PassThru = $true + } + $subOU = New-ADOrganizationalUnit -Name SubOU @adParams + + $group1 = New-ADGroup -Name Group1 -GroupCategory Security -GroupScope Global @adParams + $group2 = New-ADGroup -Name Group2 -GroupCategory Security -GroupScope Global @adParams + + $comp1 = New-ADComputer -Name Comp1 -DNSHostName CustomName -OtherAttributes @{ + comment = 'comment 1' + 'msDS-AllowedToDelegateTo' = 'dns 1' + location = 'my_location' + } @adParams + $comp2 = New-ADComputer -Name Comp2 -SamAccountName Comp2Sam -Path $subOU.DistinguishedName -PassThru -OtherAttributes @{ + comment = 'comment 1' + 'msDS-AllowedToDelegateTo' = 'dns 2' + } + + Add-ADGroupMember -Identity $group1 -Members $comp1, $comp2 + Add-ADGroupMember -Identity $group2 -Members $comp1 + + $compMembers = @{ + Property = @( + 'DistinguishedName' + 'MemberOf' + @{N='RawMemberOf'; E={ + ,@($_.memberOf | ForEach-Object { + $b = (New-Object -TypeName System.Text.UTF8Encoding).GetBytes($_) + [System.Convert]::ToBase64String($b) + }) + }} + 'PwdLastSet' + @{N='SID'; E={$_.SID.Value}} + @{N='RawSID'; E={ + $b = New-Object -TypeName byte[] -ArgumentList $_.SID.BinaryLength + $_.SID.GetBinaryForm($b, 0) + [System.Convert]::ToBase64String($b) + }} + ) + } + + [PSCustomObject]@{ + OUId = $ou.ObjectGuid + OUPath = $ou.DistinguishedName + Comp1 = $comp1 | Get-ADComputer -Properties * | Select-Object @compMembers + Comp2 = $comp2 | Get-ADComputer -Properties * | Select-Object @compMembers + } + register: test_data + + - import_tasks: invoke.yml + vars: + scenario: Search with search_base and scope + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + search_base: '{{ test_data.output[0]["OUPath"] }}' + search_scope: one_level + + - name: assert search with seach base and scope + assert: + that: + - inventory_out._meta.hostvars | length == 1 + - (inventory_out._meta.hostvars.keys() | list) == ["Comp1"] + - (inventory_out._meta.hostvars.Comp1.keys() | list) == ['ansible_host', 'microsoft_ad_distinguished_name'] + - inventory_out._meta.hostvars.Comp1.ansible_host == "CustomName" + - inventory_out._meta.hostvars.Comp1.microsoft_ad_distinguished_name == test_data.output[0].Comp1.DistinguishedName + - inventory_out.ungrouped.hosts == ["Comp1"] + + - import_tasks: invoke.yml + vars: + scenario: Search with filter + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + filter: (sAMAccountName=Comp2Sam$) + + - name: assert search with seach base and scope + assert: + that: + - inventory_out._meta.hostvars | length == 1 + - (inventory_out._meta.hostvars.keys() | list) == ["Comp2"] + - (inventory_out._meta.hostvars.Comp2.keys() | list) == ['microsoft_ad_distinguished_name'] + - inventory_out._meta.hostvars.Comp2.microsoft_ad_distinguished_name == test_data.output[0].Comp2.DistinguishedName + - inventory_out.ungrouped.hosts == ["Comp2"] + + - import_tasks: invoke.yml + vars: + scenario: Set inventory_hostname from attributes + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + search_base: '{{ test_data.output[0]["OUPath"] }}' + attributes: + sAMAccountName: + inventory_hostname: sAMAccountName[:-1] + ansible_host: inventory_hostname + + - name: assert set inventory_hostname from attributes + assert: + that: + - inventory_out._meta.hostvars | length == 2 + - (inventory_out._meta.hostvars.keys() | list | sort) == ["Comp1", "Comp2Sam"] + + - (inventory_out._meta.hostvars.Comp1.keys() | list | sort) == ['ansible_host', 'microsoft_ad_distinguished_name', 'sAMAccountName'] + - "inventory_out._meta.hostvars.Comp1.ansible_host == {'__ansible_unsafe': 'Comp1'}" + - inventory_out._meta.hostvars.Comp1.microsoft_ad_distinguished_name == test_data.output[0].Comp1.DistinguishedName + - "inventory_out._meta.hostvars.Comp1.sAMAccountName == {'__ansible_unsafe': 'Comp1$'}" + + - (inventory_out._meta.hostvars.Comp2Sam.keys() | list | sort) == ['ansible_host', 'microsoft_ad_distinguished_name', 'sAMAccountName'] + - "inventory_out._meta.hostvars.Comp2Sam.ansible_host == {'__ansible_unsafe': 'Comp2Sam'}" + - inventory_out._meta.hostvars.Comp2Sam.microsoft_ad_distinguished_name == test_data.output[0].Comp2.DistinguishedName + - "inventory_out._meta.hostvars.Comp2Sam.sAMAccountName == {'__ansible_unsafe': 'Comp2Sam$'}" + + - inventory_out.ungrouped.hosts | length == 2 + - inventory_out.ungrouped.hosts[0]['__ansible_unsafe'] in ['Comp1', 'Comp2Sam'] + - inventory_out.ungrouped.hosts[1]['__ansible_unsafe'] in ['Comp1', 'Comp2Sam'] + + - import_tasks: invoke.yml + vars: + scenario: Set inventory_hostname from compose + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + search_base: '{{ test_data.output[0]["OUPath"] }}' + attributes: + sAMAccountName: + compose: + inventory_hostname: sAMAccountName[:-1] + ansible_host: inventory_hostname + + - name: assert set inventory_hostname from compose + assert: + that: + - inventory_out._meta.hostvars | length == 2 + - (inventory_out._meta.hostvars.keys() | list | sort) == ["Comp1", "Comp2Sam"] + + - (inventory_out._meta.hostvars.Comp1.keys() | list | sort) == ['ansible_host', 'microsoft_ad_distinguished_name', 'sAMAccountName'] + - "inventory_out._meta.hostvars.Comp1.ansible_host == {'__ansible_unsafe': 'Comp1'}" + - inventory_out._meta.hostvars.Comp1.microsoft_ad_distinguished_name == test_data.output[0].Comp1.DistinguishedName + - "inventory_out._meta.hostvars.Comp1.sAMAccountName == {'__ansible_unsafe': 'Comp1$'}" + + - (inventory_out._meta.hostvars.Comp2Sam.keys() | list | sort) == ['ansible_host', 'microsoft_ad_distinguished_name', 'sAMAccountName'] + - "inventory_out._meta.hostvars.Comp2Sam.ansible_host == {'__ansible_unsafe': 'Comp2Sam'}" + - inventory_out._meta.hostvars.Comp2Sam.microsoft_ad_distinguished_name == test_data.output[0].Comp2.DistinguishedName + - "inventory_out._meta.hostvars.Comp2Sam.sAMAccountName == {'__ansible_unsafe': 'Comp2Sam$'}" + + - inventory_out.ungrouped.hosts | length == 2 + - inventory_out.ungrouped.hosts[0]['__ansible_unsafe'] in ['Comp1', 'Comp2Sam'] + - inventory_out.ungrouped.hosts[1]['__ansible_unsafe'] in ['Comp1', 'Comp2Sam'] + + - import_tasks: invoke.yml + vars: + scenario: Search with composable options + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + search_base: '{{ test_data.output[0]["OUPath"] }}' + attributes: + sAMAccountName: + objectSid: + nothing_sid: + this_sid: this + raw_sid: raw + raw_sid_filter: raw | microsoft.ad.as_sid + PwdLastSet: + location: + msDS-SupportedEncryptionTypes: + msDS-AllowedToDelegateTo: + msDS-AllowedToDelegateTo: + memberOf: + previous_reference: PwdLastSet | microsoft.ad.as_datetime + nothing_member: + this_member: this + raw_member: raw + computer_membership: this | map("regex_search", '^CN=(?P<name>.+?)((?<!\\),)', '\g<name>') | flatten + compose: + host_var: computer_sid + groups: + testing: true + production: '"Group2" in computer_membership' + keyed_groups: + - key: location | default(omit) + prefix: site + default_value: unknown + + - name: assert search with composable options + assert: + that: + - inventory_out._meta.hostvars | length == 2 + - (inventory_out._meta.hostvars.keys() | list | sort) == ["Comp1", "Comp2"] + + - (inventory_out._meta.hostvars.Comp1.keys() | list | sort) == ['ansible_host', 'computer_membership', 'location', 'microsoft_ad_distinguished_name', 'msDS-AllowedToDelegateTo', 'msDS_SupportedEncryptionTypes', 'nothing_member', 'nothing_sid', 'previous_reference', 'PwdLastSet', 'raw_member', 'raw_sid', 'raw_sid_filter', 'sAMAccountName', 'this_member', 'this_sid'] + - inventory_out._meta.hostvars.Comp1['ansible_host'] == 'CustomName' + - "inventory_out._meta.hostvars.Comp1['computer_membership'] == [{'__ansible_unsafe': 'Group2'}, {'__ansible_unsafe': 'Group1'}]" + - "inventory_out._meta.hostvars.Comp1['location'] == {'__ansible_unsafe': 'my_location'}" + - inventory_out._meta.hostvars.Comp1['microsoft_ad_distinguished_name'] == test_data.output[0].Comp1.DistinguishedName + - "inventory_out._meta.hostvars.Comp1['msDS-AllowedToDelegateTo'] == [{'__ansible_unsafe': 'dns 1'}]" + - inventory_out._meta.hostvars.Comp1['msDS_SupportedEncryptionTypes'] == None + - "inventory_out._meta.hostvars.Comp1['nothing_member'] == [{'__ansible_unsafe': test_data.output[0].Comp1.MemberOf[0]}, {'__ansible_unsafe': test_data.output[0].Comp1.MemberOf[1]}]" + - "inventory_out._meta.hostvars.Comp1['nothing_sid'] == {'__ansible_unsafe': test_data.output[0].Comp1.SID}" + - inventory_out._meta.hostvars.Comp1['previous_reference'] == test_data.output[0].Comp1.PwdLastSet | microsoft.ad.as_datetime + - inventory_out._meta.hostvars.Comp1['PwdLastSet'] == test_data.output[0].Comp1.PwdLastSet + - "inventory_out._meta.hostvars.Comp1['raw_member'] == [{'__ansible_unsafe': test_data.output[0].Comp1.RawMemberOf[0]}, {'__ansible_unsafe': test_data.output[0].Comp1.RawMemberOf[1]}]" + - "inventory_out._meta.hostvars.Comp1['raw_sid'] == [{'__ansible_unsafe': test_data.output[0].Comp1.RawSID}]" + - "inventory_out._meta.hostvars.Comp1['raw_sid_filter'] == [{'__ansible_unsafe': test_data.output[0].Comp1.SID}]" + - "inventory_out._meta.hostvars.Comp1['sAMAccountName'] == {'__ansible_unsafe': 'Comp1$'}" + - "inventory_out._meta.hostvars.Comp1['this_member'] == [{'__ansible_unsafe': test_data.output[0].Comp1.MemberOf[0]}, {'__ansible_unsafe': test_data.output[0].Comp1.MemberOf[1]}]" + - "inventory_out._meta.hostvars.Comp1['this_sid'] == {'__ansible_unsafe': test_data.output[0].Comp1.SID}" + + - (inventory_out._meta.hostvars.Comp2.keys() | list | sort) == ['computer_membership', 'location', 'microsoft_ad_distinguished_name', 'msDS-AllowedToDelegateTo', 'msDS_SupportedEncryptionTypes', 'nothing_member', 'nothing_sid', 'previous_reference', 'PwdLastSet', 'raw_member', 'raw_sid', 'raw_sid_filter', 'sAMAccountName', 'this_member', 'this_sid'] + - "inventory_out._meta.hostvars.Comp2['computer_membership'] == [{'__ansible_unsafe': 'Group1'}]" + - inventory_out._meta.hostvars.Comp2['location'] == None + - inventory_out._meta.hostvars.Comp2['microsoft_ad_distinguished_name'] == test_data.output[0].Comp2.DistinguishedName + - "inventory_out._meta.hostvars.Comp2['msDS-AllowedToDelegateTo'] == [{'__ansible_unsafe': 'dns 2'}]" + - inventory_out._meta.hostvars.Comp2['msDS_SupportedEncryptionTypes'] == None + - "inventory_out._meta.hostvars.Comp2['nothing_member'] == [{'__ansible_unsafe': test_data.output[0].Comp2.MemberOf[0]}]" + - "inventory_out._meta.hostvars.Comp2['nothing_sid'] == {'__ansible_unsafe': test_data.output[0].Comp2.SID}" + - inventory_out._meta.hostvars.Comp2['previous_reference'] == test_data.output[0].Comp2.PwdLastSet | microsoft.ad.as_datetime + - inventory_out._meta.hostvars.Comp2['PwdLastSet'] == test_data.output[0].Comp2.PwdLastSet + - "inventory_out._meta.hostvars.Comp2['raw_member'] == [{'__ansible_unsafe': test_data.output[0].Comp2.RawMemberOf[0]}]" + - "inventory_out._meta.hostvars.Comp2['raw_sid'] == [{'__ansible_unsafe': test_data.output[0].Comp2.RawSID}]" + - "inventory_out._meta.hostvars.Comp2['raw_sid_filter'] == [{'__ansible_unsafe': test_data.output[0].Comp2.SID}]" + - "inventory_out._meta.hostvars.Comp2['sAMAccountName'] == {'__ansible_unsafe': 'Comp2Sam$'}" + - "inventory_out._meta.hostvars.Comp2['this_member'] == [{'__ansible_unsafe': test_data.output[0].Comp2.MemberOf[0]}]" + - "inventory_out._meta.hostvars.Comp2['this_sid'] == {'__ansible_unsafe': test_data.output[0].Comp2.SID}" + + - inventory_out.production.hosts == ["Comp1"] + - inventory_out.site_my_location.hosts == ["Comp1"] + - inventory_out.site_unknown.hosts == ["Comp2"] + - inventory_out.testing.hosts | sort == ["Comp1", "Comp2"] + + - name: create multiple computer objects + ansible.windows.win_powershell: + parameters: + Path: '{{ test_data.output[0].OUPath }}' + script: | + param($Path) + + $ErrorActionPreference = 'Stop' + + 1..2010 | ForEach-Object { + New-ADComputer -Name "MultiComp$_" -Path $Path + } + + - import_tasks: invoke.yml + vars: + scenario: Search with large number of computer accounts + inventory: + plugin: microsoft.ad.ldap + server: '{{ ldap_server }}' + username: '{{ ldap_user }}' + password: '{{ ldap_pass }}' + filter: (name=MultiComp*) + + - name: assert search with large number of computer accounts + assert: + that: + - inventory_out._meta.hostvars | length == 2010 + + always: + - name: remove test OU + microsoft.ad.ou: + name: <My OU, !test'> + identity: '{{ test_data.output[0].OUId | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/runme.sh b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/runme.sh new file mode 100755 index 000000000..22564b656 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/runme.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -eux + +ansible-playbook main.yml -i ../../inventory.winrm "$@" diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/README.md b/ansible_collections/microsoft/ad/tests/integration/targets/membership/README.md new file mode 100644 index 000000000..9387dd0a6 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/README.md @@ -0,0 +1,34 @@ +# microsoft.ad.membership tests + +As this cannot be run in CI this is a brief guide on how to run these tests locally. +Run the following: + +```bash +vagrant up + +ansible-playbook setup.yml +``` + +It is a good idea to create a snapshot of both hosts before running the tests. +This allows you to reset the host back to a blank starting state if the tests need to be rerun. +To create a snaphost do the following: + +```bash +virsh snapshot-create-as --domain "membership_DC" --name "pretest" +virsh snapshot-create-as --domain "membership_TEST" --name "pretest" +``` + +To restore these snapshots run the following: + +```bash +virsh snapshot-revert --domain "membership_DC" --snapshotname "pretest" --running +virsh snapshot-revert --domain "membership_TEST" --snapshotname "pretest" --running +``` + +Once you are ready to run the tests run the following: + +```bash +ansible-playbook test.yml +``` + +Run `vagrant destroy` to remove the test VMs. diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/Vagrantfile b/ansible_collections/microsoft/ad/tests/integration/targets/membership/Vagrantfile new file mode 100644 index 000000000..5341185c3 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/Vagrantfile @@ -0,0 +1,27 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +require 'yaml' + +inventory = YAML.load_file('inventory.yml') + +Vagrant.configure("2") do |config| + inventory['all']['children'].each do |group,details| + details['hosts'].each do |server,host_details| + config.vm.define server do |srv| + srv.vm.box = host_details['vagrant_box'] + srv.vm.hostname = server + srv.vm.network :private_network, + :ip => host_details['ansible_host'], + :libvirt__network_name => 'microsoft.ad', + :libvirt__domain_name => inventory['all']['vars']['domain_realm'] + + srv.vm.provider :libvirt do |l| + l.memory = 4096 + l.cpus = 2 + end + end + end + end +end + diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/membership/aliases new file mode 100644 index 000000000..a0187456c --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/aliases @@ -0,0 +1,2 @@ +windows +unsupported # can never run in CI, see README.md
\ No newline at end of file diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg b/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg new file mode 100644 index 000000000..3a986973e --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory.yml +retry_files_enabled = False diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/inventory.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/inventory.yml new file mode 100644 index 000000000..06c916072 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/inventory.yml @@ -0,0 +1,22 @@ +all: + children: + windows: + hosts: + DC: + ansible_host: 192.168.11.10 + vagrant_box: jborean93/WindowsServer2022 + TEST: + ansible_host: 192.168.11.11 + vagrant_box: jborean93/WindowsServer2022 + vars: + ansible_port: 5985 + ansible_connection: psrp + + vars: + ansible_user: vagrant + ansible_password: vagrant + domain_username: vagrant-domain + domain_user_upn: '{{ domain_username }}@{{ domain_realm | upper }}' + domain_password: VagrantPass1 + domain_realm: ad.test + domain_dn_base: DC=ad,DC=test diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/setup.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/setup.yml new file mode 100644 index 000000000..5fcc09dfd --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/setup.yml @@ -0,0 +1,82 @@ +- name: create domain controller + hosts: DC + gather_facts: no + + tasks: + - name: get network connection names + ansible.windows.win_powershell: + parameters: + IPAddress: '{{ ansible_host }}' + script: | + param ($IPAddress) + + $Ansible.Changed = $false + + Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" | + ForEach-Object -Process { + $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'" + if ($config.IPAddress -contains $IPAddress) { + $_.NetConnectionID + } + } + register: connection_name + + - name: set the DNS for the internal adapters to localhost + ansible.windows.win_dns_client: + adapter_names: + - '{{ connection_name.output[0] }}' + dns_servers: + - 127.0.0.1 + + - name: ensure domain exists and DC is promoted as a domain controller + microsoft.ad.domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true + + - ansible.windows.win_feature: + name: RSAT-AD-PowerShell + state: present + + - name: create domain username + microsoft.ad.user: + name: '{{ domain_username }}' + upn: '{{ domain_user_upn }}' + description: '{{ domain_username }} Domain Account' + password: '{{ domain_password }}' + password_never_expires: yes + update_password: when_changed + groups: + add: + - Domain Admins + state: present + +- name: setup test host + hosts: TEST + gather_facts: no + + tasks: + - name: get network connection names + ansible.windows.win_powershell: + parameters: + IPAddress: '{{ ansible_host }}' + script: | + param ($IPAddress) + + $Ansible.Changed = $false + + Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" | + ForEach-Object -Process { + $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'" + if ($config.IPAddress -contains $IPAddress) { + $_.NetConnectionID + } + } + register: connection_name + + - name: set DNS for the private adapter to point to the DC + ansible.windows.win_dns_client: + adapter_names: + - '{{ connection_name.output[0] }}' + dns_servers: + - '{{ hostvars["DC"]["ansible_host"] }}' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml new file mode 100644 index 000000000..e4fa96c8e --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml @@ -0,0 +1,631 @@ +- set_fact: + get_result_script: | + $Ansible.Changed = $false + $cs = Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, PartOfDomain, Workgroup + $domainName = if ($cs.PartOfDomain) { + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name + } + catch [System.Security.Authentication.AuthenticationException] { + $cs.Domain + } + } + else { + $null + } + + [PSCustomObject]@{ + HostName = $env:COMPUTERNAME + PartOfDomain = $cs.PartOfDomain + DnsDomainName = $domainName + WorkgroupName = $cs.Workgroup + } + + get_ad_result_script: | + $Ansible.Changed = $false + Get-ADComputer -Filter { Name -ne 'DC' } -Properties DistinguishedName, Name, Enabled | + Select-Object -Property DistinguishedName, Name, Enabled + +- name: join domain - check mode + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: domain + reboot: true + check_mode: true + register: join_domain_check + +- name: get result of join domain - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: join_domain_check_actual + +- name: assert join domain - check mode + assert: + that: + - join_domain_check is changed + - join_domain_check.reboot_required == False + - join_domain_check_actual.output[0]["DnsDomainName"] == None + - join_domain_check_actual.output[0]["HostName"] == "TEST" + - join_domain_check_actual.output[0]["PartOfDomain"] == False + - join_domain_check_actual.output[0]["WorkgroupName"] == "WORKGROUP" + +- name: join domain with reboot + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: domain + reboot: true + register: join_domain + +- name: get result of join domain with reboot + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: join_domain_actual + +- name: get ad result of join domain with reboot + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: join_domain_ad_actual + +- name: assert join domain with reboot + assert: + that: + - join_domain is changed + - join_domain.reboot_required == False + - join_domain_actual.output[0]["DnsDomainName"] == domain_realm + - join_domain_actual.output[0]["HostName"] == "TEST" + - join_domain_actual.output[0]["PartOfDomain"] == True + - join_domain_actual.output[0]["WorkgroupName"] == None + - join_domain_ad_actual.output | length == 1 + - join_domain_ad_actual.output[0]["Name"] == "TEST" + - join_domain_ad_actual.output[0]["Enabled"] == True + +- name: join domain with reboot - idempotent + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: domain + reboot: true + register: join_domain_again + +- name: assert join domain with reboot - idempotent + assert: + that: + - not join_domain_again is changed + - join_domain_again.reboot_required == False + +- name: fail to change domain of already joined host + membership: + dns_domain_name: fake.realm + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: domain + reboot: true + register: fail_cannot_rename_domain + failed_when: + - fail_cannot_rename_domain.msg != "Host is already joined to '" ~ domain_realm ~ "', switching domains is not implemented" + +- name: rename hostname of domain joined host - check mode + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: OTHER + state: domain + reboot: true + register: rename_host_domain_check + check_mode: True + +- name: get result of rename hostname of domain joined host - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: rename_host_domain_check_actual + +- name: get ad result of rename hostname of domain joined host - check mode + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: rename_host_domain_check_ad_actual + +- name: assert rename hostname of domain joined host - check mode + assert: + that: + - rename_host_domain_check is changed + - rename_host_domain_check.reboot_required == False + - rename_host_domain_check_actual.output[0]["DnsDomainName"] == domain_realm + - rename_host_domain_check_actual.output[0]["HostName"] == "TEST" + - rename_host_domain_check_actual.output[0]["PartOfDomain"] == True + - rename_host_domain_check_actual.output[0]["WorkgroupName"] == None + - rename_host_domain_check_ad_actual.output | length == 1 + - rename_host_domain_check_ad_actual.output[0]["Name"] == "TEST" + - rename_host_domain_check_ad_actual.output[0]["Enabled"] == True + +- name: rename hostname of domain joined host + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: OTHER + state: domain + reboot: true + register: rename_host_domain + +- name: get result of rename hostname of domain joined host + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: rename_host_domain_actual + +- name: get ad result of rename hostname of domain joined host + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: rename_host_domain_ad_actual + +- name: assert join domain + assert: + that: + - rename_host_domain is changed + - rename_host_domain.reboot_required == False + - rename_host_domain_actual.output[0]["DnsDomainName"] == domain_realm + - rename_host_domain_actual.output[0]["HostName"] == "OTHER" + - rename_host_domain_actual.output[0]["PartOfDomain"] == True + - rename_host_domain_actual.output[0]["WorkgroupName"] == None + - rename_host_domain_ad_actual.output | length == 1 + - rename_host_domain_ad_actual.output[0]["Name"] == "OTHER" + - rename_host_domain_ad_actual.output[0]["Enabled"] == True + +- name: change domain to workgroup - check mode + membership: + workgroup_name: TEST + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + register: to_workgroup_check + check_mode: true + +- name: get result of change domain to workgroup - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: to_workgroup_check_actual + +- name: get ad result of change domain to workgroup - check mode + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: to_workgroup_check_ad_actual + +- name: assert change domain to workgroup - check mode + assert: + that: + - to_workgroup_check is changed + - to_workgroup_check.reboot_required == True + - to_workgroup_check_actual.output[0]["DnsDomainName"] == domain_realm + - to_workgroup_check_actual.output[0]["HostName"] == "OTHER" + - to_workgroup_check_actual.output[0]["PartOfDomain"] == True + - to_workgroup_check_actual.output[0]["WorkgroupName"] == None + - to_workgroup_check_ad_actual.output | length == 1 + - to_workgroup_check_ad_actual.output[0]["Name"] == "OTHER" + - to_workgroup_check_ad_actual.output[0]["Enabled"] == True + +- name: change domain to workgroup + membership: + workgroup_name: TEST + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + register: to_workgroup + +- set_fact: + local_user: OTHER\{{ ansible_user }} + +- ansible.windows.win_reboot: + when: to_workgroup.reboot_required + vars: + # To avoid conflicts with the domain account with the same name we + # explicitly prefix the user to be the local account. Failing to do so + # will have the connection fail as it will try to talk to the DC which + # ends up failing. + ansible_user: '{{ local_user }}' + +- name: get result of change domain to workgroup + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: to_workgroup_actual + +- name: get ad result of change domain to workgroup + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: to_workgroup_ad_actual + +- name: assert change domain to workgroup + assert: + that: + - to_workgroup is changed + - to_workgroup.reboot_required == True + - to_workgroup_actual.output[0]["DnsDomainName"] == None + - to_workgroup_actual.output[0]["HostName"] == "OTHER" + - to_workgroup_actual.output[0]["PartOfDomain"] == False + - to_workgroup_actual.output[0]["WorkgroupName"] == "TEST" + - to_workgroup_ad_actual.output | length == 1 + - to_workgroup_ad_actual.output[0]["Name"] == "OTHER" + - to_workgroup_ad_actual.output[0]["Enabled"] == False + +- name: remove orphaned AD account for later tests + microsoft.ad.computer: + name: OTHER + state: absent + delegate_to: DC + +- name: change domain to workgroup - idempotent + membership: + workgroup_name: TEST + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + register: to_workgroup_again + +- name: assert change domain to workgroup - idempotent + assert: + that: + - not to_workgroup_again is changed + - to_workgroup_again.reboot_required == False + +- name: change workgroup - check mode + membership: + workgroup_name: TEST2 + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + register: change_workgroup_check + check_mode: true + +- name: get result of change workgroup - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: change_workgroup_check_actual + +- name: assert change workgroup - check mode + assert: + that: + - change_workgroup_check is changed + - change_workgroup_check.reboot_required == False + - change_workgroup_check_actual.output[0]["DnsDomainName"] == None + - change_workgroup_check_actual.output[0]["HostName"] == "OTHER" + - change_workgroup_check_actual.output[0]["PartOfDomain"] == False + - change_workgroup_check_actual.output[0]["WorkgroupName"] == "TEST" + +- name: change workgroup + membership: + workgroup_name: TEST2 + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + register: change_workgroup + +- name: get result of change workgroup + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: change_workgroup_actual + +- name: assert change workgroup + assert: + that: + - change_workgroup is changed + - change_workgroup.reboot_required == False + - change_workgroup_actual.output[0]["DnsDomainName"] == None + - change_workgroup_actual.output[0]["HostName"] == "OTHER" + - change_workgroup_actual.output[0]["PartOfDomain"] == False + - change_workgroup_actual.output[0]["WorkgroupName"] == "TEST2" + +- name: change just the hostname - check mode + membership: + workgroup_name: TEST2 + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + hostname: FOO + register: change_hostname_check + check_mode: true + +- name: get result of change just the hostname - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: change_hostname_check_actual + +- name: assert change just the hostname - check mode + assert: + that: + - change_hostname_check is changed + - change_hostname_check.reboot_required == False + - change_hostname_check_actual.output[0]["DnsDomainName"] == None + - change_hostname_check_actual.output[0]["HostName"] == "OTHER" + - change_hostname_check_actual.output[0]["PartOfDomain"] == False + - change_hostname_check_actual.output[0]["WorkgroupName"] == "TEST2" + +- name: change just the hostname + membership: + workgroup_name: TEST2 + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + hostname: FOO + register: change_hostname + +- name: get result of change just the hostname + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: change_hostname_actual + +- name: assert change just the hostname - check mode + assert: + that: + - change_hostname is changed + - change_hostname.reboot_required == False + - change_hostname_actual.output[0]["DnsDomainName"] == None + - change_hostname_actual.output[0]["HostName"] == "FOO" + - change_hostname_actual.output[0]["PartOfDomain"] == False + - change_hostname_actual.output[0]["WorkgroupName"] == "TEST2" + +- name: create custom OU + ansible.windows.win_powershell: + script: | + $ou = New-ADOrganizationalUnit -Name MyHosts -PassThru + $ou.DistinguishedName + delegate_to: DC + register: custom_ou + +- name: join domain with hostname and OU - check mode + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: BAR + domain_ou_path: '{{ custom_ou.output[0] }}' + state: domain + register: join_ou_check + check_mode: true + +- name: get result of join domain with hostname and OU - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: join_ou_check_actual + +- name: assert change just the hostname - check mode + assert: + that: + - join_ou_check is changed + - join_ou_check.reboot_required == True + - join_ou_check_actual.output[0]["DnsDomainName"] == None + - join_ou_check_actual.output[0]["HostName"] == "FOO" + - join_ou_check_actual.output[0]["PartOfDomain"] == False + - join_ou_check_actual.output[0]["WorkgroupName"] == "TEST2" + +- name: join domain with hostname and OU + membership: + dns_domain_name: '{{ domain_realm }}' + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: BAR + domain_ou_path: '{{ custom_ou.output[0] }}' + state: domain + register: join_ou + +- ansible.windows.win_reboot: + when: join_ou.reboot_required + +- name: get result of join domain with hostname and OU + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: join_ou_actual + +- name: get ad result of join domain with hostname and OU + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + register: join_ou_ad_actual + delegate_to: DC + +- name: assert change just the hostname + assert: + that: + - join_ou is changed + - join_ou.reboot_required == True + - join_ou_actual.output[0]["DnsDomainName"] == domain_realm + - join_ou_actual.output[0]["HostName"] == "BAR" + - join_ou_actual.output[0]["PartOfDomain"] == True + - join_ou_actual.output[0]["WorkgroupName"] == None + - join_ou_ad_actual.output | length == 1 + - join_ou_ad_actual.output[0]["Name"] == "BAR" + - join_ou_ad_actual.output[0]["Enabled"] == True + - join_ou_ad_actual.output[0]["DistinguishedName"] == "CN=BAR," ~ custom_ou.output[0] + +- name: change domain to workgroup with hostname change - check mode + membership: + workgroup_name: WORKGROUP + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: FOO + state: workgroup + register: to_workgroup_hostname_check + check_mode: true + +- name: get result of change domain to workgroup with hostname change - check mode + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: to_workgroup_hostname_check_actual + +- name: get ad result of change domain to workgroup with hostname change - check mode + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: to_workgroup_hostname_check_ad_actual + +- name: assert change domain to workgroup with hostname change - check mode + assert: + that: + - to_workgroup_hostname_check is changed + - to_workgroup_hostname_check.reboot_required == True + - to_workgroup_hostname_check_actual.output[0]["DnsDomainName"] == domain_realm + - to_workgroup_hostname_check_actual.output[0]["HostName"] == "BAR" + - to_workgroup_hostname_check_actual.output[0]["PartOfDomain"] == True + - to_workgroup_hostname_check_actual.output[0]["WorkgroupName"] == None + - to_workgroup_hostname_check_ad_actual.output | length == 1 + - to_workgroup_hostname_check_ad_actual.output[0]["Name"] == "BAR" + - to_workgroup_hostname_check_ad_actual.output[0]["Enabled"] == True + +- name: change domain to workgroup with hostname change + membership: + workgroup_name: WORKGROUP + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + hostname: FOO + state: workgroup + reboot: true + register: to_workgroup_hostname + +- name: get result of change domain to workgroup with hostname change + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: to_workgroup_hostname_actual + +- name: get ad result of change domain to workgroup with hostname change + ansible.windows.win_powershell: + script: '{{ get_ad_result_script }}' + delegate_to: DC + register: to_workgroup_hostname_ad_actual + +- name: assert change domain to workgroup with hostname change + assert: + that: + - to_workgroup_hostname is changed + - to_workgroup_hostname.reboot_required == False + - to_workgroup_hostname_actual.output[0]["DnsDomainName"] == None + - to_workgroup_hostname_actual.output[0]["HostName"] == "FOO" + - to_workgroup_hostname_actual.output[0]["PartOfDomain"] == False + - to_workgroup_hostname_actual.output[0]["WorkgroupName"] == "WORKGROUP" + - to_workgroup_hostname_ad_actual.output | length == 1 + - to_workgroup_hostname_ad_actual.output[0]["Name"] == "BAR" + - to_workgroup_hostname_ad_actual.output[0]["Enabled"] == False + +- name: remove orphaned AD account for later tests + microsoft.ad.computer: + name: BAR + state: absent + delegate_to: DC + +- name: create computer object + microsoft.ad.computer: + name: My, Computer + path: CN=Users,{{ domain_dn_base }} + sam_account_name: MyComp$ + state: present + delegate_to: DC + register: comp_account + +- name: get offline join blob + microsoft.ad.offline_join: + identity: '{{ comp_account.object_guid }}' + delegate_to: DC + register: offline_join + +- name: get computer object info + microsoft.ad.object_info: + identity: '{{ comp_account.object_guid }}' + properties: + - pwdLastSet + delegate_to: DC + register: comp_account_pre_join + +- name: join domain by offline blob - check + microsoft.ad.membership: + offline_join_blob: '{{ offline_join.blob }}' + state: domain + register: offline_join_check + check_mode: true + +- name: get result of join domain by offline blob - check + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: offline_join_check_actual + +- name: get result of join domain by offline blob comp info - check + microsoft.ad.object_info: + identity: '{{ comp_account.object_guid }}' + properties: + - pwdLastSet + delegate_to: DC + register: offline_join_check_ad_actual + +- name: assert join domain by offline blob - check + assert: + that: + - offline_join_check is changed + - offline_join_check.reboot_required == true + - offline_join_check_actual.output[0].DnsDomainName == None + - offline_join_check_actual.output[0].PartOfDomain == False + - offline_join_check_ad_actual.objects[0].pwdLastSet == comp_account_pre_join.objects[0].pwdLastSet + +- name: join domain by offline blob + microsoft.ad.membership: + offline_join_blob: '{{ offline_join.blob }}' + state: domain + reboot: true + register: offline_join_res + +- name: get result of join domain by offline blob + ansible.windows.win_powershell: + script: '{{ get_result_script }}' + register: offline_join_actual + +- name: get result of join domain by offline blob comp info + microsoft.ad.object_info: + identity: '{{ comp_account.object_guid }}' + properties: + - pwdLastSet + delegate_to: DC + register: offline_join_ad_actual + +- name: assert join domain by offline blob + assert: + that: + - offline_join_res is changed + - offline_join_res.reboot_required == false + - offline_join_actual.output[0].DnsDomainName == domain_realm + - offline_join_actual.output[0].PartOfDomain == True + - offline_join_ad_actual.objects[0].pwdLastSet > offline_join_check_ad_actual.objects[0].pwdLastSet + +- name: join domain by offline blob - idempotent + microsoft.ad.membership: + offline_join_blob: '{{ offline_join.blob }}' + state: domain + register: offline_join_again + +- name: assert join domain by offline blob - idempotent + assert: + that: + - not offline_join_again is changed + +- name: change domain to workgroup + membership: + workgroup_name: WORKGROUP + domain_admin_user: '{{ domain_user_upn }}' + domain_admin_password: '{{ domain_password }}' + state: workgroup + reboot: true + +- name: remove orphaned AD account for later tests + microsoft.ad.computer: + name: My, Computer + path: CN=Users,{{ domain_dn_base }} + state: absent + delegate_to: DC diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/test.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/test.yml new file mode 100644 index 000000000..238d92005 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/test.yml @@ -0,0 +1,30 @@ +- name: ensure time is in sync + hosts: windows + gather_facts: no + tasks: + - name: get current host datetime + command: date +%s + changed_when: False + delegate_to: localhost + run_once: True + register: local_time + + - name: set datetime on Windows + ansible.windows.win_powershell: + parameters: + SecondsSinceEpoch: '{{ local_time.stdout | trim }}' + script: | + param($SecondsSinceEpoch) + + $utc = [System.DateTimeKind]::Utc + $epoch = New-Object -TypeName System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0, $utc + $date = $epoch.AddSeconds($SecondsSinceEpoch) + + Set-Date -Date $date + +- name: run microsoft.ad.membership tests + hosts: TEST + gather_facts: no + + tasks: + - import_tasks: tasks/main.yml diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/object/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/defaults/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object/defaults/main.yml new file mode 100644 index 000000000..e557e2d01 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/defaults/main.yml @@ -0,0 +1,2 @@ +object_name: My, Contact +object_dn: CN=My\, Contact,{{ setup_domain_info.output[0].defaultNamingContext }} diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/main.yml new file mode 100644 index 000000000..1945e3a90 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/main.yml @@ -0,0 +1,164 @@ +- name: add test attributes to the schema + object: + name: '{{ item.name }}' + path: '{{ setup_domain_info.output[0].schemaNamingContext }}' + state: present + type: attributeSchema + attributes: + set: + adminDescription: '{{ item.description }}' + lDAPDisplayName: '{{ item.name }}' + attributeId: 1.3.6.1.4.1.2312.99999.{{ item.id }} + attributeSyntax: '{{ item.syntax }}' + omSyntax: '{{ item.om_syntax }}' + isSingleValued: '{{ item.single_value }}' + systemOnly: false + isMemberOfPartialAttributeSet: false + searchFlags: 0 + showInAdvancedViewOnly: false + loop: + # https://social.technet.microsoft.com/wiki/contents/articles/52570.active-directory-syntaxes-of-attributes.aspx + - name: ansible-BoolSingle + description: Test Attribute for Boolean Single + id: 1 + syntax: 2.5.5.8 + om_syntax: 1 + single_value: true + - name: ansible-BoolMulti + description: Test Attribute for Boolean Multi + id: 2 + syntax: 2.5.5.8 + om_syntax: 1 + single_value: false + + - name: ansible-BytesSingle + description: Test Attribute for Bytes Single + id: 3 + syntax: 2.5.5.10 + om_syntax: 4 + single_value: true + - name: ansible-BytesMulti + description: Test Attribute for Bytes Multi + id: 4 + syntax: 2.5.5.10 + om_syntax: 4 + single_value: false + + - name: ansible-DateTimeSingle + description: Test Attribute for DateTime Single + id: 5 + syntax: 2.5.5.11 + om_syntax: 24 + single_value: true + - name: ansible-DateTimeMulti + description: Test Attribute for DateTime Multi + id: 6 + syntax: 2.5.5.11 + om_syntax: 24 + single_value: false + + - name: ansible-IntSingle + description: Test Attribute for Integer Single + id: 7 + syntax: 2.5.5.16 + om_syntax: 65 + single_value: true + - name: ansible-IntMulti + description: Test Attribute for Integer Multi + id: 8 + syntax: 2.5.5.16 + om_syntax: 65 + single_value: false + + - name: ansible-SDSingle + description: Test Attribute for SD Single + id: 9 + syntax: 2.5.5.15 + om_syntax: 66 + single_value: true + - name: ansible-SDMulti + description: Test Attribute for SD Multi + id: 10 + syntax: 2.5.5.15 + om_syntax: 66 + single_value: false + + - name: ansible-StringSingle + description: Test Attribute for String Single + id: 11 + syntax: 2.5.5.12 + om_syntax: 64 + single_value: true + - name: ansible-StringMulti + description: Test Attribute for String Multi + id: 12 + syntax: 2.5.5.12 + om_syntax: 64 + single_value: false + + register: schema_attributes + become: true + become_method: runas + become_user: SYSTEM + +- name: create auxilary class to house the new attributes + object: + name: ansibleTesting + path: '{{ setup_domain_info.output[0].schemaNamingContext }}' + type: classSchema + attributes: + set: + adminDescription: Test auxilary class for Ansible microsoft.ad attribute tests + adminDisplayName: ansibleTesting + lDAPDisplayName: ansibleTesting + governsId: 1.3.6.1.4.1.2312.99999 + objectClassCategory: 3 + systemOnly: false + subclassOf: top + # This is unfortunately not idempotent, the set must be the OID but + # Get-ADObject returns the lDAPDisplayName + mayContain: '{{ schema_attributes.results | map(attribute="item.id") | map("regex_replace", "^(.*)$", "1.3.6.1.4.1.2312.99999.\1") | list }}' + when: schema_attributes is changed + register: schema_class + become: true + become_method: runas + become_user: SYSTEM + +- name: add auxilary class to the contact class + object: + name: Contact + path: '{{ setup_domain_info.output[0].schemaNamingContext }}' + type: classSchema + attributes: + add: + auxiliaryClass: 1.3.6.1.4.1.2312.99999 + register: aux_reg + become: true + become_method: runas + become_user: SYSTEM + when: schema_class is changed + +- name: update schema + ansible.windows.win_powershell: + parameters: + Name: '{{ setup_domain_info.output[0].dnsHostName }}' + script: | + param($Name) + + $dse = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$Name/RootDSE" + $dse.Put("SchemaUpdateNow", 1) + $dse.SetInfo() + become: true + become_method: runas + become_user: SYSTEM + when: aux_reg is changed + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp object + object: + name: '{{ object_name }}' + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml new file mode 100644 index 000000000..b642ce6eb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml @@ -0,0 +1,1446 @@ +- name: create contact object - check + object: + name: '{{ object_name }}' + type: contact + state: present + register: create_check + check_mode: true + +- name: get result of create contact object - check + object_info: + identity: '{{ create_check.distinguished_name }}' + register: create_check_actual + +- name: assert create contact object - check + assert: + that: + - create_check is changed + - create_check.distinguished_name == object_dn + - create_check.object_guid == '00000000-0000-0000-0000-000000000000' + - create_check_actual.objects == [] + +- name: create contact object + object: + name: '{{ object_name }}' + type: contact + state: present + register: create + +- name: get result of create contact object + object_info: + identity: '{{ create_check.distinguished_name }}' + properties: + - objectClass + register: create_actual + +- name: assert create contact object + assert: + that: + - create is changed + - create_actual.objects | length == 1 + - create_actual.objects[0].ObjectClass == 'contact' + - create.distinguished_name == object_dn + - create.distinguished_name == create_actual.objects[0].DistinguishedName + - create.object_guid == create_actual.objects[0].ObjectGUID + +- set_fact: + object_identity: '{{ create.object_guid }}' + +- name: create contact object - idempotent + object: + name: '{{ object_name }}' + type: contact + state: present + register: create_again + +- name: assert create contact object - idempotent + assert: + that: + - not create_again is changed + - create_again.distinguished_name == create_actual.objects[0].DistinguishedName + - create_again.object_guid == create_actual.objects[0].ObjectGUID + +- name: fail to change type + object: + name: '{{ object_name }}' + state: present + type: failure + register: fail_change_type + failed_when: fail_change_type.msg != "Cannot change object type contact of existing object " ~ object_dn ~ " to failure" + +- name: rename and set display name of object - check + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + display_name: Display Name + state: present + type: contact + register: rename_check + check_mode: true + +- name: get result of create contact object - check + object_info: + identity: '{{ object_identity }}' + properties: + - displayName + - name + register: rename_check_actual + +- name: assert rename and set display name of object - check + assert: + that: + - rename_check is changed + - rename_check.distinguished_name == 'CN=My\, Contact 2,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_check.object_guid == object_identity + - rename_check_actual.objects[0].DisplayName == None + - rename_check_actual.objects[0].DistinguishedName == object_dn + - rename_check_actual.objects[0].Name == object_name + +- name: rename and set display name of object + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + display_name: Display Name + state: present + type: contact + register: rename + +- name: get result of create contact object + object_info: + identity: '{{ object_identity }}' + properties: + - displayName + - name + register: rename_actual + +- name: assert rename and set display name of object + assert: + that: + - rename is changed + - rename.distinguished_name == 'CN=My\, Contact 2,' ~ setup_domain_info.output[0].defaultNamingContext + - rename.object_guid == object_identity + - rename_actual.objects[0].DisplayName == 'Display Name' + - rename_actual.objects[0].DistinguishedName == 'CN=My\, Contact 2,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_actual.objects[0].Name == 'My, Contact 2' + +- name: rename and set display name of object - idempotent + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + display_name: Display Name + state: present + type: contact + register: rename_again + +- name: assert rename and set display name of object - idempotent + assert: + that: + - not rename_again is changed + - rename_again.distinguished_name == 'CN=My\, Contact 2,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_again.object_guid == object_identity + +- name: move and set description - check + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: My Description + state: present + type: contact + register: move_check + check_mode: true + +- name: move and set description - check + object_info: + identity: '{{ object_identity }}' + properties: + - description + - name + register: move_check_actual + +- name: assert move and set description - check + assert: + that: + - move_check is changed + - move_check.distinguished_name == 'CN=My\, Contact 2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_check.object_guid == object_identity + - move_check_actual.objects[0].Description == None + - move_check_actual.objects[0].DistinguishedName == 'CN=My\, Contact 2,' ~ setup_domain_info.output[0].defaultNamingContext + - move_check_actual.objects[0].Name == 'My, Contact 2' + +- name: move and set description + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: My Description + state: present + type: contact + register: move + +- name: move and set description + object_info: + identity: '{{ object_identity }}' + properties: + - description + - name + register: move_actual + +- name: assert move and set description + assert: + that: + - move is changed + - move.distinguished_name == 'CN=My\, Contact 2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move.object_guid == object_identity + - move_actual.objects[0].Description == 'My Description' + - move_actual.objects[0].DistinguishedName == 'CN=My\, Contact 2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_actual.objects[0].Name == 'My, Contact 2' + +- name: move and set description - idempotent + object: + name: My, Contact 2 + identity: '{{ object_identity }}' + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: My Description + state: present + type: contact + register: move_again + +- name: assert move and set description - idempotent + assert: + that: + - not move_again is changed + - move_again.distinguished_name == 'CN=My\, Contact 2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_again.object_guid == object_identity + +- name: rename and move - check + object: + name: '{{ object_name }}' + identity: '{{ object_identity }}' + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + state: present + type: contact + register: rename_and_move_check + check_mode: true + +- name: get result of rename and move - check + object_info: + identity: '{{ object_identity }}' + register: rename_and_move_check_actual + +- name: assert rename and move - check + assert: + that: + - rename_and_move_check is changed + - rename_and_move_check.distinguished_name == object_dn + - rename_and_move_check.object_guid == object_identity + - rename_and_move_check_actual.objects[0].DistinguishedName == 'CN=My\, Contact 2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_and_move_check_actual.objects[0].Name == 'My, Contact 2' + +- name: rename and move + object: + name: '{{ object_name }}' + identity: '{{ object_identity }}' + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + state: present + type: contact + register: rename_and_move + +- name: get result of rename and move + object_info: + identity: '{{ object_identity }}' + register: rename_and_move_actual + +- name: assert rename and move + assert: + that: + - rename_and_move is changed + - rename_and_move.distinguished_name == object_dn + - rename_and_move.object_guid == object_identity + - rename_and_move_actual.objects[0].DistinguishedName == object_dn + - rename_and_move_actual.objects[0].Name == object_name + +- name: remove object by name - check + object: + name: '{{ object_name }}' + state: absent + register: remove_check + check_mode: true + +- name: get result of remove by name - check + object_info: + identity: '{{ object_identity }}' + register: remove_check_actual + +- name: assert remove object by name - check + assert: + that: + - remove_check is changed + - remove_check.distinguished_name == object_dn + - remove_check.object_guid == object_identity + - remove_check_actual.objects | length == 1 + +- name: remove object by name + object: + name: '{{ object_name }}' + state: absent + register: remove + +- name: get result of remove by name + object_info: + identity: '{{ object_identity }}' + register: remove_actual + +- name: assert remove object by name - check + assert: + that: + - remove is changed + - remove.distinguished_name == object_dn + - remove.object_guid == object_identity + - remove_actual.objects == [] + +- name: remove object by name - idempotent + object: + name: '{{ object_name }}' + state: absent + register: remove_again + +- name: assert remove object by name - check + assert: + that: + - not remove_again is changed + - remove_again.distinguished_name == None + - remove_again.object_guid == None + +- name: create object protected from deletion + object: + name: My, Container + state: present + type: organizationalUnit + protect_from_deletion: true + register: create_deletion + +- set_fact: + object_identity: '{{ create_deletion.object_guid }}' + +- name: get result of create object protected from deletion + object_info: + identity: '{{ object_identity }}' + properties: + - ProtectedFromAccidentalDeletion + register: create_deletion_actual + +- name: assert create object protected from deletion + assert: + that: + - create_deletion is changed + - create_deletion.distinguished_name == 'OU=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - create_deletion_actual.objects[0].ProtectedFromAccidentalDeletion == true + +- name: unset protect from deletion + object: + name: My, Container + state: present + type: organizationalUnit + protect_from_deletion: false + register: unset_deletion + +- name: get result of unset protect from deletion + object_info: + identity: '{{ object_identity }}' + properties: + - ProtectedFromAccidentalDeletion + register: unset_deletion_actual + +- name: assert set protect from deletion + assert: + that: + - unset_deletion is changed + - unset_deletion.distinguished_name == 'OU=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - unset_deletion_actual.objects[0].ProtectedFromAccidentalDeletion == false + +- name: set protect from deletion + object: + name: My, Container + state: present + type: organizationalUnit + protect_from_deletion: true + register: set_deletion + +- name: get result of set protect from deletion + object_info: + identity: '{{ object_identity }}' + properties: + - ProtectedFromAccidentalDeletion + register: set_deletion_actual + +- name: assert set protect from deletion + assert: + that: + - set_deletion is changed + - set_deletion.distinguished_name == 'OU=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - set_deletion_actual.objects[0].ProtectedFromAccidentalDeletion == true + +- name: create sub ous for move test + ou: + name: '{{ item }}' + path: '{{ set_deletion.distinguished_name }}' + state: present + protect_from_deletion: true + register: sub_ous + loop: + - SubOU + - TestOU + +- name: move and rename object that is protected from deletion - check + object: + name: TestOU 2 + path: '{{ sub_ous.results[0].distinguished_name }}' + identity: '{{ sub_ous.results[1].object_guid }}' + type: organizationalUnit + register: move_ou_check + check_mode: true + +- name: get result of move and rename object that is protected from deletion - check + object_info: + identity: '{{ sub_ous.results[1].object_guid }}' + properties: + - ProtectedFromAccidentalDeletion + register: move_ou_check_actual + +- name: assert move and rename object that is protected from deletion - check + assert: + that: + - move_ou_check is changed + - move_ou_check.distinguished_name == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - move_ou_check_actual.objects[0].Name == 'TestOU' + - move_ou_check_actual.objects[0].DistinguishedName == sub_ous.results[1].distinguished_name + - move_ou_check_actual.objects[0].ProtectedFromAccidentalDeletion == true + +- name: move and rename object that is protected from deletion + object: + name: TestOU 2 + path: '{{ sub_ous.results[0].distinguished_name }}' + identity: '{{ sub_ous.results[1].object_guid }}' + type: organizationalUnit + register: move_ou + +- name: get result of move and rename object that is protected from deletion + object_info: + identity: '{{ sub_ous.results[1].object_guid }}' + properties: + - ProtectedFromAccidentalDeletion + register: move_ou_actual + +- name: assert move and rename object that is protected from deletion + assert: + that: + - move_ou is changed + - move_ou.distinguished_name == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - move_ou_actual.objects[0].Name == 'TestOU 2' + - move_ou_actual.objects[0].DistinguishedName == 'OU=TestOU 2,' ~ sub_ous.results[0].distinguished_name + - move_ou_actual.objects[0].ProtectedFromAccidentalDeletion == true + +- name: remove object that is protected from deletion - check + object: + name: My, Container + state: absent + type: organizationalUnit + register: remove_deletion_check + check_mode: true + +- name: get result of remove object that is protected from deletion - check + object_info: + identity: '{{ object_identity }}' + properties: + - ProtectedFromAccidentalDeletion + register: remove_deletion_actual_check + +- name: assert remove object that is protected from deletion - check + assert: + that: + - remove_deletion_check is changed + - remove_deletion_check.distinguished_name == 'OU=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - remove_deletion_actual_check.objects[0].ProtectedFromAccidentalDeletion == true + +- name: remove object that is protected from deletion + object: + name: My, Container + state: absent + type: organizationalUnit + register: remove_deletion + +- name: get result of remove object that is protected from deletion + object_info: + identity: '{{ object_identity }}' + register: remove_deletion_actual + +- name: assert remove object that is protected from deletion + assert: + that: + - remove_deletion is changed + - remove_deletion.distinguished_name == 'OU=My\, Container,' ~ setup_domain_info.output[0].defaultNamingContext + - remove_deletion_actual.objects == [] + +- name: create object with custom path, description, and display_name + object: + name: My, Container + description: Test Description + display_name: Display Name + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + state: present + type: container + register: create_custom + +- set_fact: + object_identity: '{{ create_custom.object_guid }}' + +- name: get result of create object with custom path, description, and display_name + object_info: + identity: '{{ object_identity }}' + properties: + - description + - displayName + - objectClass + register: create_custom_actual + +- name: assert create object with custom path, description, and display_name + assert: + that: + - create_custom is changed + - create_custom.distinguished_name == 'CN=My\, Container,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_custom.object_guid == object_identity + - create_custom_actual.objects[0].Description == 'Test Description' + - create_custom_actual.objects[0].DisplayName == 'Display Name' + - create_custom_actual.objects[0].ObjectClass == 'container' + +- name: create child object in container with attributes + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: Test Description + display_name: Display Name + attributes: + add: + ansible-BoolSingle: false + ansible-BoolMulti: + - false + - true + ansible-BytesSingle: + type: bytes + value: Zm9v + ansible-BytesMulti: + - type: bytes + value: Zm9v + - type: bytes + value: YmFy + ansible-DateTimeSingle: + type: date_time + value: '1970-01-01T00:00:00Z' + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00+01:00' + - type: date_time + value: '1601-01-01T00:00:00.1-01:00' + ansible-IntSingle: 0 + ansible-IntMulti: + - 0 + - 1 + ansible-SDSingle: + type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD) + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU) + ansible-StringSingle: single + ansible-StringMulti: + - multi 1 + - multi 2 + type: contact + state: present + register: sub_object + +- name: get result of child object attributes + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolSingle + - ansible-BoolMulti + - ansible-BytesSingle + - ansible-BytesMulti + - ansible-DateTimeSingle + - ansible-DateTimeMulti + - ansible-IntSingle + - ansible-IntMulti + - ansible-SDSingle + - ansible-SDMulti + - ansible-StringSingle + - ansible-StringMulti + register: sub_object_actual + +- name: assert create child object in container with attributes + assert: + that: + - sub_object is changed + - sub_object.distinguished_name == 'CN=Contact,CN=My\, Container,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - sub_object_actual.objects[0]['ansible-BoolSingle'] == False + - sub_object_actual.objects[0]['ansible-BoolMulti'] | length == 2 + - True in sub_object_actual.objects[0]['ansible-BoolMulti'] + - False in sub_object_actual.objects[0]['ansible-BoolMulti'] + - sub_object_actual.objects[0]['ansible-BytesSingle'] == "Zm9v" + - sub_object_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in sub_object_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in sub_object_actual.objects[0]['ansible-BytesMulti']" + - sub_object_actual.objects[0]['ansible-DateTimeSingle'] == "1970-01-01T00:00:00.0000000Z" + - sub_object_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T01:00:00.0000000Z' in sub_object_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in sub_object_actual.objects[0]['ansible-DateTimeMulti']" + - sub_object_actual.objects[0]['ansible-IntSingle'] == 0 + - sub_object_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in sub_object_actual.objects[0]['ansible-IntMulti'] + - 1 in sub_object_actual.objects[0]['ansible-IntMulti'] + - sub_object_actual.objects[0]['ansible-SDSingle'] == "O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)" + - sub_object_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in sub_object_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in sub_object_actual.objects[0]['ansible-SDMulti']" + - sub_object_actual.objects[0]['ansible-StringSingle'] == "single" + - sub_object_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in sub_object_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in sub_object_actual.objects[0]['ansible-StringMulti']" + +- name: add attribute - check + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + description: test description + display_name: display name + attributes: + add: + ansible-BoolMulti: + - false + - true + ansible-BytesMulti: + - type: bytes + value: Zm9v + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00+01:00' + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - 0 + - 2 + - 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - multi 1 + - '3' + - '4' + state: present + register: add_attr_check + check_mode: true + +- name: get result of add attribute - check + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolMulti + - ansible-BytesMulti + - ansible-DateTimeMulti + - ansible-IntMulti + - ansible-SDMulti + - ansible-StringMulti + register: add_attr_check_actual + +- name: assert add attribute - check + assert: + that: + - add_attr_check is changed + - add_attr_check_actual.objects[0]['ansible-BoolMulti'] | length == 2 + - True in add_attr_check_actual.objects[0]['ansible-BoolMulti'] + - False in add_attr_check_actual.objects[0]['ansible-BoolMulti'] + - add_attr_check_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in add_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in add_attr_check_actual.objects[0]['ansible-BytesMulti']" + - add_attr_check_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T01:00:00.0000000Z' in add_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in add_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - add_attr_check_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in add_attr_check_actual.objects[0]['ansible-IntMulti'] + - 1 in add_attr_check_actual.objects[0]['ansible-IntMulti'] + - add_attr_check_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in add_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in add_attr_check_actual.objects[0]['ansible-SDMulti']" + - add_attr_check_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in add_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in add_attr_check_actual.objects[0]['ansible-StringMulti']" + +- name: add attribute + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + description: test description + display_name: display name + attributes: + add: + ansible-BoolMulti: + - false + - true + ansible-BytesMulti: + - type: bytes + value: Zm9v + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00+01:00' + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - 0 + - 2 + - 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - multi 1 + - '3' + - '4' + state: present + register: add_attr + +- name: get result of add attribute + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolMulti + - ansible-BytesMulti + - ansible-DateTimeMulti + - ansible-IntMulti + - ansible-SDMulti + - ansible-StringMulti + register: add_attr_actual + +- name: assert add attribute + assert: + that: + - add_attr is changed + - add_attr_actual.objects[0]['ansible-BoolMulti'] | length == 2 + - True in add_attr_actual.objects[0]['ansible-BoolMulti'] + - False in add_attr_actual.objects[0]['ansible-BoolMulti'] + - add_attr_actual.objects[0]['ansible-BytesMulti'] | length == 4 + - "'YmFy' in add_attr_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in add_attr_actual.objects[0]['ansible-BytesMulti']" + - "'dGVzdA==' in add_attr_actual.objects[0]['ansible-BytesMulti']" + - "'Y2FmZQ==' in add_attr_actual.objects[0]['ansible-BytesMulti']" + - add_attr_actual.objects[0]['ansible-DateTimeMulti'] | length == 4 + - "'1601-01-01T01:00:00.0000000Z' in add_attr_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in add_attr_actual.objects[0]['ansible-DateTimeMulti']" + - "'1970-01-01T00:00:00.0000000Z' in add_attr_actual.objects[0]['ansible-DateTimeMulti']" + - "'2023-01-17T15:30:31.0000000Z' in add_attr_actual.objects[0]['ansible-DateTimeMulti']" + - add_attr_actual.objects[0]['ansible-IntMulti'] | length == 4 + - 0 in add_attr_actual.objects[0]['ansible-IntMulti'] + - 1 in add_attr_actual.objects[0]['ansible-IntMulti'] + - 2 in add_attr_actual.objects[0]['ansible-IntMulti'] + - 3 in add_attr_actual.objects[0]['ansible-IntMulti'] + - add_attr_actual.objects[0]['ansible-SDMulti'] | length == 4 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in add_attr_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in add_attr_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC)' in add_attr_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG)' in add_attr_actual.objects[0]['ansible-SDMulti']" + - add_attr_actual.objects[0]['ansible-StringMulti'] | length == 4 + - "'multi 1' in add_attr_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in add_attr_actual.objects[0]['ansible-StringMulti']" + - "'3' in add_attr_actual.objects[0]['ansible-StringMulti']" + - "'4' in add_attr_actual.objects[0]['ansible-StringMulti']" + +- name: add attribute - idempotent + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + description: test description + display_name: display name + attributes: + add: + ansible-BoolMulti: + - false + - true + ansible-BytesMulti: + - type: bytes + value: Zm9v + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00+01:00' + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - 0 + - 2 + - 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - multi 1 + - '3' + - '4' + state: present + register: add_attr_again + +- name: assert add attribute - idempotent + assert: + that: + - not add_attr_again is changed + +- name: remove attribute - check + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + remove: + ansible-BoolMulti: + - type: bool + value: '' # Will be false when casted + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - type: int + value: '2' + - type: raw + value: 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - multi 1 + - type: string + value: 3 + - type: raw + value: '4' + state: present + register: remove_attr_check + check_mode: true + +- name: get result of remove attribute - check + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolMulti + - ansible-BytesMulti + - ansible-DateTimeMulti + - ansible-IntMulti + - ansible-SDMulti + - ansible-StringMulti + register: remove_attr_check_actual + +- name: assert remove attribute - check + assert: + that: + - remove_attr_check is changed + - remove_attr_check_actual.objects[0]['ansible-BoolMulti'] | length == 2 + - True in remove_attr_check_actual.objects[0]['ansible-BoolMulti'] + - False in remove_attr_check_actual.objects[0]['ansible-BoolMulti'] + - remove_attr_check_actual.objects[0]['ansible-BytesMulti'] | length == 4 + - "'YmFy' in remove_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in remove_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'dGVzdA==' in remove_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'Y2FmZQ==' in remove_attr_check_actual.objects[0]['ansible-BytesMulti']" + - remove_attr_check_actual.objects[0]['ansible-DateTimeMulti'] | length == 4 + - "'1601-01-01T01:00:00.0000000Z' in remove_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in remove_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'1970-01-01T00:00:00.0000000Z' in remove_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'2023-01-17T15:30:31.0000000Z' in remove_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - remove_attr_check_actual.objects[0]['ansible-IntMulti'] | length == 4 + - 0 in remove_attr_check_actual.objects[0]['ansible-IntMulti'] + - 1 in remove_attr_check_actual.objects[0]['ansible-IntMulti'] + - 2 in remove_attr_check_actual.objects[0]['ansible-IntMulti'] + - 3 in remove_attr_check_actual.objects[0]['ansible-IntMulti'] + - remove_attr_check_actual.objects[0]['ansible-SDMulti'] | length == 4 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in remove_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in remove_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC)' in remove_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG)' in remove_attr_check_actual.objects[0]['ansible-SDMulti']" + - remove_attr_check_actual.objects[0]['ansible-StringMulti'] | length == 4 + - "'multi 1' in remove_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in remove_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'3' in remove_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'4' in remove_attr_check_actual.objects[0]['ansible-StringMulti']" + +- name: remove attribute + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + remove: + ansible-BoolMulti: + - type: bool + value: '' # Will be false when casted + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - type: int + value: '2' + - type: raw + value: 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - type: string + value: 3 + - type: raw + value: '4' + state: present + register: remove_attr + +- name: get result of remove attribute + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolMulti + - ansible-BytesMulti + - ansible-DateTimeMulti + - ansible-IntMulti + - ansible-SDMulti + - ansible-StringMulti + register: remove_attr_actual + +- name: assert remove attribute + assert: + that: + - remove_attr is changed + - remove_attr_actual.objects[0]['ansible-BoolMulti'] == True + - remove_attr_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in remove_attr_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in remove_attr_actual.objects[0]['ansible-BytesMulti']" + - remove_attr_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T01:00:00.0000000Z' in remove_attr_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in remove_attr_actual.objects[0]['ansible-DateTimeMulti']" + - remove_attr_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in remove_attr_actual.objects[0]['ansible-IntMulti'] + - 1 in remove_attr_actual.objects[0]['ansible-IntMulti'] + - remove_attr_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in remove_attr_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in remove_attr_actual.objects[0]['ansible-SDMulti']" + - remove_attr_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in remove_attr_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in remove_attr_actual.objects[0]['ansible-StringMulti']" + +- name: remove attribute - idempotent + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + remove: + ansible-BoolMulti: + - type: bool + value: '' # Will be false when casted + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: Y2FmZQ== + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '2023-01-17T16:00:31+00:30' + ansible-IntMulti: + - type: int + value: '2' + - type: raw + value: 3 + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DG) + ansible-StringMulti: + - type: string + value: 3 + - type: raw + value: '4' + state: present + register: remove_attr_again + +- name: assert remove attribute - idempotent + assert: + that: + - not remove_attr_again is changed + +- name: set attribute - check + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: true + ansible-BoolMulti: false + ansible-BytesSingle: + type: bytes + value: YmFy + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: YmFy + ansible-DateTimeSingle: + type: date_time + value: '1601-01-01T00:00:00Z' + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '1601-01-01T00:01:00.1-02:00' + ansible-IntSingle: 1 + ansible-IntMulti: + - 0 + - 3 + ansible-SDSingle: + type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU) + ansible-StringSingle: Single + ansible-StringMulti: + - multi 1 + - multi 3 + state: present + register: set_attr_check + check_mode: true + +- name: get result of set attribute - check + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolSingle + - ansible-BoolMulti + - ansible-BytesSingle + - ansible-BytesMulti + - ansible-DateTimeSingle + - ansible-DateTimeMulti + - ansible-IntSingle + - ansible-IntMulti + - ansible-SDSingle + - ansible-SDMulti + - ansible-StringSingle + - ansible-StringMulti + register: set_attr_check_actual + +- name: assert set attribute - check + assert: + that: + - set_attr_check is changed + - set_attr_check_actual.objects[0]['ansible-BoolSingle'] == False + - set_attr_check_actual.objects[0]['ansible-BoolMulti'] == True + - set_attr_check_actual.objects[0]['ansible-BytesSingle'] == "Zm9v" + - set_attr_check_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in set_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'Zm9v' in set_attr_check_actual.objects[0]['ansible-BytesMulti']" + - set_attr_check_actual.objects[0]['ansible-DateTimeSingle'] == "1970-01-01T00:00:00.0000000Z" + - set_attr_check_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T01:00:00.0000000Z' in set_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'1969-12-31T23:00:00.0000000Z' in set_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - set_attr_check_actual.objects[0]['ansible-IntSingle'] == 0 + - set_attr_check_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in set_attr_check_actual.objects[0]['ansible-IntMulti'] + - 1 in set_attr_check_actual.objects[0]['ansible-IntMulti'] + - set_attr_check_actual.objects[0]['ansible-SDSingle'] == "O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;WD)" + - set_attr_check_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in set_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)' in set_attr_check_actual.objects[0]['ansible-SDMulti']" + - set_attr_check_actual.objects[0]['ansible-StringSingle'] == "single" + - set_attr_check_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in set_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'multi 2' in set_attr_check_actual.objects[0]['ansible-StringMulti']" + +- name: set attribute + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: true + ansible-BoolMulti: false + ansible-BytesSingle: + type: bytes + value: YmFy + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: YmFy + ansible-DateTimeSingle: + type: date_time + value: '1601-01-01T00:00:00Z' + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '1601-01-01T00:01:00.0-02:00' + ansible-IntSingle: 1 + ansible-IntMulti: + - 0 + - 3 + ansible-SDSingle: + type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU) + ansible-StringSingle: Single + ansible-StringMulti: + - multi 1 + - multi 3 + state: present + register: set_attr + +- name: get result of set attribute + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolSingle + - ansible-BoolMulti + - ansible-BytesSingle + - ansible-BytesMulti + - ansible-DateTimeSingle + - ansible-DateTimeMulti + - ansible-IntSingle + - ansible-IntMulti + - ansible-SDSingle + - ansible-SDMulti + - ansible-StringSingle + - ansible-StringMulti + register: set_attr_actual + +- name: assert set attribute + assert: + that: + - set_attr is changed + - set_attr_actual.objects[0]['ansible-BoolSingle'] == True + - set_attr_actual.objects[0]['ansible-BoolMulti'] == False + - set_attr_actual.objects[0]['ansible-BytesSingle'] == "YmFy" + - set_attr_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in set_attr_actual.objects[0]['ansible-BytesMulti']" + - "'dGVzdA==' in set_attr_actual.objects[0]['ansible-BytesMulti']" + - set_attr_actual.objects[0]['ansible-DateTimeSingle'] == "1601-01-01T00:00:00.0000000Z" + - set_attr_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T02:01:00.0000000Z' in set_attr_actual.objects[0]['ansible-DateTimeMulti']" + - "'1970-01-01T00:00:00.0000000Z' in set_attr_actual.objects[0]['ansible-DateTimeMulti']" + - set_attr_actual.objects[0]['ansible-IntSingle'] == 1 + - set_attr_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in set_attr_actual.objects[0]['ansible-IntMulti'] + - 3 in set_attr_actual.objects[0]['ansible-IntMulti'] + - set_attr_actual.objects[0]['ansible-SDSingle'] == "O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)" + - set_attr_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in set_attr_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC)' in set_attr_actual.objects[0]['ansible-SDMulti']" + - set_attr_actual.objects[0]['ansible-StringSingle'] == "Single" + - set_attr_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in set_attr_actual.objects[0]['ansible-StringMulti']" + - "'multi 3' in set_attr_actual.objects[0]['ansible-StringMulti']" + +- name: set attribute - idempotent + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: true + ansible-BoolMulti: false + ansible-BytesSingle: + type: bytes + value: YmFy + ansible-BytesMulti: + - type: bytes + value: dGVzdA== + - type: bytes + value: YmFy + ansible-DateTimeSingle: + type: date_time + value: '1601-01-01T00:00:00Z' + ansible-DateTimeMulti: + - type: date_time + value: '1970-01-01T00:00:00' + - type: date_time + value: '1601-01-01T00:01:00.0-02:00' + ansible-IntSingle: 1 + ansible-IntMulti: + - 0 + - 3 + ansible-SDSingle: + type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN) + ansible-SDMulti: + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC) + - type: security_descriptor + value: O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU) + ansible-StringSingle: Single + ansible-StringMulti: + - multi 1 + - multi 3 + state: present + register: set_attr_again + +- name: assert set attribute - idempotent + assert: + that: + - not set_attr_again is changed + +- name: clear attribute - check + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: + ansible-BoolMulti: + ansible-BytesSingle: [] + ansible-BytesMulti: [] + ansible-DateTimeSingle: ~ + ansible-DateTimeMulti: ~ + ansible-IntSingle: + ansible-IntMulti: + ansible-SDSingle: + ansible-SDMulti: + ansible-StringSingle: + ansible-StringMulti: + state: present + register: clear_attr_check + check_mode: true + +- name: get result of clear attribute - check + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolSingle + - ansible-BoolMulti + - ansible-BytesSingle + - ansible-BytesMulti + - ansible-DateTimeSingle + - ansible-DateTimeMulti + - ansible-IntSingle + - ansible-IntMulti + - ansible-SDSingle + - ansible-SDMulti + - ansible-StringSingle + - ansible-StringMulti + register: clear_attr_check_actual + +- name: assert clear attribute - check + assert: + that: + - clear_attr_check is changed + - clear_attr_check_actual.objects[0]['ansible-BoolSingle'] == True + - clear_attr_check_actual.objects[0]['ansible-BoolMulti'] == False + - clear_attr_check_actual.objects[0]['ansible-BytesSingle'] == "YmFy" + - clear_attr_check_actual.objects[0]['ansible-BytesMulti'] | length == 2 + - "'YmFy' in clear_attr_check_actual.objects[0]['ansible-BytesMulti']" + - "'dGVzdA==' in clear_attr_check_actual.objects[0]['ansible-BytesMulti']" + - clear_attr_check_actual.objects[0]['ansible-DateTimeSingle'] == "1601-01-01T00:00:00.0000000Z" + - clear_attr_check_actual.objects[0]['ansible-DateTimeMulti'] | length == 2 + - "'1601-01-01T02:01:00.0000000Z' in clear_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - "'1970-01-01T00:00:00.0000000Z' in clear_attr_check_actual.objects[0]['ansible-DateTimeMulti']" + - clear_attr_check_actual.objects[0]['ansible-IntSingle'] == 1 + - clear_attr_check_actual.objects[0]['ansible-IntMulti'] | length == 2 + - 0 in clear_attr_check_actual.objects[0]['ansible-IntMulti'] + - 3 in clear_attr_check_actual.objects[0]['ansible-IntMulti'] + - clear_attr_check_actual.objects[0]['ansible-SDSingle'] == "O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AN)" + - clear_attr_check_actual.objects[0]['ansible-SDMulti'] | length == 2 + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)' in clear_attr_check_actual.objects[0]['ansible-SDMulti']" + - "'O:DAG:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DC)' in clear_attr_check_actual.objects[0]['ansible-SDMulti']" + - clear_attr_check_actual.objects[0]['ansible-StringSingle'] == "Single" + - clear_attr_check_actual.objects[0]['ansible-StringMulti'] | length == 2 + - "'multi 1' in clear_attr_check_actual.objects[0]['ansible-StringMulti']" + - "'multi 3' in clear_attr_check_actual.objects[0]['ansible-StringMulti']" + +- name: clear attribute + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: + ansible-BoolMulti: + ansible-BytesSingle: [] + ansible-BytesMulti: [] + ansible-DateTimeSingle: ~ + ansible-DateTimeMulti: ~ + ansible-IntSingle: + ansible-IntMulti: + ansible-SDSingle: + ansible-SDMulti: + ansible-StringSingle: + ansible-StringMulti: + state: present + register: clear_attr + +- name: get result of clear attribute + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - ansible-BoolSingle + - ansible-BoolMulti + - ansible-BytesSingle + - ansible-BytesMulti + - ansible-DateTimeSingle + - ansible-DateTimeMulti + - ansible-IntSingle + - ansible-IntMulti + - ansible-SDSingle + - ansible-SDMulti + - ansible-StringSingle + - ansible-StringMulti + register: clear_attr_actual + +- name: assert clear attribute + assert: + that: + - clear_attr is changed + - clear_attr_actual.objects[0]['ansible-BoolSingle'] == None + - clear_attr_actual.objects[0]['ansible-BoolMulti'] == None + - clear_attr_actual.objects[0]['ansible-BytesSingle'] == None + - clear_attr_actual.objects[0]['ansible-BytesMulti'] == None + - clear_attr_actual.objects[0]['ansible-DateTimeSingle'] == None + - clear_attr_actual.objects[0]['ansible-DateTimeMulti'] == None + - clear_attr_actual.objects[0]['ansible-IntSingle'] == None + - clear_attr_actual.objects[0]['ansible-IntMulti'] == None + - clear_attr_actual.objects[0]['ansible-SDSingle'] == None + - clear_attr_actual.objects[0]['ansible-SDMulti'] == None + - clear_attr_actual.objects[0]['ansible-StringSingle'] == None + - clear_attr_actual.objects[0]['ansible-StringMulti'] == None + +- name: clear attribute - idempotent + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + attributes: + set: + ansible-BoolSingle: + ansible-BoolMulti: + ansible-BytesSingle: [] + ansible-BytesMulti: [] + ansible-DateTimeSingle: ~ + ansible-DateTimeMulti: ~ + ansible-IntSingle: + ansible-IntMulti: + ansible-SDSingle: + ansible-SDMulti: + ansible-StringSingle: + ansible-StringMulti: + state: present + register: clear_attr_again + +- name: assert clear attribute - idempotent + assert: + that: + - not clear_attr_again is changed + +- name: unset display and description + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + display_name: '' + description: '' + register: unset_normal + +- name: get result of unset display and description + object_info: + identity: '{{ sub_object.object_guid }}' + properties: + - DisplayName + - Description + register: unset_normal_actual + +- name: assert unset display and description + assert: + that: + - unset_normal is changed + - unset_normal_actual.objects[0].DisplayName == None + - unset_normal_actual.objects[0].Description == None + +- name: unset display and description - idempotent + object: + name: Contact + path: CN=My\, Container,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + type: contact + display_name: '' + description: '' + register: unset_normal_again + +- name: assert unset display and description - idempotent + assert: + that: + - not unset_normal_again is changed diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object_info/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object_info/handlers/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/handlers/main.yml new file mode 100644 index 000000000..fa30dcbb3 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/handlers/main.yml @@ -0,0 +1,4 @@ +- name: remove test domain user + microsoft.ad.user: + name: '{{ test_user.distinguished_name }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object_info/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/object_info/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/tasks/main.yml new file mode 100644 index 000000000..01cee436b --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/object_info/tasks/main.yml @@ -0,0 +1,186 @@ +- name: create test ad user + microsoft.ad.user: + name: Ansible Test + firstname: Ansible + surname: Test + company: Contoso R Us + password: Password01 + state: present + password_never_expires: yes + groups: + set: + - Domain Users + enabled: false + register: test_user + notify: remove test domain user + +- name: set a binary attribute and return other useful info missing from above + ansible.windows.win_powershell: + parameters: + SecurityId: '{{ test_user.sid }}' + script: | + param($SecurityId) + + Set-ADUser -Identity $SecurityId -Replace @{ audio = @([byte[]]@(1, 2, 3, 4), [byte[]]@(5, 6, 7, 8)) } + + $user = Get-ADUser -Identity $SecurityId -Properties modifyTimestamp, ObjectGUID + + [TimeZoneInfo]::ConvertTimeToUtc($user.modifyTimestamp).ToString('o') + $user.ObjectGUID.ToString() + ([System.Security.Principal.SecurityIdentifier]$SecurityId).Translate([System.Security.Principal.NTAccount]).Value + register: test_user_extras + +- name: set other test info for easier access + set_fact: + test_user_mod_date: '{{ test_user_extras.output[0] }}' + test_user_id: '{{ test_user_extras.output[1] }}' + test_user_name: '{{ test_user_extras.output[2] }}' + +- name: get properties for single user by DN + object_info: + identity: '{{ test_user.distinguished_name }}' + register: by_identity + check_mode: yes # Just verifies it runs in check mode + +- name: assert get properties for single user by DN + assert: + that: + - not by_identity is changed + - by_identity.objects | length == 1 + - by_identity.objects[0].keys() | list | length == 4 + - by_identity.objects[0].DistinguishedName == test_user.distinguished_name + - by_identity.objects[0].Name == 'Ansible Test' + - by_identity.objects[0].ObjectClass == 'user' + - by_identity.objects[0].ObjectGUID == test_user_id + +- name: get specific properties by GUID + object_info: + identity: '{{ test_user_id }}' + properties: + - audio # byte[] + - company # string + - department # not set + - logonCount # int + - modifyTimestamp # DateTime + - nTSecurityDescriptor # SecurityDescriptor as SDDL + - objectSID # SID + - ProtectedFromAccidentalDeletion # bool + - sAMAccountType # Test out the enum string attribute that we add + - userAccountControl # Test ou the enum string attribute that we add + register: by_guid_custom_props + +- name: assert get specific properties by GUID + assert: + that: + - not by_guid_custom_props is changed + - by_guid_custom_props.objects | length == 1 + - by_guid_custom_props.objects[0].DistinguishedName == test_user.distinguished_name + - by_guid_custom_props.objects[0].Name == 'Ansible Test' + - by_guid_custom_props.objects[0].ObjectClass == 'user' + - by_guid_custom_props.objects[0].ObjectGUID == test_user_id + - not by_guid_custom_props.objects[0].ProtectedFromAccidentalDeletion + - by_guid_custom_props.objects[0].audio == ['BQYHCA==', 'AQIDBA=='] + - by_guid_custom_props.objects[0].company == 'Contoso R Us' + - by_guid_custom_props.objects[0].department == None + - by_guid_custom_props.objects[0].logonCount == 0 + - by_guid_custom_props.objects[0].modifyTimestamp == test_user_mod_date + - by_guid_custom_props.objects[0].nTSecurityDescriptor.startswith('O:') + - by_guid_custom_props.objects[0].objectSID.Name == test_user_name + - by_guid_custom_props.objects[0].objectSID.Sid == test_user.sid + - by_guid_custom_props.objects[0].sAMAccountType == 805306368 + - by_guid_custom_props.objects[0].sAMAccountType_AnsibleFlags == ['SAM_USER_OBJECT'] + - by_guid_custom_props.objects[0].userAccountControl == 66050 + - by_guid_custom_props.objects[0].userAccountControl_AnsibleFlags == ['ADS_UF_ACCOUNTDISABLE', 'ADS_UF_NORMAL_ACCOUNT', 'ADS_UF_DONT_EXPIRE_PASSWD'] + +- name: get the groupType attribute + object_info: + filter: sAMAccountName -eq 'Domain Admins' + properties: + - groupType + register: group_type_prop + +- name: assert get the groupType attribute + assert: + that: + - group_type_prop.objects[0].groupType_AnsibleFlags == ["GROUP_TYPE_ACCOUNT_GROUP", "GROUP_TYPE_SECURITY_ENABLED"] + +- name: create computer object for enc type test + computer: + name: MyComputer + state: present + kerberos_encryption_types: + set: + - des + - rc4 + - aes128 + - aes256 + register: comp_info + +- block: + - name: get the supported encryption type attribute + object_info: + identity: '{{ comp_info.object_guid }}' + properties: + - msDS-SupportedEncryptionTypes + register: enc_type_prop + + - name: assert get the supported encryption type attribute + assert: + that: + - enc_type_prop.objects[0]["msDS-SupportedEncryptionTypes"] == 31 + - enc_type_prop.objects[0]["msDS-SupportedEncryptionTypes_AnsibleFlags"] == ["DES_CBC_CRC", "DES_CBC_MD5", "RC4_HMAC", "AES128_CTS_HMAC_SHA1_96", "AES256_CTS_HMAC_SHA1_96"] + + always: + - name: remove computer object + computer: + name: MyComputer + identity: '{{ comp_info.object_guid }}' + state: absent + +- name: get invalid property + object_info: + filter: sAMAccountName -eq 'Ansible Test' + properties: + - FakeProperty + register: invalid_prop_warning + +- name: assert get invalid property + assert: + that: + - not invalid_prop_warning is changed + - invalid_prop_warning.objects | length == 0 + - invalid_prop_warning.warnings | length == 1 + - '"Failed to retrieve properties for AD object" not in invalid_prop_warning.warnings[0]' + +- name: get by ldap filter returning multiple + object_info: + ldap_filter: (&(objectClass=computer)(objectCategory=computer)) + properties: '*' + register: multiple_ldap + +- name: assert get by ldap filter returning multiple + assert: + that: + - not multiple_ldap is changed + - multiple_ldap.objects | length > 0 + +- name: get by filter returning multiple + object_info: + filter: objectClass -eq 'computer' -and objectCategory -eq 'computer' + properties: '*' + register: multiple_filter + +- name: assert get by filter returning multiple + assert: + that: + - not multiple_filter is changed + - multiple_filter.objects | length > 0 + +- name: fail trying to use variable in filter + object_info: + filter: sAMAccountName -eq $domainUsername + properties: + - sAMAccountName + register: fail_filter_var + failed_when: + - '"Variable: ''domainUsername'' found in expression: $domainUsername is not defined." not in fail_filter_var.msg' diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/main.yml new file mode 100644 index 000000000..87f814655 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/main.yml @@ -0,0 +1,14 @@ +- name: remove temp computer + computer: + name: My, Computer + state: absent + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp computer + computer: + name: My, Computer + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/tests.yml new file mode 100644 index 000000000..c16237d40 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/tests.yml @@ -0,0 +1,150 @@ +- name: create computer account + computer: + name: My, Computer + state: present + sam_account_name: MyComputer + register: comp_account + +- set_fact: + object_identity: '{{ comp_account.object_guid }}' + +- name: get initial password info + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: pwd_info + +- name: get blob - check + offline_join: + name: My, Computer + register: blob_check + check_mode: true + +- name: get result of get blob - check + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: blob_check_actual + +- name: assert get blob - check + assert: + that: + - blob_check is changed + - blob_check.blob == '' + - blob_check_actual.objects[0].pwdLastSet == pwd_info.objects[0].pwdLastSet + +- name: get blob + offline_join: + name: My, Computer + register: blob + +- name: get result of get blob + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: blob_actual + +- name: assert get blob + assert: + that: + - blob is changed + - blob.blob != None + - blob.blob != '' + - blob_actual.objects[0].pwdLastSet > pwd_info.objects[0].pwdLastSet + +- block: + - name: create blob in file + offline_join: + identity: '{{ object_identity }}' + blob_path: C:\Windows\TEMP\ansible-blob + provision_root_ca_certs: true + register: blob_path + + - name: get pwd result of create blob in file + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: blob_path_ad_actual + + - name: get path result of create blob in file + ansible.windows.win_stat: + path: C:\Windows\TEMP\ansible-blob + register: blob_path_file_actual + + - name: assert create blob in file + assert: + that: + - blob_path is changed + - blob_path.blob == None + - blob_path_ad_actual.objects[0].pwdLastSet > blob_actual.objects[0].pwdLastSet + - blob_path_file_actual.stat.exists + + - name: create blob in file - idempotent + offline_join: + identity: '{{ object_identity }}' + blob_path: C:\Windows\TEMP\ansible-blob + register: blob_path_again + + - name: get pwd result of create blob in file - idempotent + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: blob_path_ad_actual_again + + - name: get path result of create blob in file - idempotent + ansible.windows.win_stat: + path: C:\Windows\TEMP\ansible-blob + register: blob_path_file_actual_again + + - name: assert create blob in file - idempotent + assert: + that: + - not blob_path_again is changed + - blob_path_again.blob == None + - blob_path_ad_actual_again.objects[0].pwdLastSet == blob_path_ad_actual.objects[0].pwdLastSet + - blob_path_file_actual_again.stat.size == blob_path_file_actual.stat.size + - blob_path_file_actual_again.stat.lastwritetime == blob_path_file_actual.stat.lastwritetime + + always: + - name: remove temp blob file + ansible.windows.win_file: + path: C:\Windows\TEMP\ansible-blob + state: absent + +- name: move computer object + computer: + name: My, Computer + identity: '{{ object_identity }}' + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + +- name: fail to find computer outside default path + offline_join: + name: My, Computer + register: fail_find + failed_when: '("Failed to find domain computer account ''CN=My\, Computer,CN=Computers," ~ setup_domain_info.output[0].defaultNamingContext) not in fail_find.msg' + +- name: get blob of computer in different path + offline_join: + name: My, Computer + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + register: blob_ad_path + +- name: get result of get blob of computer in different pth + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: blob_ad_path_actual + +- name: assert get blob of computer in different path + assert: + that: + - blob_ad_path is changed + - blob_ad_path.blob != None + - blob_ad_path.blob != '' + - blob_ad_path_actual.objects[0].pwdLastSet > blob_path_ad_actual_again.objects[0].pwdLastSet diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/ou/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/ou/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/ou/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/ou/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/ou/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/ou/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/main.yml new file mode 100644 index 000000000..b024959e7 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/main.yml @@ -0,0 +1,14 @@ +- name: remove temp OU + ou: + name: MyOU + state: absent + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp OU + ou: + name: MyOU + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml new file mode 100644 index 000000000..49d06aefb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml @@ -0,0 +1,254 @@ +- name: create ou - check + ou: + name: MyOU + state: present + register: create_ou_check + check_mode: true + +- name: get result of create ou - check + object_info: + identity: '{{ create_ou_check.distinguished_name }}' + register: create_ou_check_actual + +- name: assert create ou - check + assert: + that: + - create_ou_check is changed + - create_ou_check.distinguished_name == 'OU=MyOU,' ~ setup_domain_info.output[0].defaultNamingContext + - create_ou_check.object_guid == '00000000-0000-0000-0000-000000000000' + - create_ou_check_actual.objects == [] + +- name: create ou + ou: + name: MyOU + state: present + register: create_ou + +- set_fact: + object_identity: '{{ create_ou.object_guid }}' + +- name: get result of create ou + object_info: + identity: '{{ object_identity }}' + properties: + - ProtectedFromAccidentalDeletion + register: create_ou_actual + +- name: assert create ou + assert: + that: + - create_ou is changed + - create_ou_actual.objects | length == 1 + - create_ou.distinguished_name == 'OU=MyOU,' ~ setup_domain_info.output[0].defaultNamingContext + - create_ou.distinguished_name == create_ou_actual.objects[0].DistinguishedName + - create_ou.object_guid == create_ou_actual.objects[0].ObjectGUID + - create_ou_actual.objects[0].Name == 'MyOU' + - create_ou_actual.objects[0].ObjectClass == 'organizationalUnit' + - create_ou_actual.objects[0].ProtectedFromAccidentalDeletion == true + +- name: remove ou - check + ou: + name: MyOU + state: absent + register: remove_ou_check + check_mode: true + +- name: get result of remove ou - check + object_info: + identity: '{{ object_identity }}' + register: remove_ou_check_actual + +- name: assert remove ou - check + assert: + that: + - remove_ou_check is changed + - remove_ou_check_actual.objects | length == 1 + +- name: remove ou + ou: + name: MyOU + state: absent + register: remove_ou + +- name: get result of remove ou + object_info: + identity: '{{ object_identity }}' + register: remove_ou_actual + +- name: assert remove ou + assert: + that: + - remove_ou is changed + - remove_ou_actual.objects == [] + +- name: remove ou - idempotent + ou: + name: MyOU + state: absent + register: remove_ou_again + +- name: assert remove ou - idempotent + assert: + that: + - not remove_ou_again is changed + +- name: create parent OU + ou: + name: MyOU + state: present + register: parent_ou + +- set_fact: + object_identity: '{{ parent_ou.object_guid }}' + +- name: create ou with custom options + ou: + name: SubOU + path: '{{ parent_ou.distinguished_name }}' + state: present + city: Brisbane + country: AU + description: Custom Description + display_name: OU Display Name + managed_by: Domain Admins + postal_code: 4000 + state_province: QLD + street: Main St + protect_from_deletion: false + attributes: + set: + postOfficeBox: My Box + register: create_ou_custom + +- name: get result of create ou with custom options + object_info: + identity: '{{ create_ou_custom.object_guid }}' + properties: + - c + - Description + - DisplayName + - l + - managedBy + - postalcode + - postOfficeBox + - st + - street + - ProtectedFromAccidentalDeletion + register: create_ou_custom_actual + +- name: assert create ou with custom options + assert: + that: + - create_ou_custom is changed + - create_ou_custom_actual.objects | length == 1 + - create_ou_custom.distinguished_name == create_ou_custom_actual.objects[0].DistinguishedName + - create_ou_custom.object_guid == create_ou_custom_actual.objects[0].ObjectGUID + - create_ou_custom_actual.objects[0].l == 'Brisbane' + - create_ou_custom_actual.objects[0].c == 'AU' + - create_ou_custom_actual.objects[0].Description == 'Custom Description' + - create_ou_custom_actual.objects[0].DisplayName == 'OU Display Name' + - create_ou_custom_actual.objects[0].managedBy == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_ou_custom_actual.objects[0].postalcode == '4000' + - create_ou_custom_actual.objects[0].st == 'QLD' + - create_ou_custom_actual.objects[0].street == 'Main St' + - create_ou_custom_actual.objects[0].ProtectedFromAccidentalDeletion == False + - create_ou_custom_actual.objects[0].postOfficeBox == 'My Box' + +- name: change ou with custom options + ou: + name: SubOU + path: '{{ parent_ou.distinguished_name }}' + state: present + city: New York + country: US + description: Custom description + display_name: OU display Name + managed_by: Domain Users + postal_code: 10001 + state_province: '' + street: Main + attributes: + set: + postOfficeBox: My box + register: change_ou + +- name: get result of change ou with custom options + object_info: + identity: '{{ create_ou_custom.object_guid }}' + properties: + - c + - Description + - DisplayName + - l + - managedBy + - postalcode + - postOfficeBox + - st + - street + - ProtectedFromAccidentalDeletion + register: change_ou_actual + +- name: assert change ou with custom options + assert: + that: + - change_ou is changed + - change_ou.distinguished_name == create_ou_custom.distinguished_name + - change_ou.object_guid == create_ou_custom.object_guid + - change_ou_actual.objects[0].l == 'New York' + - change_ou_actual.objects[0].c == 'US' + - change_ou_actual.objects[0].Description == 'Custom description' + - change_ou_actual.objects[0].DisplayName == 'OU display Name' + - change_ou_actual.objects[0].managedBy == 'CN=Domain Users,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - change_ou_actual.objects[0].postalcode == '10001' + - change_ou_actual.objects[0].st == None + - change_ou_actual.objects[0].street == 'Main' + - change_ou_actual.objects[0].ProtectedFromAccidentalDeletion == False + - change_ou_actual.objects[0].postOfficeBox == 'My box' + +- name: create new sub OU + ou: + name: NewSubOU + path: '{{ parent_ou.distinguished_name }}' + state: present + register: new_parent_ou + +- name: move and rename ou - check + ou: + name: InnerOU + path: '{{ new_parent_ou.distinguished_name }}' + identity: '{{ create_ou_custom.object_guid }}' + register: move_rename_check + check_mode: true + +- name: get result of move and rename ou - check + object_info: + identity: '{{ create_ou_custom.object_guid }}' + register: move_rename_check_actual + +- name: assert move and rename ou - check + assert: + that: + - move_rename_check is changed + - move_rename_check.distinguished_name == 'OU=InnerOU,' ~ new_parent_ou.distinguished_name + - move_rename_check_actual.objects[0].Name == 'SubOU' + - move_rename_check_actual.objects[0].DistinguishedName == create_ou_custom_actual.objects[0].DistinguishedName + +- name: move and rename ou + ou: + name: InnerOU + path: '{{ new_parent_ou.distinguished_name }}' + identity: '{{ create_ou_custom.object_guid }}' + register: move_rename + +- name: get result of move and rename ou + object_info: + identity: '{{ create_ou_custom.object_guid }}' + register: move_rename_actual + +- name: assert move and rename ou + assert: + that: + - move_rename is changed + - move_rename.distinguished_name == 'OU=InnerOU,' ~ new_parent_ou.distinguished_name + - move_rename_actual.objects[0].Name == 'InnerOU' + - move_rename_actual.objects[0].DistinguishedName == 'OU=InnerOU,' ~ new_parent_ou.distinguished_name diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/defaults/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/defaults/main.yml new file mode 100644 index 000000000..5f70cc3f3 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/defaults/main.yml @@ -0,0 +1,2 @@ +domain_realm: ansible.test +domain_password: Password123! diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/main.yml new file mode 100644 index 000000000..3376208c2 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/main.yml @@ -0,0 +1,55 @@ +- name: ensure the ActiveDirectory module is installed + ansible.windows.win_feature: + name: + - RSAT-AD-PowerShell + state: present + +- name: check if domain is already set up + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + check_mode: true + register: domain_res + +# If the domain is not already set up and this is running under the domain +# test target then run the tests for that target, otherwise do the bare +# minimum setup. This allows us to do more complex testing for that module +# if it is the only one to run. +- include_tasks: '{{ (run_domain_test | default(False)) | ternary(role_path ~ "/../domain/tasks/test.yml", "setup.yml") }}' + when: domain_res is changed + +# While usually the reboot waits until it is fully done before continuing I've seen Server 2019 in CI still waiting +# for things to initialise. By tested if ADWS is available with a simple check we can ensure the host is at least +# ready to test AD. Typically I've found it takes about 60 retries so doubling it should cover even an absolute worst# case. +- name: post domain setup test for ADWS to come online + ansible.windows.win_powershell: + parameters: + Delay: 5 + Retries: 120 + script: | + [CmdletBinding()] + param ( + [int]$Delay, + [int]$Retries + ) + $Ansible.Changed = $false + $attempts = 0 + $err = $null + while ($true) { + $attempts++ + try { + Get-ADRootDSE -ErrorAction Stop + break + } + catch { + if ($attempts -eq $Retries) { + throw + } + Start-Sleep -Seconds $Delay + } + } + $attempts + register: setup_domain_info + become: yes + become_method: runas + become_user: SYSTEM diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/setup.yml b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/setup.yml new file mode 100644 index 000000000..8c7cce1f6 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/setup.yml @@ -0,0 +1,5 @@ +- name: ensure domain is present + domain: + dns_domain_name: '{{ domain_realm }}' + safe_mode_password: '{{ domain_password }}' + reboot: true diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/user/aliases new file mode 100644 index 000000000..ccd8a25e8 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/aliases @@ -0,0 +1,2 @@ +windows +shippable/windows/group1 diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/meta/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/user/meta/main.yml new file mode 100644 index 000000000..4ce45dcfb --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_domain diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/main.yml new file mode 100644 index 000000000..6dc722602 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/main.yml @@ -0,0 +1,14 @@ +- name: remove temp user + user: + name: MyUser + state: absent + +- block: + - import_tasks: tests.yml + + always: + - name: remove temp user + user: + name: MyUser + identity: '{{ object_identity | default(omit) }}' + state: absent diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml new file mode 100644 index 000000000..e06c54959 --- /dev/null +++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml @@ -0,0 +1,1065 @@ +- name: create user with defaults - check + user: + name: MyUser + state: present + register: default_user_check + check_mode: true + +- name: get result of create user with defaults - check + object_info: + ldap_filter: (sAMAccountName=MyUser) + register: default_user_check_actual + +- name: assert create user with defaults - check + assert: + that: + - default_user_check is changed + - default_user_check_actual.objects == [] + - default_user_check.distinguished_name == 'CN=MyUser,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - default_user_check.object_guid == '00000000-0000-0000-0000-000000000000' + - default_user_check.sid == 'S-1-5-0000' + +- name: create user with defaults + user: + name: MyUser + state: present + register: default_user + +- name: get result of create user with defaults + object_info: + ldap_filter: (sAMAccountName=MyUser) + properties: + - objectSid + - sAMAccountName + - userAccountControl + - userPrincipalName + register: default_user_actual + +- name: assert create user with defaults - check + assert: + that: + - default_user is changed + - default_user_actual.objects | length == 1 + - default_user.distinguished_name == 'CN=MyUser,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - default_user.object_guid == default_user_actual.objects[0].ObjectGUID + - default_user.sid == default_user_actual.objects[0].objectSid.Sid + - default_user_actual.objects[0].sAMAccountName == 'MyUser' + - default_user_actual.objects[0].userPrincipalName == None + - '"ADS_UF_ACCOUNTDISABLE" in default_user_actual.objects[0].userAccountControl_AnsibleFlags' + +- set_fact: + object_identity: '{{ default_user.object_guid }}' + object_sid: '{{ default_user.sid }}' + +- name: create user with defaults - idempotent + user: + name: MyUser + state: present + register: default_user_again + +- name: assert create user with defaults - idempotent + assert: + that: + - not default_user_again is changed + +- name: rename user - check + user: + name: MyUser2 + identity: '{{ object_identity }}' + register: rename_user_check + check_mode: true + +- name: get result of rename user - check + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: rename_user_check_actual + +- name: assert rename user - check + assert: + that: + - rename_user_check is changed + - rename_user_check.distinguished_name == 'CN=MyUser2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_user_check_actual.objects[0].DistinguishedName == default_user.distinguished_name + - rename_user_check_actual.objects[0].Name == 'MyUser' + - rename_user_check_actual.objects[0].sAMAccountName == 'MyUser' + +- name: rename user + user: + name: MyUser2 + identity: '{{ object_identity }}' + register: rename_user + +- name: get result of rename user + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: rename_user_actual + +- name: assert rename user + assert: + that: + - rename_user is changed + - rename_user.distinguished_name == 'CN=MyUser2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_user_actual.objects[0].DistinguishedName == 'CN=MyUser2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - rename_user_actual.objects[0].Name == 'MyUser2' + - rename_user_actual.objects[0].sAMAccountName == 'MyUser' + +- name: rename user - idempotent + user: + name: MyUser2 + identity: '{{ object_identity }}' + register: rename_user_again + +- name: assert rename user - idempotent + assert: + that: + - not rename_user_again is changed + +- name: move user - check + user: + name: MyUser2 + identity: '{{ object_sid }}' # ID by SID + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + register: move_user_check + check_mode: true + +- name: get result of move user - check + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: move_user_check_actual + +- name: assert move user - check + assert: + that: + - move_user_check is changed + - move_user_check.distinguished_name == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - move_user_check_actual.objects[0].DistinguishedName == 'CN=MyUser2,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - move_user_check_actual.objects[0].Name == 'MyUser2' + - move_user_check_actual.objects[0].sAMAccountName == 'MyUser' + +- name: move user + user: + name: MyUser2 + identity: '{{ object_sid }}' # ID by SID + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + register: move_user + +- name: get result of move user + object_info: + identity: '{{ object_identity }}' + properties: + - sAMAccountName + register: move_user_actual + +- name: assert move user + assert: + that: + - move_user is changed + - move_user.distinguished_name == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - move_user_actual.objects[0].DistinguishedName == 'CN=MyUser2,' ~ setup_domain_info.output[0].defaultNamingContext + - move_user_actual.objects[0].Name == 'MyUser2' + - move_user_actual.objects[0].sAMAccountName == 'MyUser' + +- name: move user - idempotent + user: + name: MyUser2 + identity: '{{ object_sid }}' # ID by SID + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + register: move_user_again + +- name: assert move user - idempotent + assert: + that: + - not move_user_again is changed + +- name: move user back + user: + name: MyUser + identity: MyUser # By sAMAccountName + path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + +- name: update password from blank - skip for on_create + user: + name: MyUser + password: Password123! + update_password: on_create + register: change_pass_on_create + +- name: assert update password from blank - skip for on_create + assert: + that: + - not change_pass_on_create is changed + +- name: update password - check + user: + name: MyUser + password: Password123! + update_password: when_changed + enabled: true + register: change_pass_check + check_mode: true + +- name: get result of update password - check + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + - userAccountControl + register: change_pass_check_actual + +- name: assert update password - check + assert: + that: + - change_pass_check is changed + - change_pass_check_actual.objects[0].pwdLastSet == 0 + - '"ADS_UF_ACCOUNTDISABLE" in change_pass_check_actual.objects[0].userAccountControl_AnsibleFlags' + +- name: update password - check + user: + name: MyUser + password: Password123! + update_password: when_changed + enabled: true + register: change_pass + +- name: get result of update password + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + - userAccountControl + register: change_pass_actual + +- name: assert update password + assert: + that: + - change_pass is changed + - change_pass_actual.objects[0].pwdLastSet != 0 + - '"ADS_UF_ACCOUNTDISABLE" not in change_pass_actual.objects[0].userAccountControl_AnsibleFlags' + +- name: update password - idempotent + user: + name: MyUser + password: Password123! + update_password: when_changed + enabled: true + register: change_pass_again + +- name: assert update password - idempotent + assert: + that: + - not change_pass_again is changed + +- name: force update password + user: + name: MyUser + password: Password123! + register: always_update_password + +- name: get result of force update password + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: always_update_password_actual + +- name: assert force update password + assert: + that: + - always_update_password is changed + - always_update_password_actual.objects[0].pwdLastSet > change_pass_actual.objects[0].pwdLastSet + +- name: remove user - check + user: + name: MyUser + state: absent + register: remove_user_check + check_mode: true + +- name: get result of remove user - check + object_info: + identity: '{{ object_identity }}' + register: remove_user_check_actual + +- name: assert remove user - check + assert: + that: + - remove_user_check is changed + - remove_user_check_actual.objects | length == 1 + +- name: remove user + user: + name: MyUser + state: absent + register: remove_user + +- name: get result of remove user + object_info: + identity: '{{ object_identity }}' + register: remove_user_actual + +- name: assert remove user + assert: + that: + - remove_user is changed + - remove_user_actual.objects == [] + +- name: remove user - idempotent + user: + name: MyUser + state: absent + register: remove_user_again + +- name: assert remove user - idempotent + assert: + that: + - not remove_user_again is changed + +# https://github.com/ansible-collections/microsoft.ad/issues/25 +- name: create user with expired password + user: + name: MyUser + password: Password123! + password_expired: true + state: present + register: create_user_pass_expired + +- set_fact: + object_identity: '{{ create_user_pass_expired.object_guid }}' + object_sid: '{{ create_user_pass_expired.sid }}' + +- name: get result of create user with expired password + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: create_user_pass_expired_actual + +- name: assert create user with expired password + assert: + that: + - create_user_pass_expired_actual.objects[0].pwdLastSet == 0 + +- name: remove expired password flag on existing user + user: + name: MyUser + password_expired: false + state: present + register: remove_password_expiry + +- name: get result of remove expired password flag on existing user + object_info: + identity: '{{ object_identity }}' + properties: + - pwdLastSet + register: remove_password_expiry_actual + +- name: assert remove expired password flag on existing user + assert: + that: + - remove_password_expiry_actual.objects[0].pwdLastSet > 0 + +- name: remove user + user: + name: MyUser + state: absent + +- name: create user with extra info - check + user: + name: MyUser + state: present + city: Brisbane + company: Red Hat + country: au + delegates: + set: + - CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: User Description + display_name: User Name + email: user@EMAIL.COM + firstname: FirstName + groups: + set: + - Domain Admins + - Domain Users + password: Password123! + password_never_expires: true + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + postal_code: 4000 + sam_account_name: MyUserSam + spn: + set: + - HTTP/MyUser + state_province: QLD + street: Main + surname: LastName + upn: User@{{ domain_realm }} + user_cannot_change_password: true + attributes: + set: + comment: My comment + register: create_user_check + check_mode: true + +- name: get result of create user with extra info - check + object_info: + identity: '{{ create_user_check.distinguished_name }}' + register: create_user_actual_check + +- name: assert create user with extra info - check + assert: + that: + - create_user_check is changed + - create_user_actual_check.objects == [] + +- name: create user with extra info + user: + name: MyUser + state: present + city: Brisbane + company: Red Hat + country: au + delegates: + set: + - CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: User Description + display_name: User Name + email: user@EMAIL.COM + firstname: FirstName + groups: + set: + - Domain Admins + - Domain Users + password: Password123! + password_never_expires: true + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + postal_code: 4000 + sam_account_name: MyUserSam + spn: + set: + - HTTP/MyUser + state_province: QLD + street: Main + surname: LastName + upn: User@{{ domain_realm }} + user_cannot_change_password: true + attributes: + set: + comment: My comment + register: create_user + +- set_fact: + object_identity: '{{ create_user.object_guid }}' + object_sid: '{{ create_user.sid }}' + +- name: get result of create user with extra info + object_info: + identity: '{{ object_identity }}' + properties: + - c + - comment + - company + - Description + - displayName + - givenName + - l + - mail + - memberOf + - msDS-AllowedToActOnBehalfOfOtherIdentity + - objectSid + - postalcode + - primaryGroupID + - pwdLastSet + - sAMAccountName + - servicePrincipalName + - sn + - st + - streetaddress + - userAccountControl + - userPrincipalName + register: create_user_actual + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ create_user_actual.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: create_user_delegates + +- name: assert create user with extra info + assert: + that: + - create_user is changed + - create_user_actual.objects | length == 1 + - create_user.distinguished_name == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user.object_guid == create_user_actual.objects[0].ObjectGUID + - create_user.sid == create_user_actual.objects[0].objectSid.Sid + - create_user_actual.objects[0].Description == 'User Description' + - create_user_actual.objects[0].DisplayName == 'User Name' + - create_user_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user_actual.objects[0].c == 'au' + - create_user_actual.objects[0].comment == 'My comment' + - create_user_actual.objects[0].company == 'Red Hat' + - create_user_actual.objects[0].givenName == 'FirstName' + - create_user_actual.objects[0].l == 'Brisbane' + - create_user_actual.objects[0].mail == 'user@EMAIL.COM' + # Domain Users is the primaryGroupID entry + - create_user_actual.objects[0].memberOf == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - create_user_actual.objects[0].postalcode == '4000' + - create_user_actual.objects[0].primaryGroupID == 513 # Domain Users + - create_user_actual.objects[0].pwdLastSet > 0 + - create_user_actual.objects[0].sAMAccountName == 'MyUserSam' + - create_user_actual.objects[0].servicePrincipalName == 'HTTP/MyUser' + - create_user_actual.objects[0].sn == 'LastName' + - create_user_actual.objects[0].st == 'QLD' + - create_user_actual.objects[0].streetaddress == 'Main' + - create_user_actual.objects[0].userPrincipalName == 'User@' ~ domain_realm + - create_user_actual.objects[0].userAccountControl_AnsibleFlags == ["ADS_UF_NORMAL_ACCOUNT", "ADS_UF_DONT_EXPIRE_PASSWD"] + - create_user_delegates.output == ["administrator", "krbtgt"] + +- name: create user with extra info - idempotent + user: + name: MyUser + state: present + city: Brisbane + company: Red Hat + country: au + delegates: + set: + - CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: User Description + display_name: User Name + email: user@EMAIL.COM + firstname: FirstName + groups: + set: + - Domain Admins + - Domain Users + password: Password123! + password_never_expires: true + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + postal_code: 4000 + sam_account_name: MyUserSam + spn: + set: + - HTTP/MyUser + state_province: QLD + street: Main + surname: LastName + upn: User@{{ domain_realm }} + update_password: when_changed + user_cannot_change_password: true + attributes: + set: + comment: My comment + register: create_user_again + +- name: assert create user with extra info - idempotent + assert: + that: + - not create_user_again is changed + +- name: update user settings - check + user: + name: MyUser + state: present + city: New York + company: Ansible + country: us + delegates: + set: + - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: User description + display_name: User name + email: User@EMAIL.COM + firstname: firstName + groups: + set: + - Domain Users + password: Password123! + password_never_expires: false + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + postal_code: 10001 + sam_account_name: myUserSam + spn: + set: + - HTTP/myUser + state_province: NY + street: main + surname: lastName + upn: user@{{ domain_realm }} + update_password: when_changed + user_cannot_change_password: false + attributes: + set: + comment: My Comment + register: update_user_check + check_mode: true + +- name: get result of update user settings - check + object_info: + identity: '{{ object_identity }}' + properties: + - c + - comment + - company + - Description + - displayName + - givenName + - l + - mail + - memberOf + - msDS-AllowedToActOnBehalfOfOtherIdentity + - objectSid + - postalcode + - primaryGroupID + - pwdLastSet + - sAMAccountName + - servicePrincipalName + - sn + - st + - streetaddress + - userAccountControl + - userPrincipalName + register: update_user_check_actual + +- name: assert update user settings - check + assert: + that: + - update_user_check is changed + - update_user_check.distinguished_name == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_check.object_guid == create_user_actual.objects[0].ObjectGUID + - update_user_check.sid == create_user_actual.objects[0].objectSid.Sid + - update_user_check_actual.objects[0].Description == 'User Description' + - update_user_check_actual.objects[0].DisplayName == 'User Name' + - update_user_check_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_check_actual.objects[0].c == 'au' + - update_user_check_actual.objects[0].comment == 'My comment' + - update_user_check_actual.objects[0].company == 'Red Hat' + - update_user_check_actual.objects[0].givenName == 'FirstName' + - update_user_check_actual.objects[0].l == 'Brisbane' + - update_user_check_actual.objects[0].mail == 'user@EMAIL.COM' + # Domain Users is the primaryGroupID entry + - update_user_check_actual.objects[0].memberOf == 'CN=Domain Admins,CN=Users,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_check_actual.objects[0].postalcode == '4000' + - update_user_check_actual.objects[0].primaryGroupID == 513 # Domain Users + - update_user_check_actual.objects[0].pwdLastSet > 0 + - update_user_check_actual.objects[0].sAMAccountName == 'MyUserSam' + - update_user_check_actual.objects[0].servicePrincipalName == 'HTTP/MyUser' + - update_user_check_actual.objects[0].sn == 'LastName' + - update_user_check_actual.objects[0].st == 'QLD' + - update_user_check_actual.objects[0].streetaddress == 'Main' + - update_user_check_actual.objects[0].userPrincipalName == 'User@' ~ domain_realm + - update_user_check_actual.objects[0].userAccountControl_AnsibleFlags == ["ADS_UF_NORMAL_ACCOUNT", "ADS_UF_DONT_EXPIRE_PASSWD"] + - update_user_check_actual.objects[0]['msDS-AllowedToActOnBehalfOfOtherIdentity'] == create_user_actual.objects[0]['msDS-AllowedToActOnBehalfOfOtherIdentity'] + +- name: update user settings + user: + name: MyUser + state: present + city: New York + company: Ansible + country: us + delegates: + set: + - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + description: User description + display_name: User name + email: User@EMAIL.COM + firstname: firstName + groups: + set: + - Domain Users + password: Password123! + password_never_expires: false + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + postal_code: 10001 + sam_account_name: myUserSam + spn: + set: + - HTTP/myUser + state_province: NY + street: main + surname: lastName + upn: user@{{ domain_realm }} + update_password: when_changed + user_cannot_change_password: false + attributes: + set: + comment: My Comment + register: update_user + +- name: get result of update user settings + object_info: + identity: '{{ object_identity }}' + properties: + - c + - comment + - company + - Description + - displayName + - givenName + - l + - mail + - memberOf + - msDS-AllowedToActOnBehalfOfOtherIdentity + - objectSid + - postalcode + - primaryGroupID + - pwdLastSet + - sAMAccountName + - servicePrincipalName + - sn + - st + - streetaddress + - userAccountControl + - userPrincipalName + register: update_user_actual + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ update_user_actual.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: update_user_delegates + +- name: assert update user settings + assert: + that: + - update_user is changed + - update_user.distinguished_name == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user.object_guid == create_user_actual.objects[0].ObjectGUID + - update_user.sid == create_user_actual.objects[0].objectSid.Sid + - update_user_actual.objects[0].Description == 'User description' + - update_user_actual.objects[0].DisplayName == 'User name' + - update_user_actual.objects[0].DistinguishedName == 'CN=MyUser,' ~ setup_domain_info.output[0].defaultNamingContext + - update_user_actual.objects[0].c == 'us' + - update_user_actual.objects[0].comment == 'My Comment' + - update_user_actual.objects[0].company == 'Ansible' + - update_user_actual.objects[0].givenName == 'firstName' + - update_user_actual.objects[0].l == 'New York' + - update_user_actual.objects[0].mail == 'User@EMAIL.COM' + # Domain Users is the primaryGroupID entry + - update_user_actual.objects[0].memberOf == None + - update_user_actual.objects[0].postalcode == '10001' + - update_user_actual.objects[0].primaryGroupID == 513 # Domain Users + - update_user_actual.objects[0].pwdLastSet == create_user_actual.objects[0].pwdLastSet + - update_user_actual.objects[0].sAMAccountName == 'myUserSam' + - update_user_actual.objects[0].servicePrincipalName == 'HTTP/MyUser' + - update_user_actual.objects[0].sn == 'lastName' + - update_user_actual.objects[0].st == 'NY' + - update_user_actual.objects[0].streetaddress == 'main' + - update_user_actual.objects[0].userPrincipalName == 'user@' ~ domain_realm + - update_user_actual.objects[0].userAccountControl_AnsibleFlags == ["ADS_UF_NORMAL_ACCOUNT"] + - update_user_delegates.output == ["krbtgt"] + +- name: update delegates case insensitive + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + delegates: + set: + - CN=KrbTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + register: update_delegates_insensitive + +- name: assert update delegates case insensitive + assert: + that: + - not update_delegates_insensitive is changed + +- name: update delegates + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + delegates: + set: + - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + - CN=Enterprise Admins,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }} + register: update_delegates + +- name: get result of update delegates + object_info: + identity: '{{ object_identity }}' + properties: + - msDS-AllowedToActOnBehalfOfOtherIdentity + register: update_delegates_actual_raw + +- name: convert delegate SDDL to human readable string + ansible.windows.win_powershell: + parameters: + SDDL: '{{ update_delegates_actual_raw.objects[0]["msDS-AllowedToActOnBehalfOfOtherIdentity"] }}' + script: | + param($SDDL) + + $sd = New-Object -TypeName System.DirectoryServices.ActiveDirectorySecurity + $sd.SetSecurityDescriptorSddlForm($SDDL, 'All') + $sd.GetAccessRules($true, $false, [Type][System.Security.Principal.NTAccount] + ).IdentityReference.Value | ForEach-Object { + ($_ -split '\\', 2)[-1].ToLowerInvariant() + } | Sort-Object + register: update_delegates_actual + +- name: assert update delegates + assert: + that: + - update_delegates is changed + - update_delegates_actual.output == ["administrator", "enterprise admins"] + +- name: unset string option + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + firstname: '' + register: unset_option + +- name: get result of unset string option + object_info: + identity: '{{ object_identity }}' + properties: + - givenName + register: unset_option_actual + +- name: assert unset string option + assert: + that: + - unset_option is changed + - unset_option_actual.objects[0].givenName == None + +- name: set groups - check + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + set: + - Domain Admins + register: set_groups_check + check_mode: true + +- name: get result of set groups - check + object_info: + identity: '{{ object_identity }}' + properties: + - memberOf + - primaryGroupID + register: set_groups_check_actual + +- name: assert set groups - check + assert: + that: + - set_groups_check is changed + - set_groups_check_actual.objects[0].memberOf == None + - set_groups_check_actual.objects[0].primaryGroupID == 513 # Domain Users + +- name: set groups + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + set: + - Domain Admins + register: set_groups + +- name: get result of set groups + object_info: + identity: '{{ object_identity }}' + properties: + - memberOf + - primaryGroupID + register: set_groups_actual + +- name: assert set groups - check + assert: + that: + - set_groups is changed + - set_groups.warnings | length == 1 + - '"the primary group of the user, skipping" in set_groups.warnings[0]' + - set_groups_actual.objects[0].memberOf == "CN=Domain Admins,CN=Users," ~ setup_domain_info.output[0].defaultNamingContext + - set_groups_actual.objects[0].primaryGroupID == 513 # Domain Users + +- name: fail to add group that is missing + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + add: + - Invalid + register: fail_missing_group + failed_when: + - '"Failed to locate group Invalid: Cannot find an object with identity" not in fail_missing_group.msg' + +- name: warn on group that is missing + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + add: + - Invalid + missing_behaviour: warn + register: warn_missing_group + +- name: assert warn on group that is missing + assert: + that: + - not warn_missing_group is changed + - warn_missing_group.warnings | length == 1 + - '"Failed to locate group Invalid but continuing on" in warn_missing_group.warnings[0]' + +- name: ignore on group that is missing + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + add: + - Invalid + missing_behaviour: ignore + register: ignore_missing_group + +- name: assert ignore on group that is missing + assert: + that: + - not ignore_missing_group is changed + - ignore_missing_group.warnings | default([]) | length == 0 + +- name: remove group + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + remove: + - domain admins + - Enterprise Admins + register: groups_remove + +- name: get result of remove groups + object_info: + identity: '{{ object_identity }}' + properties: + - memberOf + - primaryGroupID + register: groups_remove_actual + +- name: assert remove groups + assert: + that: + - groups_remove is changed + - groups_remove_actual.objects[0].memberOf == None + - groups_remove_actual.objects[0].primaryGroupID == 513 # Domain Users + +- name: add group + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + groups: + add: + - domain users + - domain admins + register: groups_add + +- name: get result of add groups + object_info: + identity: '{{ object_identity }}' + properties: + - memberOf + - primaryGroupID + register: groups_add_actual + +- name: assert add groups + assert: + that: + - groups_add is changed + - groups_add_actual.objects[0].memberOf == "CN=Domain Admins,CN=Users," ~ setup_domain_info.output[0].defaultNamingContext + - groups_add_actual.objects[0].primaryGroupID == 513 # Domain Users + +- name: set spns + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + spn: + set: + - HTTP/host + - HTTP/host.domain + - HTTP/host.domain:8080 + register: spn_set + +- name: get result of set spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_set_actual + +- name: assert set spns + assert: + that: + - spn_set is changed + - spn_set_actual.objects[0].servicePrincipalName == ['HTTP/host.domain:8080', 'HTTP/host.domain', 'HTTP/host'] + +- name: remove spns + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + spn: + remove: + - HTTP/fake + - HTTP/Host.domain + register: spn_remove + +- name: get result of remove spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_remove_actual + +- name: assert remove spns + assert: + that: + - spn_remove is changed + - spn_remove_actual.objects[0].servicePrincipalName == ['HTTP/host.domain:8080', 'HTTP/host'] + +- name: add spns + user: + name: MyUser + path: '{{ setup_domain_info.output[0].defaultNamingContext }}' + spn: + add: + - HTTP/Host.domain:8080 + - HTTP/fake + register: spn_add + +- name: get result of add spns + object_info: + identity: '{{ object_identity }}' + properties: + - servicePrincipalName + register: spn_add_actual + +- name: assert add spns + assert: + that: + - spn_add is changed + - spn_add_actual.objects[0].servicePrincipalName == ['HTTP/fake', 'HTTP/host.domain:8080', 'HTTP/host'] |