summaryrefslogtreecommitdiffstats
path: root/ansible_collections/microsoft/ad/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:22 +0000
commit38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch)
tree356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/microsoft/ad/plugins
parentAdding upstream version 7.7.0+dfsg. (diff)
downloadansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz
ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--ansible_collections/microsoft/ad/plugins/action/debug_ldap_client.py12
-rw-r--r--ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py38
-rw-r--r--ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py13
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/as_datetime.yml40
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/as_guid.yml32
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/as_sid.yml34
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/dn_escape.yml46
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/ldap_converters.py247
-rw-r--r--ansible_collections/microsoft/ad/plugins/filter/parse_dn.yml79
-rw-r--r--ansible_collections/microsoft/ad/plugins/inventory/ldap.py103
-rw-r--r--ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm184
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/computer.py10
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain.py2
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain_controller.py1
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/group.py6
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/membership.ps122
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/membership.py2
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/object.py2
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/object_info.ps112
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/object_info.py8
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/offline_join.py2
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/ou.py2
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/user.ps13
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/user.py23
24 files changed, 694 insertions, 129 deletions
diff --git a/ansible_collections/microsoft/ad/plugins/action/debug_ldap_client.py b/ansible_collections/microsoft/ad/plugins/action/debug_ldap_client.py
index a33f21dda..fbe990f4e 100644
--- a/ansible_collections/microsoft/ad/plugins/action/debug_ldap_client.py
+++ b/ansible_collections/microsoft/ad/plugins/action/debug_ldap_client.py
@@ -52,7 +52,9 @@ class ActionModule(ActionBase):
"dns": dns_info,
"kerberos": kerb_info,
"packages": {
- "dnspython": self._import_lib("dns.resolver", package_name="dnspython"),
+ "dnspython": self._import_lib(
+ "dns.resolver", package_name="dnspython"
+ ),
"dpapi_ng": self._import_lib("dpapi_ng", package_name="dpapi-ng"),
"krb5": self._import_lib("krb5"),
"pyspnego": self._import_lib("spnego", package_name="pyspnego"),
@@ -77,8 +79,6 @@ class ActionModule(ActionBase):
ctx = krb5.init_context()
except Exception:
res["exception"] = traceback.format_exc()
-
- if not ctx:
return res
try:
@@ -106,8 +106,6 @@ class ActionModule(ActionBase):
default_cc = krb5.cc_default(ctx)
except Exception:
res["exception"] = traceback.format_exc()
-
- if not default_cc:
return res
try:
@@ -154,7 +152,9 @@ class ActionModule(ActionBase):
}
)
- highest_record = next(iter(sorted(records, key=lambda k: (k["priority"], -k["weight"]))), None)
+ highest_record = next(
+ iter(sorted(records, key=lambda k: (k["priority"], -k["weight"]))), None
+ )
if highest_record:
res["default_server"] = highest_record["target"].rstrip(".")
res["default_port"] = highest_record["port"]
diff --git a/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py b/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
index 31ed8eacd..3231e2341 100644
--- a/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
+++ b/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
@@ -79,12 +79,16 @@ options:
domain_password:
description:
- The password for I(domain_username).
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
domain_server:
description:
- Specified the Active Directory Domain Services instance to connect to.
- Can be in the form of an FQDN or NetBIOS name.
- If not specified then the value is based on the default domain of the computer running PowerShell.
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
domain_username:
description:
@@ -92,38 +96,46 @@ options:
- If this is not set then the user that is used for authentication will be the connection user.
- Ansible will be unable to use the connection user unless auth is Kerberos with credential delegation or CredSSP,
or become is used on the task.
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
identity:
description:
- The identity of the AD object used to find the AD object to manage.
- - Must be specified if I(name) is not set, when trying to rename the object
- with a new I(name), or when trying to move the object into a different
- I(path).
+ - This must be specified if; I(name) is not set, when trying to rename the
+ object with a new I(name), or when trying to move the object into a
+ different I(path).
- The identity can be in the form of a GUID representing the C(objectGUID)
value, the C(userPrincipalName), C(sAMAccountName), C(objectSid), or
C(distinguishedName).
- - If omitted, the AD object to managed is selected by the
+ - If omitted, the AD object to manage is selected by the
C(distinguishedName) using the format C(CN={{ name }},{{ path }}). If
I(path) is not defined, the C(defaultNamingContext) is used instead.
type: str
name:
description:
- - The C(name) of the AD object to manage.
- - If I(identity) is specified, and the name of the object it found does not
- match this value, the object will be renamed.
- - This must be set when I(state=present) or if I(identity) is not set.
- - This is not always going to be the same as the C(sAMAccountName) for user
- objects. It is strictly the C(name) of the object in the path specified.
- Use I(identity) to select an object to manage by C(sAMAccountName).
+ - The C(name) of the AD object to manage, this is not the C(sAMAccountName)
+ of the object but the LDAP C(cn) or C(name) entry of the object in the
+ path specified. Use I(identity) to select an object to manage by its
+ C(sAMAccountName).
+ - If I(identity) is specified, and the name of the object found by that
+ identity does not match this value, the object will be renamed.
+ - This must be specified if I(identity) is not set.
type: str
path:
description:
- The path of the OU or the container where the new object should exist in.
- - If no path is specified, the default is the C(defaultNamingContext) of
- domain for most objects.
+ - If creating a new object, the new object will be created at the path
+ specified. If no path is specified then the C(defaultNamingContext) of
+ the domain will be used as the path for most object types.
+ - If managing an existing object found by I(identity), the path of the
+ found object will be moved to the one specified by this option. If no
+ path is specified, the object will not be moved.
- The modules M(microsoft.ad.computer), M(microsoft.ad.user), and
M(microsoft.ad.group) have their own default path that is
configured on the Active Directory domain controller.
+ - This can be set to the literal value C(microsoft.ad.default_path) which
+ will equal the default value used when creating a new object.
type: str
protect_from_deletion:
description:
diff --git a/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py b/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
index 9300881cb..327c1ba76 100644
--- a/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
+++ b/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
@@ -31,6 +31,7 @@ options:
installed.
- See R(LDAP authentication,ansible_collections.microsoft.ad.docsite.guide_ldap_connection.authentication)
for more information.
+ - This option can be set using a Jinja2 template value.
choices:
- simple
- certificate
@@ -47,6 +48,7 @@ options:
certificate validation.
- If omitted, the default CA store used for validation is dependent on
the current Python settings.
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_CA_CERT
@@ -60,6 +62,7 @@ options:
hostname checks performed by TLS.
- See R(Certificate validation,ansible_collections.microsoft.ad.docsite.guide_ldap_connection.cert_validation)
for more information.
+ - This option can be set using a Jinja2 template value.
choices:
- always
- ignore
@@ -80,6 +83,7 @@ options:
- Use I(certificate_key) if the certificate specified does not contain the
key.
- Use I(certificate_password) if the key is encrypted with a password.
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_CERTIFICATE
@@ -89,6 +93,7 @@ options:
- The value can either be a path to a file containing the key in the PEM or
DER encoded form, or it can be the string of a PEM encoded key.
- Use I(certificate_password) if the key is encrypted with a password.
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_CERTIFICATE_KEY
@@ -96,6 +101,7 @@ options:
description:
- The password used to decrypt the certificate key specified by
I(certificate) or I(certificate_key).
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_CERTIFICATE_PASSWORD
@@ -103,6 +109,7 @@ options:
description:
- The timeout in seconds to wait until the connection is established before
failing.
+ - This option can be set using a Jinja2 template value.
default: 5
type: int
env:
@@ -117,6 +124,7 @@ options:
- If using C(auth_protocol=simple) over LDAP without TLS then this must be
set to C(False). As no encryption is used, all traffic will be in
plaintext and should be avoided.
+ - This option can be set using a Jinja2 template value.
default: true
type: bool
env:
@@ -129,6 +137,7 @@ options:
- If I(auth_protocol) is C(negotiate), C(kerberos), or C(ntlm) and no
password is specified, it will attempt to use the local cached credential
specified by I(username) if available.
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_PASSWORD
@@ -137,6 +146,7 @@ options:
- The LDAP port to use for the connection.
- Port 389 is used for LDAP and port 686 is used for LDAPS.
- Defaults to port C(636) if C(tls_mode=ldaps) otherwise C(389).
+ - This option can be set using a Jinja2 template value.
type: int
env:
- name: MICROSOFT_AD_LDAP_PORT
@@ -147,6 +157,7 @@ options:
C(default_realm) setting and with an SRV DNS lookup.
- See R(Server lookup,ansible_collections.microsoft.ad.docsite.guide_ldap_connection.server_lookup)
for more information.
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_SERVER
@@ -159,6 +170,7 @@ options:
operation before the authentication bind.
- It is recommended to use C(ldaps) over C(start_tls) if TLS is going to be
used.
+ - This option can be set using a Jinja2 template value.
choices:
- ldaps
- start_tls
@@ -173,6 +185,7 @@ options:
- If I(auth_protocol) is C(negotiate), C(kerberos), or C(ntlm) and no
username is specified, it will attempt to use the local cached credential
if available, for example one retrieved by C(kinit).
+ - This option can be set using a Jinja2 template value.
type: str
env:
- name: MICROSOFT_AD_LDAP_USERNAME
diff --git a/ansible_collections/microsoft/ad/plugins/filter/as_datetime.yml b/ansible_collections/microsoft/ad/plugins/filter/as_datetime.yml
index f8e7911f9..0a5fd55aa 100644
--- a/ansible_collections/microsoft/ad/plugins/filter/as_datetime.yml
+++ b/ansible_collections/microsoft/ad/plugins/filter/as_datetime.yml
@@ -4,36 +4,36 @@
DOCUMENTATION:
name: as_datetime
author:
- - Jordan Borean (@jborean93)
+ - Jordan Borean (@jborean93)
short_description: Converts an LDAP value to a datetime string
version_added: 1.1.0
seealso:
- - ref: microsoft.ad.as_guid <ansible_collections.microsoft.ad.as_guid_filter>
- description: microsoft.ad.as_guid filter
- - ref: microsoft.ad.as_sid <ansible_collections.microsoft.ad.as_sid_filter>
- description: microsoft.ad.as_sid filter
- - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
- description: microsoft.ad.ldap inventory
+ - ref: microsoft.ad.as_guid <ansible_collections.microsoft.ad.as_guid_filter>
+ description: microsoft.ad.as_guid filter
+ - ref: microsoft.ad.as_sid <ansible_collections.microsoft.ad.as_sid_filter>
+ description: microsoft.ad.as_sid filter
+ - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
+ description: microsoft.ad.ldap inventory
description:
- - Converts an LDAP integer or raw value to a datetime string.
- - Should be used with the C(microsoft.ad.ldap) plugin to convert
- attribute values to a datetime string.
+ - Converts an LDAP integer or raw value to a datetime string.
+ - Should be used with the C(microsoft.ad.ldap) plugin to convert
+ attribute values to a datetime string.
positional: _input
options:
_input:
description:
- - The LDAP attribute bytes or integer value representing a FILETIME
- integer stored in LDAP.
- - The resulting datetime will be set as a UTC datetime as that's how the
- FILETIME value is stored in LDAP.
+ - The LDAP attribute bytes or integer value representing a FILETIME
+ integer stored in LDAP.
+ - The resulting datetime will be set as a UTC datetime as that's how the
+ FILETIME value is stored in LDAP.
type: raw
required: true
format:
description:
- - The string format to format the datetime object as.
- - Defaults to an ISO 8601 compatible string, for example
- C(2023-02-06T07:39:09.195321+0000).
- default: '%Y-%m-%dT%H:%M:%S.%f%z'
+ - The string format to format the datetime object as.
+ - Defaults to an ISO 8601 compatible string, for example
+ C(2023-02-06T07:39:09.195321+0000).
+ default: "%Y-%m-%dT%H:%M:%S.%f%z"
type: str
EXAMPLES: |
@@ -50,5 +50,5 @@ EXAMPLES: |
RETURN:
_value:
description:
- - The datetime string value(s) formatted as per the I(format) option.
- type: string \ No newline at end of file
+ - The datetime string value(s) formatted as per the I(format) option.
+ type: string
diff --git a/ansible_collections/microsoft/ad/plugins/filter/as_guid.yml b/ansible_collections/microsoft/ad/plugins/filter/as_guid.yml
index 3110a5057..e704ac133 100644
--- a/ansible_collections/microsoft/ad/plugins/filter/as_guid.yml
+++ b/ansible_collections/microsoft/ad/plugins/filter/as_guid.yml
@@ -4,28 +4,28 @@
DOCUMENTATION:
name: as_guid
author:
- - Jordan Borean (@jborean93)
+ - Jordan Borean (@jborean93)
short_description: Converts an LDAP value to a GUID string
version_added: 1.1.0
seealso:
- - ref: microsoft.ad.as_datetime <ansible_collections.microsoft.ad.as_datetime_filter>
- description: microsoft.ad.as_datetime filter
- - ref: microsoft.ad.as_sid <ansible_collections.microsoft.ad.as_sid_filter>
- description: microsoft.ad.as_sid filter
- - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
- description: microsoft.ad.ldap inventory
+ - ref: microsoft.ad.as_datetime <ansible_collections.microsoft.ad.as_datetime_filter>
+ description: microsoft.ad.as_datetime filter
+ - ref: microsoft.ad.as_sid <ansible_collections.microsoft.ad.as_sid_filter>
+ description: microsoft.ad.as_sid filter
+ - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
+ description: microsoft.ad.ldap inventory
description:
- - Converts an LDAP string or raw value to a guid string.
- - Should be used with the C(microsoft.ad.ldap) plugin to convert
- attribute values to a guid string.
+ - Converts an LDAP string or raw value to a guid string.
+ - Should be used with the C(microsoft.ad.ldap) plugin to convert
+ attribute values to a guid string.
positional: _input
options:
_input:
description:
- - The LDAP attribute bytes or string value representing a GUID
- stored in LDAP.
- - If using a string as input, it must be a base64 string representing
- the GUIDs bytes.
+ - The LDAP attribute bytes or string value representing a GUID
+ stored in LDAP.
+ - If using a string as input, it must be a base64 string representing
+ the GUIDs bytes.
type: raw
required: true
@@ -38,5 +38,5 @@ EXAMPLES: |
RETURN:
_value:
description:
- - The guid string value(s).
- type: string \ No newline at end of file
+ - The guid string value(s).
+ type: string
diff --git a/ansible_collections/microsoft/ad/plugins/filter/as_sid.yml b/ansible_collections/microsoft/ad/plugins/filter/as_sid.yml
index 5e33e3189..a3b610861 100644
--- a/ansible_collections/microsoft/ad/plugins/filter/as_sid.yml
+++ b/ansible_collections/microsoft/ad/plugins/filter/as_sid.yml
@@ -4,39 +4,39 @@
DOCUMENTATION:
name: as_sid
author:
- - Jordan Borean (@jborean93)
+ - Jordan Borean (@jborean93)
short_description: Converts an LDAP value to a Security Identifier string
version_added: 1.1.0
seealso:
- - ref: microsoft.ad.as_datetime <ansible_collections.microsoft.ad.as_datetime_filter>
- description: microsoft.ad.as_datetime filter
- - ref: microsoft.ad.as_guid <ansible_collections.microsoft.ad.as_guid_filter>
- description: microsoft.ad.as_guid filter
- - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
- description: microsoft.ad.ldap inventory
+ - ref: microsoft.ad.as_datetime <ansible_collections.microsoft.ad.as_datetime_filter>
+ description: microsoft.ad.as_datetime filter
+ - ref: microsoft.ad.as_guid <ansible_collections.microsoft.ad.as_guid_filter>
+ description: microsoft.ad.as_guid filter
+ - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
+ description: microsoft.ad.ldap inventory
description:
- - Converts an LDAP string or raw value to a security identifier string.
- - Should be used with the C(microsoft.ad.ldap) plugin to convert
- attribute values to a security identifier string.
+ - Converts an LDAP string or raw value to a security identifier string.
+ - Should be used with the C(microsoft.ad.ldap) plugin to convert
+ attribute values to a security identifier string.
positional: _input
options:
_input:
description:
- - The LDAP attribute bytes or string value representing a Security
- Identifier stored in LDAP.
- - If using a string as input, it must be a base64 string representing
- the SIDs bytes.
+ - The LDAP attribute bytes or string value representing a Security
+ Identifier stored in LDAP.
+ - If using a string as input, it must be a base64 string representing
+ the SIDs bytes.
type: raw
required: true
EXAMPLES: |
# This is an example used in the microsoft.ad.ldap plugin
-
+
attributes:
objectSid: raw | microsoft.ad.as_sid
RETURN:
_value:
description:
- - The security identifier string value(s).
- type: string \ No newline at end of file
+ - The security identifier string value(s).
+ type: string
diff --git a/ansible_collections/microsoft/ad/plugins/filter/dn_escape.yml b/ansible_collections/microsoft/ad/plugins/filter/dn_escape.yml
new file mode 100644
index 000000000..bd14f336b
--- /dev/null
+++ b/ansible_collections/microsoft/ad/plugins/filter/dn_escape.yml
@@ -0,0 +1,46 @@
+# Copyright (c) 2023 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ name: dn_escape
+ author:
+ - Jordan Borean (@jborean93)
+ short_description: Escape an LDAP DistinguishedName value string.
+ version_added: 1.5.0
+ seealso:
+ - ref: microsoft.ad.parse_dn <ansible_collections.microsoft.ad.parse_dn_filter>
+ description: microsoft.ad.parse_dn filter
+ - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
+ description: microsoft.ad.ldap inventory
+ description:
+ - Escapes a string value for use in an LDAP DistinguishedName.
+ - This can be used to escape special characters when building a
+ DistinguishedName value.
+ positional: _input
+ options:
+ _input:
+ description:
+ - The string value to escape.
+ - This should be just the RDN value not including the attribute type
+ that prefixes the value, for example C(MyValue) and not C(CN=MyValue).
+ type: str
+ required: true
+
+EXAMPLES: |
+ # This is an example used in the microsoft.ad.ldap plugin
+
+ search_base: OU={{ my_ou_variable | microsoft.ad.dn_escape }},DC=domain,DC=com
+
+ # This is an example with the microsoft.ad.user module
+
+ - microsoft.ad.user:
+ name: MyUser
+ password: MyPassword123
+ state: present
+ path: OU={{ my_ou_variable | microsoft.ad.dn_escape }},DC=domain,DC=com
+
+RETURN:
+ _value:
+ description:
+ - The escaped RDN attribute value.
+ type: string
diff --git a/ansible_collections/microsoft/ad/plugins/filter/ldap_converters.py b/ansible_collections/microsoft/ad/plugins/filter/ldap_converters.py
index aa7ee669b..0f4c2af5e 100644
--- a/ansible_collections/microsoft/ad/plugins/filter/ldap_converters.py
+++ b/ansible_collections/microsoft/ad/plugins/filter/ldap_converters.py
@@ -3,6 +3,7 @@
import base64
import datetime
+import re
import struct
import typing as t
import uuid
@@ -11,6 +12,154 @@ from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.collections import is_sequence
+_RDN_TYPE_PATTERN = re.compile(
+ r"""
+[\ ]* # Ignore leading spaces
+(
+ (
+ # Lead char is a letter, subsequent chars can be numbers or -
+ [a-zA-Z][a-zA-Z0-9-]*
+ )
+ |
+ (
+ # First number must a decimal without a leading 0 unless 0.
+ # Must also contain at least another entry separated by '.'.
+ ([0-9]|[1-9][0-9]+)
+ (
+ \.([0-9]|[1-9][0-9]+)
+ )+
+ )
+)
+[\ ]*= # Ignore trailing spaces before the =
+""".encode(
+ "utf-8"
+ ),
+ re.VERBOSE,
+)
+
+_RDN_VALUE_HEXSTRING_PATTERN = re.compile(
+ r"""
+[\ ]* # Ignore leading spaces
+\# # Starts with '#'
+(
+ ([0-9a-fA-F]{2})+
+)
+[\ ]* # Ignore trailing spaces
+(?:[+,]|$) # Terminated by '+', ',', or the end of the string
+""".encode(
+ "utf-8"
+ ),
+ re.VERBOSE,
+)
+
+_RDN_VALUE_ESCAPE_PATTERN = re.compile(
+ r"""
+(
+ (?P<literal>
+ [+,;<>#=\\\"\ ]
+ )
+ |
+ (?P<hex>
+ ([0-9a-fA-F]{2})
+ )
+)
+""".encode(
+ "utf-8"
+ ),
+ re.VERBOSE,
+)
+
+
+def _parse_rdn_type(value: memoryview) -> t.Optional[t.Tuple[bytes, int]]:
+ if match := _RDN_TYPE_PATTERN.match(value):
+ return match.group(1), len(match.group(0))
+
+ return None
+
+
+def _parse_rdn_value(value: memoryview) -> t.Optional[t.Tuple[bytes, int, bool]]:
+ if hex_match := _RDN_VALUE_HEXSTRING_PATTERN.match(value):
+ full_value = hex_match.group(0)
+ more_rdns = full_value.endswith(b"+")
+
+ b_value = base64.b16decode(hex_match.group(1).upper())
+ return b_value, len(full_value), more_rdns
+
+ # Parsing the string value variant as regex is too complicated due to the
+ # myriad of rules and escaping so it is done manually.
+ read = 0
+ new_value = bytearray()
+ found_spaces = 0
+
+ total_len = len(value)
+ while read < total_len:
+ current_value = value[read]
+ current_char = chr(current_value)
+ read += 1
+
+ # We only count the spaces in the middle of the string so we need to
+ # keep track of how many have been found until the next character.
+ if current_char == " ":
+ if new_value:
+ found_spaces += 1
+
+ continue
+
+ if current_char in [",", "+"]:
+ break
+
+ # We can add any spaces we are still tentatively collecting as there's
+ # a real value after it.
+ if found_spaces:
+ new_value += b" " * found_spaces
+ found_spaces = 0
+
+ if current_char == "#" and not new_value:
+ remaining = (
+ value[read - 1:].tobytes().decode("utf-8", errors="surrogateescape")
+ )
+ raise AnsibleFilterError(
+ f"Found leading # for attribute value but does not match hexstring format at '{remaining}'"
+ )
+
+ elif current_char in ["\00", '"', ";", "<", ">"]:
+ remaining = (
+ value[read - 1:].tobytes().decode("utf-8", errors="surrogateescape")
+ )
+ raise AnsibleFilterError(
+ f"Found unescaped character '{current_char}' in attribute value at '{remaining}'"
+ )
+
+ elif current_char == "\\":
+ if escape_match := _RDN_VALUE_ESCAPE_PATTERN.match(value, pos=read):
+ if literal_value := escape_match.group("literal"):
+ new_value += literal_value
+ read += 1
+
+ else:
+ new_value += base64.b16decode(escape_match.group("hex").upper())
+ read += 2
+
+ else:
+ remaining = (
+ value[read - 1:]
+ .tobytes()
+ .decode("utf-8", errors="surrogateescape")
+ )
+ raise AnsibleFilterError(
+ f"Found invalid escape sequence in attribute value at '{remaining}"
+ )
+
+ else:
+ new_value.append(current_value)
+
+ if new_value:
+ return bytes(new_value), read, current_char == "+"
+
+ else:
+ return None
+
+
def per_sequence(func: t.Callable[[t.Any], t.Any]) -> t.Any:
def wrapper(value: t.Any, *args: t.Any, **kwargs: t.Any) -> t.Any:
if is_sequence(value):
@@ -22,7 +171,10 @@ def per_sequence(func: t.Callable[[t.Any], t.Any]) -> t.Any:
@per_sequence
-def as_datetime(value: t.Any, format: str = "%Y-%m-%dT%H:%M:%S.%f%z") -> str:
+def as_datetime(
+ value: t.Any,
+ format: str = "%Y-%m-%dT%H:%M:%S.%f%z",
+) -> str:
if isinstance(value, bytes):
value = value.decode("utf-8")
@@ -31,8 +183,14 @@ def as_datetime(value: t.Any, format: str = "%Y-%m-%dT%H:%M:%S.%f%z") -> str:
# FILETIME is 100s of nanoseconds since 1601-01-01. As Python does not
# support nanoseconds the delta is number of microseconds.
+ ft_epoch = datetime.datetime(
+ year=1601,
+ month=1,
+ day=1,
+ tzinfo=datetime.timezone.utc,
+ )
delta = datetime.timedelta(microseconds=value // 10)
- dt = datetime.datetime(year=1601, month=1, day=1, tzinfo=datetime.timezone.utc) + delta
+ dt = ft_epoch + delta
return dt.strftime(format)
@@ -77,10 +235,95 @@ def as_sid(value: t.Any) -> str:
return f"S-{revision}-{authority}-{'-'.join(sub_authorities)}"
+@per_sequence
+def dn_escape(value: str) -> str:
+ """Escapes a DistinguisedName attribute value."""
+ escaped_value = []
+
+ end_idx = len(value) - 1
+ for idx, c in enumerate(value):
+ if (
+ # Starting char cannot be ' ' or #
+ (idx == 0 and c in [" ", "#"])
+ # Ending char cannot be ' '
+ or (idx == end_idx and c == " ")
+ # Any of these chars need to be escaped
+ # These are documented in RFC 4514
+ or (c in ['"', "+", ",", ";", "<", ">", "\\"])
+ ):
+ escaped_value.append(rf"\{c}")
+
+ elif c in ["\00", "\n", "\r", "=", "/"]:
+ # These are extra chars MS says to escape, it must be done using
+ # the hex syntax
+ # https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names
+ escaped_int = ord(c)
+ escaped_value.append(rf"\{escaped_int:02X}")
+
+ else:
+ escaped_value.append(c)
+
+ return "".join(escaped_value)
+
+
+@per_sequence
+def parse_dn(value: str) -> t.List[t.List[str]]:
+ """Parses a DistinguishedName and emits a structured object."""
+
+ # This behaviour is defined in RFC 4514 and while not defined in that RFC
+ # this will also remove any extra spaces before and after , = and +.
+ dn: t.List[t.List[str]] = []
+
+ # This operates on bytes for 2 reasons:
+ # 1. We can use a memoryview for more efficient slicing
+ # 2. Attribute value hex escaping is done per byte, we cannot decode
+ # back to a string until we have the final value.
+ # surrogateescape is used for all conversions to ensure non-unicode bytes
+ # are preserved using the escape behaviour in UTF-8.
+ b_value = value.encode("utf-8", errors="surrogateescape")
+ b_view = memoryview(b_value)
+
+ while b_view:
+ rdns: t.List[str] = []
+
+ while True:
+ attr_type = _parse_rdn_type(b_view)
+ if not attr_type:
+ remaining = b_view.tobytes().decode("utf-8", errors="surrogateescape")
+ raise AnsibleFilterError(
+ f"Expecting attribute type in RDN entry from '{remaining}'"
+ )
+
+ rdns.append(attr_type[0].decode("utf-8", errors="surrogateescape"))
+ b_view = b_view[attr_type[1]:]
+
+ attr_value = _parse_rdn_value(b_view)
+ if not attr_value:
+ remaining = b_view.tobytes().decode("utf-8", errors="surrogateescape")
+ raise AnsibleFilterError(
+ f"Expecting attribute value in RDN entry from '{remaining}'"
+ )
+
+ rdns.append(attr_value[0].decode("utf-8", errors="surrogateescape"))
+ b_view = b_view[attr_value[1]:]
+
+ # If ended with + we want to continue parsing the AVA values
+ if attr_value[2]:
+ continue
+ else:
+ break
+
+ dn.append(rdns)
+
+ return dn
+
+
class FilterModule:
def filters(self) -> t.Dict[str, t.Callable]:
return {
"as_datetime": as_datetime,
"as_guid": as_guid,
"as_sid": as_sid,
+ "dn_escape": dn_escape,
+ "parse_dn": parse_dn,
}
diff --git a/ansible_collections/microsoft/ad/plugins/filter/parse_dn.yml b/ansible_collections/microsoft/ad/plugins/filter/parse_dn.yml
new file mode 100644
index 000000000..18feacb64
--- /dev/null
+++ b/ansible_collections/microsoft/ad/plugins/filter/parse_dn.yml
@@ -0,0 +1,79 @@
+# Copyright (c) 2023 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ name: parse_dn
+ author:
+ - Jordan Borean (@jborean93)
+ short_description: Parses an LDAP DistinguishedName string into an object.
+ version_added: 1.5.0
+ seealso:
+ - ref: microsoft.ad.dn_escape <ansible_collections.microsoft.ad.dn_escape_filter>
+ description: microsoft.ad.dn_escape filter
+ - ref: microsoft.ad.ldap <ansible_collections.microsoft.ad.ldap_inventory>
+ description: microsoft.ad.ldap inventory
+ description:
+ - Parses the provided LDAP DistinguishedName (C(DN)) string value into a
+ structured object.
+ - The rules for parsing as defined in
+ L(RFC 4514,https://www.ietf.org/rfc/rfc4514.txt).
+ - Each DN contains Relative DistinguishedNames (C(RDN)) separated by C(,) and
+ each RDN can contain multiple attribute type values also known as an
+ C(AVA). While Microsoft Active Directory DNs can only contain 1 AVA in an
+ RDN this parser supports multiple AVAs.
+ - The returned object for each DN will be provided as a list of lists where
+ the outer list is each RDN component separated by C(,) and the inner list
+ is each AVA separated by C(=) and C(+). Each RDN entry is guaranteed to
+ have 2 string values for the AVA type and value but can contain more if the
+ RDN contains multiple AVAs separated by C(+).
+ - The parsed RDN attribute values will be unescaped to represent the actual
+ value rather than the raw string in the DN.
+ - A DN that is invalid will raise a filter error.
+ positional: _input
+ options:
+ _input:
+ description:
+ - The LDAP DistinguishedName string to parse.
+ type: str
+ required: true
+
+EXAMPLES: |
+ - name: Parses a simple DN
+ set_fact:
+ my_dn: '{{ "CN=Foo,DC=domain,DC=com" | microsoft.ad.parse_dn }}'
+
+ # [
+ # ["CN", "Foo"],
+ # ["DC", "domain"],
+ # ["DC", "com"],
+ # ]
+
+ - name: Parses a DN with an escaped and multi attribute values
+ set_fact:
+ my_dn: '{{ "CN=CA,O=Acme\, Inc.,C=AU+ST=Queensland" | microsoft.ad.parse_dn }}'
+
+ # [
+ # ["CN", "CA"],
+ # ["O", "Acme, Inc."],
+ # ["C", "AU", "ST", "Queensland"]
+ # ]
+
+ # Extract the group names the computer is a member of in the ldap inventory
+ # plugin, for example gets the first RDN value inside the parsed DN.
+ attributes:
+ memberOf:
+ computer_membership: this | microsoft.ad.parse_dn | map(attribute="0.1")
+
+RETURN:
+ _value:
+ description:
+ - The parsed LDAP DN values.
+ type: list
+ elements: list
+ sample:
+ - - CN
+ - Foo
+ - - DC
+ - domain
+ - - DC
+ - com
diff --git a/ansible_collections/microsoft/ad/plugins/inventory/ldap.py b/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
index 23ee31d67..0a329daff 100644
--- a/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
+++ b/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
@@ -8,7 +8,7 @@ short_description: Inventory plugin for Active Directory
version_added: 1.1.0
description:
- Inventory plugin for Active Directory or other LDAP sources.
-- Uses a YAML configuration file that ends with C(microsoft.ad.{yml|yaml}).
+- Uses a YAML configuration file that ends with C(microsoft.ad.ldap.{yml|yaml}).
- Each host that is added will set the C(inventory_hostname) to the C(name) of
the LDAP computer object and C(ansible_host) to the value of the
C(dNSHostName) LDAP attribute if set. If the C(dNSHostName) attribute is not
@@ -40,8 +40,19 @@ options:
filter:
description:
- The LDAP filter string used to query the computer objects.
- - This will be combined with the filter "(objectClass=computer)".
+ - By default, this will be combined with the filter
+ "(objectClass=computer)". Use I(filter_without_computer) to override
+ this behavior and have I(filter) be the only filter used.
type: str
+ filter_without_computer:
+ description:
+ - Will not combine the I(filter) value with the filter
+ "(objectClass=computer)".
+ - In most cases this should be C(false) but can be set to C(true) to have
+ the I(filter) value specified be the only filter used.
+ type: bool
+ default: false
+ version_added: '1.3.0'
search_base:
description:
- The LDAP search base to find the computer objects in.
@@ -73,6 +84,9 @@ notes:
information.
- This plugin is a tech preview and the module options are subject to change
based on feedback received.
+- Unless specified otherwise in the option description, the value specified in
+ the config file is used as is. Only the LDAP connection options allow using
+ a Jinja2 template.
extends_documentation_fragment:
- constructed
- microsoft.ad.ldap_connection
@@ -114,6 +128,11 @@ auth_protocol: kerberos
tls_mode: ldaps
ca_cert: /home/user/certs/ldap.pem
+# The username and password can be retrieved using a template with a lookup.
+# Other connection options can also be set this way, the option description
+# tells you whether it can be set to a template.
+username: '{{ lookup("ansible.builtin.env", "LDAP_USERNAME") }}'
+password: '{{ lookup("ansible.builtin.env", "LDAP_PASSWORD") }}'
##############################################
# Search Options #
@@ -143,7 +162,10 @@ attributes:
comment:
host_comment
memberOf:
- computer_membership: this | map("regex_search", '^CN=(?P<name>.+?)((?<!\\),)', '\g<name>') | flatten
+ # Gets the value (1) of the first RDN (0) of each memberOf instance (this).
+ # For example 'CN=Domain Admins,CN=Users,DC=domain,DC=test'
+ # will be returned as just 'Domain Admins'
+ computer_membership: this | microsoft.ad.parse_dn | map(attribute="0.1")
location:
@@ -259,6 +281,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
groups = self.get_option("groups")
keyed_groups = self.get_option("keyed_groups")
ldap_filter = self.get_option("filter")
+ ldap_filter_without_computer = self.get_option("filter_without_computer")
search_base = self.get_option("search_base")
search_scope = self.get_option("search_scope")
strict = self.get_option("strict")
@@ -272,26 +295,56 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
computer_filter = sansldap.FilterEquality("objectClass", b"computer")
final_filter: sansldap.LDAPFilter
if ldap_filter:
- final_filter = sansldap.FilterAnd(
- filters=[
- computer_filter,
- sansldap.LDAPFilter.from_string(ldap_filter),
- ]
- )
+ ldap_filter_obj = sansldap.LDAPFilter.from_string(ldap_filter)
+
+ if ldap_filter_without_computer:
+ final_filter = ldap_filter_obj
+ else:
+ final_filter = sansldap.FilterAnd(
+ filters=[computer_filter, ldap_filter_obj]
+ )
else:
final_filter = computer_filter
custom_attributes = self._get_custom_attributes()
- attributes = {"name", "dnshostname"}.union([a.lower() for a in custom_attributes.keys()])
+ attributes = {"name", "dnshostname"}.union(
+ [a.lower() for a in custom_attributes.keys()]
+ )
# If inventory_hostname was defined in compose, set it in the custom
# attributes so we can set the hostname before processing the rest of
# compose entries.
inventory_hostname = compose.pop("inventory_hostname", None)
if inventory_hostname:
- custom_attributes["inventory_hostname"] = {"inventory_hostname": inventory_hostname}
-
+ custom_attributes["inventory_hostname"] = {
+ "inventory_hostname": inventory_hostname
+ }
connection_options = self.get_options()
+
+ # These options are in ../doc_fragments/ldap_connection.py
+ template_fields = {
+ 'auth_protocol',
+ 'ca_cert',
+ 'cert_validation',
+ 'certificate',
+ 'certificate_key',
+ 'certificate_password',
+ 'connection_timeout',
+ 'encrypt',
+ 'password',
+ 'port',
+ 'server',
+ 'tls_mode',
+ 'username',
+ }
+ for option_name, option_value in connection_options.items():
+ if option_name in template_fields and self.templar.is_template(option_value):
+ self.display.vvv(f"Templating option {option_name}")
+ connection_options[option_name] = self.templar.template(
+ variable=option_value,
+ disable_lookups=False,
+ )
+
laps_decryptor = LAPSDecryptor(**connection_options)
with create_ldap_connection(**connection_options) as client:
schema = LDAPSchema.load_schema(client)
@@ -317,9 +370,11 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
raw_values = insensitive_info.get(name.lower(), [])
values = schema.cast_object(name, raw_values)
- host_vars["raw"] = wrap_var([base64.b64encode(r).decode() for r in raw_values])
+ host_vars["raw"] = wrap_var(
+ [base64.b64encode(r).decode() for r in raw_values]
+ )
- if name.lower() == 'mslaps-encryptedpassword' and raw_values:
+ if name.lower() == "mslaps-encryptedpassword" and raw_values:
host_vars["this"] = laps_decryptor.decrypt(raw_values[0])
else:
host_vars["this"] = wrap_var(values)
@@ -329,7 +384,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
composite = self._compose(v, host_vars)
except Exception as e:
if strict:
- raise AnsibleError(f"Could not set {n} for host {host_name}: {e}") from e
+ raise AnsibleError(
+ f"Could not set {n} for host {host_name}: {e}"
+ ) from e
continue
host_vars[n] = composite
@@ -344,9 +401,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
continue
inventory.set_variable(actual_host_name, n, v)
- self._set_composite_vars(compose, host_vars, actual_host_name, strict=strict)
- self._add_host_to_composed_groups(groups, host_vars, actual_host_name, strict=strict)
- self._add_host_to_keyed_groups(keyed_groups, host_vars, actual_host_name, strict=strict)
+ self._set_composite_vars(
+ compose, host_vars, actual_host_name, strict=strict
+ )
+ self._add_host_to_composed_groups(
+ groups, host_vars, actual_host_name, strict=strict
+ )
+ self._add_host_to_keyed_groups(
+ keyed_groups, host_vars, actual_host_name, strict=strict
+ )
def _get_custom_attributes(self) -> t.Dict[str, t.Dict[str, str]]:
custom_attributes = self.get_option("attributes")
@@ -358,7 +421,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
elif isinstance(info, str):
info = {name.replace("-", "_"): info}
elif not isinstance(info, dict):
- raise AnsibleError(f"Attribute {name} value was {type(info).__name__} but was expecting a dictionary")
+ raise AnsibleError(
+ f"Attribute {name} value was {type(info).__name__} but was expecting a dictionary"
+ )
for var_name in list(info.keys()):
var_template = info[var_name]
diff --git a/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
index 4a7ccf87c..e51c974cb 100644
--- a/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
+++ b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
@@ -75,7 +75,7 @@ Function Compare-AnsibleADAttribute {
[System.Convert]::ToBase64String($_)
}
elseif ($_ -is [System.DateTime]) {
- $_.ToUniversalTime().ToString('o')
+ $_.ToUniversalTime().ToFileTimeUtc()
}
elseif ($_ -is [System.DirectoryServices.ActiveDirectorySecurity]) {
$_.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All)
@@ -559,9 +559,6 @@ Function Get-AnsibleADObject {
elseif ($Identity -match '^.*\@.*\..*$') {
$getParams.LDAPFilter = "(userPrincipalName=$($Matches[0]))"
}
- elseif ($Identity -match '^(?:[^:*?""<>|\/\\]+\\)?(?<username>[^;:""<>|?,=\*\+\\\(\)]{1,20})$') {
- $getParams.LDAPFilter = "(sAMAccountName=$($Matches.username))"
- }
else {
try {
$sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $Identity
@@ -574,8 +571,13 @@ Function Get-AnsibleADObject {
$getParams.LDAPFilter = "(objectSid=$value)"
}
catch [System.ArgumentException] {
- # Finally fallback to DistinguishedName.
- $getParams.Identity = $Identity
+ if ($Identity -match '^(?:[^:*?""<>|\/\\]+\\)?(?<username>[^;:""<>|?,=\*\+\\\(\)]+)$') {
+ $getParams.LDAPFilter = "(sAMAccountName=$($Matches.username))"
+ }
+ else {
+ # Finally fallback to DistinguishedName.
+ $getParams.Identity = $Identity
+ }
}
}
@@ -685,6 +687,8 @@ Function Invoke-AnsibleADObject {
$PostAction
)
+ $defaultPathSentinel = 'microsoft.ad.default_path'
+
$spec = @{
options = @{
attributes = @{
@@ -738,7 +742,7 @@ Function Invoke-AnsibleADObject {
}
$stateRequiredIf = @{
- present = @('name')
+ present = @()
absent = @()
}
@@ -828,7 +832,7 @@ Function Invoke-AnsibleADObject {
}
else {
$ouPath = $defaultObjectPath
- if ($module.Params.path) {
+ if ($module.Params.path -and $module.Params.path -ne $defaultPathSentinel) {
$ouPath = $module.Params.path
}
"$namePrefix=$($Module.Params.name -replace ',', '\,'),$ouPath"
@@ -883,7 +887,7 @@ Function Invoke-AnsibleADObject {
# Remove-ADObject -Recursive fails with access is denied, use this
# instead to remove the child objects manually
- Get-ADObject -Filter * -Properties ProtectedFromAccidentalDeletion -Searchbase $adObject.DistinguishedName |
+ Get-ADObject -Filter * -Properties ProtectedFromAccidentalDeletion -Searchbase $adObject.DistinguishedName @adParams |
Sort-Object -Property { $_.DistinguishedName.Length } -Descending |
ForEach-Object -Process {
if ($_.ProtectedFromAccidentalDeletion) {
@@ -903,15 +907,21 @@ Function Invoke-AnsibleADObject {
$objectGuid = $null
if (-not $adObject) {
+ $adName = if ($module.Params.name) {
+ $module.Params.name
+ }
+ else {
+ $module.Params.identity
+ }
$newParams = @{
Confirm = $false
- Name = $module.Params.name
+ Name = $adName
WhatIf = $module.CheckMode
PassThru = $true
}
$objectPath = $null
- if ($module.Params.path) {
+ if ($module.Params.path -and $module.Params.path -ne $defaultPathSentinel) {
$objectPath = $path
$newParams.Path = $module.Params.path
}
@@ -924,7 +934,7 @@ Function Invoke-AnsibleADObject {
$module.Diff.after = @{
attributes = $diffAttributes.after
- name = $module.Params.name
+ name = $adName
path = $objectPath
}
@@ -959,6 +969,19 @@ Function Invoke-AnsibleADObject {
}
}
+ # Only New-ADObject has the -ProtectedFromAccidentialDeletion while
+ # other cmdlets do not. Check for this and manually run with
+ # Set-ADObject later if protection is desired.
+ # https://github.com/ansible-collections/microsoft.ad/issues/47
+ $protectFromDeletion = $false
+ if (
+ $newParams.ContainsKey('ProtectedFromAccidentalDeletion') -and
+ -not $newCommand.Parameters.ContainsKey('ProtectedFromAccidentalDeletion')
+ ) {
+ $protectFromDeletion = $newParams.ProtectedFromAccidentalDeletion
+ $newParams.Remove('ProtectedFromAccidentalDeletion')
+ }
+
try {
$adObject = & $newCommand @newParams @adParams
}
@@ -970,12 +993,16 @@ Function Invoke-AnsibleADObject {
$module.Result.changed = $true
if ($module.CheckMode) {
- $objectDN = "$namePrefix=$($module.Params.name -replace ',', '\,'),$objectPath"
+ $objectDN = "$namePrefix=$($adName -replace ',', '\,'),$objectPath"
$objectGuid = [Guid]::Empty # Dummy value for check mode
}
else {
$objectDN = $adObject.DistinguishedName
$objectGuid = $adObject.ObjectGUID
+
+ if ($protectFromDeletion) {
+ $adObject | Set-ADObject @adParams -ProtectedFromAccidentalDeletion $true
+ }
}
}
else {
@@ -1056,16 +1083,21 @@ Function Invoke-AnsibleADObject {
}
$finalADObject = $null
- if ($module.Params.name -cne $objectName) {
- $objectName = $module.Params.name
+ $desiredName = $module.Params.name
+ if ($desiredName -and $desiredName -cne $objectName) {
+ $objectName = $desiredName
$module.Diff.after.name = $objectName
- $finalADObject = Rename-ADObject @commonParams -NewName $objectName
+ $finalADObject = Rename-ADObject @commonParams @adParams -NewName $objectName
$module.Result.changed = $true
}
- if ($module.Params.path -and $module.Params.path -ne $objectPath) {
- $objectPath = $module.Params.path
+ $desiredPath = $module.Params.path
+ if ($desiredPath -eq $defaultPathSentinel) {
+ $desiredPath = $defaultObjectPath
+ }
+ if ($desiredPath -and $desiredPath -ne $objectPath) {
+ $objectPath = $desiredPath
$module.Diff.after.path = $objectPath
$addProtection = $false
@@ -1075,7 +1107,7 @@ Function Invoke-AnsibleADObject {
}
try {
- $finalADObject = Move-ADObject @commonParams -TargetPath $objectPath
+ $finalADObject = Move-ADObject @commonParams @adParams -TargetPath $objectPath
}
finally {
if ($addProtection) {
@@ -1086,6 +1118,15 @@ Function Invoke-AnsibleADObject {
$module.Result.changed = $true
}
+ $protectFromDeletion = $null
+ if (
+ $setParams.ContainsKey('ProtectedFromAccidentalDeletion') -and
+ -not $setCommand.Parameters.ContainsKey('ProtectedFromAccidentalDeletion')
+ ) {
+ $protectFromDeletion = $setParams.ProtectedFromAccidentalDeletion
+ $setParams.Remove('ProtectedFromAccidentalDeletion')
+ }
+
if ($setParams.Count) {
try {
$finalADObject = & $setCommand @commonParams @setParams @adParams
@@ -1098,6 +1139,11 @@ Function Invoke-AnsibleADObject {
$module.Result.changed = $true
}
+ if ($null -ne $protectFromDeletion) {
+ $finalADObject = Set-ADObject -ProtectedFromAccidentalDeletion $protectFromDeletion @commonParams @adParams
+ $module.Result.changed = $true
+ }
+
# Won't be set in check mode
if ($finalADObject) {
$objectDN = $finalADObject.DistinguishedName
diff --git a/ansible_collections/microsoft/ad/plugins/modules/computer.py b/ansible_collections/microsoft/ad/plugins/modules/computer.py
index 498b882ba..ab336d6b4 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/computer.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/computer.py
@@ -184,6 +184,8 @@ notes:
- See R(win_domain_computer migration,ansible_collections.microsoft.ad.docsite.guide_migration.migrated_modules.win_domain_computer)
for help on migrating from M(community.windows.win_domain_computer) to this
module.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- microsoft.ad.ad_object
- ansible.builtin.action_common_attributes
@@ -223,12 +225,12 @@ EXAMPLES = r"""
- name: Remove linux computer from Active Directory using a windows machine
microsoft.ad.computer:
- name: one_linux_server
+ identity: one_linux_server
state: absent
- name: Add SPNs to computer
microsoft.ad.computer:
- name: TheComputer
+ identity: TheComputer
spn:
add:
- HOST/TheComputer
@@ -237,7 +239,7 @@ EXAMPLES = r"""
- name: Remove SPNs on the computer
microsoft.ad.computer:
- name: TheComputer
+ identity: TheComputer
spn:
remove:
- HOST/TheComputer
@@ -246,7 +248,7 @@ EXAMPLES = r"""
- name: Set the principals the computer trusts for delegation from
microsoft.ad.computer:
- name: TheComputer
+ identity: TheComputer
delegates:
set:
- CN=FileShare,OU=Computers,DC=domain,DC=test
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain.py b/ansible_collections/microsoft/ad/plugins/modules/domain.py
index 72d4fc21a..15578f7fd 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/domain.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain.py
@@ -78,6 +78,8 @@ options:
Sysvol file will be created.
- If not set then the default path is C(%SYSTEMROOT%\SYSVOL).
type: path
+notes:
+- This module must be run on a Windows target host.
extends_documentation_fragment:
- ansible.builtin.action_common_attributes
- ansible.builtin.action_common_attributes.flow
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
index 3ef2488bb..df4641741 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
@@ -92,6 +92,7 @@ notes:
- It is highly recommended to set I(reboot=true) to have Ansible manage the host reboot phase as the actions done by
this module puts the host in a state where it may not be possible for Ansible to reconnect in a subsequent task
without a reboot.
+- This module must be run on a Windows target host.
extends_documentation_fragment:
- ansible.builtin.action_common_attributes
- ansible.builtin.action_common_attributes.flow
diff --git a/ansible_collections/microsoft/ad/plugins/modules/group.py b/ansible_collections/microsoft/ad/plugins/modules/group.py
index d34e4584b..9fb28e819 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/group.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/group.py
@@ -90,6 +90,8 @@ notes:
- See R(win_group migration,ansible_collections.microsoft.ad.docsite.guide_migration.migrated_modules.win_domain_group)
for help on migrating from M(community.windows.win_domain_group) to this
module.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- microsoft.ad.ad_object
- ansible.builtin.action_common_attributes
@@ -118,12 +120,12 @@ author:
EXAMPLES = r"""
- name: Ensure a group exists
microsoft.ad.group:
- name: Cow
+ identity: Cow
scope: global
- name: Remove a group
microsoft.ad.group:
- name: Cow
+ identity: Cow
state: absent
- name: Create a group in a custom path
diff --git a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
index 2b37bcdfd..d2be34e9f 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
@@ -207,7 +207,27 @@ if ($state -eq 'domain') {
$joinParams.OUPath = $domainOUPath
}
- Add-Computer @joinParams
+ try {
+ Add-Computer @joinParams
+ }
+ catch {
+ $failMsg = [string]$_
+
+ # The error if the domain_ou_path does not exist is a bit
+ # vague, we try to catch that specific error type and provide
+ # a more helpful hint to what is wrong. As the exception does
+ # not have an error code to check, we compare the Win32 error
+ # code message with a localized variant for
+ # ERROR_FILE_NOT_FOUND. .NET Framework does not end with .
+ # whereas .NET 5+ does so we use regex to match both patterns.
+ # https://github.com/ansible-collections/microsoft.ad/issues/88
+ $fileNotFound = [System.ComponentModel.Win32Exception]::new(2).Message
+ if ($_.Exception.Message -match ".*$([Regex]::Escape($fileNotFound))\.?`$") {
+ $failMsg += " Check domain_ou_path is pointing to a valid OU in the target domain."
+ }
+
+ $module.FailJson($failMsg, $_)
+ }
$module.Result.changed = $true
$module.Result.reboot_required = $true
diff --git a/ansible_collections/microsoft/ad/plugins/modules/membership.py b/ansible_collections/microsoft/ad/plugins/modules/membership.py
index f4d8521cf..87b4c85dc 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/membership.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/membership.py
@@ -72,6 +72,8 @@ options:
description:
- When I(state=workgroup), this is the name of the workgroup that the Windows host should be in.
type: str
+notes:
+- This module must be run on a Windows target host.
extends_documentation_fragment:
- ansible.builtin.action_common_attributes
- ansible.builtin.action_common_attributes.flow
diff --git a/ansible_collections/microsoft/ad/plugins/modules/object.py b/ansible_collections/microsoft/ad/plugins/modules/object.py
index db7c7e5f6..c6396619a 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/object.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/object.py
@@ -24,6 +24,8 @@ notes:
Directory. It will not validate all the correct defaults are set for each
type when it is created. If a type specific module is available to manage
that AD object type it is recommend to use that.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- microsoft.ad.ad_object
- ansible.builtin.action_common_attributes
diff --git a/ansible_collections/microsoft/ad/plugins/modules/object_info.ps1 b/ansible_collections/microsoft/ad/plugins/modules/object_info.ps1
index d386417fd..4e304feeb 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/object_info.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/object_info.ps1
@@ -46,6 +46,14 @@ $properties = $module.Params.properties
$searchBase = $module.Params.search_base
$searchScope = $module.Params.search_scope
+# Attempt import of ActiveDirectory module
+try {
+ Import-Module -Name ActiveDirectory
+}
+catch {
+ $module.FailJson("The ActiveDirectory module failed to load properly: $($_.Exception.Message)", $_)
+}
+
$credential = $null
if ($domainUsername) {
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
@@ -223,7 +231,9 @@ try {
# We run this in a custom PowerShell pipeline so that users of this module can't use any of the variables defined
# above in their filter. While the cmdlet won't execute sub expressions we don't want anyone implicitly relying on
# a defined variable in this module in case we ever change the name or remove it.
- $ps = [PowerShell]::Create()
+ $iss = [InitialSessionState]::CreateDefault()
+ $iss.ImportPSModule("ActiveDirectory")
+ $ps = [PowerShell]::Create($iss)
$null = $ps.AddCommand('Get-ADObject').AddParameters($commonParams).AddParameters($getParams)
$null = $ps.AddCommand('Select-Object').AddParameter('Property', @('DistinguishedName', 'ObjectGUID'))
diff --git a/ansible_collections/microsoft/ad/plugins/modules/object_info.py b/ansible_collections/microsoft/ad/plugins/modules/object_info.py
index 0fe2f54ed..0cdcf06a7 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/object_info.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/object_info.py
@@ -16,12 +16,16 @@ options:
domain_password:
description:
- The password for I(domain_username).
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
domain_server:
description:
- Specified the Active Directory Domain Services instance to connect to.
- Can be in the form of an FQDN or NetBIOS name.
- If not specified then the value is based on the default domain of the computer running PowerShell.
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
domain_username:
description:
@@ -29,6 +33,8 @@ options:
- If this is not set then the user that is used for authentication will be the connection user.
- Ansible will be unable to use the connection user unless auth is Kerberos with credential delegation or CredSSP,
or become is used on the task.
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
type: str
filter:
description:
@@ -88,6 +94,8 @@ notes:
and C(userAccountControl_AnsibleFlags) return property is something set by the module itself as an easy way to view
what those flags represent. These properties cannot be used as part of the I(filter) or I(ldap_filter) and are
automatically added if those properties were requested.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- ansible.builtin.action_common_attributes
attributes:
diff --git a/ansible_collections/microsoft/ad/plugins/modules/offline_join.py b/ansible_collections/microsoft/ad/plugins/modules/offline_join.py
index 0b07bc36f..f0c8aa54e 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/offline_join.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/offline_join.py
@@ -85,6 +85,8 @@ notes:
- Generating a new blob will reset the password of the computer object, take
care that this isn't called under a computer account that has already been
joined.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
seealso:
- module: microsoft.ad.domain
- module: microsoft.ad.membership
diff --git a/ansible_collections/microsoft/ad/plugins/modules/ou.py b/ansible_collections/microsoft/ad/plugins/modules/ou.py
index d7ac85007..5d1d60503 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/ou.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/ou.py
@@ -49,6 +49,8 @@ notes:
specified.
- See R(win_domain_ou migration,ansible_collections.microsoft.ad.docsite.guide_migration.migrated_modules.win_domain_ou)
for help on migrating from M(community.windows.win_domain_ou) to this module.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- microsoft.ad.ad_object
- ansible.builtin.action_common_attributes
diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.ps1 b/ansible_collections/microsoft/ad/plugins/modules/user.ps1
index d975272c7..267c77627 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/user.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/user.ps1
@@ -39,6 +39,7 @@ Function Test-Credential {
$failed_codes = @(
0x0000052E, # ERROR_LOGON_FAILURE
0x00000532, # ERROR_PASSWORD_EXPIRED
+ 0x00000701, # ERROR_ACCOUNT_EXPIRED
0x00000773, # ERROR_PASSWORD_MUST_CHANGE
0x00000533 # ERROR_ACCOUNT_DISABLED
)
@@ -278,7 +279,7 @@ $setParams = @{
$SetParams.ServicePrincipalNames.Remove = $res.ToRemove
}
}
- $module.Diff.after.kerberos_encryption_types = @($res.Value | Sort-Object)
+ $module.Diff.after.spn = @($res.Value | Sort-Object)
}
}
diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.py b/ansible_collections/microsoft/ad/plugins/modules/user.py
index 30d1c6412..a3e7d1ecb 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/user.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/user.py
@@ -104,6 +104,10 @@ options:
- To clear all group memberships, use I(set) with an empty list.
- Note that users cannot be removed from their principal group (for
example, "Domain Users"). Attempting to do so will display a warning.
+ - Each subkey is set to a list of groups objects to add, remove or
+ set as the membership of this AD user respectively. A group can be in
+ the form of a C(distinguishedName), C(objectGUID), C(objectSid), or
+ C(sAMAccountName).
- See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values)
for more information on how to add/remove/set list options.
type: dict
@@ -221,7 +225,8 @@ options:
- C(always) will always update passwords.
- C(on_create) will only set the password for newly created users.
- C(when_changed) will only set the password when changed.
- - Using C(when_changed) will not work if the account is not enabled.
+ - Using C(when_changed) will not work if the account is not enabled or is
+ expired.
choices:
- always
- on_create
@@ -244,6 +249,8 @@ options:
notes:
- See R(win_domain_user migration,ansible_collections.microsoft.ad.docsite.guide_migration.migrated_modules.win_domain_user)
for help on migrating from M(community.windows.win_domain_user) to this module.
+- This module must be run on a Windows target host with the C(ActiveDirectory)
+ module installed.
extends_documentation_fragment:
- microsoft.ad.ad_object
- ansible.builtin.action_common_attributes
@@ -272,7 +279,7 @@ author:
EXAMPLES = r"""
- name: Ensure user bob is present with address information
microsoft.ad.user:
- name: bob
+ identity: bob
firstname: Bob
surname: Smith
company: BobCo
@@ -292,7 +299,7 @@ EXAMPLES = r"""
- name: Ensure user bob is created and use custom credentials to create the user
microsoft.ad.user:
- name: bob
+ identity: bob
firstname: Bob
surname: Smith
password: B0bP4ssw0rd
@@ -303,7 +310,7 @@ EXAMPLES = r"""
- name: Ensure user bob is present in OU ou=test,dc=domain,dc=local
microsoft.ad.user:
- name: bob
+ identity: bob
password: B0bP4ssw0rd
state: present
path: ou=test,dc=domain,dc=local
@@ -314,12 +321,12 @@ EXAMPLES = r"""
- name: Ensure user bob is absent
microsoft.ad.user:
- name: bob
+ identity: bob
state: absent
- name: Ensure user has only these spn's defined
microsoft.ad.user:
- name: liz.kenyon
+ identity: liz.kenyon
spn:
set:
- MSSQLSvc/us99db-svr95:1433
@@ -327,14 +334,14 @@ EXAMPLES = r"""
- name: Ensure user has spn added
microsoft.ad.user:
- name: liz.kenyon
+ identity: liz.kenyon
spn:
add:
- MSSQLSvc/us99db-svr95:2433
- name: Ensure user is created with delegates and spn's defined
microsoft.ad.user:
- name: shmemmmy
+ identity: shmemmmy
password: The3rubberducki33!
state: present
groups: