Merge remote-tracking branch 'remotes/from/ce/main'

This commit is contained in:
hc-github-team-secure-vault-core 2026-04-08 17:22:40 +00:00
commit 08503e7dd0
8 changed files with 647 additions and 26 deletions

View File

@ -152,30 +152,61 @@ globals {
protocol = "tcp"
}
}
// Ports that we'll open up for ingress in the security group for all external integration target machines.
integration_host_ports = {
ldap : {
description = "LDAP"
port = 389
protocol = "tcp"
// Database configurations for Vault database secrets engine testing
database_configs = {
postgres : {
port = 5432
version = "16-alpine"
username = "postgres"
password = "secret"
database = "postgres"
description = "PostgreSQL Server"
},
ldaps : {
description = "LDAPS"
port = 636
protocol = "tcp"
mongodb : {
port = 27017
version = "7.0"
username = "admin"
password = "secret"
database = "admin"
description = "MongoDB Server"
},
mysql : {
description = "MySQL Server"
port = 3306
protocol = "tcp"
},
kmip : {
description = "KMIP Server"
port = 5696
protocol = "tcp"
},
version = "8.0"
username = "root"
password = "secret"
database = "mysql"
description = "MySQL Server"
}
}
// Ports that we'll open up for ingress in the security group for all external integration target machines.
integration_host_ports = merge(
{
ldap : {
description = "LDAP"
port = 389
protocol = "tcp"
},
ldaps : {
description = "LDAPS"
port = 636
protocol = "tcp"
},
kmip : {
description = "KMIP Server"
port = 5696
protocol = "tcp"
}
},
// Add database ports dynamically from database_configs
{ for db_name, db_config in global.database_configs : db_name => {
description = db_config.description
port = db_config.port
protocol = "tcp"
} }
)
// Combine all ports into a single map
ports = merge(
global.vault_cluster_ports,

View File

@ -234,10 +234,11 @@ scenario "plugin" {
}
variables {
hosts = step.create_plugin_integration_target.hosts
ip_version = matrix.ip_version
packages = concat(global.packages, global.distro_packages["ubuntu"]["24.04"], ["podman", "podman-docker"])
ports = global.integration_host_ports
hosts = step.create_plugin_integration_target.hosts
ip_version = matrix.ip_version
packages = concat(global.packages, global.distro_packages["ubuntu"]["24.04"], ["podman", "podman-docker"])
ports = global.integration_host_ports
database_configs = global.database_configs
}
}

View File

@ -0,0 +1,80 @@
# Copyright IBM Corp. 2016, 2026
# SPDX-License-Identifier: BUSL-1.1
terraform {
required_providers {
enos = {
source = "registry.terraform.io/hashicorp-forge/enos"
}
}
}
locals {
# Database-specific configurations
database_configs = {
postgres = {
image_template = "docker.io/postgres:${var.db_version}"
env_vars = {
POSTGRES_USER = var.username
POSTGRES_PASSWORD = var.password
POSTGRES_DB = var.database
}
}
mongodb = {
image_template = "docker.io/mongo:${var.db_version}"
env_vars = {
MONGO_INITDB_ROOT_USERNAME = var.username
MONGO_INITDB_ROOT_PASSWORD = var.password
MONGO_INITDB_DATABASE = var.database
}
}
mysql = {
image_template = "docker.io/mysql:${var.db_version}"
env_vars = {
MYSQL_ROOT_PASSWORD = var.password
MYSQL_USER = var.username
MYSQL_PASSWORD = var.password
MYSQL_DATABASE = var.database
}
}
}
config = local.database_configs[var.database_type]
image = local.config.image_template
env_vars_map = local.config.env_vars
env_vars = join(",", [for k, v in local.env_vars_map : "${k}=${v}"])
}
# Creating Database Server using generic container script
resource "enos_remote_exec" "create_database" {
depends_on = [var.depends_on_modules]
scripts = [abspath("${path.module}/../../modules/set_up_external_integration_target/scripts/start-container.sh")]
environment = {
CONTAINER_IMAGE = local.image
CONTAINER_NAME = "${var.database_type}-${var.instance_name}"
CONTAINER_PORTS = var.port
CONTAINER_ENVS = local.env_vars
}
transport = {
ssh = {
host = var.host.public_ip
}
}
}
# Outputs
output "config" {
description = "Database configuration details"
value = {
type = var.database_type
username = var.username
password = var.password
database = var.database
version = var.db_version
port = var.port
host = var.host
}
}

View File

@ -0,0 +1,58 @@
# Copyright IBM Corp. 2016, 2026
# SPDX-License-Identifier: BUSL-1.1
variable "database_type" {
description = "Type of database to create (postgres, mongodb, mysql)"
type = string
validation {
condition = contains(["postgres", "mongodb", "mysql"], var.database_type)
error_message = "database_type must be one of: postgres, mongodb, mysql"
}
}
variable "db_version" {
description = "Database version to use"
type = string
}
variable "username" {
description = "Database username"
type = string
}
variable "password" {
description = "Database password"
type = string
sensitive = true
}
variable "database" {
description = "Database name"
type = string
}
variable "port" {
description = "Database port"
type = number
}
variable "host" {
description = "Host configuration with public_ip"
type = object({
public_ip = string
private_ip = string
ipv6 = optional(string)
})
}
variable "instance_name" {
description = "Unique instance name for the container (defaults to 'default')"
type = string
default = "default"
}
variable "depends_on_modules" {
description = "List of modules this depends on"
type = list(any)
default = []
}

View File

@ -29,14 +29,24 @@ locals {
port = var.ports.mysql.port
host = var.hosts[0]
}
# Database configurations are now pulled from var.database_configs
database_servers = {
for db_name, db_config in var.database_configs : db_name => merge(db_config, {
host = var.hosts[0]
})
}
}
# Outputs
output "state" {
value = {
ldap = local.ldap_server
kmip = local.kmip_client
}
value = merge(
{
ldap = local.ldap_server
kmip = local.kmip_client
},
# Add database servers dynamically
{ for db_name, db_server in local.database_servers : db_name => db_server }
)
}
# We run install_packages before we install Vault because for some combinations of
@ -125,3 +135,19 @@ resource "enos_remote_exec" "create_kmip" {
}
}
}
# Creating Database Servers using generic database_container module
module "database_servers" {
for_each = var.database_configs
source = "../database_container"
database_type = each.key
db_version = each.value.version
username = each.value.username
password = each.value.password
database = each.value.database
port = each.value.port
host = var.hosts[0]
instance_name = "default"
depends_on_modules = [module.install_packages]
}

View File

@ -41,3 +41,17 @@ variable "ports" {
description = string
}))
}
variable "database_configs" {
description = "Database configurations for setting up database servers"
type = map(object({
port = number
version = string
username = string
password = string
database = string
description = string
}))
default = {}
}

View File

@ -0,0 +1,145 @@
// Copyright IBM Corp. 2025, 2026
// SPDX-License-Identifier: BUSL-1.1
package mongodb
import (
"testing"
"github.com/hashicorp/vault/sdk/helper/testcluster/blackbox"
)
// TestMongoDBConnectionConfigCRUDWorkflows runs all MongoDB connection
// config CRUD workflow tests.
func TestMongoDBConnectionConfigCRUDWorkflows(t *testing.T) {
t.Run("CreateBasic", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigCreateBasic(t, v)
})
t.Run("ListReturnsNamesOnly", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigListReturnsNamesOnly(t, v)
})
t.Run("ReadRedactsSensitiveFields", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigReadRedactsSensitiveFields(t, v)
})
t.Run("ResetConnection", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigResetConnection(t, v)
})
t.Run("DeleteConnection", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigDeleteConnection(t, v)
})
}
// TestMongoDBConnectionConfigValidationWorkflows runs all MongoDB
// connection config validation workflow tests.
func TestMongoDBConnectionConfigValidationWorkflows(t *testing.T) {
t.Run("VerifyConnectionValid", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigVerifyConnectionValid(t, v)
})
t.Run("VerifyConnectionInvalid", func(t *testing.T) {
t.Parallel()
v := blackbox.New(t)
testMongoDBConnectionConfigVerifyConnectionInvalid(t, v)
})
}
// testMongoDBConnectionConfigCreateBasic verifies that configuring a
// MongoDB database connection succeeds at database/config/{name}.
func testMongoDBConnectionConfigCreateBasic(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test following this pattern:
// 1. requireVaultEnv(t)
// 2. cleanup, connURL := PrepareTestContainer(t)
// 3. defer cleanup()
// 4. mount := fmt.Sprintf("database-%s", sanitize(t.Name()))
// 5. v.MustEnableSecretsEngine(mount, &api.MountInput{Type: "database"})
// 6. v.MustWrite(mount+"/config/my-mongodb-db", mongoConnectionConfigPayload(...))
// 7. config := v.MustReadRequired(mount + "/config/my-mongodb-db")
// 8. v.AssertSecret(config).Data().HasKey("plugin_name", "mongodb-database-plugin")
}
// testMongoDBConnectionConfigListReturnsNamesOnly verifies LIST
// /database/config returns configured connection names only, without
// sensitive config details.
func testMongoDBConnectionConfigListReturnsNamesOnly(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create multiple MongoDB connections
// 2. List connections
// 3. Verify only names are returned, no sensitive data
}
// testMongoDBConnectionConfigReadRedactsSensitiveFields verifies that reading
// a MongoDB connection config returns sanitized connection details.
func testMongoDBConnectionConfigReadRedactsSensitiveFields(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create MongoDB connection with credentials
// 2. Read connection config
// 3. Verify password and other sensitive fields are redacted
}
// testMongoDBConnectionConfigVerifyConnectionValid verifies that
// configuration succeeds with verify_connection=true when credentials are valid.
func testMongoDBConnectionConfigVerifyConnectionValid(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create MongoDB connection with valid credentials and verify_connection=true
// 2. Verify connection succeeds
}
// testMongoDBConnectionConfigVerifyConnectionInvalid verifies that
// configuration fails with verify_connection=true when credentials are invalid.
func testMongoDBConnectionConfigVerifyConnectionInvalid(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create MongoDB connection with invalid credentials and verify_connection=true
// 2. Verify connection fails with appropriate error
}
// testMongoDBConnectionConfigResetConnection verifies that resetting a
// MongoDB database connection succeeds and preserves the stored connection
// configuration.
func testMongoDBConnectionConfigResetConnection(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create MongoDB connection
// 2. Reset connection
// 3. Verify config is preserved
}
// testMongoDBConnectionConfigDeleteConnection verifies that deleting a
// MongoDB database connection removes it, prevents new credential generation,
// and remains idempotent when deleted again.
func testMongoDBConnectionConfigDeleteConnection(t *testing.T, v *blackbox.Session) {
t.Skip("Test implementation pending - MongoDB framework setup complete")
// TODO: Implement test to verify:
// 1. Create MongoDB connection and role
// 2. Generate credentials
// 3. Delete connection
// 4. Verify connection is gone and credentials can't be generated
// 5. Delete again to verify idempotency
}

View File

@ -0,0 +1,266 @@
// Copyright IBM Corp. 2025, 2026
// SPDX-License-Identifier: BUSL-1.1
package mongodb
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/sdk/helper/docker"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
defaultMongoImage = "docker.mirror.hashicorp.services/mongo"
defaultMongoVersion = "7.0"
defaultMongoUser = "admin"
defaultMongoPass = "secret"
)
// defaultRunOpts returns default Docker run options for MongoDB container
// Uses test name to ensure unique container names for parallel execution
func defaultRunOpts(t *testing.T) docker.RunOptions {
return docker.RunOptions{
ContainerName: fmt.Sprintf("mongodb-%s", sanitize(t.Name())),
ImageRepo: defaultMongoImage,
ImageTag: defaultMongoVersion,
Env: []string{
"MONGO_INITDB_ROOT_USERNAME=" + defaultMongoUser,
"MONGO_INITDB_ROOT_PASSWORD=" + defaultMongoPass,
"MONGO_INITDB_DATABASE=admin",
},
Ports: []string{"27017/tcp"},
DoNotAutoRemove: false,
OmitLogTimestamps: true,
LogConsumer: func(s string) {
if t.Failed() {
t.Logf("container logs: %s", s)
}
},
}
}
// requireVaultEnv skips the test if required Vault environment variables are not set
func requireVaultEnv(t *testing.T) {
t.Helper()
if os.Getenv("VAULT_ADDR") == "" || os.Getenv("VAULT_TOKEN") == "" {
t.Skip("skipping blackbox test: VAULT_ADDR and VAULT_TOKEN are required")
}
}
// sanitize converts test name to a valid container name
// Removes special characters and converts to lowercase
func sanitize(name string) string {
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "/", "-")
name = strings.ReplaceAll(name, "_", "-")
name = strings.ReplaceAll(name, " ", "-")
// Remove any remaining special characters
var result strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
result.WriteRune(r)
}
}
return result.String()
}
// PrepareTestContainer starts a MongoDB container for testing
// Returns cleanup function and connection URL
// If MONGO_URL environment variable is set, uses that instead of starting a container
func PrepareTestContainer(t *testing.T) (func(), string) {
_, cleanup, connURL, _ := prepareTestContainer(t, defaultRunOpts(t), defaultMongoPass, true, false)
return cleanup, connURL
}
// prepareTestContainer is the internal function that handles container setup
// Supports both Docker container creation and external MongoDB via environment variable
func prepareTestContainer(
t *testing.T,
runOpts docker.RunOptions,
password string,
addSuffix bool,
forceLocalAddr bool,
) (*docker.Runner, func(), string, string) {
requireVaultEnv(t)
// Check for external MongoDB URL
if os.Getenv("MONGO_URL") != "" {
envMongoURL := os.Getenv("MONGO_URL")
// Create unique database for this test
dbName := fmt.Sprintf("test_%s_%d", sanitize(t.Name()), time.Now().Unix())
testURL := replaceDatabase(envMongoURL, dbName)
// Create the database
if err := createDatabase(t, envMongoURL, dbName); err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
cleanup := func() {
dropDatabase(t, envMongoURL, dbName)
}
return nil, cleanup, testURL, ""
}
// Start Docker container
runner, err := docker.NewServiceRunner(runOpts)
if err != nil {
errStr := strings.ToLower(err.Error())
if strings.Contains(errStr, "docker") &&
(strings.Contains(errStr, "daemon") || strings.Contains(errStr, "connect")) {
t.Skipf("skipping blackbox test: docker not available: %v", err)
}
t.Fatalf("Could not start docker MongoDB: %s", err)
}
svc, containerID, err := runner.StartNewService(
context.Background(),
addSuffix,
forceLocalAddr,
connectMongoDB(password),
)
if err != nil {
errStr := strings.ToLower(err.Error())
if strings.Contains(errStr, "docker") &&
(strings.Contains(errStr, "daemon") || strings.Contains(errStr, "connect")) {
t.Skipf("skipping blackbox test: docker not available: %v", err)
}
t.Fatalf("Could not start docker MongoDB: %s", err)
}
connURL := svc.Config.URL().String()
// Create unique database for this test
dbName := fmt.Sprintf("test_%s_%d", sanitize(t.Name()), time.Now().Unix())
testURL := replaceDatabase(connURL, dbName)
// Create the database
if err := createDatabase(t, connURL, dbName); err != nil {
svc.Cleanup()
t.Fatalf("Failed to create test database: %v", err)
}
cleanup := func() {
dropDatabase(t, connURL, dbName)
svc.Cleanup()
}
return runner, cleanup, testURL, containerID
}
// connectMongoDB returns a ServiceAdapter that connects to MongoDB
// Includes retry logic with 30-second timeout for container startup
func connectMongoDB(password string) docker.ServiceAdapter {
return func(ctx context.Context, host string, port int) (docker.ServiceConfig, error) {
u := url.URL{
Scheme: "mongodb",
User: url.UserPassword(defaultMongoUser, password),
Host: fmt.Sprintf("%s:%d", host, port),
Path: "/admin",
}
// Retry connection with timeout
deadline := time.Now().Add(30 * time.Second)
var lastErr error
for time.Now().Before(deadline) {
client, err := mongo.Connect(ctx, options.Client().ApplyURI(u.String()))
if err != nil {
lastErr = err
time.Sleep(1 * time.Second)
continue
}
// Ping to verify connection
if err = client.Ping(ctx, nil); err != nil {
client.Disconnect(ctx)
lastErr = err
time.Sleep(1 * time.Second)
continue
}
// Connection successful
client.Disconnect(ctx)
return docker.NewServiceURL(u), nil
}
return nil, fmt.Errorf("mongodb not ready after 30s: %w", lastErr)
}
}
// replaceDatabase replaces the database name in a MongoDB connection URL
func replaceDatabase(connURL, dbName string) string {
u, err := url.Parse(connURL)
if err != nil {
return connURL
}
u.Path = "/" + dbName
return u.String()
}
// createDatabase creates a new database in MongoDB
// MongoDB creates databases lazily, so we create a collection to ensure it exists
func createDatabase(t *testing.T, connURL, dbName string) error {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL))
if err != nil {
return fmt.Errorf("failed to connect to MongoDB: %w", err)
}
defer client.Disconnect(ctx)
// Create a collection to ensure database exists
db := client.Database(dbName)
if err := db.CreateCollection(ctx, "_init"); err != nil {
return fmt.Errorf("failed to create database: %w", err)
}
t.Logf("Created test database: %s", dbName)
return nil
}
// dropDatabase drops a database from MongoDB
func dropDatabase(t *testing.T, connURL, dbName string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL))
if err != nil {
t.Logf("Warning: failed to connect for cleanup: %v", err)
return
}
defer client.Disconnect(ctx)
if err := client.Database(dbName).Drop(ctx); err != nil {
t.Logf("Warning: failed to drop database %s: %v", dbName, err)
return
}
t.Logf("Dropped test database: %s", dbName)
}
// mongoConnectionConfigPayload returns a standard MongoDB connection configuration payload
func mongoConnectionConfigPayload(connURL, allowedRoles string, verifyConnection bool) map[string]any {
return map[string]any{
"plugin_name": "mongodb-database-plugin",
"connection_url": connURL,
"allowed_roles": allowedRoles,
"verify_connection": verifyConnection,
}
}