-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
193 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
# Copyright: (c) 2024, Ansible Project | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
|
||
DOCUMENTATION = r""" | ||
name: laps | ||
author: Nico Ohnezat (@no-12) | ||
short_description: Inventory plugin for Active Directory | ||
version_added: 2.2.0 | ||
description: | ||
- Lookup plugin that retrieves the LAPS password information for multiple hosts from an Active Directory server. | ||
options: | ||
_terms: | ||
description: One or multiple C(dNSHostName) of the computer objects to search for. | ||
required: True | ||
laps: | ||
description: | ||
- The LAPS password type to retrieve. | ||
- Defaults to the C(auto). This will attempt to retrieve the LAPS password in the following order: C(windows_encrypted), C(windows_plain_text), C(legacy_microsoft). | ||
type: str | ||
choices: | ||
- auto | ||
- windows_encrypted | ||
- windows_plain_text | ||
- legacy_microsoft | ||
default: auto | ||
search_base: | ||
description: | ||
- The LDAP search base to find the computer objects in. | ||
- Defaults to the C(defaultNamingContext) of the Active Directory server | ||
if not specified. | ||
- If searching a larger Active Directory database, it is recommended to | ||
narrow the search base to speed up the queries. | ||
type: str | ||
search_scope: | ||
description: | ||
- The scope of the LDAP search to perform. | ||
- C(base) will search only the current path or object specified by | ||
I(search_base). This is typically not useful for inventory plugins. | ||
- C(one_level) will search only the immediate child objects in | ||
I(search_base). | ||
- C(subtree) will search the immediate child objects and any nested | ||
objects in I(search_base). | ||
choices: | ||
- base | ||
- one_level | ||
- subtree | ||
default: subtree | ||
type: str | ||
notes: | ||
- This plugin is a tech preview and the module options are subject to change | ||
based on feedback received. | ||
extends_documentation_fragment: | ||
- microsoft.ad.ldap_connection | ||
""" | ||
|
||
import json | ||
|
||
from ansible.errors import AnsibleError | ||
from ansible.module_utils.basic import missing_required_lib | ||
from ansible.plugins.lookup import LookupBase | ||
|
||
try: | ||
import sansldap | ||
|
||
from ..plugin_utils._ldap import create_ldap_connection | ||
from ..plugin_utils._ldap.schema import LDAPSchema | ||
from ..plugin_utils._ldap.laps import LAPSDecryptor | ||
from ..filter.ldap_converters import as_datetime | ||
|
||
HAS_LDAP = True | ||
LDAP_IMP_ERR = None | ||
except Exception as e: | ||
HAS_LDAP = False | ||
LDAP_IMP_ERR = e | ||
|
||
|
||
class LookupModule(LookupBase): | ||
NAME = "microsoft.ad.laps" | ||
|
||
def _parse_value(self, parser, values): | ||
if values: | ||
return parser(values[0]) | ||
return None | ||
|
||
def run(self, terms, variables=None, **kwargs): | ||
self.set_options(var_options=variables, direct=kwargs) | ||
|
||
if not HAS_LDAP: | ||
msg = missing_required_lib( | ||
"sansldap and pyspnego", | ||
url="https://pypi.org/project/sansldap/ and https://pypi.org/project/pyspnego/", | ||
reason="for ldap lookups", | ||
) | ||
raise AnsibleError(f"{msg}: {LDAP_IMP_ERR}") from LDAP_IMP_ERR | ||
|
||
laps = self.get_option("laps") | ||
search_base = self.get_option("search_base") | ||
search_scope = self.get_option("search_scope") | ||
ldap_search_scope = { | ||
"base": sansldap.SearchScope.BASE, | ||
"one_level": sansldap.SearchScope.ONE_LEVEL, | ||
"subtree": sansldap.SearchScope.SUBTREE, | ||
}[search_scope] | ||
|
||
computer_filter = sansldap.FilterEquality("objectClass", b"computer") | ||
dnshostname_filter = sansldap.FilterOr( | ||
filters=[ | ||
sansldap.FilterEquality("dnshostname", bytes(t, "utf-8")) for t in terms | ||
] | ||
) | ||
final_filter = sansldap.FilterAnd(filters=[computer_filter, dnshostname_filter]) | ||
|
||
attributes = { | ||
"dnshostname", | ||
"mslaps-encryptedpassword", | ||
"mslaps-password", | ||
"mslaps-passwordexpirationtime", | ||
"ms-mcs-admpwd", | ||
"ms-mcs-admpwdexpirationtime", | ||
} | ||
|
||
connection_options = self.get_options() | ||
laps_decryptor = LAPSDecryptor(**connection_options) | ||
return_value = [] | ||
with create_ldap_connection(**connection_options) as client: | ||
for _, info in client.search( | ||
filter=final_filter, | ||
attributes=list(attributes), | ||
search_base=search_base, | ||
search_scope=ldap_search_scope, | ||
).items(): | ||
insensitive_info = {k.lower(): v for k, v in info.items()} | ||
|
||
hostname = self._parse_value( | ||
bytes.decode, insensitive_info.get("dnshostname") | ||
) | ||
|
||
result = { | ||
"hostname": hostname, | ||
"laps": None, | ||
} | ||
|
||
if laps in ["auto", "windows_encrypted"]: | ||
raw_mslaps_encrypted_password = self._parse_value( | ||
laps_decryptor.decrypt, | ||
insensitive_info.get("mslaps-encryptedpassword"), | ||
) | ||
if raw_mslaps_encrypted_password: | ||
result["laps"] = "windows_encrypted" | ||
parsed_mslaps_password = json.loads(raw_mslaps_password) | ||
result["laps_username"] = parsed_mslaps_password.get("n") | ||
result["laps_password"] = parsed_mslaps_password.get("p") | ||
result["laps_password_expiration_time"] = self._parse_value( | ||
as_datetime, | ||
insensitive_info.get("mslaps-passwordexpirationtime"), | ||
) | ||
return_value.append(result) | ||
continue | ||
|
||
if laps in ["auto", "windows_plain_text"]: | ||
raw_mslaps_password = self._parse_value( | ||
bytes.decode, insensitive_info.get("mslaps-password") | ||
) | ||
if raw_mslaps_password: | ||
result["laps"] = "windows_plain_text" | ||
parsed_mslaps_password = json.loads(raw_mslaps_password) | ||
result["laps_username"] = parsed_mslaps_password.get("n") | ||
result["laps_password"] = parsed_mslaps_password.get("p") | ||
result["laps_password_expiration_time"] = self._parse_value( | ||
as_datetime, | ||
insensitive_info.get("mslaps-passwordexpirationtime"), | ||
) | ||
return_value.append(result) | ||
continue | ||
|
||
if laps in ["auto", "legacy_microsoft"]: | ||
legacy_laps_password = self._parse_value( | ||
bytes.decode, insensitive_info.get("ms-mcs-admpwd") | ||
) | ||
if legacy_laps_password: | ||
result["laps"] = "legacy_microsoft" | ||
result["laps_username"] = "Administrator" | ||
result["laps_password"] = legacy_laps_password | ||
result["laps_password_expiration_time"] = self._parse_value( | ||
as_datetime, | ||
insensitive_info.get("ms-mcs-admpwdexpirationtime"), | ||
) | ||
return_value.append(result) | ||
continue | ||
|
||
return_value.append(result) | ||
|
||
return return_value |