From 6d5e4c863bb495fa4dd93443d2ad83d6cbca046f Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 25 Mar 2026 11:34:17 -0600 Subject: [PATCH] Backport enos(ldap): always verify base DN connection before setup into ce/main Refactor our connection checking into a new LDAP module that is capable of running a search and waiting for success. We now call this module while setting up the integration host and before enabling the LDAP secrets engine. We also fix two race conditions in the Agent and HA Seal scenarios where we might attempt to verify and/or test LDAP before the integration host has been set up. Signed-off-by: Ryan Cragun Co-authored-by: Ryan Cragun Co-authored-by: LT Carbonell --- enos/enos-scenario-agent.hcl | 3 +- enos/enos-scenario-seal-ha.hcl | 60 ++++++------- enos/modules/ldap_wait_for_search/main.tf | 89 +++++++++++++++++++ .../scripts/wait-for-search.sh | 46 ++++++++++ .../main.tf | 29 ++++-- .../scripts/populate-ldap.sh | 58 +++--------- .../variables.tf | 18 ++-- enos/modules/vault_run_blackbox_test/main.tf | 10 +-- .../scripts/run-test.sh | 17 +++- .../modules/create/ldap/ldap.tf | 71 +++++++++------ sdk/helper/testcluster/blackbox/session.go | 36 ++++++-- .../blackbox/secrets_ldap_test.go | 47 ++++++++-- 12 files changed, 339 insertions(+), 145 deletions(-) create mode 100644 enos/modules/ldap_wait_for_search/main.tf create mode 100755 enos/modules/ldap_wait_for_search/scripts/wait-for-search.sh diff --git a/enos/enos-scenario-agent.hcl b/enos/enos-scenario-agent.hcl index 6e63984d7a..4a5f7971c3 100644 --- a/enos/enos-scenario-agent.hcl +++ b/enos/enos-scenario-agent.hcl @@ -517,7 +517,8 @@ scenario "agent" { module = module.vault_verify_secrets_engines_create depends_on = [ step.verify_vault_unsealed, - step.get_vault_cluster_ips + step.get_vault_cluster_ips, + step.set_up_external_integration_target, ] providers = { diff --git a/enos/enos-scenario-seal-ha.hcl b/enos/enos-scenario-seal-ha.hcl index f65b609464..c421932401 100644 --- a/enos/enos-scenario-seal-ha.hcl +++ b/enos/enos-scenario-seal-ha.hcl @@ -472,7 +472,8 @@ scenario "seal_ha" { depends_on = [ step.create_vault_cluster, step.get_vault_cluster_ips, - step.verify_vault_unsealed + step.verify_vault_unsealed, + step.set_up_external_integration_target, ] providers = { @@ -862,34 +863,6 @@ scenario "seal_ha" { } } - step "verify_log_secrets" { - skip_step = !var.vault_enable_audit_devices || !var.verify_log_secrets - - description = global.description.verify_log_secrets - module = module.verify_log_secrets - depends_on = [ - step.verify_secrets_engines_read, - ] - - providers = { - enos = local.enos_provider[matrix.distro] - } - - verifies = [ - quality.vault_audit_log_secrets, - quality.vault_journal_secrets, - quality.vault_radar_index_create, - quality.vault_radar_scan_file, - ] - - variables { - audit_log_file_path = step.create_vault_cluster.audit_device_file_path - leader_host = step.get_updated_cluster_ips.leader_host - vault_addr = step.create_vault_cluster.api_addr_localhost - vault_root_token = step.create_vault_cluster.root_token - } - } - step "verify_ui" { description = global.description.verify_ui module = module.vault_verify_ui @@ -937,7 +910,6 @@ scenario "seal_ha" { depends_on = [ step.wait_for_seal_rewrap, step.verify_secrets_engines_read, - step.verify_log_secrets, ] providers = { @@ -1140,6 +1112,34 @@ scenario "seal_ha" { } } + step "verify_log_secrets" { + skip_step = !var.vault_enable_audit_devices || !var.verify_log_secrets + + description = global.description.verify_log_secrets + module = module.verify_log_secrets + depends_on = [ + step.verify_secrets_engines_read_after_migration + ] + + providers = { + enos = local.enos_provider[matrix.distro] + } + + verifies = [ + quality.vault_audit_log_secrets, + quality.vault_journal_secrets, + quality.vault_radar_index_create, + quality.vault_radar_scan_file, + ] + + variables { + audit_log_file_path = step.create_vault_cluster.audit_device_file_path + leader_host = step.get_updated_cluster_ips.leader_host + vault_addr = step.create_vault_cluster.api_addr_localhost + vault_root_token = step.create_vault_cluster.root_token + } + } + output "audit_device_file_path" { description = "The file path for the file audit device, if enabled" value = step.create_vault_cluster.audit_device_file_path diff --git a/enos/modules/ldap_wait_for_search/main.tf b/enos/modules/ldap_wait_for_search/main.tf new file mode 100644 index 0000000000..402143e022 --- /dev/null +++ b/enos/modules/ldap_wait_for_search/main.tf @@ -0,0 +1,89 @@ +# Copyright IBM Corp. 2016, 2025 +# SPDX-License-Identifier: BUSL-1.1 + +terraform { + required_providers { + enos = { + source = "registry.terraform.io/hashicorp-forge/enos" + } + } +} + +variable "hosts" { + description = "The target machines to run the test query from" + type = map(object({ + ipv6 = string + private_ip = string + public_ip = string + })) +} + +variable "ldap_base_dn" { + type = string + description = "The LDAP base dn to search from" +} + +variable "ldap_bind_dn" { + type = string + description = "The LDAP bind dn" +} + +variable "ldap_host" { + type = object({ + ipv6 = string + private_ip = string + public_ip = string + }) + description = "The LDAP host" +} + +variable "ldap_password" { + type = string + description = "The LDAP password" +} + +variable "ldap_port" { + type = string + description = "The LDAP port" +} + +variable "ldap_query" { + type = string + description = "The LDAP query to use when testing the connection" + default = null +} + +variable "retry_interval" { + type = number + description = "How many seconds to wait between each retry" + default = 2 +} + +variable "timeout" { + type = number + description = "The max number of seconds to wait before timing out" + default = 60 +} + +# Wait for the search to succeed +resource "enos_remote_exec" "wait_for_search" { + for_each = var.hosts + + environment = { + LDAP_BASE_DN = var.ldap_base_dn + LDAP_BIND_DN = var.ldap_bind_dn + LDAP_HOST = var.ldap_host.public_ip + LDAP_PASSWORD = var.ldap_password + LDAP_PORT = var.ldap_port + LDAP_QUERY = var.ldap_query == null ? "" : var.ldap_query + RETRY_INTERVAL = var.retry_interval + TIMEOUT_SECONDS = var.timeout + } + scripts = [abspath("${path.module}/scripts/wait-for-search.sh")] + + transport = { + ssh = { + host = each.value.public_ip + } + } +} diff --git a/enos/modules/ldap_wait_for_search/scripts/wait-for-search.sh b/enos/modules/ldap_wait_for_search/scripts/wait-for-search.sh new file mode 100755 index 0000000000..08a94938be --- /dev/null +++ b/enos/modules/ldap_wait_for_search/scripts/wait-for-search.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Copyright IBM Corp. 2026 +# SPDX-License-Identifier: BUSL-1.1 + +fail() { + echo "$1" 1>&2 + exit 1 +} + +[[ -z "$LDAP_BASE_DN" ]] && fail "LDAP_BASE_DN env variable has not been set" +[[ -z "$LDAP_BIND_DN" ]] && fail "LDAP_BIND_DN env variable has not been set" +[[ -z "$LDAP_HOST" ]] && fail "LDAP_HOST env variable has not been set" +[[ -z "$LDAP_PORT" ]] && fail "LDAP_PORT env variable has not been set" +[[ -z "$LDAP_PASSWORD" ]] && fail "LDAP_PASSWORD env variable has not been set" +[[ -z "$RETRY_INTERVAL" ]] && fail "RETRY_INTERVAL env variable has not been set" +[[ -z "$TIMEOUT_SECONDS" ]] && fail "TIMEOUT_SECONDS env variable has not been set" + +# NOTE: An LDAP_QUERY is not technically required here as long as we have a base +# DN and bind DN to search and bind to. + +search="ldap://${LDAP_HOST}:${LDAP_PORT} -b ${LDAP_BASE_DN} -D ${LDAP_BIND_DN} -w ${LDAP_PASSWORD} -s base ${LDAP_QUERY}" +safe_search="ldap://${LDAP_HOST}:${LDAP_PORT} -b ${LDAP_BASE_DN} -D ${LDAP_BIND_DN} -s base ${LDAP_QUERY}" +echo "Running test search: ${safe_search}" + +begin_time=$(date +%s) +end_time=$((begin_time + TIMEOUT_SECONDS)) +test_search_out="" +test_search_res="" +declare -i tries=0 +while [ "$(date +%s)" -lt "$end_time" ]; do + tries+=1 + if test_search_out=$(eval "ldapsearch -x -H ${search}" 2>&1); then + test_search_res=0 + break + fi + + test_search_res=$? + echo "Test search failed!, search: ${safe_search}, attempt: ${tries}, exit code: ${test_search_res}, error: ${test_search_out}, retrying..." + sleep "$RETRY_INTERVAL" +done + +if [ "$test_search_res" -ne 0 ]; then + echo "Timed out waiting for search!, search: ${safe_search}, attempt: ${tries}, exit code: ${test_search_res}, error: ${test_search_out}" 2>&1 + # Exit with the ldapsearch exit code so we bubble that up to error diagnostic + exit "$test_search_res" +fi diff --git a/enos/modules/set_up_external_integration_target/main.tf b/enos/modules/set_up_external_integration_target/main.tf index 72dc8974ef..61de6adcd8 100755 --- a/enos/modules/set_up_external_integration_target/main.tf +++ b/enos/modules/set_up_external_integration_target/main.tf @@ -11,8 +11,10 @@ terraform { locals { test_server_address = var.ip_version == "6" ? var.hosts[0].ipv6 : var.hosts[0].public_ip + ldap_base_dn = join(",", formatlist("dc=%s", split(".", var.ldap_domain))) ldap_server = { - domain = "enos.com" + domain = var.ldap_domain + base_dn = local.ldap_base_dn org = "hashicorp" admin_pw = "password1" version = var.ldap_version @@ -66,19 +68,30 @@ resource "enos_remote_exec" "setup_openldap" { } } +// Wait for the base DN to be available before we populate it +module "wait_for_ldap_base_dn" { + depends_on = [enos_remote_exec.setup_openldap] + source = "../ldap_wait_for_search" + + hosts = var.hosts + ldap_base_dn = local.ldap_base_dn + ldap_bind_dn = "cn=admin,${local.ldap_base_dn}" + ldap_host = local.ldap_server.host + ldap_password = local.ldap_server.admin_pw + ldap_port = local.ldap_server.port +} + # Populate LDAP server with required users and organizational units resource "enos_remote_exec" "populate_ldap" { - depends_on = [enos_remote_exec.setup_openldap] + depends_on = [module.wait_for_ldap_base_dn] scripts = [abspath("${path.module}/scripts/populate-ldap.sh")] environment = { - LDAP_SERVER = local.ldap_server.host.private_ip - LDAP_PORT = local.ldap_server.port - LDAP_ADMIN_PW = local.ldap_server.admin_pw - LDAP_DOMAIN = local.ldap_server.domain - RETRY_INTERVAL = var.retry_interval - TIMEOUT_SECONDS = var.timeout + LDAP_SERVER = local.ldap_server.host.private_ip + LDAP_PORT = local.ldap_server.port + LDAP_ADMIN_PW = local.ldap_server.admin_pw + LDAP_BASE_DN = local.ldap_server.base_dn } transport = { diff --git a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh index 5ee641f024..677a5674c5 100644 --- a/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh +++ b/enos/modules/set_up_external_integration_target/scripts/populate-ldap.sh @@ -12,68 +12,32 @@ fail() { [[ -z "$LDAP_SERVER" ]] && fail "LDAP_SERVER env variable has not been set" [[ -z "$LDAP_PORT" ]] && fail "LDAP_PORT env variable has not been set" [[ -z "$LDAP_ADMIN_PW" ]] && fail "LDAP_ADMIN_PW env variable has not been set" -[[ -z "$LDAP_DOMAIN" ]] && fail "LDAP_DOMAIN env variable has not been set" -[[ -z "$RETRY_INTERVAL" ]] && fail "RETRY_INTERVAL env variable has not been set" -[[ -z "$TIMEOUT_SECONDS" ]] && fail "TIMEOUT_SECONDS env variable has not been set" - -# Extract domain components from LDAP_DOMAIN (e.g., "enos.com" -> "dc=enos,dc=com") -IFS='.' read -ra DOMAIN_PARTS <<< "$LDAP_DOMAIN" -DOMAIN_DN="" -for part in "${DOMAIN_PARTS[@]}"; do - if [[ -n "$DOMAIN_DN" ]]; then - DOMAIN_DN="${DOMAIN_DN},dc=${part}" - else - DOMAIN_DN="dc=${part}" - fi -done +[[ -z "$LDAP_BASE_DN" ]] && fail "LDAP_BASE_DN env variable has not been set" echo "OpenLDAP: Checking for OpenLDAP Server Connection: ${LDAP_SERVER}:${LDAP_PORT}" -echo "OpenLDAP: Using domain DN: ${DOMAIN_DN}" +echo "OpenLDAP: Using base DN: ${LDAP_BASE_DN}" echo "OpenLDAP: Testing connection with admin credentials" -begin_time=$(date +%s) -end_time=$((begin_time + TIMEOUT_SECONDS)) -test_conn_out="" -test_conn_res="" -declare -i tries=0 -while [ "$(date +%s)" -lt "$end_time" ]; do - # Test connection - tries+=1 - if test_conn_out=$(ldapsearch -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -b "${DOMAIN_DN}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -s base 2>&1); then - test_conn_res=0 - break - fi - - test_conn_res=$? - echo "Unable to connect to ldap://${LDAP_SERVER}:${LDAP_PORT} cn=admin,${DOMAIN_DN}, attempt: ${tries}, exit code: ${test_conn_res}, error: ${test_conn_out}, retrying..." - sleep "$RETRY_INTERVAL" -done - -if [ "$test_conn_res" -ne 0 ]; then - echo "Timed out waiting to connect to ldap://${LDAP_SERVER}:${LDAP_PORT} cn=admin,${DOMAIN_DN}, attempt: ${tries}, exit code: ${test_conn_res}, error: ${test_conn_out}" 2>&1 - exit "$test_conn_res" -fi - echo "OpenLDAP: Creating organizational units" # Creating Users and Groups Org Units LDIF file OU_LDIF="ou.ldif" cat << EOF > "${OU_LDIF}" -dn: ou=users,${DOMAIN_DN} +dn: ou=users,${LDAP_BASE_DN} objectClass: organizationalUnit ou: users -dn: ou=groups,${DOMAIN_DN} +dn: ou=groups,${LDAP_BASE_DN} objectClass: organizationalUnit ou: groups EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f "${OU_LDIF}" || echo "OUs may already exist" +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PW}" -f "${OU_LDIF}" || echo "OUs may already exist" echo "OpenLDAP: Creating test users" USER_LDIF="users.ldif" cat << EOF > "${USER_LDIF}" # User: enos -dn: uid=enos,ou=users,${DOMAIN_DN} +dn: uid=enos,ou=users,${LDAP_BASE_DN} objectClass: inetOrgPerson sn: enos cn: enos user @@ -81,7 +45,7 @@ uid: enos userPassword: ${LDAP_ADMIN_PW} # Static-role test user (for LDAP verification tests) -dn: uid=vault-static-user,ou=users,${DOMAIN_DN} +dn: uid=vault-static-user,ou=users,${LDAP_BASE_DN} objectClass: inetOrgPerson sn: vault-static-user cn: Vault Static User @@ -89,21 +53,21 @@ uid: vault-static-user userPassword: ${LDAP_ADMIN_PW} # Service accounts for library tests -dn: uid=svc-account-1,ou=users,${DOMAIN_DN} +dn: uid=svc-account-1,ou=users,${LDAP_BASE_DN} objectClass: inetOrgPerson sn: svc-account-1 cn: Service Account 1 uid: svc-account-1 userPassword: ${LDAP_ADMIN_PW} -dn: uid=svc-account-2,ou=users,${DOMAIN_DN} +dn: uid=svc-account-2,ou=users,${LDAP_BASE_DN} objectClass: inetOrgPerson sn: svc-account-2 cn: Service Account 2 uid: svc-account-2 userPassword: ${LDAP_ADMIN_PW} -dn: uid=svc-delete,ou=users,${DOMAIN_DN} +dn: uid=svc-delete,ou=users,${LDAP_BASE_DN} objectClass: inetOrgPerson sn: svc-delete cn: Service Account Delete @@ -111,6 +75,6 @@ uid: svc-delete userPassword: ${LDAP_ADMIN_PW} EOF -ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${DOMAIN_DN}" -w "${LDAP_ADMIN_PW}" -f "${USER_LDIF}" || echo "Users may already exist" +ldapadd -x -H "ldap://${LDAP_SERVER}:${LDAP_PORT}" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PW}" -f "${USER_LDIF}" || echo "Users may already exist" echo "LDAP population completed successfully." diff --git a/enos/modules/set_up_external_integration_target/variables.tf b/enos/modules/set_up_external_integration_target/variables.tf index 09bd5b9f6d..aa40888bf4 100644 --- a/enos/modules/set_up_external_integration_target/variables.tf +++ b/enos/modules/set_up_external_integration_target/variables.tf @@ -16,6 +16,12 @@ variable "ip_version" { default = "4" } +variable "ldap_domain" { + type = string + description = "The name of the domain" + default = "enos.com" +} + variable "ldap_version" { type = string description = "OpenLDAP Server Version to use" @@ -35,15 +41,3 @@ variable "ports" { description = string })) } - -variable "retry_interval" { - type = number - description = "How many seconds to wait between each retry" - default = 2 -} - -variable "timeout" { - type = number - description = "The max number of seconds to wait before timing out" - default = 60 -} diff --git a/enos/modules/vault_run_blackbox_test/main.tf b/enos/modules/vault_run_blackbox_test/main.tf index 0ea0696f53..2076a3adcf 100644 --- a/enos/modules/vault_run_blackbox_test/main.tf +++ b/enos/modules/vault_run_blackbox_test/main.tf @@ -52,14 +52,10 @@ locals { domain_dn = try(local.ldap_config.domain, "") != "" ? join(",", [for part in split(".", local.ldap_config.domain) : "dc=${part}"]) : "" # Set up LDAP environment variables when LDAP integration is available - # LDAP_SERVER uses private_ip for Vault operations (runs on Vault leader host) - # LDAP_SERVER_PUBLIC uses public_ip for test setup operations (runs from GitHub runner) ldap_environment = try(local.ldap_config.domain, "") != "" ? { - LDAP_SERVER = local.ldap_config.host.private_ip - LDAP_SERVER_PUBLIC = local.ldap_config.host.public_ip - LDAP_PORT = tostring(local.ldap_config.port) - LDAP_BIND_DN = "cn=admin,${local.domain_dn}" - LDAP_BIND_PASS = local.ldap_config.admin_pw + LDAP_SERVER = "ldap://${local.ldap_config.host.private_ip}:${local.ldap_config.port}" + LDAP_BIND_DN = "cn=admin,${local.domain_dn}" + LDAP_BIND_PASS = local.ldap_config.admin_pw } : {} } diff --git a/enos/modules/vault_run_blackbox_test/scripts/run-test.sh b/enos/modules/vault_run_blackbox_test/scripts/run-test.sh index 10bad74f98..5a91e3854c 100755 --- a/enos/modules/vault_run_blackbox_test/scripts/run-test.sh +++ b/enos/modules/vault_run_blackbox_test/scripts/run-test.sh @@ -13,6 +13,7 @@ fail() { [[ -z "${VAULT_TOKEN}" ]] && fail "VAULT_TOKEN env variable has not been set" [[ -z "${VAULT_ADDR}" ]] && fail "VAULT_ADDR env variable has not been set" [[ -z "${VAULT_TEST_PACKAGE}" ]] && fail "VAULT_TEST_PACKAGE env variable has not been set" +[[ -z "${VAULT_EDITION}" ]] && fail "VAULT_EDITION env variable has not been set" # Check required dependencies echo "Checking required dependencies..." @@ -76,6 +77,18 @@ echo "Running tests..." echo "Vault environment variables:" env | grep VAULT | sed 's/VAULT_TOKEN=.*/VAULT_TOKEN=***REDACTED***/' +case $VAULT_EDITION in + ent | ent.hsm | ent.hsm.fips1402 | ent.hsm.fips1403 | ent.fips1403 | ent.fips1402) + tags="-tags=ent,enterprise" + ;; + ce) + tags="" + ;; + *) + fail "unknown VAULT_EDITION: $VAULT_EDITION" + ;; +esac + # Build gotestsum command based on whether we have specific tests set -x # Show commands being executed set +e # Temporarily disable exit on error @@ -84,10 +97,10 @@ if [ -n "$VAULT_TEST_MATRIX" ] && [ -f "$VAULT_TEST_MATRIX" ]; then # Extract test names from matrix and create regex pattern test_pattern=$(jq -r '.include[].test' "$VAULT_TEST_MATRIX" | paste -sd '|' -) echo "Running specific tests: $test_pattern" - gotestsum --junitfile="$junit_output" --format=standard-verbose --jsonfile="$json_output" -- -count=1 -run="$test_pattern" "$VAULT_TEST_PACKAGE" + gotestsum --junitfile="$junit_output" --format=standard-verbose --jsonfile="$json_output" -- -count=1 "${tags}" -run="$test_pattern" "$VAULT_TEST_PACKAGE" else echo "Running all tests in package" - gotestsum --junitfile="$junit_output" --format=standard-verbose --jsonfile="$json_output" -- -count=1 "$VAULT_TEST_PACKAGE" + gotestsum --junitfile="$junit_output" --format=standard-verbose --jsonfile="$json_output" -- -count=1 "${tags}" "$VAULT_TEST_PACKAGE" fi test_exit_code=$? set -e # Re-enable exit on error diff --git a/enos/modules/verify_secrets_engines/modules/create/ldap/ldap.tf b/enos/modules/verify_secrets_engines/modules/create/ldap/ldap.tf index 54cdc2e46d..1211197d7c 100644 --- a/enos/modules/verify_secrets_engines/modules/create/ldap/ldap.tf +++ b/enos/modules/verify_secrets_engines/modules/create/ldap/ldap.tf @@ -79,8 +79,25 @@ output "ldap" { value = local.ldap_output } +# Ensure that our base DN is available on the LDAP server before we attempt to +# to mount the engine. +module "wait_for_ldap_base_dn" { + source = "../../../../ldap_wait_for_search" + + hosts = { 0 : var.leader_host } + ldap_base_dn = var.integration_host_state.ldap.base_dn + ldap_bind_dn = "cn=admin,${var.integration_host_state.ldap.base_dn}" + ldap_host = var.integration_host_state.ldap.host + ldap_password = var.integration_host_state.ldap.admin_pw + ldap_port = var.integration_host_state.ldap.port +} + # Enable LDAP secrets engine resource "enos_remote_exec" "secrets_enable_ldap_secret" { + depends_on = [ + module.wait_for_ldap_base_dn, + ] + environment = { ENGINE = local.ldap_output.ldap_mount MOUNT = local.ldap_output.ldap_mount @@ -98,7 +115,7 @@ resource "enos_remote_exec" "secrets_enable_ldap_secret" { } } -# Setup OpenLDAP infrastructure +# Setup OpenLDAP infrastructure resource "enos_remote_exec" "ldap_setup" { depends_on = [ enos_remote_exec.secrets_enable_ldap_secret @@ -151,11 +168,38 @@ resource "enos_remote_exec" "ldap_secrets_config" { } } +resource "enos_remote_exec" "ldap_password_policy" { + depends_on = [ + enos_remote_exec.secrets_enable_ldap_secret, + enos_remote_exec.ldap_setup + ] + + environment = { + MOUNT = local.ldap_output.ldap_mount + LDAP_SERVER = local.ldap_output.host.private_ip + LDAP_PORT = local.ldap_output.port + LDAP_USERNAME = local.ldap_output.username + LDAP_ADMIN_PW = local.ldap_output.pw + VAULT_ADDR = var.vault_addr + VAULT_INSTALL_DIR = var.vault_install_dir + VAULT_TOKEN = var.vault_root_token + } + + scripts = [abspath("${path.module}/../../../scripts/ldap/add-ldap-password-policy.sh")] + + transport = { + ssh = { + host = var.leader_host.public_ip + } + } +} + # Create a new Library set of service accounts # Test Case: Service Account Library - Create a new Library set of service accounts resource "enos_remote_exec" "ldap_library_set_create" { depends_on = [ enos_remote_exec.ldap_secrets_config, + enos_remote_exec.ldap_password_policy, ] environment = { @@ -352,28 +396,3 @@ resource "enos_remote_exec" "ldap_library_self_checkin" { } } } - -resource "enos_remote_exec" "ldap_password_policy" { - depends_on = [ - enos_remote_exec.secrets_enable_ldap_secret - ] - - environment = { - MOUNT = local.ldap_output.ldap_mount - LDAP_SERVER = local.ldap_output.host.private_ip - LDAP_PORT = local.ldap_output.port - LDAP_USERNAME = local.ldap_output.username - LDAP_ADMIN_PW = local.ldap_output.pw - VAULT_ADDR = var.vault_addr - VAULT_INSTALL_DIR = var.vault_install_dir - VAULT_TOKEN = var.vault_root_token - } - - scripts = [abspath("${path.module}/../../../scripts/ldap/add-ldap-password-policy.sh")] - - transport = { - ssh = { - host = var.leader_host.public_ip - } - } -} diff --git a/sdk/helper/testcluster/blackbox/session.go b/sdk/helper/testcluster/blackbox/session.go index 5b32dbf1b9..e135069017 100644 --- a/sdk/helper/testcluster/blackbox/session.go +++ b/sdk/helper/testcluster/blackbox/session.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path" + "slices" "testing" "time" @@ -19,11 +20,24 @@ import ( // Session holds the test context and Vault client type Session struct { t *testing.T + NoCleanup bool Client *api.Client Namespace string } -func New(t *testing.T) *Session { +func (s *Session) T() *testing.T { + return s.t +} + +type SessionOpts func(s *Session) + +func WithNoCleanup() SessionOpts { + return func(s *Session) { + s.NoCleanup = true + } +} + +func New(t *testing.T, opts ...SessionOpts) *Session { t.Helper() addr := os.Getenv("VAULT_ADDR") @@ -50,12 +64,6 @@ func New(t *testing.T) *Session { _, err = privClient.Logical().Write(nsURLPath, nil) require.NoError(t, err) - t.Cleanup(func() { - _, err = privClient.Logical().Delete(nsURLPath) - require.NoError(t, err) - t.Logf("Cleaned up namespace %s", nsName) - }) - // session client should get the full namespace of parent + test fullNSPath := nsName if parentNS != "" { @@ -74,6 +82,20 @@ func New(t *testing.T) *Session { Namespace: nsName, } + for opt := range slices.Values(opts) { + opt(session) + } + + t.Cleanup(func() { + if session.NoCleanup { + t.Logf("WARN: NoDebug has been set, not cleaning up namespace") + return + } + _, err = privClient.Logical().Delete(nsURLPath) + require.NoError(t, err) + t.Logf("Cleaned up namespace %s", nsName) + }) + // make sure the namespace has been created session.Eventually(func() error { // this runs inside the new namespace, so if it succeeds, we're good diff --git a/vault/external_tests/blackbox/secrets_ldap_test.go b/vault/external_tests/blackbox/secrets_ldap_test.go index 1cac6565b8..a76ac89eba 100644 --- a/vault/external_tests/blackbox/secrets_ldap_test.go +++ b/vault/external_tests/blackbox/secrets_ldap_test.go @@ -4,17 +4,43 @@ package blackbox import ( + "net" + "net/url" "os" "testing" + "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// requireLDAPAvailable verifies LDAP server connectivity using testify Eventually +func requireLDAPAvailable(t *testing.T, timeout, interval time.Duration) { + t.Helper() + + // Use public IP for external connectivity testing + ldapServerPublic := os.Getenv("LDAP_URL_PUBLIC") + require.NotEmpty(t, ldapServerPublic, "LDAP_URL_PUBLIC environment variable not set") + + u, err := url.Parse(ldapServerPublic) + require.NoError(t, err, "Failed to parse LDAP URL: %s", ldapServerPublic) + + require.EventuallyWithT(t, func(ct *assert.CollectT) { + d := &net.Dialer{} + conn, err := d.DialContext(t.Context(), "tcp", u.Host) + require.NoError(ct, err) + require.NoError(ct, conn.Close()) + }, timeout, interval, "LDAP server not available at %s", u.Host) + + t.Logf("LDAP server connectivity verified at %s", u.Host) +} + // testLDAPSecretsCreate tests LDAP secrets engine creation func testLDAPSecretsCreate(t *testing.T, v *blackbox.Session) { // Check if LDAP server configuration is available from integration host - ldapServer := os.Getenv("LDAP_SERVER") + ldapServer := os.Getenv("LDAP_URL_PRIVATE") ldapBindDN := os.Getenv("LDAP_BIND_DN") ldapBindPass := os.Getenv("LDAP_BIND_PASS") @@ -22,6 +48,9 @@ func testLDAPSecretsCreate(t *testing.T, v *blackbox.Session) { t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") } + // Verify LDAP server is ready before proceeding + requireLDAPAvailable(t, 1*time.Minute, 2*time.Second) + // Enable LDAP secrets engine v.MustEnableSecretsEngine("ldap-create", &api.MountInput{Type: "ldap"}) @@ -53,7 +82,7 @@ func testLDAPSecretsCreate(t *testing.T, v *blackbox.Session) { // testLDAPSecretsRead tests LDAP secrets engine read operations func testLDAPSecretsRead(t *testing.T, v *blackbox.Session) { // Check if LDAP server configuration is available from integration host - ldapServer := os.Getenv("LDAP_SERVER") + ldapServer := os.Getenv("LDAP_URL_PRIVATE") ldapBindDN := os.Getenv("LDAP_BIND_DN") ldapBindPass := os.Getenv("LDAP_BIND_PASS") @@ -61,6 +90,10 @@ func testLDAPSecretsRead(t *testing.T, v *blackbox.Session) { t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") } + // Verify LDAP server is ready before proceeding + requireLDAPAvailable(t, 1*time.Minute, 2*time.Second) + serviceAccounts := []string{"svc-account-1", "svc-account-2"} + // Enable LDAP secrets engine v.MustEnableSecretsEngine("ldap-read", &api.MountInput{Type: "ldap"}) @@ -75,7 +108,7 @@ func testLDAPSecretsRead(t *testing.T, v *blackbox.Session) { // Create a library set for service account management v.MustWrite("ldap-read/library/test-set", map[string]any{ - "service_account_names": []string{"svc-account-1", "svc-account-2"}, + "service_account_names": serviceAccounts, "ttl": "10h", "max_ttl": "20h", "disable_check_in_enforcement": false, @@ -106,7 +139,7 @@ func testLDAPSecretsRead(t *testing.T, v *blackbox.Session) { // testLDAPSecretsDelete tests LDAP secrets engine delete operations func testLDAPSecretsDelete(t *testing.T, v *blackbox.Session) { // Check if LDAP server configuration is available from integration host - ldapServer := os.Getenv("LDAP_SERVER") + ldapServer := os.Getenv("LDAP_URL_PRIVATE") ldapBindDN := os.Getenv("LDAP_BIND_DN") ldapBindPass := os.Getenv("LDAP_BIND_PASS") @@ -114,6 +147,10 @@ func testLDAPSecretsDelete(t *testing.T, v *blackbox.Session) { t.Skip("LDAP server configuration not available - skipping LDAP secrets engine test") } + // Verify LDAP server is ready before proceeding + requireLDAPAvailable(t, 1*time.Minute, 2*time.Second) + serviceAccounts := []string{"svc-delete"} + // Enable LDAP secrets engine v.MustEnableSecretsEngine("ldap-delete", &api.MountInput{Type: "ldap"}) @@ -128,7 +165,7 @@ func testLDAPSecretsDelete(t *testing.T, v *blackbox.Session) { // Create a library set v.MustWrite("ldap-delete/library/delete-set", map[string]any{ - "service_account_names": []string{"svc-delete"}, + "service_account_names": serviceAccounts, "ttl": "1h", })