Files
2026-06-23 11:27:54 +08:00

608 lines
24 KiB
Python

# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import ldap3 as ldap
import logging
import re
from lib import certificate_utils as certutil
from lib.environment import Environment
from lib.certificate_utils import build_pem_certificate
from lib.command_runner import CommandRunner
from lib.console import print_task, print_text_error
from lib.host_utils import VcVersion, get_hostname, get_vc_version
from lib.ldap_utils import Ldap, LdapException, get_user_dn
from lib.text_utils import TextFilter
from lib.console import (print_task_status)
from lib.exceptions import OperationFailed, CommandExecutionError
DIR_CLI = '/usr/lib/vmware-vmafd/bin/dir-cli'
VDCADMINTOOL = '/usr/lib/vmware-vmdir/bin/vdcadmintool'
cache_ca_keyids = None
cache_ca_certificate_map = dict()
logger = logging.getLogger(__name__)
def get_ldap_connection(use_machine_account=False):
env = Environment.get_environment()
if use_machine_account:
user_dn = env.get_value('VMDIR_MACHINE_ACCOUNT_DN')
user_password = env.get_value('VMDIR_MACHINE_ACCOUNT_PASSWORD')
else:
sso_username = env.get_value('SSO_USERNAME')
user_password = env.get_value('SSO_PASSWORD')
user_dn = get_user_dn(sso_username)
hostname = get_hostname()
return Ldap.open_ldap_connection(node=hostname, user_dn=user_dn, password=user_password)
def close_ldap_connection(ldap_connection):
Ldap.close_ldap_connection(ldap_connection)
def perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.SUBTREE,
use_machine_account=False, throw_exception=False):
"""
Utility method to perform LDAP search
:param search_base: LDAP search base DN
:param search_filter: LDAP search filter
:param search_attributes: LDAP search attribute
:param search_scope: LDAP search scope (default: ldap.SUBTREE)
:param use_machine_account: Flag to use VMDir machine account instead of SSO user account
:param throw_exception: When LDAP search fails, throw exception instead of empty result
:return: list of matching object, as dict with keys from search_attributes paramerter
"""
results = []
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_search(connection, search_base, search_filter, search_scope, search_attributes):
logger.error("Unable to perform LDAP search base_dn={}, filter={}" \
.format(search_base, search_filter))
if throw_exception:
raise LdapException(connection.result['result'], connection.result['description'])
else:
return results
if connection.result:
for entry in connection.response:
attributes = entry['attributes']
if 'dn' in search_attributes and not attributes['dn']:
attributes['dn'] = entry['dn'].replace(', cn=', ',cn=')
results.append(attributes)
finally:
if connection:
close_ldap_connection(connection)
return results
def perform_ldap_add(service_dn, object_class, attributes, use_machine_account=False):
"""
A wrapper for LDAP modify operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_add(connection, service_dn, object_class, attributes):
logger.error("Unable to add LDAP entry dn={}, attributes={}".format(service_dn, str(attributes)))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def perform_ldap_modify(service_dn, attribute, value, operation=ldap.MODIFY_REPLACE,
use_machine_account=False):
"""
A wrapper for LDAP modify operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_modify(connection, service_dn, attribute, operation, value):
logger.error("Unable to modify LDAP entry dn={}, attribute={}".format(service_dn, attribute))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def perform_ldap_delete(service_dn, use_machine_account=False):
"""
A wrapper for LDAP delete operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_delete(connection, service_dn):
logger.error("Unable to delete LDAP entry dn={}".format(service_dn))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def get_solution_users():
"""
Get solution users list from VMDir via LDAP
:return: list of solution users
"""
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
domain_dn = env.get_value('SSO_DOMAIN_DN')
postfix = "-{}".format(machine_id)
search_base = "cn=ServicePrincipals,{}".format(domain_dn)
search_filter = "(&(objectClass=vmwServicePrincipal)(cn=*{}))".format(postfix)
search_attributes = ['cn']
results = perform_ldap_search(search_base, search_filter, search_attributes)
return [entry['cn'].replace(postfix, '') for entry in results]
def get_sts_tenant_certificates(include_tenant_credential=True,
include_certificate_chain=True):
"""
Get STS tenant user certificates
:return: dict(cn: certificate list)
"""
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
base_dn = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
if include_certificate_chain and include_tenant_credential:
ldap_filter = \
'(|(objectClass=vmwSTSTenantCredential)(&(objectclass=vmwSTSTenantTrustedCertificateChain)(cn=TrustedCertChain*)))'
elif include_tenant_credential:
ldap_filter = '(objectClass=vmwSTSTenantCredential)'
elif include_certificate_chain:
ldap_filter = '(&(objectclass=vmwSTSTenantTrustedCertificateChain)(cn=TrustedCertChain*))'
else:
return None
ldap_attributes = ['cn', 'userCertificate']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
tenant_certs = dict()
for entry in results:
tenant_certs[entry['cn']] = [build_pem_certificate(cert) for cert in entry['userCertificate']]
return tenant_certs
def get_solution_user_certificate(solution_user):
"""
Get solution user certificate from VMDir via LDAP
:param solution_user: solution user
:return: Solution user certificate in PEM format
"""
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
domain_dn = env.get_value('SSO_DOMAIN_DN')
base_dn = "cn={}-{},cn=ServicePrincipals,{}".format(solution_user, machine_id, domain_dn)
ldap_filter = '(objectClass=vmwServicePrincipal)'
ldap_attributes = ['userCertificate']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes, search_scope=ldap.BASE)
certs = []
for entry in results:
for cert in entry['userCertificate']:
# only expect single certificate
certs.append(build_pem_certificate(cert))
return '\n'.join(certs)
def get_all_ca_subject_keyids(use_cache=False):
"""
Get list of subject keyId of trusted CA certificates via 'dir-cli trustedcert list' output
The result will be cached
:param use_cache: If True, the previous cached result will be used (default: True)
:return: list of subject keyIds of trusted CA certificates
"""
global cache_ca_keyids
if use_cache and cache_ca_keyids is not None:
return cache_ca_keyids
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'list', '--login', sso_username, '--password', sso_password]
ret, stdout, _ = CommandRunner(*args).run()
cache_ca_keyids = TextFilter(stdout).start_with('CN(id):').cut(':', [1]).remove_white_spaces().get_lines()
return cache_ca_keyids
def get_ca_certificate(subject_keyid, use_cache=True):
"""
Get CA certificate in VMDir via 'dir-cli trustedcert get' command output
:param subject_keyid: the certificate' subject keyId
:param use_cache: if True, the previous cached result will be used instead
:return: trusted CA certificate in PEM format
"""
global cache_ca_certificate_map
if not subject_keyid:
return None
if use_cache:
cert_cache = cache_ca_certificate_map.get(subject_keyid)
if cert_cache:
return cert_cache
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'get', '--login', sso_username, '--password', sso_password,
'--id', subject_keyid, '--outcert', '/dev/stdout']
ret, stdout, _ = CommandRunner(*args).run()
lines = TextFilter(stdout).match_block('-----BEGIN CERTIFICATE-----',
'-----END CERTIFICATE-----').get_lines()
pem_cert = '\n'.join(lines)
cache_ca_certificate_map[subject_keyid] = pem_cert
return pem_cert
def get_all_ca_certificates():
"""
Get all trusted CA certificates in VMDir via dir-cli command)
:return: list of CA certificates in PEM format
"""
subject_keyids = get_all_ca_subject_keyids(False)
certs = []
for subject_keyid in subject_keyids:
pem_cert = get_ca_certificate(subject_keyid)
certs.append(pem_cert)
return certs, subject_keyids
def get_service_principals():
"""
Get service principal list from VMDir via dir-cli
:return: list of service principals
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'service', 'list', '--login', sso_username, '--password', sso_password]
ret, stdout, _ = CommandRunner(*args).run()
return TextFilter(stdout).cut(fields=[1]).get_lines()
def init_env_identity_source():
env = Environment.get_environment()
if env.get_map().get('SSO_USERNAME') is None or \
env.get_value('EXTERNAL_IDENTITY_SOURCE_CONFIGURED') is not None:
return
logger.info('Checking identity source settings')
identity_sources = get_identity_sources()
source_types = [item['type'] for item in identity_sources]
logger.info("Identity sources: {}".format(source_types))
env.set_value('EXTERNAL_IDENTITY_SOURCE_CONFIGURED', len(source_types) > 0)
def get_identity_sources(use_machine_account=False):
"""
Get the identity sources settings
return: list of configured identity source represented in the following dict object:
{
"type": one of ('AD over LDAP', 'ADFS', 'OpenLDAP')
"domain_name": domain name
"certificates": CA certificate for this identity source
}
"""
logger.info('Obtaining identity source settings')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_list = [
('AD over LDAP', 'IdentityProviders', 'IDENTITY_STORE_TYPE_LDAP_WITH_AD_MAPPING'),
('ADFS', 'VCIdentityProviders', 'IDENTITY_STORE_TYPE_LDAP_WITH_AD_MAPPING'),
('OpenLDAP', 'IdentityProviders', 'IDENTITY_STORE_TYPE_LDAP')
]
identity_sources = []
connection = None
try:
connection = get_ldap_connection(use_machine_account=use_machine_account)
ldap_attributes = ['vmwSTSDomainName', 'userCertificate']
for type_name, provider_cn, provider_type in search_list:
base_dn = "cn={},cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}"\
.format(provider_cn, sso_domain, domain_dn)
ldap_filter = "(vmwSTSProviderType={})".format(provider_type)
if not Ldap.ldap_search(connection, base_dn, ldap_filter, ldap.SUBTREE, ldap_attributes):
# this search may return noSuchObject due to an invalid base_dn
if connection.result['result'] == 32:
continue
logger.error('Unable to perform LDAP search for the identity source setting')
raise LdapException(connection.result['result'], connection.result['description'])
if not connection.result:
continue
for entry in connection.response:
attributes = entry['attributes']
result = dict()
result['type'] = type_name
result['domain_name'] = attributes['vmwSTSDomainName']
certs = []
for cert in attributes['userCertificate']:
# only expect single certificate
certs.append(build_pem_certificate(cert))
result['certificates'] = certs
identity_sources.append(result)
logger.info("Identity source: type={}, domain={}".format(type_name, result['domain_name']))
finally:
if connection:
close_ldap_connection(connection)
return identity_sources
def get_sso_domain_nodes():
logger.info('Obtaining SSO domain nodes')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
base_dn_list = ["ou=Domain Controllers,{}".format(domain_dn), "ou=Computers,{}".format(domain_dn)]
ldap_filter = '(objectClass=computer)'
ldap_attributes = ['cn']
nodes = []
for base_dn in base_dn_list:
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
for entry in results:
node = entry['cn']
if node not in nodes:
nodes.append(node)
logger.info("Found node: {}".format(node))
return nodes
def get_all_lookup_service_endpoints(search_base=None):
"""
Get all endpoint entries
:return:
"""
logger.info('Obtaining all endpoints')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
if search_base is None:
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = '(|(objectclass=vmwLKUPServiceEndpoint)(objectClass=vmwLKUPEndpointRegistration))'
search_attributes = ['dn', 'objectClass', 'vmwLKUPURI', 'vmwLKUPEndpointSslTrust', 'vmwLKUPSslTrustAnchor']
return perform_ldap_search(search_base, search_filter, search_attributes)
def get_node_trust_anchors(fqdn_or_ip):
"""
Get SSL trust anchors by filtering the endpoint URI using {fqdn_or_ip}
:param fqdn_or_ip:
:return:
"""
logger.info("Obtaining node trust anchors: fqdn_or_ip: {}".format(fqdn_or_ip))
trust_anchors = []
pattern = "^https://{0}:.*|^https://{0}/.*".format(fqdn_or_ip)
endpoints = get_all_lookup_service_endpoints()
for endpoint in endpoints:
if not re.match(pattern, endpoint['vmwLKUPURI']):
continue
for cert in sum([endpoint['vmwLKUPEndpointSslTrust'], endpoint['vmwLKUPSslTrustAnchor']], []):
pem_cert = certutil.build_pem_certificate(cert)
if pem_cert and pem_cert not in trust_anchors:
trust_anchors.append(pem_cert)
logger.info('Trust anchors found for {}: {}'.format(fqdn_or_ip, trust_anchors))
return trust_anchors
def get_endpoint_service_type(service_id):
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = "(cn={})".format(service_id)
search_attributes = ['vmwLKUPType']
result = perform_ldap_search(search_base, search_filter, search_attributes)
if result:
return result[0]['vmwLKUPType']
search_filter = '(objectClass=vmwLKUPService)'
search_attributes = ['dn', 'vmwLKUPServiceType']
result = perform_ldap_search(search_base, search_filter, search_attributes)
for entry in result:
if service_id in entry['dn']:
# remove prefix 'urn:'
return entry['vmwLKUPServiceType'][4:]
return ''
def get_registered_vcenters():
"""
Get registered VCenters
:return: list of tuple (deployment_node_id, node_dn)
"""
logger.info('Obtaining list of registered vCenters')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
base_dn = "cn=Sites,cn=Configuration,{}".format(domain_dn)
ldap_filter = '(vmwLKUPType=vcenterserver)'
ldap_attributes = ['vmwLKUPDeploymentNodeId', 'dn']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
return [(entry['vmwLKUPDeploymentNodeId'], entry['dn']) for entry in results]
def get_endpoint_registrations(base_dn):
ldap_filter = '(objectClass=vmwLKUPEndpointRegistration)'
ldap_attributes = ['vmwLKUPURI']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
return [entry['vmwLKUPURI'] for entry in results]
def init_env_cac():
env = Environment.get_environment()
# if env.get_map().get('SSO_USERNAME') is None or \
# env.get_value('CAC_CONFIGURED') is not None:
# return
if env.get_value('CAC_CONFIGURED') is not None:
return
vc_version = get_vc_version()
if vc_version >= VcVersion.V9:
logger.info('SmartCards are not supported since version {}'.format(VcVersion.V9.value))
env.set_value('CAC_CONFIGURED', False)
return
logger.info('Checking SmartCard Authentication settings')
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectclass=vmwSTSTenant)'
search_attributes = ['vmwSTSAuthnTypes']
results = perform_ldap_search(search_base, search_filter, search_attributes, use_machine_account=True)
is_cac_configured = False
logger.info('LDAP search results: {}'.format(results))
for entry in results:
if entry['vmwSTSAuthnTypes'] == 4 or 4 in entry['vmwSTSAuthnTypes']:
is_cac_configured = True
env.set_value('CAC_CONFIGURED', is_cac_configured)
def get_smart_card_issuing_ca_certs():
logger.info('Obtaining SmartCard issuing CA certificates')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = \
"cn=DefaultClientCertCAStore,cn=ClientCertAuthnTrustedCAs,cn=Default,cn=ClientCertificatePolicies,"\
"cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectClass=vmwSTSTenantTrustedCertificateChain)'
search_attributes = ['userCertificate']
result = perform_ldap_search(search_base, search_filter, search_attributes, use_machine_account=True)
certs = []
for entry in result:
certs.extend(build_pem_certificate(cert) for cert in entry['userCertificate'])
return certs
def get_all_sso_sites():
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = '(objectClass=*)'
search_attributes = ['cn']
result = perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.LEVEL)
return [entry['cn'] for entry in result]
def unpublish_trusted_certificate(cert_file):
"""
Unpublish certificate in VMDir
:param cert_file: Certificate file to unpublish
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
CommandRunner(DIR_CLI, 'trustedcert', 'unpublish', '--login', sso_username, '--password',
sso_password, '--cert', cert_file, expected_return_code=0).run()
def publish_trusted_certificate(cert_file, is_chain=False):
"""
Publish certificate in VMDir
:param cert_file: Certificate file to unpublish
:param is_chain: Publish all certificate in chain
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'publish', '--login', sso_username, '--password', sso_password,
'--cert', cert_file]
if is_chain:
args.append('--chain')
CommandRunner(*args, expected_return_code=0).run()
def remove_ca_certificate_from_ldap(subject_keyid):
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
cert_dn = "cn={},cn=Certificate-Authorities,cn=Configuration,{}".format(subject_keyid, domain_dn)
perform_ldap_delete(cert_dn)
def get_sddc_manager():
# SDDC_MANAGER=$($LDAP_SEARCH -LLL -h $PSC_LOCATION -b
# "cn=$SSO_DOMAIN,cn=Tenants,cn=IdentityManager,cn=Services,$VMDIR_DOMAIN_DN"
# -D "$VMDIR_MACHINE_ACCOUNT_DN" -y $STAGE_DIR/.machine-account-password '(objectclass=vmwSTSTenant)'
# vmwSTSLogonBanner | tr -d '\n' | awk -F'::' '{print $NF}' | tr -d ' ' | base64 -d 2>/dev/null | grep 'SDDC Manager' | awk -F '[()]' '{print $2}' | grep -v '^$')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectClass=vmwSTSTenant)'
search_attributes = ['vmwSTSLogonBanner']
result = perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.LEVEL)
return [entry['cn'] for entry in result]
def update_solution_user_certificate_in_vmdir(soluser, cert_file):
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
machine_id = env.get_value('MACHINE_ID')
args = [DIR_CLI, 'service', 'update', '--name', f"{soluser}-{machine_id}", '--cert',
cert_file, '--login', sso_username, '--password', sso_password]
try:
CommandRunner(*args, expected_return_code=0).run_and_get_output()
except CommandExecutionError:
error_message = f"Unable to update {soluser}-{machine_id} solution user certificate in VMDir"
logger.error(error_message)
raise OperationFailed(error_message)
# ------------------------------
# Replace a Solution User certificate in VMDir
# ------------------------------
def replace_service_principal_certificates(soluser, cert_file):
print_task(f" {soluser}")
update_solution_user_certificate_in_vmdir(soluser, cert_file)
print_task_status("OK")
def verify_service_principals():
print_task('Verifying Service Principal entries exist')
service_principals = get_service_principals()
if service_principals:
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
solution_users = env.get_value('SOLUTION_USERS')
missing_service_principals = []
for solution_user in solution_users:
service_principal = "{}-{}".format(solution_user, machine_id)
if service_principal not in service_principals:
missing_service_principals.append(service_principal)
if missing_service_principals:
print_text_error('ERROR')
print_text_error("\n------------------------!!! Attention !!!------------------------ ")
print_text_error('The following Service Principal entries are missing:')
for sp in missing_service_principals:
print_text_error(" - {}".format(sp))
print_text_error('\nPlease refer to the following Knowledge Base article')
print_text_error('on using the lsdoctor utility to recreate the missing')
print_text_error('Solution User/Service Principal entries:')
print_text_error('https://knowledge.broadcom.com/external/article/320837/using-the-lsdoctor-tool.html')
else:
print_task_status("OK")
else:
print_text_error('Could not get list of Service Principal entries from VMware Directory')
def get_vmdir_state():
state_output = CommandRunner(VDCADMINTOOL, command_input='6', expected_return_code=0).run_and_get_output()
logger.info('VMware Directory state: {}'.format(state_output))
service_state = TextFilter(state_output).contain('VmDir State').cut('-', [1]).get_text().strip()
return service_state