Files
vmware-tools/vCert-6.1.1-20260401/lib/ldap_utils.py
T
2026-06-23 11:27:54 +08:00

267 lines
11 KiB
Python

# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import copy
import hashlib
import json
import logging
import ldap3 as ldap
from lib.environment import Environment
from lib.exceptions import LdapException
from lib.execution_replay import ReplayContext
logger = logging.getLogger(__name__)
class Ldap:
@staticmethod
def open_ldap_connection(node, user_dn, password):
"""
Open ldap connection to the ldap server provided with
ldap admin user credentials.
:param node: hostname of the ldap server
:param user_dn: User DN for authentication
:param password: Password for authentication
:return:
"""
logger.info("Opening connection to {} with user {}".format(node, user_dn))
server = ldap.Server(get_uri_from_hostname(node), get_info=ldap.ALL)
env = Environment.get_environment()
is_remote_exec = env.get_value('VCERT_REMOTE_EXEC') is True
is_replay = env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True
is_capture = env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True
if is_remote_exec and (is_replay or is_capture):
context = ReplayContext.get_replay_context()
ldap_connection = LdapConnectionReplay(context, server, user_dn, password)
else:
ldap_connection = ldap.Connection(server, user=user_dn, password=password)
if not ldap_connection.bind():
logger.error("Failed to do LDAP bind with host %s with %s error",
node, ldap_connection.result['description'])
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
return ldap_connection
@staticmethod
def close_ldap_connection(ldap_connection) -> None:
"""
Close the ldap bind connection
:param ldap_connection:ldap connection to be closed
:return:
"""
logger.info("Closing LDAP connection")
ldap_connection.unbind()
if not ldap_connection.closed:
logger.error("Error closing connection. Error Msg: %s", ldap_connection.result["message"])
@staticmethod
def ldap_search(ldap_connection, base_dn, ldap_filter, ldap_scope, ldap_attributes) -> bool:
"""
This method takes the ldap connection, base dn, filter , scope
and list of ldap attributes to be returned.
:param ldap_connection connection to ldap server
:param base_dn dn where the search starts
:param ldap_filter to filter the search the results
:param ldap_scope scope of the search
:param ldap_attributes list of ldap attributes to be returned
"""
logger.info("LDAP search with\n base DN:%s\n filter: %s\n scope: %s\n attributes: %s",
base_dn, ldap_filter, str(ldap_scope), str(" ".join(ldap_attributes)))
if ldap_connection is None:
raise LdapException("-1", "No LDAP connection")
else:
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
result = ldap_connection.search(base_dn, ldap_filter, ldap_scope, attributes=ldap_attributes)
# When a filter doesn't match any entry result will be false
if result or (ldap_connection.result and ldap_connection.result['result'] == 0):
return True
logger.error("LDAP search failed. Error message: %s", ldap_connection.result["message"])
return False
@staticmethod
def get_attribute(ldap_entry, attribute) -> str:
"""
get the attribute value for a given ldap entry
This function can only be used for single value attributes.
:param ldap_entry: ldap entry
:param attribute: attribute to be returned
:return: value which is string
"""
val = ldap_entry['attributes'][attribute]
if isinstance(val, str):
return val
return val[0]
@staticmethod
def ldap_delete(ldap_connection, entry_dn):
"""
This method takes ldap connection and deletes a given entry
:param ldap_connection: LDAP connection
:param entry_dn: entry DN to be delete
:return:
"""
logger.info("LDAP delete with DN: %s", entry_dn)
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No LDAP connection")
else:
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
if ldap_connection.delete(entry_dn):
logger.debug("Deleted entry DN: %s", entry_dn)
return True
else:
if ldap_connection.result['result'] == 32:
logger.debug("Entry to be deleted %s doesn't exist", entry_dn)
return True
logger.error("Error deleting entry DN: %s, error code %s error msg %s",
entry_dn, ldap_connection.result['result'],
ldap_connection.result['description'])
return False
@staticmethod
def ldap_modify(ldap_connection, dn, attribute, operation, value=None):
"""
This method takes ldap connection, DN and single attribute modification
for that particular DN
:param ldap_connection:
:param dn:
:param attribute:
:param operation:
:param value:
:return:
"""
logger.info("LDAP modify for DN %s with\n attribute: %s\n operation: %s\n value: %s",
dn, attribute, operation, value)
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No Ldap connection")
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
if value is None:
value = []
changes = [(operation, value)] if type(value) is list else [(operation, [value])]
result = ldap_connection.modify(dn, {attribute: changes})
if result:
logger.info("Modified DN:%s", dn)
return True
logger.error("Modifying entry %s failed with error code %s, description %s",
dn, ldap_connection.result['result'], ldap_connection.result['description'])
return False
@staticmethod
def ldap_add(ldap_connection, dn, object_class, attributes):
"""
Add new LDAP entry with DN and attributes {attributes}
:param ldap_connection: LDAP connection
:param dn: DN to add
:param attributes: attributes
"""
logger.info("LDAP add for DN %s with\n attributes: %s\n", dn, str(attributes))
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No Ldap connection")
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
result = ldap_connection.add(dn, object_class, attributes)
if result:
logger.info("Added DN:%s", dn)
return True
logger.error("Adding entry %s failed with error code %s, description %s",
dn, ldap_connection.result['result'], ldap_connection.result['description'])
return False
class LdapConnectionReplay(object):
"""
Class for capturing and replaying the previous LDAP operation results
"""
def __init__(self, context: ReplayContext, server, user_dn, password):
self.context = context
self.server = server
self.user_dn = user_dn
self.password = password
self.credential_hash = hashlib.sha1("{}:{}".format(user_dn, password).encode('utf-8')).hexdigest()
self.ldap_connection = ldap.Connection(server, user_dn, password) if self.context.is_capturing else None
self.closed = True
self.result = None
self.response = None
def operation_wrapper(self, name, func, args, pass_connection=True):
final_args = [name]
final_args.extend(args)
if self.context.is_replaying:
prev_result = self.context.get_execution_result('ldap', final_args, self.credential_hash)
if prev_result is not None:
return_value, result_text, _ = prev_result
result = ReplayContext.decode_bytes(json.loads(result_text))
self.result = result['_result_']
self.response = result['_response_']
self.closed = result['_closed_']
if self.ldap_connection and func == self.ldap_connection.unbind:
self.ldap_connection.unbind()
return return_value
return_value = func(self.ldap_connection, *args) if pass_connection else func(*args)
self.result = self.ldap_connection.result
self.response = self.ldap_connection.response
self.closed = self.ldap_connection.closed
result = dict()
result['_result_'] = ReplayContext.encode_bytes(copy.deepcopy(self.result))
result['_response_'] = ReplayContext.encode_bytes(copy.deepcopy(self.response))
result['_closed_'] = self.closed
try:
result_text = json.dumps(result, skipkeys=True)
except TypeError as te:
raise LdapException("Unserializable object: {}".format(result), str(te))
self.context.store_result('ldap', final_args, self.credential_hash, return_value, result_text, None)
return return_value
def bind(self):
func = self.ldap_connection.bind if self.ldap_connection else None
return self.operation_wrapper('bind', func, [], False)
def unbind(self):
func = self.ldap_connection.unbind if self.ldap_connection else None
return self.operation_wrapper('unbind', func, [], False)
def search(self, search_base, search_filter, search_scope, attributes):
args = [search_base, search_filter, search_scope, attributes]
return self.operation_wrapper('search', Ldap.ldap_search, args)
def modify(self, dn, changes):
attribute = list(changes.keys())[0]
operation, values = changes[attribute][0]
args = [dn, attribute, operation, values]
return self.operation_wrapper('modify', Ldap.ldap_modify, args)
def delete(self, dn):
return self.operation_wrapper('delete', Ldap.ldap_delete, [dn])
def add(self, dn, object_class, attributes):
return self.operation_wrapper('add', Ldap.ldap_add, [dn, object_class, attributes])
def get_uri_from_hostname(hostname):
return "ldap://{}".format(hostname)
def get_domain_dn(domain):
if '@' in domain:
domain = domain.split('@', 2)[1]
domain_parts = domain.split('.')
return "dc={}".format(",dc=".join(domain_parts))
def get_user_dn(user_upn: str):
username, domain = tuple(user_upn.split('@'))
domain_dn = get_domain_dn(domain)
return "cn={},cn=users,{}".format(username, domain_dn)