diff --git a/enos/enos-scenario-e2e-docker-base-with-vault.hcl b/enos/enos-scenario-e2e-docker-base-with-vault.hcl index 8411f2987..08c99064d 100644 --- a/enos/enos-scenario-e2e-docker-base-with-vault.hcl +++ b/enos/enos-scenario-e2e-docker-base-with-vault.hcl @@ -3,6 +3,8 @@ # For this scenario to work, add the following line to /etc/hosts # 127.0.0.1 localhost boundary +# 127.0.0.1 localhost worker +# 127.0.0.1 localhost vault scenario "e2e_docker_base_with_vault" { terraform_cli = terraform_cli.default diff --git a/enos/modules/docker_vault/config/config.json b/enos/modules/docker_vault/config/config.json index 14a8599d5..2b7e842ac 100644 --- a/enos/modules/docker_vault/config/config.json +++ b/enos/modules/docker_vault/config/config.json @@ -1,4 +1,5 @@ { + "api_addr": "https://127.0.0.1:8300", "storage": { "file": { "path": "/vault/file" @@ -7,7 +8,7 @@ "listener": [ { "tcp": { - "address": "0.0.0.0:8200", + "address": "0.0.0.0:8300", "tls_disable": true } } diff --git a/enos/modules/docker_vault/main.tf b/enos/modules/docker_vault/main.tf index b94a24585..c013e9952 100644 --- a/enos/modules/docker_vault/main.tf +++ b/enos/modules/docker_vault/main.tf @@ -37,6 +37,11 @@ variable "vault_port" { type = string default = "8300" } +variable "vault_port_internal" { + description = "Internal Port to use" + type = string + default = "8300" +} resource "docker_image" "vault" { name = var.image_name @@ -48,7 +53,7 @@ resource "docker_container" "vault" { name = var.container_name command = ["vault", "server", "-config", "/vault/config.d/config.json"] ports { - internal = 8200 + internal = var.vault_port_internal external = var.vault_port } mounts { @@ -128,11 +133,11 @@ resource "enos_local_exec" "check_health" { } output "address" { - value = "0.0.0.0" + value = "http://${var.container_name}:${var.vault_port}" } output "address_internal" { - value = var.container_name + value = "http://${var.container_name}:${var.vault_port_internal}" } output "token" { diff --git a/enos/modules/test_e2e_docker/main.tf b/enos/modules/test_e2e_docker/main.tf index 839acfefe..254e5d627 100644 --- a/enos/modules/test_e2e_docker/main.tf +++ b/enos/modules/test_e2e_docker/main.tf @@ -286,12 +286,9 @@ resource "enos_local_exec" "get_go_version" { } locals { - go_version = var.go_version == "" ? enos_local_exec.get_go_version[0].stdout : var.go_version - image_name = trimspace("${var.docker_mirror}/library/golang:${local.go_version}") - + go_version = var.go_version == "" ? enos_local_exec.get_go_version[0].stdout : var.go_version + image_name = trimspace("${var.docker_mirror}/library/golang:${local.go_version}") aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) - vault_addr = var.vault_addr != "" ? "http://${var.vault_addr}:${var.vault_port}" : "" - vault_addr_internal = var.vault_addr_internal != "" ? "http://${var.vault_addr_internal}:8200" : local.vault_addr package_name = reverse(split("/", var.test_package))[0] } @@ -317,10 +314,10 @@ resource "enos_local_exec" "run_e2e_test" { E2E_SSH_USER = var.target_user E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path E2E_SSH_CA_KEY = var.target_ca_key - VAULT_ADDR = local.vault_addr - VAULT_ADDR_INTERNAL = local.vault_addr_internal + VAULT_ADDR = var.vault_addr + VAULT_ADDR_INTERNAL = var.vault_addr_internal VAULT_TOKEN = var.vault_root_token - E2E_VAULT_ADDR = local.vault_addr_internal + E2E_VAULT_ADDR = var.vault_addr_internal E2E_BUCKET_NAME = var.bucket_name E2E_BUCKET_ENDPOINT_URL = var.bucket_endpoint_url E2E_BUCKET_USER_ID = var.bucket_user_id diff --git a/enos/modules/test_e2e_ui/main.tf b/enos/modules/test_e2e_ui/main.tf index 1eacf4c09..2f3348dba 100644 --- a/enos/modules/test_e2e_ui/main.tf +++ b/enos/modules/test_e2e_ui/main.tf @@ -196,8 +196,6 @@ variable "worker_tag_egress" { locals { aws_ssh_private_key_path = abspath(var.aws_ssh_private_key_path) - vault_addr = var.vault_addr != "" ? "http://${var.vault_addr}:${var.vault_port}" : "" - vault_addr_internal = var.vault_addr_internal != "" ? "http://${var.vault_addr_internal}:8200" : local.vault_addr aws_host_set_ips = jsonencode(var.aws_host_set_ips) } @@ -213,9 +211,9 @@ resource "enos_local_exec" "run_e2e_test" { E2E_SSH_KEY_PATH = local.aws_ssh_private_key_path E2E_SSH_CA_KEY = var.target_ca_key E2E_SSH_CA_KEY_PUBLIC = var.target_ca_key_public - VAULT_ADDR = local.vault_addr + VAULT_ADDR = var.vault_addr VAULT_TOKEN = var.vault_root_token - E2E_VAULT_ADDR = local.vault_addr_internal + E2E_VAULT_ADDR = var.vault_addr_internal E2E_AWS_ACCESS_KEY_ID = var.aws_access_key_id E2E_AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key E2E_AWS_HOST_SET_FILTER = var.aws_host_set_filter diff --git a/testing/internal/e2e/tests/base_with_vault/auth_method_oidc_vault_test.go b/testing/internal/e2e/tests/base_with_vault/auth_method_oidc_vault_test.go new file mode 100644 index 000000000..67a63bc03 --- /dev/null +++ b/testing/internal/e2e/tests/base_with_vault/auth_method_oidc_vault_test.go @@ -0,0 +1,315 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_with_vault_test + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/boundary/api/authmethods" + "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/stretchr/testify/require" +) + +func TestAuthMethodOidcVault(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + ctx := context.Background() + + t.Log("Setting up Vault OIDC provider...") + // Configure vault authentication: Enable the userpass auth method at the + // default path + output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("auth", "enable", "userpass")) + require.NoError(t, output.Err, string(output.Stderr)) + t.Cleanup(func() { + output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("auth", "disable", "userpass")) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Configure vault authentication: Create a policy granting the user read capabilities on the authorization endpoint + authPolicyPath := fmt.Sprintf("%s/%s", t.TempDir(), "auth-policy.hcl") + _, err = os.Create(authPolicyPath) + require.NoError(t, err) + authPolicyFile, err := os.OpenFile(authPolicyPath, os.O_APPEND|os.O_WRONLY, 0o644) + require.NoError(t, err) + _, err = authPolicyFile.WriteString(fmt.Sprintf( + `path "identity/oidc/provider/my-provider/authorize" { capabilities = [ "read" ] }%s`, + "\n", + )) + require.NoError(t, err) + authPolicyName := vault.WritePolicy(t, ctx, authPolicyPath) + t.Cleanup(func() { + output := e2e.RunCommand(ctx, "vault", + e2e.WithArgs("policy", "delete", authPolicyName), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Configuration vault authentication: Create a user with the auth policy + userName := "end-user" + userPassword := "password" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("auth/userpass/users/%s", userName), + fmt.Sprintf("password=%s", userPassword), + fmt.Sprintf("token_policies=%s", authPolicyName), + "token_ttl=1h", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create an identity entity + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + "identity/entity", + fmt.Sprintf("name=%s", userName), + `metadata=email=vault@hashicorp.com`, + `metadata=phone_number=123-456-7890`, + "disabled=false", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "read", + "-field=id", + fmt.Sprintf("identity/entity/name/%s", userName), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + entityId := strings.TrimSpace(string(output.Stdout)) + + // Create an identity group + groupName := "engineering" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + "identity/group", + fmt.Sprintf("name=%s", groupName), + fmt.Sprintf(`member_entity_ids=%s`, entityId), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "read", + "-field=id", + fmt.Sprintf("identity/group/name/%s", groupName), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + groupId := strings.TrimSpace(string(output.Stdout)) + + // Get accessor value of the userpass authentication method. + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "auth", + "list", + "-detailed", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + type AuthListUserPassAttributes struct { + Accessor string `json:"accessor"` + } + type AuthListResponse struct { + UserPass AuthListUserPassAttributes `json:"userpass/"` + } + var authListResult AuthListResponse + err = json.Unmarshal(output.Stdout, &authListResult) + require.NoError(t, err) + userpassAccessor := authListResult.UserPass.Accessor + + // Create an entity alias that maps the end-user entity with the end-user + // userpass user + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + "identity/entity-alias", + fmt.Sprintf("name=%s", userName), + fmt.Sprintf(`canonical_id=%s`, entityId), + fmt.Sprintf(`mount_accessor=%s`, userpassAccessor), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create an OIDC assignment, which describes the list of the Vault entities + // and groups allowed to authenticate with this client. + assignmentName := "my-assignment" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("identity/oidc/assignment/%s", assignmentName), + fmt.Sprintf(`entity_ids=%s`, entityId), + fmt.Sprintf(`group_ids="%s"`, groupId), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create a key + keyName := "my-key" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("identity/oidc/key/%s", keyName), + "allowed_client_ids=*", + "verification_ttl=2h", + "rotation_period=1h", + "algorithm=RS256", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create an OIDC client + oidcClientName := "boundary" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("identity/oidc/client/%s", oidcClientName), + "redirect_uris=http://127.0.0.1:9200/v1/auth-methods/oidc:authenticate:callback", + fmt.Sprintf(`assignments=%s`, assignmentName), + fmt.Sprintf(`key=%s`, keyName), + "id_token_ttl=30m", + "access_token_ttl=1h", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "read", + "-field=client_id", + fmt.Sprintf("identity/oidc/client/%s", oidcClientName), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + clientId := strings.TrimSpace(string(output.Stdout)) + + // Define a Vault OIDC scope for the user + userScopeTemplate := `{ + "username": {{identity.entity.name}}, + "contact": { + "email": {{identity.entity.metadata.email}}, + "phone_number": {{identity.entity.metadata.phone_number}} + } + }` + userScopeEncoded := base64.StdEncoding.EncodeToString([]byte(userScopeTemplate)) + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + "identity/oidc/scope/user", + `description="The user scope provides claims using Vault identity entity metadata"`, + fmt.Sprintf(`template=%s`, userScopeEncoded), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Define a Vault OIDC scope for the group + groupScopeTemplate := `{ + "groups": {{identity.entity.groups.names}} + }` + groupScopeEncoded := base64.StdEncoding.EncodeToString([]byte(groupScopeTemplate)) + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + "identity/oidc/scope/groups", + `description="The groups scope provides the groups claim using Vault group membership"`, + fmt.Sprintf(`template=%s`, groupScopeEncoded), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create a Vault OIDC provider + providerName := "my-provider" + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("identity/oidc/provider/%s", providerName), + fmt.Sprintf("allowed_client_ids=%s", clientId), + "scopes_supported=groups,user", + fmt.Sprintf("issuer=%s", c.VaultAddr), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Get Issuer URL + output = e2e.RunCommand(ctx, "curl", + e2e.WithArgs( + "-s", + fmt.Sprintf("%s/v1/identity/oidc/provider/%s/.well-known/openid-configuration", c.VaultAddr, providerName), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + type oidcConfigResponse struct { + Issuer string `json:"issuer"` + } + var oidcConfig oidcConfigResponse + err = json.Unmarshal(output.Stdout, &oidcConfig) + require.NoError(t, err) + + // Get client secret + output = e2e.RunCommand(ctx, "vault", + e2e.WithArgs( + "read", + "-field=client_secret", + fmt.Sprintf("identity/oidc/client/%s", oidcClientName), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + clientSecret := strings.TrimSpace(string(output.Stdout)) + + // Create OIDC auth method in Boundary + boundary.AuthenticateAdminCli(t, ctx) + orgId, err := boundary.CreateOrgCli(t, ctx) + require.NoError(t, err) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "create", "oidc", + "-scope-id", orgId, + "-issuer", oidcConfig.Issuer, + "-client-id", clientId, + "-client-secret", clientSecret, + "-signing-algorithm", "RS256", + "-api-url-prefix", "http://127.0.0.1:9200", + "-claims-scopes", "groups", + "-claims-scopes", "user", + "-max-age", "20", + "-name", "e2e Vault OIDC", + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var createAuthMethodResult authmethods.AuthMethodCreateResult + err = json.Unmarshal(output.Stdout, &createAuthMethodResult) + require.NoError(t, err) + authMethodId := createAuthMethodResult.Item.Id + t.Logf("Created OIDC Auth Method: %s", authMethodId) + + // Set the auth method to active-public + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "change-state", "oidc", + "-id", authMethodId, + "-state", "active-public", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) +}