diff --git a/enos/enos-scenario-e2e-docker-base-with-vault.hcl b/enos/enos-scenario-e2e-docker-base-with-vault.hcl index 6c800c847..5e4b75f9a 100644 --- a/enos/enos-scenario-e2e-docker-base-with-vault.hcl +++ b/enos/enos-scenario-e2e-docker-base-with-vault.hcl @@ -113,6 +113,17 @@ scenario "e2e_docker_base_with_vault" { } } + step "create_ldap_server" { + module = module.docker_ldap + depends_on = [ + step.create_docker_network + ] + variables { + image_name = "${var.docker_mirror}/osixia/openldap:latest" + network_name = [local.network_cluster] + } + } + step "run_e2e_test" { module = module.test_e2e_docker depends_on = [ @@ -141,6 +152,13 @@ scenario "e2e_docker_base_with_vault" { vault_root_token = step.create_vault.token vault_port = step.create_vault.port max_page_size = step.create_boundary.max_page_size + ldap_address = step.create_ldap_server.address + ldap_domain_dn = step.create_ldap_server.domain_dn + ldap_admin_dn = step.create_ldap_server.admin_dn + ldap_admin_password = step.create_ldap_server.admin_password + ldap_user_name = step.create_ldap_server.user_name + ldap_user_password = step.create_ldap_server.user_password + ldap_group_name = step.create_ldap_server.group_name } } } diff --git a/testing/internal/e2e/boundary/boundary.go b/testing/internal/e2e/boundary/boundary.go index bda8a0d9f..ecc852eb5 100644 --- a/testing/internal/e2e/boundary/boundary.go +++ b/testing/internal/e2e/boundary/boundary.go @@ -6,12 +6,14 @@ package boundary import ( "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/targets" ) // ConnectCliOutput parses the json response from running `boundary connect` type ConnectCliOutput struct { - Port int `json:"port"` - Address string `json:"address"` + Port int `json:"port"` + Address string `json:"address"` + Credentials []*targets.SessionCredential `json:"credentials"` } // AuthenticateCliOutput parses the json response from running `boundary authenticate` diff --git a/testing/internal/e2e/boundary/credential.go b/testing/internal/e2e/boundary/credential.go index 24ebe8746..12eeb6166 100644 --- a/testing/internal/e2e/boundary/credential.go +++ b/testing/internal/e2e/boundary/credential.go @@ -333,6 +333,54 @@ func CreateStaticCredentialPasswordApi(t testing.TB, ctx context.Context, client return credentialId, nil } +func CreateVaultLdapCredentialLibraryApi(t testing.TB, ctx context.Context, client *api.Client, credentialStoreId string, vaultPath string) (string, error) { + name, err := base62.Random(16) + if err != nil { + return "", err + } + + res, err := credentiallibraries.NewClient(client).Create(ctx, "vault-ldap", credentialStoreId, + credentiallibraries.WithName(fmt.Sprintf("e2e vault credential library %s", name)), + credentiallibraries.WithVaultLdapCredentialLibraryPath(vaultPath), + ) + if err != nil { + return "", err + } + + return res.GetItem().Id, nil +} + +func CreateVaultLdapCredentialLibraryCli(t testing.TB, ctx context.Context, credentialStoreId string, vaultPath string) (string, error) { + name, err := base62.Random(16) + if err != nil { + return "", err + } + + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "credential-libraries", "create", "vault-ldap", + "-credential-store-id", credentialStoreId, + "-vault-path", vaultPath, + "-name", fmt.Sprintf("e2e vault credential library %s", name), + "-description", "e2e", + "-format", "json", + ), + ) + if output.Err != nil { + return "", fmt.Errorf("%w: %s", output.Err, string(output.Stderr)) + } + + var createCredentialLibraryResult credentiallibraries.CredentialLibraryCreateResult + err = json.Unmarshal(output.Stdout, &createCredentialLibraryResult) + if err != nil { + return "", err + } + + credentialLibraryId := createCredentialLibraryResult.Item.Id + t.Logf("Created Vault LDAP Credential Library: %s", credentialLibraryId) + return credentialLibraryId, nil +} + // CreateStaticCredentialPasswordDomainApi uses the API to create a new username-password-domain credential in the // provided static credential store. // Returns the id of the new credential diff --git a/testing/internal/e2e/tests/base_with_vault/credential_library_vault_ldap_test.go b/testing/internal/e2e/tests/base_with_vault/credential_library_vault_ldap_test.go new file mode 100644 index 000000000..1f4f6b06c --- /dev/null +++ b/testing/internal/e2e/tests/base_with_vault/credential_library_vault_ldap_test.go @@ -0,0 +1,368 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_with_vault_test + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + "testing" + + "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/stretchr/testify/require" +) + +// TestApiVaultLdapCredentialLibrary uses the Boundary API to test CRUDL on a +// Vault LDAP credential library. Additionally, it tests assigning it as a +// brokered credential source on a TCP target and connecting. +func TestApiVaultLdapCredentialLibrary(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + require.NotNil(t, c) + + client, err := boundary.NewApiClient() + require.NoError(t, err) + require.NotNil(t, client) + + orgId, err := boundary.CreateOrgApi(t, t.Context(), client) + require.NoError(t, err) + t.Cleanup(func() { + _, err := scopes.NewClient(client).Delete(context.Background(), orgId) + require.NoError(t, err) + }) + projectId, err := boundary.CreateProjectApi(t, t.Context(), client, orgId) + require.NoError(t, err) + + // Create TCP target. + targetPort, err := strconv.ParseUint(c.TargetPort, 10, 32) + require.NoError(t, err) + targetId, err := boundary.CreateTargetApi(t, t.Context(), client, projectId, "tcp", + targets.WithTcpTargetDefaultPort(uint32(targetPort)), + targets.WithTcpTargetDefaultClientPort(uint32(targetPort)), + ) + require.NoError(t, err) + + // Create host catalog, host set and host; Add host set to the target as a + // host source. + hostCatalogId, err := boundary.CreateHostCatalogApi(t, t.Context(), client, projectId) + require.NoError(t, err) + hostSetId, err := boundary.CreateHostSetApi(t, t.Context(), client, hostCatalogId) + require.NoError(t, err) + hostId, err := boundary.CreateHostApi(t, t.Context(), client, hostCatalogId, c.TargetAddress) + require.NoError(t, err) + err = boundary.AddHostToHostSetApi(t, t.Context(), client, hostSetId, hostId) + require.NoError(t, err) + err = boundary.AddHostSourceToTargetApi(t, t.Context(), client, targetId, hostSetId) + require.NoError(t, err) + + // Configure Vault for LDAP. + boundaryPolicyName, _, ldapPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + t.Cleanup(func() { + output := e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("policy", "delete", boundaryPolicyName), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + ldapPolicyName := vault.SetupLdap(t, c.VaultLdapPath, ldapPolicyFilePath, + c.LdapAddress, c.LdapAdminDn, c.LdapAdminPassword, + c.LdapDomainDn, c.LdapUserName, c.LdapGroupName, + ) + t.Cleanup(func() { + // Destroy LDAP secrets engine and LDAP policy. + output := e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("secrets", "disable", c.VaultLdapPath), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + output = e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("policy", "delete", ldapPolicyName), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create Vault token for Boundary. + output := e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs( + "token", "create", + "-no-default-policy=true", + fmt.Sprintf("-policy=%s", boundaryPolicyName), + fmt.Sprintf("-policy=%s", ldapPolicyName), + "-orphan=true", + "-period=20m", + "-renewable=true", + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var tokenCreateResult vault.CreateTokenResponse + err = json.Unmarshal(output.Stdout, &tokenCreateResult) + require.NoError(t, err) + vaultToken := tokenCreateResult.Auth.Client_Token + + // Create two Vault LDAP credential libraries: One with static, one with + // dynamic creds. + storeId, err := boundary.CreateCredentialStoreVaultApi(t, t.Context(), client, projectId, c.VaultAddr, vaultToken) + require.NoError(t, err) + staticCredVclId, err := boundary.CreateVaultLdapCredentialLibraryApi(t, t.Context(), client, storeId, fmt.Sprintf("%s/static-cred/%s", c.VaultLdapPath, c.LdapUserName)) + require.NoError(t, err) + dynamicCredVclId, err := boundary.CreateVaultLdapCredentialLibraryApi(t, t.Context(), client, storeId, fmt.Sprintf("%s/creds/%s", c.VaultLdapPath, c.LdapGroupName)) + require.NoError(t, err) + err = boundary.AddBrokeredCredentialSourceToTargetApi(t, t.Context(), client, targetId, staticCredVclId) + require.NoError(t, err) + err = boundary.AddBrokeredCredentialSourceToTargetApi(t, t.Context(), client, targetId, dynamicCredVclId) + require.NoError(t, err) + + // Authorize session and assert that session credentials exist. + sar, err := targets.NewClient(client).AuthorizeSession(t.Context(), targetId) + require.NoError(t, err) + require.NotNil(t, sar) + + sa, err := sar.GetSessionAuthorization() + require.NoError(t, err) + require.NotNil(t, sa) + + require.Len(t, sa.Credentials, 2) // One for static, one for dynamic role. + slices.SortFunc(sa.Credentials, func(a *targets.SessionCredential, b *targets.SessionCredential) int { + // Sort by static role credential library first. + if strings.EqualFold(staticCredVclId, a.CredentialSource.Id) { + return -1 + } + return 1 + }) + + // Determine the LDAP server's domain using LDAP dn. LdapDomainDn is a + // string like "dc=subdomain,dc=domain,dc=tld". + expDomain := strings.ReplaceAll( + strings.ReplaceAll(c.LdapDomainDn, "dc=", ""), // Transforms into "subdomain,domain,tld". + ",", ".", // Then replace commas with periods to get "subdomain.domain.tld". + ) + + // Static credential. + require.NotEmpty(t, sa.Credentials[0].Credential) + require.Contains(t, sa.Credentials[0].Credential, "username") + require.Contains(t, sa.Credentials[0].Credential, "password") + require.Contains(t, sa.Credentials[0].Credential, "domain") + require.EqualValues(t, c.LdapUserName, sa.Credentials[0].Credential["username"]) + require.NotEmpty(t, sa.Credentials[0].Credential["password"]) + require.EqualValues(t, expDomain, sa.Credentials[0].Credential["domain"]) + + require.NotNil(t, sa.Credentials[0].CredentialSource) + require.EqualValues(t, staticCredVclId, sa.Credentials[0].CredentialSource.Id) + require.Contains(t, sa.Credentials[0].CredentialSource.Name, "e2e vault credential library") + require.EqualValues(t, "vault-ldap", sa.Credentials[0].CredentialSource.Type) + require.EqualValues(t, storeId, sa.Credentials[0].CredentialSource.CredentialStoreId) + require.EqualValues(t, "username_password_domain", sa.Credentials[0].CredentialSource.CredentialType) + + require.NotNil(t, sa.Credentials[0].Secret) + require.NotEmpty(t, sa.Credentials[0].Secret.Decoded) + require.Contains(t, sa.Credentials[0].Secret.Decoded, "username") + require.Contains(t, sa.Credentials[0].Secret.Decoded, "password") + require.Contains(t, sa.Credentials[0].Secret.Decoded, "dn") + require.EqualValues(t, c.LdapUserName, sa.Credentials[0].Secret.Decoded["username"]) + require.EqualValues(t, sa.Credentials[0].Credential["password"], sa.Credentials[0].Secret.Decoded["password"]) + require.EqualValues(t, fmt.Sprintf("cn=%s,%s", c.LdapUserName, c.LdapDomainDn), sa.Credentials[0].Secret.Decoded["dn"]) + + // Dynamic credential. + require.NotEmpty(t, sa.Credentials[1].Credential) + require.Contains(t, sa.Credentials[1].Credential, "username") + require.Contains(t, sa.Credentials[1].Credential, "password") + require.Contains(t, sa.Credentials[1].Credential, "domain") + require.Contains(t, sa.Credentials[1].Credential["username"], fmt.Sprintf("b_token_%s_", c.LdapGroupName)) + require.NotEmpty(t, sa.Credentials[1].Credential["password"]) + require.EqualValues(t, expDomain, sa.Credentials[1].Credential["domain"]) + + require.NotNil(t, sa.Credentials[1].CredentialSource) + require.EqualValues(t, dynamicCredVclId, sa.Credentials[1].CredentialSource.Id) + require.Contains(t, sa.Credentials[1].CredentialSource.Name, "e2e vault credential library") + require.EqualValues(t, "vault-ldap", sa.Credentials[1].CredentialSource.Type) + require.EqualValues(t, storeId, sa.Credentials[1].CredentialSource.CredentialStoreId) + require.EqualValues(t, "username_password_domain", sa.Credentials[1].CredentialSource.CredentialType) + + require.NotNil(t, sa.Credentials[1].Secret) + require.NotEmpty(t, sa.Credentials[1].Secret.Decoded) + require.Contains(t, sa.Credentials[1].Secret.Decoded, "username") + require.Contains(t, sa.Credentials[1].Secret.Decoded, "password") + require.Contains(t, sa.Credentials[1].Secret.Decoded, "distinguished_names") + require.EqualValues(t, sa.Credentials[1].Credential["username"], sa.Credentials[1].Secret.Decoded["username"]) + require.EqualValues(t, sa.Credentials[1].Credential["password"], sa.Credentials[1].Secret.Decoded["password"]) + distinguishedNames, ok := sa.Credentials[1].Secret.Decoded["distinguished_names"].([]any) + require.True(t, ok) + require.NotEmpty(t, distinguishedNames) + require.Contains(t, distinguishedNames, fmt.Sprintf("cn=%s,%s", sa.Credentials[1].Secret.Decoded["username"], c.LdapDomainDn)) + require.Contains(t, distinguishedNames, fmt.Sprintf("cn=%s,%s", c.LdapGroupName, c.LdapDomainDn)) +} + +// TestCliVaultLdapCredentialLibrary uses the Boundary CLI to test CRUDL on a +// Vault LDAP credential library. Additionally, it tests assigning it as a +// brokered credential source on a TCP target and connecting. +func TestCliVaultLdapCredentialLibrary(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + boundary.AuthenticateAdminCli(t, t.Context()) + orgId, err := boundary.CreateOrgCli(t, t.Context()) + require.NoError(t, err) + t.Cleanup(func() { + boundary.AuthenticateAdminCli(t, context.Background()) + output := e2e.RunCommand(context.Background(), "boundary", e2e.WithArgs("scopes", "delete", "-id", orgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + projectId, err := boundary.CreateProjectCli(t, t.Context(), orgId) + require.NoError(t, err) + hostCatalogId, err := boundary.CreateHostCatalogCli(t, t.Context(), projectId) + require.NoError(t, err) + hostSetId, err := boundary.CreateHostSetCli(t, t.Context(), hostCatalogId) + require.NoError(t, err) + hostId, err := boundary.CreateHostCli(t, t.Context(), hostCatalogId, c.TargetAddress) + require.NoError(t, err) + err = boundary.AddHostToHostSetCli(t, t.Context(), hostSetId, hostId) + require.NoError(t, err) + targetId, err := boundary.CreateTargetCli(t, t.Context(), projectId, c.TargetPort) + require.NoError(t, err) + err = boundary.AddHostSourceToTargetCli(t, t.Context(), targetId, hostSetId) + require.NoError(t, err) + + // Configure Vault for LDAP. + boundaryPolicyName, _, ldapPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + t.Cleanup(func() { + output := e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("policy", "delete", boundaryPolicyName), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + ldapPolicyName := vault.SetupLdap(t, c.VaultLdapPath, ldapPolicyFilePath, + c.LdapAddress, c.LdapAdminDn, c.LdapAdminPassword, + c.LdapDomainDn, c.LdapUserName, c.LdapGroupName, + ) + t.Cleanup(func() { + // Destroy LDAP secrets engine and LDAP policy. + output := e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("secrets", "disable", c.VaultLdapPath), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + output = e2e.RunCommand(context.Background(), "vault", + e2e.WithArgs("policy", "delete", ldapPolicyName), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create Vault token for Boundary. + output := e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs( + "token", "create", + "-no-default-policy=true", + fmt.Sprintf("-policy=%s", boundaryPolicyName), + fmt.Sprintf("-policy=%s", ldapPolicyName), + "-orphan=true", + "-period=20m", + "-renewable=true", + "-format=json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var tokenCreateResult vault.CreateTokenResponse + err = json.Unmarshal(output.Stdout, &tokenCreateResult) + require.NoError(t, err) + vaultToken := tokenCreateResult.Auth.Client_Token + + // Create two Vault LDAP credential libraries: One with static, one with + // dynamic creds. + storeId, err := boundary.CreateCredentialStoreVaultCli(t, t.Context(), projectId, c.VaultAddr, vaultToken) + require.NoError(t, err) + staticCredVclId, err := boundary.CreateVaultLdapCredentialLibraryCli(t, t.Context(), storeId, fmt.Sprintf("%s/static-cred/%s", c.VaultLdapPath, c.LdapUserName)) + require.NoError(t, err) + dynamicCredVclId, err := boundary.CreateVaultLdapCredentialLibraryCli(t, t.Context(), storeId, fmt.Sprintf("%s/creds/%s", c.VaultLdapPath, c.LdapGroupName)) + require.NoError(t, err) + err = boundary.AddBrokeredCredentialSourceToTargetCli(t, t.Context(), targetId, staticCredVclId) + require.NoError(t, err) + err = boundary.AddBrokeredCredentialSourceToTargetCli(t, t.Context(), targetId, dynamicCredVclId) + require.NoError(t, err) + + // Connect and assert that session credentials exist. + out := boundary.ConnectCli(t, t.Context(), targetId) + require.NotEmpty(t, out) + + require.Len(t, out.Credentials, 2) // One for static, one for dynamic role. + slices.SortFunc(out.Credentials, func(a *targets.SessionCredential, b *targets.SessionCredential) int { + // Sort by static role credential library first. + if strings.EqualFold(staticCredVclId, a.CredentialSource.Id) { + return -1 + } + return 1 + }) + + // Determine the LDAP server's domain using LDAP dn. LdapDomainDn is a + // string like "dc=subdomain,dc=domain,dc=tld". + expDomain := strings.ReplaceAll( + strings.ReplaceAll(c.LdapDomainDn, "dc=", ""), // Transforms into "subdomain,domain,tld". + ",", ".", // Then replace commas with periods to get "subdomain.domain.tld". + ) + + // Static credential. + require.NotEmpty(t, out.Credentials[0].Credential) + require.Contains(t, out.Credentials[0].Credential, "username") + require.Contains(t, out.Credentials[0].Credential, "password") + require.Contains(t, out.Credentials[0].Credential, "domain") + require.EqualValues(t, c.LdapUserName, out.Credentials[0].Credential["username"]) + require.NotEmpty(t, out.Credentials[0].Credential["password"]) + require.EqualValues(t, expDomain, out.Credentials[0].Credential["domain"]) + + require.NotNil(t, out.Credentials[0].CredentialSource) + require.EqualValues(t, staticCredVclId, out.Credentials[0].CredentialSource.Id) + require.Contains(t, out.Credentials[0].CredentialSource.Name, "e2e vault credential library") + require.EqualValues(t, "vault-ldap", out.Credentials[0].CredentialSource.Type) + require.EqualValues(t, storeId, out.Credentials[0].CredentialSource.CredentialStoreId) + require.EqualValues(t, "username_password_domain", out.Credentials[0].CredentialSource.CredentialType) + + require.NotNil(t, out.Credentials[0].Secret) + require.NotEmpty(t, out.Credentials[0].Secret.Decoded) + require.Contains(t, out.Credentials[0].Secret.Decoded, "username") + require.Contains(t, out.Credentials[0].Secret.Decoded, "password") + require.Contains(t, out.Credentials[0].Secret.Decoded, "dn") + require.EqualValues(t, c.LdapUserName, out.Credentials[0].Secret.Decoded["username"]) + require.EqualValues(t, out.Credentials[0].Credential["password"], out.Credentials[0].Secret.Decoded["password"]) + require.EqualValues(t, fmt.Sprintf("cn=%s,%s", c.LdapUserName, c.LdapDomainDn), out.Credentials[0].Secret.Decoded["dn"]) + + // Dynamic credential. + require.NotEmpty(t, out.Credentials[1].Credential) + require.Contains(t, out.Credentials[1].Credential, "username") + require.Contains(t, out.Credentials[1].Credential, "password") + require.Contains(t, out.Credentials[1].Credential, "domain") + require.Contains(t, out.Credentials[1].Credential["username"], fmt.Sprintf("b_token_%s_", c.LdapGroupName)) + require.NotEmpty(t, out.Credentials[1].Credential["password"]) + require.EqualValues(t, expDomain, out.Credentials[1].Credential["domain"]) + + require.NotNil(t, out.Credentials[1].CredentialSource) + require.EqualValues(t, dynamicCredVclId, out.Credentials[1].CredentialSource.Id) + require.Contains(t, out.Credentials[1].CredentialSource.Name, "e2e vault credential library") + require.EqualValues(t, "vault-ldap", out.Credentials[1].CredentialSource.Type) + require.EqualValues(t, storeId, out.Credentials[1].CredentialSource.CredentialStoreId) + require.EqualValues(t, "username_password_domain", out.Credentials[1].CredentialSource.CredentialType) + + require.NotNil(t, out.Credentials[1].Secret) + require.NotEmpty(t, out.Credentials[1].Secret.Decoded) + require.Contains(t, out.Credentials[1].Secret.Decoded, "username") + require.Contains(t, out.Credentials[1].Secret.Decoded, "password") + require.Contains(t, out.Credentials[1].Secret.Decoded, "distinguished_names") + require.EqualValues(t, out.Credentials[1].Credential["username"], out.Credentials[1].Secret.Decoded["username"]) + require.EqualValues(t, out.Credentials[1].Credential["password"], out.Credentials[1].Secret.Decoded["password"]) + distinguishedNames, ok := out.Credentials[1].Secret.Decoded["distinguished_names"].([]any) + require.True(t, ok) + require.NotEmpty(t, distinguishedNames) + require.Contains(t, distinguishedNames, fmt.Sprintf("cn=%s,%s", out.Credentials[1].Secret.Decoded["username"], c.LdapDomainDn)) + require.Contains(t, distinguishedNames, fmt.Sprintf("cn=%s,%s", c.LdapGroupName, c.LdapDomainDn)) +} diff --git a/testing/internal/e2e/tests/base_with_vault/credential_store_test.go b/testing/internal/e2e/tests/base_with_vault/credential_store_test.go index 6c25c8d37..d9d46d675 100644 --- a/testing/internal/e2e/tests/base_with_vault/credential_store_test.go +++ b/testing/internal/e2e/tests/base_with_vault/credential_store_test.go @@ -55,7 +55,7 @@ func TestCliVaultCredentialStore(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), @@ -243,7 +243,7 @@ func TestApiVaultCredentialStore(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("secrets", "enable", "-path="+c.VaultSecretPath, "kv-v2"), ) diff --git a/testing/internal/e2e/tests/base_with_vault/env_test.go b/testing/internal/e2e/tests/base_with_vault/env_test.go index c213a2e80..c4bf073c8 100644 --- a/testing/internal/e2e/tests/base_with_vault/env_test.go +++ b/testing/internal/e2e/tests/base_with_vault/env_test.go @@ -16,7 +16,16 @@ type config struct { VaultAddr string `envconfig:"E2E_VAULT_ADDR_PUBLIC" required:"true"` // e.g. "http://127.0.0.1:8200" VaultAddrPrivate string `envconfig:"E2E_VAULT_ADDR_PRIVATE" required:"true"` // e.g. "http://10.10.10.10:8200" VaultSecretPath string `envconfig:"E2E_VAULT_SECRET_PATH" default:"e2e_secrets"` + VaultLdapPath string `envconfig:"E2E_VAULT_LDAP_PATH" default:"e2e_ldap"` MaxPageSize int `envconfig:"E2E_MAX_PAGE_SIZE" default:"1000"` + // Ldap configuration. + LdapAddress string `envconfig:"E2E_LDAP_ADDR" required:"true"` // e.g. ldap://ldap + LdapDomainDn string `envconfig:"E2E_LDAP_DOMAIN_DN" required:"true"` // e.g. dc=example,dc=org + LdapAdminDn string `envconfig:"E2E_LDAP_ADMIN_DN" required:"true"` // e.g. cn=admin,dc=example,dc=org + LdapAdminPassword string `envconfig:"E2E_LDAP_ADMIN_PASSWORD" required:"true"` + LdapUserName string `envconfig:"E2E_LDAP_USER_NAME" required:"true"` + LdapUserPassword string `envconfig:"E2E_LDAP_USER_PASSWORD" required:"true"` + LdapGroupName string `envconfig:"E2E_LDAP_GROUP_NAME" required:"true"` } func loadTestConfig() (*config, error) { diff --git a/testing/internal/e2e/tests/base_with_vault/paginate_credential_library_test.go b/testing/internal/e2e/tests/base_with_vault/paginate_credential_library_test.go index a6d590430..995003250 100644 --- a/testing/internal/e2e/tests/base_with_vault/paginate_credential_library_test.go +++ b/testing/internal/e2e/tests/base_with_vault/paginate_credential_library_test.go @@ -41,7 +41,7 @@ func TestCliPaginateCredentialLibraries(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), @@ -194,7 +194,7 @@ func TestApiPaginateCredentialLibraries(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), diff --git a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_authz_token_test.go b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_authz_token_test.go index 37e34d386..b28559772 100644 --- a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_authz_token_test.go +++ b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_authz_token_test.go @@ -53,7 +53,7 @@ func TestCliTcpTargetVaultGenericConnectTargetWithAuthzToken(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), diff --git a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_ssh_test.go b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_ssh_test.go index 797d34e5e..bb4144e0f 100644 --- a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_ssh_test.go +++ b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_ssh_test.go @@ -53,7 +53,7 @@ func TestCliTcpTargetVaultGenericConnectTargetWithSsh(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), diff --git a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_test.go b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_test.go index 6a60b2532..13bf23339 100644 --- a/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_test.go +++ b/testing/internal/e2e/tests/base_with_vault/target_tcp_vault_generic_connect_test.go @@ -54,7 +54,7 @@ func TestCliTcpTargetVaultGenericConnectTarget(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), diff --git a/testing/internal/e2e/tests/base_with_worker/target_tcp_worker_connect_ssh_test.go b/testing/internal/e2e/tests/base_with_worker/target_tcp_worker_connect_ssh_test.go index 9e9c6c9dc..46fc65652 100644 --- a/testing/internal/e2e/tests/base_with_worker/target_tcp_worker_connect_ssh_test.go +++ b/testing/internal/e2e/tests/base_with_worker/target_tcp_worker_connect_ssh_test.go @@ -49,7 +49,7 @@ func TestCliTcpTargetWorkerConnectTarget(t *testing.T) { require.NoError(t, err) // Configure vault - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") t.Cleanup(func() { output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("policy", "delete", boundaryPolicyName), diff --git a/testing/internal/e2e/tests/database/migration_test.go b/testing/internal/e2e/tests/database/migration_test.go index cf1746993..d7f39debb 100644 --- a/testing/internal/e2e/tests/database/migration_test.go +++ b/testing/internal/e2e/tests/database/migration_test.go @@ -278,7 +278,7 @@ func populateBoundaryDatabase(t testing.TB, ctx context.Context, c *config, te T require.NoError(t, err) // Create vault credentials - boundaryPolicyName, kvPolicyFilePath := vault.Setup(t, "testdata/boundary-controller-policy.hcl") + boundaryPolicyName, kvPolicyFilePath, _ := vault.Setup(t, "testdata/boundary-controller-policy.hcl") output := e2e.RunCommand(ctx, "vault", e2e.WithArgs("secrets", "enable", "-path="+c.VaultSecretPath, "kv-v2"), ) diff --git a/testing/internal/e2e/vault/vault.go b/testing/internal/e2e/vault/vault.go index 1ee00bbb8..02c4da450 100644 --- a/testing/internal/e2e/vault/vault.go +++ b/testing/internal/e2e/vault/vault.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/hashicorp/boundary/testing/internal/e2e" @@ -25,7 +26,7 @@ type CreateTokenResponse struct { // Setup verifies if appropriate credentials are set and adds the boundary controller // policy to vault. Returns the vault address. -func Setup(t testing.TB, boundaryControllerFilePath string) (boundaryPolicyName string, kvPolicyFilePath string) { +func Setup(t testing.TB, boundaryControllerFilePath string) (boundaryPolicyName, kvPolicyFilePath, ldapPolicyFilePath string) { // Set up boundary policy boundaryPolicyFilePath, err := filepath.Abs(boundaryControllerFilePath) require.NoError(t, err) @@ -36,7 +37,115 @@ func Setup(t testing.TB, boundaryControllerFilePath string) (boundaryPolicyName _, err = os.Create(kvPolicyFilePath) require.NoError(t, err) - return boundaryPolicyName, kvPolicyFilePath + // Create ldap policy + ldapPolicyFilePath = fmt.Sprintf("%s/%s", t.TempDir(), "ldap-policy.hcl") + _, err = os.Create(ldapPolicyFilePath) + require.NoError(t, err) + + return boundaryPolicyName, kvPolicyFilePath, ldapPolicyFilePath +} + +// SetupLdap sets a Vault server up for LDAP against an OpenLDAP server. It +// enables the LDAP secrets engine, configures it and creates a static user +// according to what is in OpenLDAP. Additionally, it sets up Vault's ability to +// manage LDAP users dynamically. Note that this function does not put any +// clean-up in place to run after a test is complete. When applicable, callers +// should destroy the Vault LDAP policy and LDAP secrets engine this function +// creates. +func SetupLdap(t testing.TB, vaultLdapMountPath, vaultLdapPolicyFilePath, ldapAddr, ldapAdminDn, ldapAdminPw, ldapDn, ldapUser, ldapGroup string) string { + // Enable LDAP secrets engine. + output := e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs("secrets", "enable", fmt.Sprintf("-path=%s", vaultLdapMountPath), "ldap"), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Define and write LDAP access policy to Vault. + f, err := os.OpenFile(vaultLdapPolicyFilePath, os.O_APPEND|os.O_WRONLY, 0o644) + require.NoError(t, err) + _, err = fmt.Fprintf(f, ` + path "%[1]s/static-cred/%[2]s" { + capabilities = ["read"] + } + path "%[1]s/static-role/%[2]s" { + capabilities = ["create", "read", "update", "patch", "delete", "list"] + } + + path "%[1]s/creds/%[3]s" { + capabilities = ["read"] + } + path "%[1]s/role/%[3]s" { + capabilities = ["create", "read", "update", "patch", "delete", "list"] + } + `, vaultLdapMountPath, ldapUser, ldapGroup) + require.NoError(t, err) + require.NoError(t, f.Sync()) + _ = f.Close() + + policyName := WritePolicy(t, t.Context(), vaultLdapPolicyFilePath) + + // Configure LDAP secrets engine to point to existing OpenLDAP server. + output = e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("%s/config", vaultLdapMountPath), + fmt.Sprintf("url=%s", ldapAddr), + fmt.Sprintf("binddn=%s", ldapAdminDn), + fmt.Sprintf("bindpass=%s", ldapAdminPw), + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create static LDAP user in Vault (already defined in OpenLDAP server). + output = e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("%s/static-role/%s", vaultLdapMountPath, ldapUser), + fmt.Sprintf("dn=cn=%s,%s", ldapUser, ldapDn), + fmt.Sprintf("username=%s", ldapUser), + "rotation_period=24h", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Create Vault dynamic role for LDAP group. + createLdif := fmt.Sprintf(` + dn: cn={{.Username}},%[1]s + changetype: add + objectClass: inetOrgPerson + cn: {{.Username}} + sn: {{.Username}} + uid: {{.Username}} + userPassword:{{.Password}} + + dn: cn=%s,%[1]s + changetype: modify + add: uniqueMember + uniqueMember: cn={{.Username}},%[1]s + - + `, ldapDn, ldapGroup) + createLdif = strings.ReplaceAll(createLdif, "\t", "") + + deleteLdif := fmt.Sprintf(` + dn: cn={{.Username}},%s + changetype: delete + `, ldapDn) + deleteLdif = strings.ReplaceAll(deleteLdif, "\t", "") + + output = e2e.RunCommand(t.Context(), "vault", + e2e.WithArgs( + "write", + fmt.Sprintf("%s/role/%s", vaultLdapMountPath, ldapGroup), + "username_template=b_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}", + fmt.Sprintf("creation_ldif=%s", createLdif), + fmt.Sprintf("rollback_ldif=%s", deleteLdif), + fmt.Sprintf("deletion_ldif=%s", deleteLdif), + "default_ttl=1h", + "max_ttxl=24h", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + return policyName } // CreateKvPrivateKeyCredential creates a private key credential in vault and creates a vault policy