summaryrefslogtreecommitdiffstats
path: root/ansible_collections/microsoft/ad/tests/integration/targets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/microsoft/ad/tests/integration/targets
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/main.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml425
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/debug_ldap_client/tasks/main.yml17
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain/meta/main.yml4
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/main.yml66
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain/tasks/test.yml95
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/README.md34
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/Vagrantfile27
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/ansible.cfg3
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/inventory.yml21
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/setup.yml82
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/tasks/main.yml232
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_controller/test.yml30
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/main.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml516
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/aliases3
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/main.yml25
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/files/generate_cert.sh82
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/handlers/main.yml10
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/setup_certificate/tasks/main.yml196
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/invoke.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/roles/test/tasks/main.yml527
-rwxr-xr-xansible_collections/microsoft/ad/tests/integration/targets/inventory_ldap/runme.sh5
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/README.md34
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/Vagrantfile27
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/ansible.cfg3
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/inventory.yml22
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/setup.yml82
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml631
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/test.yml30
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/defaults/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/main.yml164
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object/tasks/tests.yml1446
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object_info/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object_info/handlers/main.yml4
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object_info/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/object_info/tasks/main.yml186
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/offline_join/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/offline_join/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/main.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/offline_join/tasks/tests.yml150
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/ou/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/ou/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/main.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml254
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/defaults/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/main.yml55
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/setup_domain/tasks/setup.yml5
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/meta/main.yml2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/main.yml14
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml1065
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']