diff --git a/common/vm_check_vmtools_capability.yml b/common/vm_check_vmtools_capability.yml new file mode 100644 index 000000000..73945027e --- /dev/null +++ b/common/vm_check_vmtools_capability.yml @@ -0,0 +1,24 @@ +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +--- +# Description: +# Check VMware Tools capability for host verified SAML token is TRUE in VM +# advanced settings +# +- name: "Initialize fact of VMware Tools capability for host verified SAML token" + ansible.builtin.set_fact: + vmtools_capability_key: "tools.capability.verifiedSamlToken" + +- name: "Get VM's extra configs" + include_tasks: vm_get_extra_config.yml + +- name: "Check VMware Tools capability {{ vmtools_capability_key }} is TRUE" + ansible.builtin.assert: + that: + - vmtools_capability_key in vm_extra_config + - vm_extra_config[vmtools_capability_key] == "TRUE" + fail_msg: >- + VMware Tools capability '{{ vmtools_capability_key }} = TRUE' doesn't exsit in VM's advanced settings. + Current {{ vmtools_capability_key }} is {{ vm_extra_config[vmtools_capability_key] | default('undefined') }} + success_msg: >- + VMware Tools capability '{{ vmtools_capability_key }}' is TRUE in VM's advanced settings diff --git a/linux/gosv_testcase_list.yml b/linux/gosv_testcase_list.yml index 92e07c804..333b3ef59 100644 --- a/linux/gosv_testcase_list.yml +++ b/linux/gosv_testcase_list.yml @@ -7,6 +7,7 @@ - import_playbook: open_vm_tools/ovt_verify_src_install.yml - import_playbook: open_vm_tools/ovt_verify_status.yml - import_playbook: vgauth_check_service/vgauth_check_service.yml +- import_playbook: host_verify_saml_token/host_verify_saml_token.yml - import_playbook: check_ip_address/check_ip_address.yml - import_playbook: check_os_fullname/check_os_fullname.yml - import_playbook: stat_balloon/stat_balloon.yml diff --git a/linux/host_verify_saml_token/check_host_verified_token.yml b/linux/host_verify_saml_token/check_host_verified_token.yml new file mode 100644 index 000000000..39bb60868 --- /dev/null +++ b/linux/host_verify_saml_token/check_host_verified_token.yml @@ -0,0 +1,53 @@ +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +--- +# Description: +# Check VC SSO user's SAML token munged (signature value is replaced) and token is verified by host +# in VGAuthService log file +# Parameter: +# vgauth_log_path_local: The collected VGAuthService log file at localhost +# +- name: "Set facts of keywords for host verified SMAL token" + ansible.builtin.set_fact: + munged_signature: 'JUhPU1RfVkVSSUZJRURfU0lHTkFUVVJFJQA=' + host_verified_value: "hostVerified 'TRUE'" + host_verified_result: 'VerifySignature: token is hostVerified, skipping signature check' + +- name: "Look for keywords about hostVerified value" + ansible.builtin.shell: + cmd: "grep -o -e \"{{ host_verified_value }}\" '{{ vgauth_log_path_local }}'" + ignore_errors: true + register: get_host_verified_value + +- name: "Check hostVerified is 'TRUE'" + ansible.builtin.assert: + that: + - get_host_verified_value.rc is defined + - get_host_verified_value.rc == 0 + - get_host_verified_value.stdout_lines is defined + - get_host_verified_value.stdout_lines | length == 1 + - get_host_verified_value.stdout_lines[0] == host_verified_value + fail_msg: "Failed to find {{ host_verified_value }} in VGAuthService log" + success_msg: "Found {{ host_verified_value }} in VGAuthService log" + +# Flatcar can't enable debug mode, so no debug log checking +- name: "Check SAML token is munged and verified by host" + when: guest_os_ansible_distribution != 'Flatcar' + block: + - name: "Look for keywords about host verified SAML token" + ansible.builtin.shell: + cmd: "grep -o -e '{{ munged_signature }}' -e '{{ host_verified_result }}' '{{ vgauth_log_path_local }}'" + ignore_errors: true + register: get_host_verified_result + + - name: "Check VC SSO user's SAML token is munged and verified by host" + ansible.builtin.assert: + that: + - get_host_verified_result.rc is defined + - get_host_verified_result.rc == 0 + - get_host_verified_result.stdout_lines is defined + - get_host_verified_result.stdout_lines | length == 2 + - get_host_verified_result.stdout_lines[0] == munged_signature + - get_host_verified_result.stdout_lines[1] == host_verified_result + fail_msg: "Failed to find munged SAML token or host verified result" + success_msg: "VC SSO user's SAML token signature is munged, and verified by host" diff --git a/linux/host_verify_saml_token/host_verify_saml_token.yml b/linux/host_verify_saml_token/host_verify_saml_token.yml new file mode 100644 index 000000000..5dee1ad89 --- /dev/null +++ b/linux/host_verify_saml_token/host_verify_saml_token.yml @@ -0,0 +1,118 @@ +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +--- +# Description: +# This test case is used for test host verified SAML token in guest operations +# +- name: host_verify_saml_token + hosts: localhost + gather_facts: false + tasks: + - name: "Test case block" + block: + - name: "Skip test case due to missing vCenter Server variables" + include_tasks: ../../common/skip_test_case.yml + vars: + skip_msg: "Skip test case {{ ansible_play_name }} is because of missing vCenter Server variables." + skip_reason: "Not Applicable" + when: > + (vcenter_is_defined is undefined or not vcenter_is_defined) or + (vcenter_ssh_username is undefined or not vcenter_ssh_username) or + (vcenter_ssh_password is undefined or not vcenter_ssh_password) + + - name: "Test setup" + include_tasks: ../setup/test_setup.yml + vars: + skip_test_no_vmtools: true + + - name: "Skip test case for {{ guest_os_ansible_distribution }}" + include_tasks: ../../common/skip_test_case.yml + vars: + skip_msg: >- + Skip test case {{ ansible_play_name }} because {{ guest_os_ansible_distribution }} + {{ guest_os_ansible_distribution_ver }} doesn't have VGAuthService. + skip_reason: "Not Supported" + when: >- + (guest_os_ansible_distribution == 'FreeBSD' or + (guest_os_ansible_distribution == 'Flatcar' and + guest_os_ansible_distribution_ver is version('3760.2.0', '<'))) + + - name: "Skip test case for old ESXi server or VMware Tools" + include_tasks: ../../common/skip_test_case.yml + vars: + skip_msg: >- + Skip test case {{ ansible_play_name }} because ESXi version is {{ esxi_version }} < 8.0.2 or + VMware Tools version is {{ vmtools_version }} < 12.3.0. + skip_reason: "Not Supported" + when: esxi_version is version('8.0.2', '<') or vmtools_version is version('12.3.0', '<') + + - name: "Check VMware Tools capability exists for host verified SAML token" + include_tasks: ../../common/vm_check_vmtools_capability.yml + + - name: "Initialize facts about domain user information" + ansible.builtin.set_fact: + vcenter_admin_user_name: "{{ vcenter_username.split('@')[0] }}" + vcenter_domain_name: "{{ vcenter_username.split('@')[-1] }}" + vcenter_domain_user_name: "vcuser_{{ current_test_timestamp }}" + vcenter_domain_user_password: "VP@ssw0rd" + vcenter_domain_user_group: "DCAdmins" + vm_guest_user_name: "gosuser_{{ current_test_timestamp }}" + vm_guest_user_password: "GP@ssw0rd" + + - name: "Add domain user '{{ vcenter_domain_user_name }}'" + include_tasks: ../../common/vcenter_manage_domain_user.yml + vars: + vcenter_domain_user_op: "add" + + - name: "Add a new guest user '{{ vm_guest_user_name }}'" + include_tasks: ../utils/add_user.yml + vars: + guest_user_name: "{{ vm_guest_user_name }}" + guest_user_password: "{{ vm_guest_user_password }}" + + # Flatcar's filesystem is read-only, which can't enable debug logging" + - name: "Enable debug logging for VGAuthService and VMware Tools" + when: guest_os_ansible_distribution != 'Flatcar' + block: + - name: "Enable debug logging for VGAuthService" + include_tasks: ../utils/enable_vgauth_logging.yml + + - name: "Enable debug logging for VMware Tools" + include_tasks: ../utils/enable_vmtools_logging.yml + + - name: "Test guest operation with VC SSO user's SAML token" + include_tasks: test_guest_ops_with_token.yml + + - name: "Collect VGAuthServcie log" + include_tasks: ../utils/collect_vgauth_logs.yml + + - name: "Check VGAuthService log is collected successfully" + ansible.builtin.assert: + that: + - vgauth_log_file_exists + - vgauth_log_is_collected + - vgauth_log_file_dest + fail_msg: "Failed to collect VGAuthService log" + success_msg: "The VGAuthService log is collected to {{ vgauth_log_file_dest }}" + + - name: "Check VC SSO user's SAML token is verified by host" + include_tasks: check_host_verified_token.yml + vars: + vgauth_log_path_local: "{{ vgauth_log_file_dest }}" + + - name: "Delete domain user '{{ vcenter_domain_user_name }}'" + include_tasks: ../../common/vcenter_manage_domain_user.yml + vars: + vcenter_domain_user_op: "delete" + rescue: + - name: "Test rescue" + include_tasks: ../../common/test_rescue.yml + always: + - name: "Collect VGAuthServcie log" + include_tasks: ../utils/collect_vgauth_logs.yml + vars: + vgauth_log_file_src: "{{ vgauth_latest_log_file | default('') }}" + when: vgauth_log_is_collected is undefined or not vgauth_log_is_collected + + - name: "Collect VMware Tools logs" + include_tasks: ../utils/collect_vmtools_logs.yml diff --git a/linux/host_verify_saml_token/test_guest_ops_with_token.yml b/linux/host_verify_saml_token/test_guest_ops_with_token.yml new file mode 100644 index 000000000..61cd6a63b --- /dev/null +++ b/linux/host_verify_saml_token/test_guest_ops_with_token.yml @@ -0,0 +1,122 @@ +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +--- +# Description: +# Perform guest operation testing with guest user alias to check +# VC SSO user's SAML token is verified by host. +# +- name: "Set log file of testing host verified SAML token and checking messages" + ansible.builtin.set_fact: + saml_token_test_log: "{{ current_test_log_folder }}/test_saml_token.log" + add_alias_check_msg: >- + Successfuly added guest user mapping: + {{ vm_guest_user_name }}:{{ vcenter_domain_user_name }}@{{ vcenter_domain_name }} + remove_alias_check_msg: >- + Successfuly removed guest user mapping: + {{ vm_guest_user_name }}:{{ vcenter_domain_user_name }}@{{ vcenter_domain_name }} + guest_user_environment: [] + +- name: "Set command for testing host verified SAML token" + ansible.builtin.set_fact: + saml_token_test_cmd: >- + python ../../tools/vgauth_guestops.py -l {{ saml_token_test_log }} + -H {{ vcenter_hostname }} -d '{{ vcenter_domain_name }}' -vm '{{ vm_name }}' + -au '{{ vcenter_admin_user_name }}' -ap '{{ vcenter_password }}' + +# Add guest alias +- name: "Add guest user mapping for guest user {{ vm_guest_user_name }}" + ansible.builtin.command: >- + {{ saml_token_test_cmd }} + -tu '{{ vcenter_domain_user_name }}' -tp '{{ vcenter_domain_user_password }}' + -gu '{{ vm_guest_user_name }}' -gp '{{ vm_guest_user_password }}' + -o AddGuestAlias + register: add_alias_result + ignore_errors: true + +- name: "Display the result of adding guest user mapping" + debug: var=add_alias_result + when: enable_debug + +- name: "Check the result of adding guest user mapping" + ansible.builtin.assert: + that: + - add_alias_result.rc is defined + - add_alias_result.rc == 0 + - add_alias_result.stdout_lines is defined + - add_alias_result.stdout_lines | select('search', add_alias_check_msg) | length > 0 + fail_msg: >- + Failed to add guest user mapping + {{ vm_guest_user_name }}:{{ vcenter_domain_user_name }}@{{ vcenter_domain_name }} + on VM {{ vm_name }}. + Return code is '{{ add_alias_result.rc | default("") }}'. + Output is '{{ add_alias_result.stdout | default("") }}'. + Hit error '{{ add_alias_result.stderr | default("") }}'. + success_msg: "{{ add_alias_check_msg }}" + +# Test guest operation +- name: "Perform guest operation of reading guest user's environment variables" + ansible.builtin.command: >- + {{ saml_token_test_cmd }} + -tu '{{ vcenter_domain_user_name }}' -tp '{{ vcenter_domain_user_password }}' + -gu '{{ vm_guest_user_name }}' -o PerformGuestOps + register: perform_guestops_result + ignore_errors: true + +- name: "Display the result of reading guest user's environment variables by guest operation" + debug: var=perform_guestops_result + when: enable_debug + +- name: "Set fact of guest user's environment variables retrieved by guest operation" + ansible.builtin.set_fact: + guest_user_environment: >- + {{ + perform_guestops_result.stdout_lines | + select('match', 'USER(NAME)?=' ~ vm_guest_user_name) + }} + when: + - perform_guestops_result.stdout_lines is defined + - perform_guestops_result.stdout_lines | length > 0 + +- name: "Check the result of reading guest user's environment variables by guest operation" + ansible.builtin.assert: + that: + - perform_guestops_result.rc is defined + - perform_guestops_result.rc == 0 + - guest_user_environment | length == 1 + fail_msg: >- + Failed to read guest user's environment variables by guest operation with VC SSO user + {{ vcenter_domain_user_name }}@{{ vcenter_domain_name }} on VM {{ vm_name }}. + Return code is '{{ perform_guestops_result.rc | default("") }}'. + Output is '{{ perform_guestops_result.stdout | default("") }}'. + Hit error '{{ perform_guestops_result.stderr | default("") }}'. + success_msg: "Successfully read guest user's environment variable {{ guest_user_environment }}" + +# Remove guest alias +- name: "Remove guest user mapping for guest user {{ vm_guest_user_name }}" + ansible.builtin.command: >- + {{ saml_token_test_cmd }} + -tu '{{ vcenter_domain_user_name }}' -tp '{{ vcenter_domain_user_password }}' + -gu '{{ vm_guest_user_name }}' -gp '{{ vm_guest_user_password }}' + -o RemoveGuestAlias + register: remove_alias_result + ignore_errors: true + +- name: "Display the result of removing guest user mapping" + debug: var=remove_alias_result + when: enable_debug + +- name: "Check the result of removing guest user mapping" + ansible.builtin.assert: + that: + - remove_alias_result.rc is defined + - remove_alias_result.rc == 0 + - remove_alias_result.stdout_lines is defined + - remove_alias_result.stdout_lines | select('search', remove_alias_check_msg) | length > 0 + fail_msg: >- + Failed to remove guest user mapping + {{ vm_guest_user_name }}:{{ vcenter_domain_user_name }}@{{ vcenter_domain_name }} + on VM {{ vm_name }}. + Return code is '{{ remove_alias_result.rc | default("") }}'. + Output is '{{ remove_alias_result.stdout | default("") }}'. + Hit error '{{ remove_alias_result.stderr | default("") }}'. + success_msg: "{{ remove_alias_check_msg }}" diff --git a/linux/utils/collect_vgauth_logs.yml b/linux/utils/collect_vgauth_logs.yml index 154f6897a..6306572af 100644 --- a/linux/utils/collect_vgauth_logs.yml +++ b/linux/utils/collect_vgauth_logs.yml @@ -3,40 +3,43 @@ --- # Collect VGAuthService logs to local test case log directory # Parameter: -# vgauth_log_file_src: The VGAuthService log file path +# vgauth_latest_log_file: The VGAuthService log file path # - name: "Initialize the latest VGAuthService log path in guest OS" ansible.builtin.set_fact: - vgauth_log_file_src: "/var/log/vmware-vgauthsvc.log.0" - when: vgauth_log_file_src is undefined or not vgauth_log_file_src + vgauth_latest_log_file: "/var/log/vmware-vgauthsvc.log.0" + when: vgauth_latest_log_file is undefined -- name: "Initialize facts of collected VGAuthService log path at localhost" +- debug: var=vgauth_latest_log_file + +- name: "Initialize facts for collecting VGAuthService log file" ansible.builtin.set_fact: + vgauth_log_file_exists: false vgauth_log_file_dest: "" vgauth_log_is_collected: false - name: "Get VGAuthService log file info" include_tasks: get_file_stat_info.yml vars: - guest_file_path: "{{ vgauth_log_file_src }}" + guest_file_path: "{{ vgauth_latest_log_file }}" - name: "Set fact of VGAuthService log exists or not" ansible.builtin.set_fact: vgauth_log_file_exists: "{{ guest_file_exists }}" -- name: "Collect VGAuthService log file" +- name: "Collect VGAuthService log file to localhost" + when: vgauth_log_file_exists | bool block: - name: "Collect VGAuthServce log to test case log dir" include_tasks: fetch_file.yml vars: - fetch_file_src_path: "{{ vgauth_log_file_src }}" + fetch_file_src_path: "{{ vgauth_latest_log_file }}" fetch_file_dst_path: "{{ current_test_log_folder }}/" - name: "Set facts of VGAuthService file collected at localhost" ansible.builtin.set_fact: vgauth_log_file_dest: "{{ fetch_file_local_path }}" - vgauth_log_is_collected: True + vgauth_log_is_collected: true when: - fetch_file_local_path is defined - fetch_file_local_path - when: vgauth_log_file_exists | bool diff --git a/linux/utils/config_os_release_repo.yml b/linux/utils/config_os_release_repo.yml new file mode 100644 index 000000000..e69de29bb diff --git a/linux/vgauth_check_service/vgauth_check_service.yml b/linux/vgauth_check_service/vgauth_check_service.yml index 443de5a16..d1a332aa4 100644 --- a/linux/vgauth_check_service/vgauth_check_service.yml +++ b/linux/vgauth_check_service/vgauth_check_service.yml @@ -84,8 +84,3 @@ always: - name: "Collect VGAuthServcie logs" include_tasks: ../utils/collect_vgauth_logs.yml - vars: - vgauth_log_file_src: "{{ vgauth_latest_log_file }}" - when: - - vgauth_latest_log_file is defined - - vgauth_latest_log_file diff --git a/tools/vgauth_guestops.py b/tools/vgauth_guestops.py new file mode 100755 index 000000000..f9809f105 --- /dev/null +++ b/tools/vgauth_guestops.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +""" +This is a script to test host verified SAML token by reading guest OS environment variables. +""" + +import os +import ssl +import argparse +import sys +import base64 +import logging +import traceback +import xml.dom.minidom +import xml.etree.ElementTree as ET +from OpenSSL import crypto +from pyVmomi import vim, SoapStubAdapter +from pyVim import sso +from pyVim.connect import SmartConnect, Disconnect + +logger = None +log_dir = None + +def get_logger(debug=False, log_file=None): + """ + Get logger object + :param debug: True to print debug message in console + :param log_file: The log file path + :return: + """ + global log_dir + logger = logging.getLogger() + if debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logger.setLevel(logging.DEBUG) + + logFormat = '%(asctime)s|%(levelname)5s| %(message)s' + + formatter = logging.Formatter(logFormat) + if log_file is not None: + log_dir = os.path.dirname(os.path.abspath(log_file)) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + lh = logging.FileHandler(log_file) + lh.setLevel(logging.DEBUG) + lh.setFormatter(formatter) + logger.addHandler(lh) + + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(log_level) + ch.setFormatter(formatter) + logger.addHandler(ch) + + return logger + +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("-v", dest="verbose", action="store_true", default=False, + help="print debug messages to console output") + parser.add_argument("-l", dest="log", + help="log file path") + parser.add_argument("-H", dest="host", required=True, + help="vCenter Server hostname or IP address") + parser.add_argument("-d", dest="domain", default="vsphere.local", + help="vCenter Server user domain") + parser.add_argument("-vm", dest="vm_name", required=True, + help="VM name") + parser.add_argument("-au", dest="admin_user", + default='Administrator', + help="VC admin username without domain name. Default is Administrator") + parser.add_argument("-ap", dest="admin_pwd", required=True, + help="VC admin password") + parser.add_argument("-tu", dest="test_user", + default='vcuser', + help="VC test username without domain name. Default is vcuser") + parser.add_argument("-tp", dest="test_pwd", + default='VP@ssw0rd', + help="VC test password. Default is VP@ssw0rd") + parser.add_argument("-gu", dest="guest_user", default="gosuser", + help="guest username. Default is gosuser") + parser.add_argument("-gp", dest="guest_pwd", default='GP@ssw0rd', + help="guest user password. Default is GP@ssw0rd") + parser.add_argument("-o", dest="operation", + required=True, + choices = ['ListGuestAlias', + 'AddGuestAlias', + 'RemoveGuestAlias', + 'PerformGuestOps'], + help="guest operations") + args = parser.parse_args() + for kwarg in args._get_kwargs(): + if(kwarg[0] in ['domain', 'guest_user'] and kwarg[1] == ''): + parser.error(f"{kwarg[0]} can't be empty.") + if(kwarg[0] in ['admin_user', 'test_user'] and len(kwarg[1].split('@')) > 1): + parser.error(f"{kwarg[0]} can't include domain name.") + + if args.operation != 'PerformGuestOps' and not args.test_pwd: + parser.error(f"test_pwd is required for {args.operation}") + return args + +def get_unverified_context(): + """ + Get an unverified ssl context. Used to disable the server certificate + verification. + @return: unverified ssl context. + """ + context = None + if hasattr(ssl, '_create_unverified_context'): + context = ssl._create_unverified_context() + return context + +def prettify_xml(xml_string): + """ + Print XML string in pretty format + :param xml_string: + :return: + """ + dom = xml.dom.minidom.parseString(xml_string) + pretty_xml = dom.toprettyxml(indent=" ") + return pretty_xml + +def get_sso_saml_token(host, username, password, domain='vsphere.local', context=None): + """ + Get Single Sign-On SAML token for VC domain user + :param host: vCenter Server hostname + :param username: VC user name without domain + :param password: VC user password + :param domain: VC user domain + :param context: SSL context + :return: SSO user SAML token in XML + """ + sso_url = 'https://{}/sts/STSService/{}'.format(host, domain) + logger.debug("The SSO URL is " + sso_url) + auth = sso.SsoAuthenticator(sso_url) + + user = f"{username}@{domain}" + logger.debug(f'Retrieving the SAML bearer token from SSO for VC user {user}.') + saml_token = auth.get_bearer_saml_assertion(user, password, + delegatable=True, + # renewable=True, + ssl_context=context) + pretty_xml = prettify_xml(saml_token) + # logger.debug("The SSO SAML token is:\n" + pretty_xml) + saml_token_file = f"{username}_saml_token.xml" + if log_dir is not None and log_dir: + saml_token_file = os.path.join(log_dir, saml_token_file) + with open(saml_token_file, 'w') as f: + f.write(pretty_xml) + logger.debug(f"SAML token for VC domain {user} is saved to: {saml_token_file}") + return saml_token + +def extract_x509_cert_from_token(saml_token): + """ + Extract X.509 certificate from SAML token + :param saml_token: VC SSO user SAML token string + :return: The X.509 certificate in PEM format + """ + namespace = {'ds': 'http://www.w3.org/2000/09/xmldsig#'} + root = ET.fromstring(saml_token) + x509_cert_element = root.find('.//ds:X509Certificate', namespace) + + if x509_cert_element is not None and x509_cert_element.text: + x509_cert = x509_cert_element.text.strip() + + # Extract the X.509 certificate + if x509_cert != '': + x509_cert = "\n".join([x509_cert[i:i+64] for i in range(0, len(x509_cert), 64)]) + # X.509 certificate string + x509_cert = f"-----BEGIN CERTIFICATE-----\n{x509_cert}\n-----END CERTIFICATE-----\n" + + # File path to write the certificate + cert_file_path = "x509_pem.crt.txt" + if log_dir is not None and log_dir: + cert_file_path = os.path.join(log_dir, cert_file_path) + # Write the certificate string into the file + with open(cert_file_path, "w") as file: + file.write(x509_cert) + logger.debug(f"X.509 certification in PEM format is saved to {cert_file_path}") + + return x509_cert + +def get_base64_cert_from_token(saml_token): + """ + Retrieve X.509 certificate from SSO user SAML token with base64 encoded + :param saml_token: + :return: + """ + x509_cert = extract_x509_cert_from_token(saml_token) + # Convert X.509 certificate in PEM to DER format + cert_pem = crypto.load_certificate(crypto.FILETYPE_PEM, x509_cert) + cert_der = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert_pem) + base64_cert = base64.b64encode(cert_der).decode('utf-8') + # logger.debug("Base64 encoded X.509 certificate:\n" + str(base64_cert)) + return base64_cert + +def find_vm_by_name(service_instance, vm_name): + """ + Find a virtual machine object + :param service_instance: + :param vm_name: + :return: + """ + content = service_instance.RetrieveContent() + container_view = content.viewManager.CreateContainerView(content.rootFolder, + [vim.VirtualMachine], True) + for vm in container_view.view: + if vm.name == vm_name: + logger.debug(f"Find VM {vm_name}: " + str(vm)) + return vm + return None + +def list_guest_alias(args, service_instance, vm): + """ + List VC SSO users mapping to a guest user + """ + # Get guest user alias manager + guest_ops_manager = service_instance.content.guestOperationsManager + alias_manager = guest_ops_manager.aliasManager + + # Add a guest authentication object with SAML token + guest_auth = vim.vm.guest.NamePasswordAuthentication() + guest_auth.username = args.guest_user + guest_auth.password = args.guest_pwd + + logger.debug(f"Retrieving guest user alias for guest user '{args.guest_user}'") + alias_mappings = alias_manager.ListGuestAliases(vm, guest_auth, args.guest_user) + sso_users = [] + for mapping in alias_mappings: + if (mapping.aliases and len(mapping.aliases) > 0 and + mapping.aliases[0].subject): + sso_users.append(mapping.aliases[0].subject.name) + logger.info(f"SSO users mapping to guest user '{args.guest_user}': " + ','.join(sso_users)) + return sso_users + +def add_guest_alias(args, service_instance, vm, base64_cert): + """ + Add a guest user mapping from VC SSO user to guest user + """ + # Get guest user alias manager + guest_ops_manager = service_instance.content.guestOperationsManager + alias_manager = guest_ops_manager.aliasManager + + # Create a guest user name specification + guest_auth = vim.vm.guest.NamePasswordAuthentication() + guest_auth.username = args.guest_user + guest_auth.password = args.guest_pwd + + # Create a guest alias mapping + alias_info = vim.vm.guest.AliasManager.GuestAuthAliasInfo() + guest_auth_subject = vim.vm.guest.AliasManager.GuestAuthNamedSubject() + vc_user = f"{args.test_user}@{args.domain}" + guest_auth_subject.name = vc_user + alias_info.subject = guest_auth_subject + alias_info.comment = "Guest user mapping for testing host verified SAML token" + + # Add the alias mapping + logger.debug(f"Adding user alias for guest user {args.guest_user}: {vc_user}") + alias_manager.AddGuestAlias(vm=vm, auth=guest_auth, username=args.guest_user, + mapCert=True, base64Cert=base64_cert, aliasInfo=alias_info) + + sso_users = list_guest_alias(args, service_instance, vm) + if vc_user in sso_users: + logger.info(f"Successfuly added guest user mapping: {args.guest_user}:{vc_user}") + else: + raise Exception(f"Failed to add guest user mapping: {args.guest_user}:{vc_user}") + +def remove_guest_alias(args, service_instance, vm, base64_cert): + """ + Remove a guest user mapping from VC SSO user to guest user + """ + # Get guest user alias manager + guest_ops_manager = service_instance.content.guestOperationsManager + alias_manager = guest_ops_manager.aliasManager + + # Add a guest authentication object with SAML token + guest_auth = vim.vm.guest.NamePasswordAuthentication() + guest_auth.username = args.guest_user + guest_auth.password = args.guest_pwd + + guest_auth_subject = vim.vm.guest.AliasManager.GuestAuthNamedSubject() + + vc_user = f"{args.test_user}@{args.domain}" + guest_auth_subject.name = vc_user + + # Remove guest user alias + logger.debug(f"Removing user alias for guest user {args.guest_user}: {vc_user}") + alias_manager.RemoveGuestAlias(vm=vm, auth=guest_auth, username=args.guest_user, + base64Cert=base64_cert, subject=guest_auth_subject) + + sso_users = list_guest_alias(args, service_instance, vm) + if vc_user not in sso_users: + logger.info(f"Successfuly removed guest user mapping: {args.guest_user}:{vc_user}") + else: + raise Exception(f"Failed to remove guest user mapping: {args.guest_user}:{vc_user}") + + return sso_users + +def perform_guest_ops(args, service_instance, vm, context=None): + """ + Perform guest operation with VC SSO user + E.g. ReadEnvironmentVariableInGuest in this sample + :param args: + :param service_instance: + :param vm: + :param context: + :return: + """ + # Create a GuestAuth object for test user with the acquired SAML token + saml_token = get_sso_saml_token(args.host, args.test_user, args.test_pwd, args.domain, context) + guest_auth = vim.vm.guest.SAMLTokenAuthentication() + guest_auth.username = args.guest_user + guest_auth.token = saml_token + + # Get GuestProcessManager object + guest_ops_manager = service_instance.content.guestOperationsManager + process_manager = guest_ops_manager.processManager + + # List guest environment variables + guest_envs = process_manager.ReadEnvironmentVariableInGuest(vm, guest_auth) + logger.info("Guest user's environment variables are:\n" + '\n'.join(guest_envs)) + +# Two ways to get service instance, each of them works +def get_si_with_token(host, saml_token, context=None): + """ + Get VC service instance by SSO log in with SAML token + """ + logger.debug("SSO Log in with SAML token and get service instance") + return SmartConnect(host=host, token=saml_token, tokenType='saml', sslContext=context) + +def get_si_with_usernam_password(host, user, password, context=None): + """ + Get VC service instance by SSO log in with username and password + """ + logger.debug("SSO Log in with username and password and get service instance") + return SmartConnect(host=host, user=user, pwd=password, sslContext=context) + +def main(): + global logger + args = parse_arguments() + logger = get_logger(debug=args.verbose, log_file=args.log) + + # Connect to vCenter Server with SAML token + service_instance = None + try: + context = get_unverified_context() + admin_saml_token = get_sso_saml_token(args.host, args.admin_user, args.admin_pwd, args.domain, context) + # Get service instance by SSO log in with SAML token + service_instance = get_si_with_token(args.host, admin_saml_token, context) + + # Find the virtual machine + vm = find_vm_by_name(service_instance, args.vm_name) + if vm is None: + raise Exception(f"Failed to find VM with name {args.vm_name}") + + base64_cert = get_base64_cert_from_token(admin_saml_token) + + if args.operation == "ListGuestAlias": + list_guest_alias(args, service_instance, vm) + elif args.operation == "AddGuestAlias": + add_guest_alias(args, service_instance, vm, base64_cert) + elif args.operation == "RemoveGuestAlias": + remove_guest_alias(args, service_instance, vm, base64_cert) + elif args.operation == "PerformGuestOps": + perform_guest_ops(args, service_instance, vm, context) + + return 0 + except Exception as ex: + logger.error(str(ex) + "\n" + traceback.format_exc()) + return 1 + finally: + if service_instance: + # Disconnect from vCenter Server + Disconnect(service_instance) + +# Main Execution point +if __name__ == "__main__": + sys.exit(main()) diff --git a/windows/gosv_testcase_list.yml b/windows/gosv_testcase_list.yml index 272b9df4a..3ea9fc2bb 100644 --- a/windows/gosv_testcase_list.yml +++ b/windows/gosv_testcase_list.yml @@ -12,6 +12,7 @@ - import_playbook: check_os_fullname/check_os_fullname.yml - import_playbook: mouse_driver_vmtools/mouse_driver_vmtools.yml - import_playbook: vgauth_check_service/vgauth_check_service.yml +- import_playbook: host_verify_saml_token/host_verify_saml_token.yml - import_playbook: stat_balloon/stat_balloon.yml - import_playbook: stat_hosttime/stat_hosttime.yml - import_playbook: vhba_hot_add_remove/paravirtual_vhba_device_ops.yml diff --git a/windows/host_verify_saml_token/host_verify_saml_token.yml b/windows/host_verify_saml_token/host_verify_saml_token.yml new file mode 100644 index 000000000..1da5f6e95 --- /dev/null +++ b/windows/host_verify_saml_token/host_verify_saml_token.yml @@ -0,0 +1,89 @@ +# Copyright 2024 VMware, Inc. +# SPDX-License-Identifier: BSD-2-Clause +--- +# Description: +# This test case is used for test host verified SAML token in guest operations +# +- name: host_verify_saml_token + hosts: localhost + gather_facts: false + tasks: + - name: "Test case block" + block: + - name: "Skip test case due to missing vCenter Server variables" + include_tasks: ../../common/skip_test_case.yml + vars: + skip_msg: "Skip test case {{ ansible_play_name }} is because of missing vCenter Server variables." + skip_reason: "Not Applicable" + when: > + (vcenter_is_defined is undefined or not vcenter_is_defined) or + (vcenter_ssh_username is undefined or not vcenter_ssh_username) or + (vcenter_ssh_password is undefined or not vcenter_ssh_password) + + - name: "Test setup" + include_tasks: ../setup/test_setup.yml + vars: + skip_test_no_vmtools: true + create_current_test_folder: true + + - name: "Skip test case for old ESXi server or VMware Tools" + include_tasks: ../../common/skip_test_case.yml + vars: + skip_msg: >- + Skip test case {{ ansible_play_name }} because ESXi version is {{ esxi_version }} < 8.0.2 or + VMware Tools version is {{ vmtools_version }} < 12.3.0. + skip_reason: "Not Supported" + when: esxi_version is version('8.0.2', '<') or vmtools_version is version('12.3.0', '<') + + - name: "Check VMware Tools capability exists for host verified SAML token" + include_tasks: ../../common/vm_check_vmtools_capability.yml + + - name: "Initialize facts about domain user information" + ansible.builtin.set_fact: + vcenter_admin_user_name: "{{ vcenter_username.split('@')[0] }}" + vcenter_domain_name: "{{ vcenter_username.split('@')[-1] }}" + vcenter_domain_user_name: "vcuser_{{ current_test_timestamp }}" + vcenter_domain_user_password: "VP@ssw0rd" + vcenter_domain_user_group: "DCAdmins" + vm_guest_user_name: "{{ vm_username }}" + vm_guest_user_password: "{{ vm_password }}" + + - name: "Add domain user '{{ vcenter_domain_user_name }}'" + include_tasks: ../../common/vcenter_manage_domain_user.yml + vars: + vcenter_domain_user_op: "add" + + - name: "Enable debug logging for VGAuthService" + include_tasks: ../utils/win_enable_vgauth_log.yml + + - name: "Test guest operation with VC SSO user's SAML token" + include_tasks: ../../linux/host_verify_saml_token/test_guest_ops_with_token.yml + + - name: "Collect VGAuthService log" + include_tasks: ../utils/win_collect_vgauth_logs.yml + + - name: "Check VGAuthService log is collected successfully" + ansible.builtin.assert: + that: + - vgauth_log_file_exists + - vgauth_log_is_collected + - vgauth_log_file_dest + fail_msg: "Failed to collect VGAuthService log" + success_msg: "The VGAuthService log is collected to {{ vgauth_log_file_dest }}" + + - name: "Check VC SSO user's SAML token is verified by host" + include_tasks: ../../linux/host_verify_saml_token/check_host_verified_token.yml + vars: + vgauth_log_path_local: "{{ vgauth_log_file_dest }}" + + - name: "Delete domain user '{{ vcenter_domain_user_name }}'" + include_tasks: ../../common/vcenter_manage_domain_user.yml + vars: + vcenter_domain_user_op: "delete" + rescue: + - name: "Test failure" + include_tasks: ../../common/test_rescue.yml + always: + - name: "Collect VGAuthService log" + include_tasks: ../utils/win_collect_vgauth_logs.yml + when: vgauth_log_is_collected is undefined or not vgauth_log_is_collected diff --git a/windows/utils/win_collect_vgauth_logs.yml b/windows/utils/win_collect_vgauth_logs.yml index bc3263c45..24f629501 100644 --- a/windows/utils/win_collect_vgauth_logs.yml +++ b/windows/utils/win_collect_vgauth_logs.yml @@ -8,13 +8,19 @@ vgauth_log_file_src: "C:\\ProgramData\\VMware\\VMware VGAuth\\logfile.txt.0" vgauth_log_file_dest: "" vgauth_log_is_collected: false + vgauth_log_file_exists: false - name: "Check VGAuthService log file exists or not" include_tasks: ../utils/win_check_file_exist.yml vars: win_check_file_exist_file: "{{ vgauth_log_file_src }}" +- name: "Set fact of VGAuthService log file existence" + ansible.builtin.set_fact: + vgauth_log_file_exists: "{{ win_check_file_exist_result }}" + - name: "Collect VGAuthService log file" + when: vgauth_log_file_exists block: - name: "Get VGAuthService status" include_tasks: win_get_service_status.yml @@ -53,4 +59,3 @@ - not fetch_file.failed - fetch_file.dest is defined - fetch_file.dest - when: win_check_file_exist_result