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", })