Source code for pyad.pyadutils

import datetime
import win32api
import win32security
import pywintypes

from .adbase import ADBase
from .pyadconstants import GENERIC_ADSI_ERRORS, GENERIC_COM_ERRORS, WIN32_ERRORS
from .pyadexceptions import genericADSIException, comException, win32Exception


[docs]def validate_credentials( username: str, password: str, domain: str = None ) -> "win32.PyHandle": """ validate_credentials Validates credentials; returns a PyHANDLE object with a bool value of True if the credentials are valid, else returns None. Note that if the user would not be able to log on; for example, due to the account being expired; None will be returned. :param username: username :type username: str :param password: password :type password: str :param domain: domain name, defaults to None :type domain: str, optional :return: PyHandle Object with a bool of true. or None :rtype: win32.PyHandle """ try: valid = win32security.LogonUser( username, domain, password, win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT, ) assert valid, "Valid should ALWAYS have a true value" return valid except pywintypes.error: return None
[docs]def convert_error_code(error_code: int) -> int: """ Convert error code from the format returned by pywin32 to the format that Microsoft documents everything in. :param error_code: error code :type error_code: int :return: The error code in the format Microsoft documents it :rtype: int """ return error_code % 2**32
[docs]def interpret_com_exception( excp: "pywintype.com_error", additional_info: dict = {} ) -> dict: """ Convert a pywin32 com_error exception into a dictionary of error information. :param excp: pywin32 com_error exception :type excp: pywintype.com_error :param additional_info: any additional information with the error, defaults to {} :type additional_info: dict, optional :return: a dictionary of error information :rtype: dict """ d = {} d["error_num"] = convert_error_code(excp.args[2][5]) # for some reason hex() includes the L for long in the hex... # however since it's a string, we don't care... # since L would never be in a hex code, we can safely just remove it. d["error_code"] = hex(d["error_num"]).rstrip("L") if d["error_code"][0:7] == "0x80005": if d["error_num"] in list(GENERIC_ADSI_ERRORS.keys()): d["exception_type"] = "known_generic_adsi_error" d["error_constant"] = GENERIC_ADSI_ERRORS[d["error_num"]][0] d["message"] = " ".join(GENERIC_ADSI_ERRORS[d["error_num"]][1:3]) else: # this supposedly should not happen, but I'd rather be ready for # the case that Microsoft made a typo somewhere than die weirdly. d["error_constant"] = None d["exception_type"] = "unknown_generic_adsi_error" d["message"] = "unknown generic ADSI error" d["exception"] = genericADSIException elif d["error_code"][0:6] == "0x8007": d["exception_type"] = "win32_error" d["error_constant"] = None # returns information about error from winerror.h file... d["message"] = win32api.FormatMessage(d["error_num"]) elif d["error_num"] in list(GENERIC_COM_ERRORS.keys()): d["exception_type"] = "generic_com_error" d["error_constant"] = GENERIC_COM_ERRORS[d["error_num"]][0] d["message"] = GENERIC_COM_ERRORS[d["error_num"]][1] else: d["exception_type"] = "unknown" d["error_constant"] = None d["message"] = excp.args[2][4] d["additional_info"] = additional_info = {} return d
[docs]def pass_up_com_exception(excp: "pywintype.com_error", additional_info: dict = {}): """ reparse the com_error into a sane exception and raise it. :param excp: the com_error exception :type excp: pywintype.com_error :param additional_info: Additional exception details, defaults to {} :type additional_info: dict, optional :raises excp: if we don't know how to handle the exception raise the original exception """ if excp.__class__ in (genericADSIException, comException, win32Exception): raise excp else: info = interpret_com_exception(excp) type_ = info["exception_type"] if type_ == "win32_error": # raise exception defined in WIN32_ERRORs if there is one... # otherwise, just raise a generic win32Exception raise WIN32_ERRORS.get(info["error_num"], win32Exception)( error_info=info, additional_info=additional_info ) elif type_ == "known_generic_adsi_error": raise GENERIC_ADSI_ERRORS[info["error_num"]][3]( error_info=info, additional_info=additional_info ) elif type_ == "unknown_generic_adsi_error": raise genericADSIException(error_info=info, additional_info=additional_info) else: raise comException(error_info=info, additional_info=additional_info)
[docs]def convert_datetime(adsi_time_com_obj): """ Converts 64-bit integer COM object representing time into a python datetime object. Credit goes to John Nielsen who documented this at `<http://docs.activestate.com/activepython/2.6/pywin32/html/com/help/active_directory.html>`_. """ if not hasattr(adsi_time_com_obj, "highpart") or not hasattr( adsi_time_com_obj, "lowpart" ): raise ValueError( f"Expected adsi_time object got '{adsi_time_com_obj.__class__.__name__}'" ) high_part = int(adsi_time_com_obj.highpart) << 32 low_part = int(adsi_time_com_obj.lowpart) date_value = ((high_part + low_part) - 116444736000000000) // 10000000 # # The "fromtimestamp" function in datetime cannot take a # negative value, so if the resulting date value is negative, # explicitly set it to 18000. This will result in the date # 1970-01-01 00:00:00 being returned from this function # if date_value < 0: date_value = 18000 return datetime.datetime.fromtimestamp(date_value)
[docs]def convert_bigint(obj) -> int: """ Converts a ADSI time object to an integer. based on http://www.selfadsi.org/ads-attributes/user-usnChanged.htm :param obj: the AD bigint object :raises AttributeError: invalid object type :return: the decimal value of the object :rtype: int """ if hasattr(obj, "HighPart") and hasattr(obj, "LowPart"): h, l = obj.HighPart, obj.LowPart if l < 0: h += 1 return (h << 32) + l else: raise AttributeError( f"Expected adsi time object got '{obj.__class__.__name__}'" )
[docs]def convert_timespan(obj) -> datetime.timedelta: """ Converts COM object representing time span to a python time span object. :param obj: ADSI time span object :return: the python timedelta object :rtype: datetime.timedelta """ as_seconds = ( abs(convert_bigint(obj)) / 10000000 ) # number of 100 nanoseconds in a second return datetime.timedelta(seconds=as_seconds)
[docs]def convert_guid(guid_object): return pywintypes.IID(guid_object, True)
[docs]def convert_sid(sid_object): return pywintypes.SID(bytes(sid_object))
[docs]def generate_list(input) -> list: """ converts a set or tuple to a list or returns the input in a list if it is not a list. :param input: a list like object or any :type input: list, set, tuple, Any :return: a list :rtype: list """ if type(input) is list: return input elif type(input) in (set, tuple): return list(input) else: return [ input, ]
[docs]def escape_path(path: str) -> str: """ escapes a path for use in ADSI. :param path: the raw path to escape :type path: str :return: the escaped path :rtype: str """ escapes = ( ("\+", "+"), ("\*", "*"), ("\(", "("), ("\)", ")"), ("\/", "/"), ("\\,", ",,"), ("\\", "\\5c"), ("*", "\\2a"), ("(", "\\28"), (")", "\\29"), ("/", "\\2f"), ("+", "\\2b"), (chr(0), "\\00"), ) for char, escape in escapes: path = path.replace(char, escape) path = path.replace(",,", "\\2c") return path
[docs]def generate_ads_path( distinguished_name: str, type: str, server: str = None, port: int = None ) -> str: """ Generates a proper ADsPath to be used when connecting to an active directory object or when searching active directory. :param distinguished_name: DN of object or search base such as 'cn=John Smith,cn=users,dc=example,dc=com' :type distinguished_name: str :param type: the connection type, either 'LDAP', 'LDAPS', or 'GC' :type type: str :param server: FQDN of domain controller if necessary to connect to a particular server, defaults to the global catalog server :type server: str, optional :param port: port number for directory service if not default port. If port is specified, server must be specified. :type port: int, optional :raises TypeError: Invalid type for type :return: the ADsPath to be used when connecting to Active Directory :rtype: str """ if type == "LDAP" or type == "LDAPS": server = server if server else ADBase.default_ldap_server port = port if port else ADBase.default_ldap_port elif type == "GC": server = server if server else ADBase.default_gc_server port = port if port else ADBase.default_gc_port else: raise TypeError("Invalid type specified.") ads_path = "".join((type, "://")) if server: ads_path = "".join((ads_path, server)) if port: ads_path = ":".join((ads_path, str(port))) ads_path = "".join((ads_path, "/")) ads_path = "".join((ads_path, escape_path(distinguished_name))) return ads_path