From 8fe0d13caa746952c8080db008e7316b8159b822 Mon Sep 17 00:00:00 2001 From: Michael Li Date: Fri, 16 Sep 2022 11:45:55 -0400 Subject: [PATCH] test(e2e): Add vault tests Adds the first set of vault tests. This test sets up vault as a credential store and creates a set of credentials for a target. --- enos/enos-modules.hcl | 19 + enos/enos-scenario-e2e-target.hcl | 12 +- enos/enos-scenario-e2e-vault.hcl | 145 ++++++ enos/enos-variables.hcl | 6 + enos/modules/test_e2e/main.tf | 19 +- testing/internal/e2e/README.md | 8 +- testing/internal/e2e/boundary/boundary.go | 19 +- testing/internal/e2e/boundary/scopes.go | 58 +++ .../e2e/credential/vault/vault_test.go | 419 ++++++++++++++++++ testing/internal/e2e/helpers.go | 91 +++- testing/internal/e2e/target/target_test.go | 95 ++-- .../e2e/vault/boundary-controller-policy.hcl | 23 + testing/internal/e2e/vault/vault.go | 100 +++++ 13 files changed, 922 insertions(+), 92 deletions(-) create mode 100644 enos/enos-scenario-e2e-vault.hcl create mode 100644 testing/internal/e2e/boundary/scopes.go create mode 100644 testing/internal/e2e/credential/vault/vault_test.go create mode 100644 testing/internal/e2e/vault/boundary-controller-policy.hcl create mode 100644 testing/internal/e2e/vault/vault.go diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 2fc4c0dac..692ad5f5f 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -2,6 +2,10 @@ module "az_finder" { source = "./modules/az_finder" } +module "binary_finder" { + source = "./modules/binary_finder" +} + module "boundary" { source = "app.terraform.io/hashicorp-qti/aws-boundary/enos" version = ">= 0.2.6" @@ -58,6 +62,21 @@ module "target" { enos_user = var.enos_user } +module "vault" { + source = "app.terraform.io/hashicorp-qti/aws-vault/enos" + + project_name = "qti-enos-boundary" + environment = var.environment + common_tags = { + "Project" : "Enos", + "Project Name" : "qti-enos-boundary", + "Enos User" : var.enos_user, + "Environment" : var.environment + } + + ssh_aws_keypair = var.aws_ssh_keypair_name +} + module "test_smoke" { source = "./modules/test_smoke" } diff --git a/enos/enos-scenario-e2e-target.hcl b/enos/enos-scenario-e2e-target.hcl index b4c35bea1..c764d338d 100644 --- a/enos/enos-scenario-e2e-target.hcl +++ b/enos/enos-scenario-e2e-target.hcl @@ -79,7 +79,7 @@ scenario "e2e_target" { } } - step "launch_targets" { + step "create_target" { module = module.target depends_on = [step.create_base_infra] @@ -89,14 +89,15 @@ scenario "e2e_target" { enos_user = var.enos_user instance_type = var.target_instance_type vpc_id = step.create_base_infra.vpc_id + target_count = 1 } } - step "run_e2e_targets_test" { + step "run_e2e_test" { module = module.test_e2e depends_on = [ step.create_boundary_cluster, - step.launch_targets + step.create_target ] variables { @@ -107,11 +108,12 @@ scenario "e2e_target" { auth_password = step.create_boundary_cluster.auth_password local_boundary_dir = local.local_boundary_dir aws_ssh_private_key_path = local.aws_ssh_private_key_path - target_ips = step.launch_targets.target_ips + target_ips = step.create_target.target_ips + target_user = "ubuntu" } } output "test_results" { - value = step.run_e2e_targets_test.test_results + value = step.run_e2e_test.test_results } } diff --git a/enos/enos-scenario-e2e-vault.hcl b/enos/enos-scenario-e2e-vault.hcl new file mode 100644 index 000000000..b901a9c28 --- /dev/null +++ b/enos/enos-scenario-e2e-vault.hcl @@ -0,0 +1,145 @@ +scenario "e2e_vault" { + terraform_cli = terraform_cli.default + terraform = terraform.default + providers = [ + provider.aws.default, + provider.enos.default + ] + + matrix { + builder = ["local", "crt"] + } + + locals { + aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) + boundary_install_dir = abspath(var.boundary_install_dir) + local_boundary_dir = abspath(var.local_boundary_dir) + build_path = { + "local" = "/tmp", + "crt" = var.crt_bundle_path == null ? null : abspath(var.crt_bundle_path) + } + } + + step "check_jq_is_installed" { + module = module.binary_finder + + variables { + name = "jq" + } + } + + step "find_azs" { + module = module.az_finder + + variables { + instance_type = [ + var.worker_instance_type, + var.controller_instance_type + ] + } + } + + step "create_db_password" { + module = module.random_stringifier + } + + step "build_boundary" { + module = matrix.builder == "crt" ? module.build_crt : module.build_local + + variables { + path = local.build_path[matrix.builder] + } + } + + step "create_base_infra" { + module = module.infra + + variables { + availability_zones = step.find_azs.availability_zones + } + } + + step "create_boundary_cluster" { + module = module.boundary + depends_on = [ + step.create_base_infra, + step.build_boundary + ] + + variables { + boundary_install_dir = local.boundary_install_dir + controller_instance_type = var.controller_instance_type + controller_count = var.controller_count + db_pass = step.create_db_password.string + kms_key_arn = step.create_base_infra.kms_key_arn + local_artifact_path = step.build_boundary.artifact_path + ubuntu_ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] + vpc_id = step.create_base_infra.vpc_id + worker_count = var.worker_count + worker_instance_type = var.worker_instance_type + } + } + + step "create_vault_cluster" { + module = module.vault + depends_on = [ + step.create_base_infra, + ] + + variables { + ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] + instance_type = var.vault_instance_type + instance_count = 1 + kms_key_arn = step.create_base_infra.kms_key_arn + storage_backend = "raft" + sg_additional_ips = step.create_boundary_cluster.controller_ips + unseal_method = "awskms" + vault_release = { + version = "1.11.0" + edition = "oss" + } + vpc_id = step.create_base_infra.vpc_id + } + } + + step "create_target" { + module = module.target + depends_on = [step.create_base_infra] + + variables { + ami_id = step.create_base_infra.ami_ids["ubuntu"]["amd64"] + aws_ssh_keypair_name = var.aws_ssh_keypair_name + enos_user = var.enos_user + instance_type = var.target_instance_type + vpc_id = step.create_base_infra.vpc_id + target_count = 1 + } + } + + step "run_e2e_test" { + module = module.test_e2e + depends_on = [ + step.create_boundary_cluster, + step.create_target, + step.create_vault_cluster + ] + + variables { + test_package = "github.com/hashicorp/boundary/testing/internal/e2e/credential/vault" + alb_boundary_api_addr = step.create_boundary_cluster.alb_boundary_api_addr + auth_method_id = step.create_boundary_cluster.auth_method_id + auth_login_name = step.create_boundary_cluster.auth_login_name + auth_password = step.create_boundary_cluster.auth_password + local_boundary_dir = local.local_boundary_dir + aws_ssh_private_key_path = local.aws_ssh_private_key_path + target_ips = step.create_target.target_ips + target_user = "ubuntu" + vault_addr = step.create_vault_cluster.instance_public_ips[0] + vault_root_token = step.create_vault_cluster.vault_root_token + } + } + + output "test_results" { + value = step.run_e2e_test.test_results + } +} diff --git a/enos/enos-variables.hcl b/enos/enos-variables.hcl index 7d6ee3e26..c6869d1ed 100644 --- a/enos/enos-variables.hcl +++ b/enos/enos-variables.hcl @@ -103,3 +103,9 @@ variable "skip_failing_bats_tests" { type = string default = "false" } + +variable "vault_instance_type" { + description = "Instance type for test target nodes" + type = string + default = "t3a.small" +} diff --git a/enos/modules/test_e2e/main.tf b/enos/modules/test_e2e/main.tf index 00cbf418d..f0bec224e 100644 --- a/enos/modules/test_e2e/main.tf +++ b/enos/modules/test_e2e/main.tf @@ -30,6 +30,10 @@ variable "local_boundary_dir" { description = "Local Path to boundary executable" type = string } +variable "target_user" { + description = "SSH username for target" + type = string +} variable "aws_ssh_private_key_path" { description = "Local Path to key used to SSH onto created hosts" type = string @@ -38,9 +42,20 @@ variable "target_ips" { description = "List of IP Addresses of created hosts" type = list(string) } +variable "vault_addr" { + description = "URL of Vault instance" + type = string + default = "" +} +variable "vault_root_token" { + description = "Root token for vault instance" + type = string + default = "" +} locals { aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) + vault_addr = var.vault_addr != "" ? "http://${var.vault_addr}:8200" : "" } resource "enos_local_exec" "run_e2e_test" { @@ -50,8 +65,10 @@ resource "enos_local_exec" "run_e2e_test" { E2E_PASSWORD_ADMIN_LOGIN_NAME = var.auth_login_name, E2E_PASSWORD_ADMIN_PASSWORD = var.auth_password, E2E_TARGET_IP = var.target_ips[0], - E2E_SSH_USER = "ubuntu" + E2E_SSH_USER = var.target_user, E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path, + VAULT_ADDR = local.vault_addr, + VAULT_TOKEN = var.vault_root_token } inline = ["PATH=\"${var.local_boundary_dir}:$PATH\" go test -v ${var.test_package}"] diff --git a/testing/internal/e2e/README.md b/testing/internal/e2e/README.md index e4332de2d..0e3396c89 100644 --- a/testing/internal/e2e/README.md +++ b/testing/internal/e2e/README.md @@ -30,7 +30,13 @@ export E2E_PASSWORD_ADMIN_PASSWORD= # e.g. "password" export E2E_TARGET_IP= # e.g. 192.168.0.1 export E2E_SSH_KEY_PATH= # e.g. /Users/username/key.pem export E2E_SSH_USER= # e.g. ubuntu -export E2E_SSH_PORT= # e.g. 22 + +# For e2e/credential/vault +export VAULT_ADDR= # e.g. http://127.0.0.1:8200 +export VAULT_TOKEN= +export E2E_TARGET_IP= # e.g. 192.168.0.1 +export E2E_SSH_KEY_PATH= # e.g. /Users/username/key.pem +export E2E_SSH_USER= # e.g. ubuntu ``` Then, run... diff --git a/testing/internal/e2e/boundary/boundary.go b/testing/internal/e2e/boundary/boundary.go index 57022e9e7..7ab40ad16 100644 --- a/testing/internal/e2e/boundary/boundary.go +++ b/testing/internal/e2e/boundary/boundary.go @@ -5,11 +5,13 @@ import ( "context" "errors" "fmt" + "testing" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/testing/internal/e2e" "github.com/kelseyhightower/envconfig" + "github.com/stretchr/testify/require" ) type config struct { @@ -80,22 +82,17 @@ func NewApiClient() (*api.Client, error) { } // AuthenticateCli uses the cli to authenticate the specified Boundary instance. -// Returns the result of the command. -func AuthenticateCli() *e2e.CommandResult { +func AuthenticateCli(t testing.TB) { c, err := loadConfig() - if err != nil { - return &e2e.CommandResult{Err: err} - } + require.NoError(t, err) err = c.validate() - if err != nil { - return &e2e.CommandResult{Err: err} - } + require.NoError(t, err) - return e2e.RunCommand( - "boundary", "authenticate", "password", + output := e2e.RunCommand("boundary", e2e.WithArgs("authenticate", "password", "-addr", c.Address, "-auth-method-id", c.AuthMethodId, "-login-name", c.AdminLoginName, "-password", "env://E2E_PASSWORD_ADMIN_PASSWORD", - ) + )) + require.NoError(t, output.Err, string(output.Stderr)) } diff --git a/testing/internal/e2e/boundary/scopes.go b/testing/internal/e2e/boundary/scopes.go new file mode 100644 index 000000000..a03b078d1 --- /dev/null +++ b/testing/internal/e2e/boundary/scopes.go @@ -0,0 +1,58 @@ +package boundary + +import ( + "context" + "encoding/json" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/stretchr/testify/require" +) + +// CreateNewOrgApi creates a new organization in boundary using the go api. +// Returns the id of the new org. +func CreateNewOrgApi(t testing.TB, ctx context.Context, client *api.Client) string { + scopeClient := scopes.NewClient(client) + newOrgResult, err := scopeClient.Create(ctx, "global", scopes.WithName("e2e Automated Test Org")) + require.NoError(t, err) + t.Cleanup(func() { + _, err := scopeClient.Delete(ctx, newOrgResult.Item.Id) + require.NoError(t, err) + }) + + return newOrgResult.Item.Id +} + +// CreateNewOrgCli creates a new organization in boundary using the cli. +// Returns the id of the new org. +func CreateNewOrgCli(t testing.TB) string { + return createNewScopeCli(t, "e2e Automated Test Org", "global") +} + +// CreateNewProjectCli creates a new project in boundary using the cli. +// Returns the id of the new project. +func CreateNewProjectCli(t testing.TB, scopeId string) string { + return createNewScopeCli(t, "e2e Automated Test Project", scopeId) +} + +func createNewScopeCli(t testing.TB, name string, scopeId string) string { + output := e2e.RunCommand("boundary", e2e.WithArgs("scopes", "create", + "-name", name, + "-scope-id", scopeId, + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + + var newOrgResult scopes.ScopeCreateResult + err := json.Unmarshal(output.Stdout, &newOrgResult) + require.NoError(t, err) + + t.Cleanup(func() { + output := e2e.RunCommand("boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgResult.Item.Id)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + return newOrgResult.Item.Id +} diff --git a/testing/internal/e2e/credential/vault/vault_test.go b/testing/internal/e2e/credential/vault/vault_test.go new file mode 100644 index 000000000..733acb596 --- /dev/null +++ b/testing/internal/e2e/credential/vault/vault_test.go @@ -0,0 +1,419 @@ +package vaultcredential_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/api/credentialstores" + "github.com/hashicorp/boundary/api/hostcatalogs" + "github.com/hashicorp/boundary/api/hosts" + "github.com/hashicorp/boundary/api/hostsets" + "github.com/hashicorp/boundary/api/scopes" + "github.com/hashicorp/boundary/api/targets" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/hashicorp/boundary/testing/internal/e2e/vault" + "github.com/kelseyhightower/envconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type config struct { + TargetIp string `envconfig:"E2E_TARGET_IP"` // e.g. 192.168.0.1 + TargetSshUser string `envconfig:"E2E_SSH_USER"` // e.g. ubuntu + TargetSshKeyPath string `envconfig:"E2E_SSH_KEY_PATH"` // e.g. /Users/username/key.pem + TargetPort string `envconfig:"E2E_SSH_PORT" default:"22"` + VaultSecretPath string `envconfig:"E2E_VAULT_SECRET_PATH" default:"e2e_secrets"` +} + +func (c *config) validate() error { + if c.TargetIp == "" { + return errors.New("TargetIp is empty. Set environment variable: E2E_TARGET_IP") + } + if c.TargetSshUser == "" { + return errors.New("TargetSshUser is empty. Set environment variable: E2E_SSH_USER") + } + if c.TargetSshKeyPath == "" { + return errors.New("TargetSshKeyPath is empty. Set environment variable: E2E_SSH_KEY_PATH") + } + if c.TargetPort == "" { + return errors.New("TargetPort is empty. Set environment variable: E2E_SSH_PORT") + } + + return nil +} + +func loadConfig() (*config, error) { + var c config + err := envconfig.Process("", &c) + if err != nil { + return nil, err + } + + return &c, err +} + +// TestCreateVaultCredentialStoreCli uses the boundary and vault clis to add secrets management +// for a target. The test sets up vault as a credential store, creates a set of credentials +// in vault to be attached to a target, and attempts to connect to that target using those +// credentials. +func TestCreateVaultCredentialStoreCli(t *testing.T) { + e2e.MaybeSkipTest(t) + + c, err := loadConfig() + require.NoError(t, err) + err = c.validate() + require.NoError(t, err) + + // Configure vault + vaultAddr, boundaryPolicyName := vault.Setup(t) + + output := e2e.RunCommand("vault", e2e.WithArgs("secrets", "enable", "-path="+c.VaultSecretPath, "kv-v2")) + require.NoError(t, output.Err, string(output.Stderr)) + t.Cleanup(func() { + output := e2e.RunCommand("vault", e2e.WithArgs("secrets", "disable", c.VaultSecretPath)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create credential in vault + secretName := "TestCreateVaultCredentialStoreCli" + credentialPolicyName := vault.CreateKvPrivateKeyCredential(t, secretName, c.VaultSecretPath, c.TargetSshUser, c.TargetSshKeyPath) + t.Log("Created Vault Credential") + + // Create vault token for boundary + output = e2e.RunCommand("vault", e2e.WithArgs("token", "create", + "-no-default-policy=true", + "-policy="+boundaryPolicyName, + "-policy="+credentialPolicyName, + "-orphan=true", + "-period=20m", + "-renewable=true", + "-format=json", + ), + e2e.WithPipe("jq", "-r", ".auth.client_token"), + ) + require.NoError(t, output.Err, string(output.Stderr)) + credStoreToken := strings.TrimSpace(string(output.Stdout)) + t.Log("Created Vault Cred Store Token") + + // Authenticate boundary cli + boundary.AuthenticateCli(t) + + // Create an org + newOrgId := boundary.CreateNewOrgCli(t) + t.Logf("Created Org Id: %s", newOrgId) + + // Create a project + newProjectId := boundary.CreateNewProjectCli(t, newOrgId) + t.Logf("Created Project Id: %s", newProjectId) + + // Create a credential store + output = e2e.RunCommand("boundary", e2e.WithArgs("credential-stores", "create", "vault", + "-scope-id", newProjectId, + "-vault-address", vaultAddr, + "-vault-token", credStoreToken, + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + var newCredentialStoreResult credentialstores.CredentialStoreCreateResult + err = json.Unmarshal(output.Stdout, &newCredentialStoreResult) + require.NoError(t, err) + newCredentialStore := newCredentialStoreResult.Item + t.Logf("Created Credential Store: %s", newCredentialStore.Id) + + // Create a credential library + output = e2e.RunCommand("boundary", e2e.WithArgs("credential-libraries", "create", "vault", + "-credential-store-id", newCredentialStore.Id, + "-vault-path", c.VaultSecretPath+"/data/"+secretName, + "-name", "e2e Automated Test Vault Credential Library", + "-credential-type", "ssh_private_key", + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + var newCredentialLibraryResult credentiallibraries.CredentialLibraryCreateResult + err = json.Unmarshal(output.Stdout, &newCredentialLibraryResult) + require.NoError(t, err) + newCredentialLibrary := newCredentialLibraryResult.Item + t.Logf("Created Credential Library: %s", newCredentialLibrary.Id) + + // Create a host catalog + output = e2e.RunCommand("boundary", e2e.WithArgs("host-catalogs", "create", "static", + "-scope-id", newProjectId, + "-name", "e2e Automated Test Host Catalog", + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostCatalogResult hostcatalogs.HostCatalogCreateResult + err = json.Unmarshal(output.Stdout, &newHostCatalogResult) + require.NoError(t, err) + newHostCatalog := newHostCatalogResult.Item + t.Logf("Created Host Catalog: %s", newHostCatalog.Id) + + // Create a host set and add to catalog + output = e2e.RunCommand("boundary", e2e.WithArgs("host-sets", "create", "static", + "-host-catalog-id", newHostCatalog.Id, + "-name", "e2e Automated Test Host Set", + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostSetResult hostsets.HostSetCreateResult + err = json.Unmarshal(output.Stdout, &newHostSetResult) + require.NoError(t, err) + newHostSet := newHostSetResult.Item + t.Logf("Created Host Set: %s", newHostSet.Id) + + // Create a host + output = e2e.RunCommand("boundary", e2e.WithArgs("hosts", "create", "static", + "-host-catalog-id", newHostCatalog.Id, + "-name", c.TargetIp, + "-address", c.TargetIp, + "-format", "json"), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newHostResult hosts.HostCreateResult + err = json.Unmarshal(output.Stdout, &newHostResult) + require.NoError(t, err) + newHost := newHostResult.Item + t.Logf("Created Host: %s", newHost.Id) + + // Add host to host set + output = e2e.RunCommand("boundary", e2e.WithArgs("host-sets", "add-hosts", + "-id", newHostSet.Id, + "-host", newHost.Id, + )) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create Target + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "create", "tcp", + "-scope-id", newProjectId, + "-default-port", c.TargetPort, + "-name", "e2e Automated Test Target", + "-format", "json", + )) + require.NoError(t, output.Err, string(output.Stderr)) + var newTargetResult targets.TargetCreateResult + err = json.Unmarshal(output.Stdout, &newTargetResult) + require.NoError(t, err) + newTarget := newTargetResult.Item + t.Logf("Created Target: %s", newTarget.Id) + + // Add host set to target + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "add-host-sources", + "-id", newTarget.Id, + "-host-source", newHostSet.Id, + )) + require.NoError(t, output.Err, string(output.Stderr)) + + // Add brokered credentials to target + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "add-credential-sources", + "-id", newTarget.Id, + "-brokered-credential-source", newCredentialLibrary.Id, + )) + require.NoError(t, output.Err, string(output.Stderr)) + + // Get credentials for target + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "authorize-session", "-id", newTarget.Id, "-format", "json")) + require.NoError(t, output.Err, string(output.Stderr)) + var newSessionAuthorizationResult targets.SessionAuthorizationResult + err = json.Unmarshal(output.Stdout, &newSessionAuthorizationResult) + require.NoError(t, err) + + newSessionAuthorization := newSessionAuthorizationResult.Item + retrievedUser := fmt.Sprintf("%s", newSessionAuthorization.Credentials[0].Credential["username"]) + retrievedKey := fmt.Sprintf("%s", newSessionAuthorization.Credentials[0].Credential["private_key"]) + assert.Equal(t, c.TargetSshUser, retrievedUser) + + k, err := os.ReadFile(c.TargetSshKeyPath) + require.NoError(t, err) + require.Equal(t, string(k), retrievedKey) + t.Log("Successfully retrieved credentials for target") + + // Create key file + retrievedKeyPath := fmt.Sprintf("%s/%s", t.TempDir(), "target_private_key.pem") + f, err := os.Create(retrievedKeyPath) + require.NoError(t, err) + _, err = f.WriteString(retrievedKey) + require.NoError(t, err) + err = os.Chmod(retrievedKeyPath, 0o400) + require.NoError(t, err) + + // Connect to target and print host's IP address using retrieved credentials + output = e2e.RunCommand("boundary", e2e.WithArgs("connect", + "-target-id", newTarget.Id, + "-exec", "/usr/bin/ssh", "--", + "-l", retrievedUser, + "-i", retrievedKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname", "-i", + )) + require.NoError(t, output.Err, string(output.Stderr)) + + parts := strings.Fields(string(output.Stdout)) + hostIp := parts[len(parts)-1] + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + t.Log("Successfully connected to target") +} + +// TestCreateVaultCredentialStoreApi uses the boundary go api along with the vault cli to +// add secrets management for a target. The test sets up vault as a creential stores and creates +// a set of credentials in vault that is attached to a target. +func TestCreateVaultCredentialStoreApi(t *testing.T) { + e2e.MaybeSkipTest(t) + + c, err := loadConfig() + require.NoError(t, err) + err = c.validate() + require.NoError(t, err) + + // Configure vault + vaultAddr, boundaryPolicyName := vault.Setup(t) + + output := e2e.RunCommand("vault", e2e.WithArgs("secrets", "enable", "-path="+c.VaultSecretPath, "kv-v2")) + require.NoError(t, output.Err, string(output.Stderr)) + t.Cleanup(func() { + output := e2e.RunCommand("vault", e2e.WithArgs("secrets", "disable", c.VaultSecretPath)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create credential in vault + secretName := "TestCreateVaultCredentialStoreCli" + credentialPolicyName := vault.CreateKvPrivateKeyCredential(t, secretName, c.VaultSecretPath, c.TargetSshUser, c.TargetSshKeyPath) + t.Log("Created Vault Credential") + + // Create vault token for boundary + output = e2e.RunCommand("vault", e2e.WithArgs("token", "create", + "-no-default-policy=true", + "-policy="+boundaryPolicyName, + "-policy="+credentialPolicyName, + "-orphan=true", + "-period=20m", + "-renewable=true", + "-format=json", + ), + e2e.WithPipe("jq", "-r", ".auth.client_token"), + ) + require.NoError(t, output.Err, string(output.Stderr)) + credStoreToken := strings.TrimSpace(string(output.Stdout)) + t.Log("Created Vault Cred Store Token") + + // Create boundary api client + client, err := boundary.NewApiClient() + require.NoError(t, err) + ctx := context.Background() + + // Create an org + scopeClient := scopes.NewClient(client) + newOrgResult, err := scopeClient.Create(ctx, "global", scopes.WithName("e2e Automated Test Org")) + require.NoError(t, err) + newOrg := newOrgResult.Item + t.Cleanup(func() { + _, err := scopeClient.Delete(ctx, newOrg.Id) + require.NoError(t, err) + }) + t.Logf("Created Org Id: %s", newOrg.Id) + + // Create a project + newProjectResult, err := scopeClient.Create(ctx, newOrg.Id, scopes.WithName("e2e Automated Test Project")) + require.NoError(t, err) + newProject := newProjectResult.Item + t.Logf("Created Project Id: %s", newProject.Id) + + // Create a credential store + csClient := credentialstores.NewClient(client) + newCredentialStoreResult, err := csClient.Create(ctx, "vault", newProject.Id, + credentialstores.WithVaultCredentialStoreAddress(vaultAddr), + credentialstores.WithVaultCredentialStoreToken(credStoreToken), + ) + require.NoError(t, err) + newCredentialStore := newCredentialStoreResult.Item + t.Logf("Created Credential Store: %s", newCredentialStore.Id) + + // Create a credential library + clClient := credentiallibraries.NewClient(client) + newCredentialLibraryResult, err := clClient.Create(ctx, newCredentialStore.Id, + credentiallibraries.WithVaultCredentialLibraryPath(c.VaultSecretPath+"/data/"+secretName), + credentiallibraries.WithCredentialType("ssh_private_key"), + ) + require.NoError(t, err) + newCredentialLibrary := newCredentialLibraryResult.Item + t.Logf("Created Credential Library: %s", newCredentialLibrary.Id) + + // Create a host catalog + hcClient := hostcatalogs.NewClient(client) + newHostCatalogResult, err := hcClient.Create(ctx, "static", newProject.Id, + hostcatalogs.WithName("e2e Automated Test Host Catalog"), + ) + require.NoError(t, err) + newHostCatalog := newHostCatalogResult.Item + t.Logf("Created Host Catalog: %s", newHostCatalog.Id) + + // Create a host set and add to catalog + hsClient := hostsets.NewClient(client) + newHostSetResult, err := hsClient.Create(ctx, newHostCatalog.Id) + require.NoError(t, err) + newHostSet := newHostSetResult.Item + t.Logf("Created Host Set: %s", newHostSet.Id) + + // Create a host + hClient := hosts.NewClient(client) + newHostResult, err := hClient.Create(ctx, newHostCatalog.Id, + hosts.WithName(c.TargetIp), + hosts.WithStaticHostAddress(c.TargetIp), + ) + require.NoError(t, err) + newHost := newHostResult.Item + t.Logf("Created Host: %s", newHost.Id) + + // Add host to host set + _, err = hsClient.AddHosts(ctx, newHostSet.Id, 0, []string{newHost.Id}, hostsets.WithAutomaticVersioning(true)) + require.NoError(t, err) + + // Create a target + tClient := targets.NewClient(client) + targetPort, err := strconv.ParseInt(c.TargetPort, 10, 32) + newTargetResult, err := tClient.Create(ctx, "tcp", newProject.Id, + targets.WithName("e2e Automated Test Target"), + targets.WithTcpTargetDefaultPort(uint32(targetPort)), + ) + require.NoError(t, err) + newTarget := newTargetResult.Item + t.Logf("Created Target: %s", newTarget.Id) + + // Add host set to target + _, err = tClient.AddHostSources(ctx, newTarget.Id, 0, + []string{newHostSet.Id}, + targets.WithAutomaticVersioning(true), + ) + require.NoError(t, err) + + // Add brokered credentials to target + _, err = tClient.AddCredentialSources(ctx, newTarget.Id, 0, + targets.WithBrokeredCredentialSourceIds([]string{newCredentialLibrary.Id}), + targets.WithAutomaticVersioning(true), + ) + require.NoError(t, err) + + // Get credentials for target + newSessionAuthorizationResult, err := tClient.AuthorizeSession(ctx, newTarget.Id) + require.NoError(t, err) + newSessionAuthorization := newSessionAuthorizationResult.Item + retrievedUser := fmt.Sprintf("%s", newSessionAuthorization.Credentials[0].Credential["username"]) + retrievedKey := fmt.Sprintf("%s", newSessionAuthorization.Credentials[0].Credential["private_key"]) + assert.Equal(t, c.TargetSshUser, retrievedUser) + + k, err := os.ReadFile(c.TargetSshKeyPath) + require.NoError(t, err) + require.Equal(t, string(k), retrievedKey) + t.Log("Successfully retrieved credentials for target") +} diff --git a/testing/internal/e2e/helpers.go b/testing/internal/e2e/helpers.go index 4d38f35e3..1f583997d 100644 --- a/testing/internal/e2e/helpers.go +++ b/testing/internal/e2e/helpers.go @@ -4,11 +4,37 @@ import ( "bytes" "errors" "fmt" + "io" "os" "os/exec" "testing" ) +// Option is a func that sets optional attributes for a call. This does not need +// to be used directly, but instead option arguments are built from the +// functions in this package. WithX options set a value to that given in the +// argument; DefaultX options indicate that the value should be set to its +// default. When an API call is made options are processed in ther order they +// appear in the function call, so for a given argument X, a succession of WithX +// or DefaultX calls will result in the last call taking effect. +type Option func(*options) + +type options struct { + withArgs []string + withPipe []string +} + +func getOpts(opt ...Option) options { + opts := options{} + for _, o := range opt { + if o != nil { + o(&opts) + } + } + + return opts +} + // CommandResult encapsulates the output from running an external command type CommandResult struct { Stdout []byte @@ -20,16 +46,48 @@ type CommandResult struct { const EnvToCheckSkip = "E2E_PASSWORD_AUTH_METHOD_ID" // RunCommand executes external commands on the system. Returns the results -// of running the provided command. CommandResult is always valid even if there is -// an error. -func RunCommand(name string, args ...string) *CommandResult { +// of running the provided command. +// +// RunCommand("ls") +// RunCommand("ls", WithArgs("-al", "/path")) +// RunCommand("ls", WithArgs("-al", "/path"), WithPipe("grep", "file")) +// +// CommandResult is always valid even if there is an error. +func RunCommand(command string, opt ...Option) *CommandResult { var outbuf, errbuf bytes.Buffer + var err error + var c1, c2 *exec.Cmd - cmd := exec.Command(name, args...) - cmd.Stdout = &outbuf - cmd.Stderr = &errbuf + opts := getOpts(opt...) - err := cmd.Run() + if opts.withArgs == nil { + c1 = exec.Command(command) + } else { + c1 = exec.Command(command, opts.withArgs...) + } + + if opts.withPipe == nil { + c1.Stdout = &outbuf + c1.Stderr = &errbuf + err = c1.Run() + } else { + pipeCommand := opts.withPipe[0] + pipeArgs := opts.withPipe[1:] + c2 = exec.Command(pipeCommand, pipeArgs...) + + r, w := io.Pipe() + c1.Stdout = w + c2.Stdin = r + + c2.Stdout = &outbuf + c2.Stderr = &errbuf + + c1.Start() + c2.Start() + c1.Wait() + w.Close() + c2.Wait() + } var ee *exec.ExitError var exitCode int @@ -45,7 +103,24 @@ func RunCommand(name string, args ...string) *CommandResult { } } -func MaybeSkipTest(t *testing.T) { +// WithArgs is an option to RunCommand that allows the user to specify arguments +// for the provided command +func WithArgs(args ...string) Option { + return func(o *options) { + o.withArgs = args + } +} + +// WithPipe is an option to RunCommand that allows the user to specify a command+arguments +// to pipe to +func WithPipe(command ...string) Option { + return func(o *options) { + o.withPipe = command + } +} + +// MaybeSkipTest is a check used at the start of the test to determine if the test should run +func MaybeSkipTest(t testing.TB) { if _, ok := os.LookupEnv(EnvToCheckSkip); !ok { t.Skip(fmt.Sprintf( "Skipping test because environment variable '%s' is not set. This is needed for e2e tests.", diff --git a/testing/internal/e2e/target/target_test.go b/testing/internal/e2e/target/target_test.go index 194097d80..4637987f9 100644 --- a/testing/internal/e2e/target/target_test.go +++ b/testing/internal/e2e/target/target_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "strconv" "strings" "testing" @@ -15,7 +16,6 @@ import ( "github.com/hashicorp/boundary/testing/internal/e2e" "github.com/hashicorp/boundary/testing/internal/e2e/boundary" "github.com/kelseyhightower/envconfig" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,137 +64,99 @@ func TestConnectTargetCli(t *testing.T) { err = c.validate() require.NoError(t, err) - output := boundary.AuthenticateCli() - require.NoError(t, output.Err, string(output.Stderr)) + boundary.AuthenticateCli(t) // Create an org - output = e2e.RunCommand( - "boundary", "scopes", "create", - "-name", "e2e Automated Test Org", - "-format", "json", - ) - require.NoError(t, output.Err, string(output.Stderr)) - var newOrgResult scopes.ScopeCreateResult - err = json.Unmarshal(output.Stdout, &newOrgResult) - require.NoError(t, err) - newOrg := newOrgResult.Item - t.Cleanup(func() { - output := e2e.RunCommand("boundary", "scopes", "delete", "-id", newOrg.Id) - require.NoError(t, output.Err, string(output.Stderr)) - }) - t.Logf("Created Org Id: %s", newOrg.Id) + newOrgId := boundary.CreateNewOrgCli(t) + t.Logf("Created Org Id: %s", newOrgId) // Create a project - output = e2e.RunCommand( - "boundary", "scopes", "create", - "-scope-id", newOrg.Id, - "-name", "e2e Automated Test Project", - "-format", "json", - ) - require.NoError(t, output.Err, string(output.Stderr)) - var newProjectResult scopes.ScopeCreateResult - err = json.Unmarshal(output.Stdout, &newProjectResult) - require.NoError(t, err) - newProject := newProjectResult.Item - t.Cleanup(func() { - output := e2e.RunCommand("boundary", "scopes", "delete", "-id", newProject.Id) - require.NoError(t, output.Err, string(output.Stderr)) - }) - t.Logf("Created Project Id: %s", newProject.Id) + newProjectId := boundary.CreateNewProjectCli(t, newOrgId) + t.Logf("Created Project Id: %s", newProjectId) // Create a host catalog - output = e2e.RunCommand( - "boundary", "host-catalogs", "create", "static", - "-scope-id", newProject.Id, + output := e2e.RunCommand("boundary", e2e.WithArgs("host-catalogs", "create", "static", + "-scope-id", newProjectId, "-name", "e2e Automated Test Host Catalog", "-format", "json", - ) + )) require.NoError(t, output.Err, string(output.Stderr)) var newHostCatalogResult hostcatalogs.HostCatalogCreateResult err = json.Unmarshal(output.Stdout, &newHostCatalogResult) require.NoError(t, err) newHostCatalog := newHostCatalogResult.Item t.Cleanup(func() { - output := e2e.RunCommand("boundary", "host-catalogs", "delete", "-id", newHostCatalog.Id) + output := e2e.RunCommand("boundary", e2e.WithArgs("host-catalogs", "delete", "-id", newHostCatalog.Id)) require.NoError(t, output.Err, string(output.Stderr)) }) t.Logf("Created Host Catalog: %s", newHostCatalog.Id) // Create a host set and add to catalog - output = e2e.RunCommand( - "boundary", "host-sets", "create", "static", + output = e2e.RunCommand("boundary", e2e.WithArgs("host-sets", "create", "static", "-host-catalog-id", newHostCatalog.Id, "-name", "e2e Automated Test Host Set", "-format", "json", - ) + )) require.NoError(t, output.Err, string(output.Stderr)) var newHostSetResult hostsets.HostSetCreateResult err = json.Unmarshal(output.Stdout, &newHostSetResult) require.NoError(t, err) newHostSet := newHostSetResult.Item t.Cleanup(func() { - output := e2e.RunCommand("boundary", "host-sets", "delete", "-id", newHostSet.Id) + output := e2e.RunCommand("boundary", e2e.WithArgs("host-sets", "delete", "-id", newHostSet.Id)) require.NoError(t, output.Err, string(output.Stderr)) }) t.Logf("Created Host Set: %s", newHostSet.Id) // Create a host - output = e2e.RunCommand( - "boundary", "hosts", "create", "static", + output = e2e.RunCommand("boundary", e2e.WithArgs("hosts", "create", "static", "-host-catalog-id", newHostCatalog.Id, "-name", c.TargetIp, "-address", c.TargetIp, "-format", "json", - ) + )) require.NoError(t, output.Err, string(output.Stderr)) var newHostResult hosts.HostCreateResult err = json.Unmarshal(output.Stdout, &newHostResult) require.NoError(t, err) newHost := newHostResult.Item t.Cleanup(func() { - output := e2e.RunCommand("boundary", "hosts", "delete", "-id", newHost.Id) + output := e2e.RunCommand("boundary", e2e.WithArgs("hosts", "delete", "-id", newHost.Id)) require.NoError(t, output.Err, string(output.Stderr)) }) t.Logf("Created Host: %s", newHost.Id) // Add host to host set - output = e2e.RunCommand( - "boundary", "host-sets", "add-hosts", - "-id", newHostSet.Id, - "-host", newHost.Id, - ) + output = e2e.RunCommand("boundary", e2e.WithArgs("host-sets", "add-hosts", "-id", newHostSet.Id, "-host", newHost.Id)) require.NoError(t, output.Err, string(output.Stderr)) // Create a target - output = e2e.RunCommand( - "boundary", "targets", "create", "tcp", - "-scope-id", newProject.Id, + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "create", "tcp", + "-scope-id", newProjectId, "-default-port", c.TargetPort, "-name", "e2e Automated Test Target", "-format", "json", - ) + )) require.NoError(t, output.Err, string(output.Stderr)) var newTargetResult targets.TargetCreateResult err = json.Unmarshal(output.Stdout, &newTargetResult) require.NoError(t, err) newTarget := newTargetResult.Item t.Cleanup(func() { - output := e2e.RunCommand("boundary", "targets", "delete", "-id", newTarget.Id) + output := e2e.RunCommand("boundary", e2e.WithArgs("targets", "delete", "-id", newTarget.Id)) require.NoError(t, output.Err, string(output.Stderr)) }) t.Logf("Created Target: %s", newTarget.Id) // Add host set to target - output = e2e.RunCommand( - "boundary", "targets", "add-host-sources", + output = e2e.RunCommand("boundary", e2e.WithArgs("targets", "add-host-sources", "-id", newTarget.Id, - "-host-source", newHostSet.Id, + "-host-source", newHostSet.Id), ) require.NoError(t, output.Err, string(output.Stderr)) // Connect to target and print host's IP address - output = e2e.RunCommand( - "boundary", "connect", + output = e2e.RunCommand("boundary", e2e.WithArgs("connect", "-target-id", newTarget.Id, "-exec", "/usr/bin/ssh", "--", "-l", c.TargetSshUser, @@ -205,12 +167,12 @@ func TestConnectTargetCli(t *testing.T) { "-p", "{{boundary.port}}", // this is provided by boundary "{{boundary.ip}}", "hostname", "-i", - ) + )) require.NoError(t, output.Err, string(output.Stderr)) parts := strings.Fields(string(output.Stdout)) hostIp := parts[len(parts)-1] - assert.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") + require.Equal(t, c.TargetIp, hostIp, "SSH session did not return expected output") t.Log("Successfully connected to target") } @@ -225,9 +187,9 @@ func TestCreateTargetApi(t *testing.T) { err = c.validate() require.NoError(t, err) + // Create boundary api client client, err := boundary.NewApiClient() require.NoError(t, err) - ctx := context.Background() // Create an org @@ -295,9 +257,10 @@ func TestCreateTargetApi(t *testing.T) { // Create a target tClient := targets.NewClient(client) + targetPort, err := strconv.ParseInt(c.TargetPort, 10, 32) newTargetResult, err := tClient.Create(ctx, "tcp", newProject.Id, targets.WithName("e2e Automated Test Target"), - targets.WithTcpTargetDefaultPort(22), + targets.WithTcpTargetDefaultPort(uint32(targetPort)), ) require.NoError(t, err) newTarget := newTargetResult.Item diff --git a/testing/internal/e2e/vault/boundary-controller-policy.hcl b/testing/internal/e2e/vault/boundary-controller-policy.hcl new file mode 100644 index 000000000..429177c01 --- /dev/null +++ b/testing/internal/e2e/vault/boundary-controller-policy.hcl @@ -0,0 +1,23 @@ +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/renew-self" { + capabilities = ["update"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} + +path "sys/leases/renew" { + capabilities = ["update"] +} + +path "sys/leases/revoke" { + capabilities = ["update"] +} + +path "sys/capabilities-self" { + capabilities = ["update"] +} diff --git a/testing/internal/e2e/vault/vault.go b/testing/internal/e2e/vault/vault.go new file mode 100644 index 000000000..8d0d5e2c8 --- /dev/null +++ b/testing/internal/e2e/vault/vault.go @@ -0,0 +1,100 @@ +// Package vault provides methods for commonly used vault actions that are used in end-to-end tests. +package vault + +import ( + "errors" + "fmt" + "os" + "path" + "runtime" + "testing" + + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/kelseyhightower/envconfig" + "github.com/stretchr/testify/require" +) + +type config struct { + VaultAddr string `envconfig:"VAULT_ADDR"` // e.g. "http://127.0.0.1:8200" + VaultToken string `envconfig:"VAULT_TOKEN"` +} + +func (c *config) validate() error { + if c.VaultAddr == "" { + return errors.New("VaultAddr is empty. Set environment variable: VAULT_ADDR") + } + if c.VaultToken == "" { + return errors.New("VaultToken is empty. Set environment variable: VAULT_TOKEN") + } + + return nil +} + +func loadConfig() (*config, error) { + var c config + err := envconfig.Process("", &c) + if err != nil { + return nil, err + } + + return &c, err +} + +// Setup verifies if appropriate credentials are set and adds the boundary controller +// policy to vault. Returns the vault address. +func Setup(t testing.TB) (string, string) { + c, err := loadConfig() + require.NoError(t, err) + err = c.validate() + require.NoError(t, err) + + _, filename, _, ok := runtime.Caller(0) + require.True(t, ok) + policyName := "boundary-controller" + output := e2e.RunCommand("vault", e2e.WithArgs("policy", "write", policyName, + path.Join(path.Dir(filename), "boundary-controller-policy.hcl")), + ) + require.NoError(t, output.Err, string(output.Stderr)) + t.Cleanup(func() { + output := e2e.RunCommand("vault", e2e.WithArgs("policy", "delete", policyName)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + return c.VaultAddr, policyName +} + +// CreateKvPrivateKeyCredential creates a private key credential in vault and creates a vault policy +// to be able to read that credential. Returns the name of the policy. +func CreateKvPrivateKeyCredential(t testing.TB, secretName string, secretPath string, user string, keyPath string) string { + // Create policy file to read secret + kvPolicyFileName := "kv-policy-test.hcl" + kvPolicyFilePath := fmt.Sprintf("%s/%s", t.TempDir(), kvPolicyFileName) + + f, err := os.Create(kvPolicyFilePath) + require.NoError(t, err) + _, err = f.WriteString(fmt.Sprintf("path \"%s/data/%s\" { capabilities = [\"read\"] }", + secretPath, + secretName, + )) + require.NoError(t, err) + + // Add policy to vault + policyName := "kv-read" + output := e2e.RunCommand("vault", e2e.WithArgs("policy", "write", policyName, kvPolicyFilePath)) + require.NoError(t, output.Err, string(output.Stderr)) + t.Cleanup(func() { + output := e2e.RunCommand("vault", e2e.WithArgs("policy", "delete", policyName)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create secret + output = e2e.RunCommand("vault", e2e.WithArgs("kv", "put", + "-mount", secretPath, + secretName, + "username="+user, + "private_key=@"+keyPath, + )) + require.NoError(t, output.Err, string(output.Stderr)) + + return policyName +}