diff --git a/.github/workflows/enos-run.yml b/.github/workflows/enos-run.yml index cf02de567e..e110cac033 100644 --- a/.github/workflows/enos-run.yml +++ b/.github/workflows/enos-run.yml @@ -79,8 +79,8 @@ jobs: - filter: 'e2e_aws builder:crt' - filter: 'e2e_database' - filter: 'e2e_docker_base builder:crt' + - filter: 'e2e_docker_base_plus builder:crt' - filter: 'e2e_docker_base_with_vault builder:crt' - - filter: 'e2e_docker_base_with_postgres builder:crt' - filter: 'e2e_docker_base_with_worker builder:crt' - filter: 'e2e_docker_worker_registration_controller_led builder:crt' - filter: 'e2e_docker_worker_registration_worker_led builder:crt' diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index 7958a99730..5670e1ed47 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -170,3 +170,7 @@ module "docker_network" { module "docker_check_health" { source = "./modules/docker_check_health" } + +module "docker_ldap" { + source = "./modules/docker_ldap" +} diff --git a/enos/enos-scenario-e2e-docker-base-with-postgres.hcl b/enos/enos-scenario-e2e-docker-base-plus.hcl similarity index 82% rename from enos/enos-scenario-e2e-docker-base-with-postgres.hcl rename to enos/enos-scenario-e2e-docker-base-plus.hcl index 0c5e183a1d..37da265cb7 100644 --- a/enos/enos-scenario-e2e-docker-base-with-postgres.hcl +++ b/enos/enos-scenario-e2e-docker-base-plus.hcl @@ -4,7 +4,7 @@ # For this scenario to work, add the following line to /etc/hosts # 127.0.0.1 localhost boundary -scenario "e2e_docker_base_with_postgres" { +scenario "e2e_docker_base_plus" { terraform_cli = terraform_cli.default terraform = terraform.default providers = [ @@ -88,13 +88,24 @@ scenario "e2e_docker_base_with_postgres" { } } + 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 = [ step.create_boundary, ] variables { - test_package = "github.com/hashicorp/boundary/testing/internal/e2e/tests/base_with_postgres" + test_package = "github.com/hashicorp/boundary/testing/internal/e2e/tests/base_plus" docker_mirror = var.docker_mirror network_name = step.create_docker_network.network_name go_version = var.go_version @@ -112,6 +123,13 @@ scenario "e2e_docker_base_with_postgres" { postgres_user = step.create_boundary_database.user postgres_password = step.create_boundary_database.password postgres_database_name = step.create_boundary_database.database_name + 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/enos/modules/docker_ldap/entries/group.ldif b/enos/modules/docker_ldap/entries/group.ldif new file mode 100644 index 0000000000..5a8859e7b8 --- /dev/null +++ b/enos/modules/docker_ldap/entries/group.ldif @@ -0,0 +1,4 @@ +dn: cn=${group_name},${domain_dn} +objectClass: groupOfUniqueNames +cn: ${group_name} +uniqueMember: cn=${user_name},${domain_dn} diff --git a/enos/modules/docker_ldap/entries/user.ldif b/enos/modules/docker_ldap/entries/user.ldif new file mode 100644 index 0000000000..f84a8f5884 --- /dev/null +++ b/enos/modules/docker_ldap/entries/user.ldif @@ -0,0 +1,6 @@ +dn: cn=${user_name},${domain_dn} +objectClass: inetOrgPerson +cn: ${user_name} +sn: ${user_name} +uid: ${user_name} +userPassword: ${user_password} diff --git a/enos/modules/docker_ldap/main.tf b/enos/modules/docker_ldap/main.tf new file mode 100644 index 0000000000..c970a7baa6 --- /dev/null +++ b/enos/modules/docker_ldap/main.tf @@ -0,0 +1,131 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "3.0.1" + } + + enos = { + source = "app.terraform.io/hashicorp-qti/enos" + } + } +} + +variable "image_name" { + description = "Name of Docker Image" + type = string + default = "docker.mirror.hashicorp.services/osixia/openldap:latest" +} +variable "network_name" { + description = "Name of Docker Networks to join" + type = list(string) +} +variable "container_name" { + description = "Name of Docker Container" + type = string + default = "ldap" +} + +locals { + user_name = "einstein" + user_password = "password" + group_name = "scientists" + domain = "example.org" + domain_dn = "dc=example,dc=org" + admin_dn = "cn=admin,${local.domain_dn}" + admin_password = "admin" +} + +resource "docker_image" "ldap" { + name = var.image_name + keep_locally = true +} + +resource "docker_container" "ldap" { + image = docker_image.ldap.image_id + name = var.container_name + env = [ + "LDAP_DOMAIN=${local.domain}", + "LDAP_ADMIN_PASSWORD=${local.admin_password}", + ] + upload { + content = templatefile("${abspath(path.module)}/entries/user.ldif", { + user_name = local.user_name + user_password = local.user_password + domain_dn = local.domain_dn + }) + file = "/tmp/ldap/user.ldif" + } + upload { + content = templatefile("${abspath(path.module)}/entries/group.ldif", { + group_name = local.group_name + user_name = local.user_name + domain_dn = local.domain_dn + }) + file = "/tmp/ldap/group.ldif" + } + + + healthcheck { + test = ["CMD", "ldapsearch", "-H", "ldap://localhost", "-b", "${local.domain_dn}", "-D", "${local.admin_dn}", "-w", "${local.admin_password}"] + } + wait = true + must_run = true + dynamic "networks_advanced" { + for_each = var.network_name + content { + name = networks_advanced.value + } + } +} + +resource "enos_local_exec" "create_ldap_user" { + depends_on = [ + docker_container.ldap + ] + + inline = ["docker exec ${var.container_name} ldapadd -x -H ldap://localhost -D \"${local.admin_dn}\" -w ${local.admin_password} -f /tmp/ldap/user.ldif"] +} + +resource "enos_local_exec" "create_ldap_group" { + depends_on = [ + docker_container.ldap, + enos_local_exec.create_ldap_user, + ] + + inline = ["docker exec ${var.container_name} ldapadd -x -H ldap://localhost -D \"${local.admin_dn}\" -w ${local.admin_password} -f /tmp/ldap/group.ldif"] +} + +output "address" { + value = "ldap://${var.container_name}" +} + +output "domain_dn" { + value = local.domain_dn +} + +output "admin_dn" { + value = local.admin_dn +} +output "admin_password" { + value = local.admin_password +} + +output "container_name" { + value = var.container_name +} + +output "user_name" { + value = local.user_name +} + +output "user_password" { + value = local.user_password +} + +output "group_name" { + value = local.group_name +} diff --git a/enos/modules/test_e2e_docker/main.tf b/enos/modules/test_e2e_docker/main.tf index 5b6d751d6c..0563315485 100644 --- a/enos/modules/test_e2e_docker/main.tf +++ b/enos/modules/test_e2e_docker/main.tf @@ -161,28 +161,69 @@ variable "aws_bucket_name" { default = "" } variable "worker_tag_ingress" { - type = string - default = "" + description = "Worker tag for the ingress worker" + type = string + default = "" } variable "worker_tag_egress" { - type = string - default = "" + description = "Worker tag for the egress worker" + type = string + default = "" } variable "worker_tag_collocated" { - type = string - default = "" + description = "Worker tag for the collocated worker" + type = string + default = "" } variable "postgres_user" { - type = string - default = "" + description = "Username for accessing the postgres database" + type = string + default = "" } variable "postgres_password" { - type = string - default = "" + description = "Password for accessing the postgres database" + type = string + default = "" } variable "postgres_database_name" { - type = string - default = "" + description = "Name of postgres database" + type = string + default = "" +} +variable "ldap_address" { + description = "URL to LDAP server" + type = string + default = "" +} +variable "ldap_domain_dn" { + description = "Distinguished Name to the LDAP domain" + type = string + default = "" +} +variable "ldap_admin_dn" { + description = "Distinguished Name to the LDAP admin user" + type = string + default = "" +} +variable "ldap_admin_password" { + description = "Password for the LDAP admin user" + type = string + default = "" +} +variable "ldap_user_name" { + description = "Username of an LDAP user" + type = string + default = "" +} +variable "ldap_user_password" { + description = "Password for an LDAP user" + type = string + default = "" +} +variable "ldap_group_name" { + description = "Name of LDAP group" + type = string + default = "" } variable "test_timeout" { type = string @@ -246,6 +287,13 @@ resource "enos_local_exec" "run_e2e_test" { E2E_WORKER_TAG_INGRESS = var.worker_tag_ingress E2E_WORKER_TAG_EGRESS = var.worker_tag_egress E2E_WORKER_TAG_COLLOCATED = var.worker_tag_collocated + E2E_LDAP_ADDR = var.ldap_address + E2E_LDAP_DOMAIN_DN = var.ldap_domain_dn + E2E_LDAP_ADMIN_DN = var.ldap_admin_dn + E2E_LDAP_ADMIN_PASSWORD = var.ldap_admin_password + E2E_LDAP_USER_NAME = var.ldap_user_name + E2E_LDAP_USER_PASSWORD = var.ldap_user_password + E2E_LDAP_GROUP_NAME = var.ldap_group_name BOUNDARY_DIR = abspath(var.local_boundary_src_dir) BOUNDARY_CLI_DIR = abspath(var.local_boundary_dir) MODULE_DIR = abspath(path.module) diff --git a/enos/modules/test_e2e_docker/test_runner.sh b/enos/modules/test_e2e_docker/test_runner.sh index 55392e667b..2f8baf0965 100644 --- a/enos/modules/test_e2e_docker/test_runner.sh +++ b/enos/modules/test_e2e_docker/test_runner.sh @@ -39,6 +39,13 @@ docker run \ -e "E2E_WORKER_TAG_INGRESS=$E2E_WORKER_TAG_INGRESS" \ -e "E2E_WORKER_TAG_EGRESS=$E2E_WORKER_TAG_EGRESS" \ -e "E2E_WORKER_TAG_COLLOCATED=$E2E_WORKER_TAG_COLLOCATED" \ + -e "E2E_LDAP_ADDR=$E2E_LDAP_ADDR" \ + -e "E2E_LDAP_DOMAIN_DN=$E2E_LDAP_DOMAIN_DN" \ + -e "E2E_LDAP_ADMIN_DN=$E2E_LDAP_ADMIN_DN" \ + -e "E2E_LDAP_ADMIN_PASSWORD=$E2E_LDAP_ADMIN_PASSWORD" \ + -e "E2E_LDAP_USER_NAME=$E2E_LDAP_USER_NAME" \ + -e "E2E_LDAP_USER_PASSWORD=$E2E_LDAP_USER_PASSWORD" \ + -e "E2E_LDAP_GROUP_NAME=$E2E_LDAP_GROUP_NAME" \ --mount type=bind,src=$BOUNDARY_DIR,dst=/src/boundary/ \ --mount type=bind,src=$MODULE_DIR/../..,dst=/testlogs \ --mount type=bind,src=$(go env GOCACHE),dst=/root/.cache/go-build \ diff --git a/testing/internal/e2e/tests/base_plus/env_test.go b/testing/internal/e2e/tests/base_plus/env_test.go new file mode 100644 index 0000000000..f34a9b1680 --- /dev/null +++ b/testing/internal/e2e/tests/base_plus/env_test.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_plus_test + +import "github.com/kelseyhightower/envconfig" + +type config struct { + TargetAddress string `envconfig:"E2E_TARGET_ADDRESS" required:"true"` // e.g. 192.168.0.1 + TargetSshKeyPath string `envconfig:"E2E_SSH_KEY_PATH" required:"true"` // e.g. /Users/username/key.pem + TargetSshUser string `envconfig:"E2E_SSH_USER" required:"true"` // e.g. ubuntu + TargetPort string `envconfig:"E2E_TARGET_PORT" required:"true"` // e.g. 22 + PostgresDbName string `envconfig:"E2E_POSTGRES_DB_NAME" required:"true"` + PostgresUser string `envconfig:"E2E_POSTGRES_USER" required:"true"` + PostgresPassword string `envconfig:"E2E_POSTGRES_PASSWORD" required:"true"` + 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) { + var c config + err := envconfig.Process("", &c) + if err != nil { + return nil, err + } + + return &c, nil +} diff --git a/testing/internal/e2e/tests/base/key_destruction_test.go b/testing/internal/e2e/tests/base_plus/key_destruction_test.go similarity index 99% rename from testing/internal/e2e/tests/base/key_destruction_test.go rename to testing/internal/e2e/tests/base_plus/key_destruction_test.go index 3768f1d127..fa9199d53e 100644 --- a/testing/internal/e2e/tests/base/key_destruction_test.go +++ b/testing/internal/e2e/tests/base_plus/key_destruction_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package base_test +package base_plus_test import ( "context" diff --git a/testing/internal/e2e/tests/base_plus/ldap_test.go b/testing/internal/e2e/tests/base_plus/ldap_test.go new file mode 100644 index 0000000000..f20c426e7b --- /dev/null +++ b/testing/internal/e2e/tests/base_plus/ldap_test.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package base_plus_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/managedgroups" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/require" +) + +// TestCliLdap uses the boundary cli to set up an LDAP auth method and confirm +// that an LDAP user can authenticate to boundary. It also confirms that an LDAP +// managed group can be added as a principal to a role. +func TestCliLdap(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadTestConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + + // Create an LDAP auth method + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "create", "ldap", + "-scope-id", newOrgId, + "-name", "e2e LDAP", + "-urls", c.LdapAddress, + "-user-dn", c.LdapDomainDn, + "-user-attr", "uid", + "-group-dn", c.LdapDomainDn, + "-bind-dn", c.LdapAdminDn, + "-bind-password", "env://LDAP_PW", + "-state", "active-public", + "-enable-groups", "true", + "-discover-dn", "true", + "-format", "json", + ), + e2e.WithEnv("LDAP_PW", c.LdapAdminPassword), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newAuthMethodResult authmethods.AuthMethodCreateResult + err = json.Unmarshal(output.Stdout, &newAuthMethodResult) + require.NoError(t, err) + ldapAuthMethodId := newAuthMethodResult.Item.Id + t.Logf("Create Auth Method: %s", ldapAuthMethodId) + + // Create an LDAP account + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "accounts", "create", "ldap", + "-auth-method-id", ldapAuthMethodId, + "-name", "einstein", + "-login-name", c.LdapUserName, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var newAccountResult accounts.AccountCreateResult + err = json.Unmarshal(output.Stdout, &newAccountResult) + require.NoError(t, err) + newAccountId := newAccountResult.Item.Id + t.Logf("Created Account: %s", newAccountId) + + // Create a user and attach the LDAP account + newUserId := boundary.CreateNewUserCli(t, ctx, newOrgId) + boundary.SetAccountToUserCli(t, ctx, newUserId, newAccountId) + + // Try to log in with the wrong password + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "authenticate", "ldap", + "-auth-method-id", ldapAuthMethodId, + "-login-name", c.LdapUserName, + "-password", "env://LDAP_PW", + ), + e2e.WithEnv("LDAP_PW", c.LdapAdminPassword), + ) + require.Error(t, output.Err, string(output.Stderr)) + + // Log in as the LDAP user + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "authenticate", "ldap", + "-auth-method-id", ldapAuthMethodId, + "-login-name", c.LdapUserName, + "-password", "env://LDAP_PW", + ), + e2e.WithEnv("LDAP_PW", c.LdapUserPassword), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Confirm there is a permissions error when trying to read an auth method + // as an LDAP user + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "read", + "-id", ldapAuthMethodId, + "-format", "json", + ), + ) + require.Error(t, output.Err, string(output.Stderr)) + var response boundary.CliError + err = json.Unmarshal(output.Stderr, &response) + require.NoError(t, err) + require.Equal(t, http.StatusForbidden, response.Status) + + // Create an LDAP managed group + boundary.AuthenticateAdminCli(t, ctx) + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "managed-groups", "create", "ldap", + "-auth-method-id", ldapAuthMethodId, + "-name", c.LdapGroupName, + "-group-names", c.LdapGroupName, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + var newManagedGroupResult managedgroups.ManagedGroupCreateResult + err = json.Unmarshal(output.Stdout, &newManagedGroupResult) + require.NoError(t, err) + managedGroupId := newManagedGroupResult.Item.Id + t.Logf("Created Managed Group: %s", managedGroupId) + + // Confirm that LDAP user is in the managed group + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "managed-groups", "read", + "-id", managedGroupId, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) + var managedGroupReadResult managedgroups.ManagedGroupReadResult + err = json.Unmarshal(output.Stdout, &managedGroupReadResult) + require.NoError(t, err) + require.Contains(t, managedGroupReadResult.Item.MemberIds, newAccountId) + + // Add managed group as a principal to a role with permissions to read auth methods + newRoleId := boundary.CreateNewRoleCli(t, ctx, newOrgId) + boundary.AddPrincipalToRoleCli(t, ctx, newRoleId, managedGroupId) + boundary.AddGrantToRoleCli(t, ctx, newRoleId, "ids=*;type=auth-method;actions=read") + + // Log in as the LDAP user again + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "authenticate", "ldap", + "-auth-method-id", ldapAuthMethodId, + "-login-name", c.LdapUserName, + "-password", "env://LDAP_PW", + ), + e2e.WithEnv("LDAP_PW", c.LdapUserPassword), + ) + require.NoError(t, output.Err, string(output.Stderr)) + + // Read the auth method. Expect no error + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs( + "auth-methods", "read", + "-id", ldapAuthMethodId, + "-format", "json", + ), + ) + require.NoError(t, output.Err, string(output.Stderr)) +} diff --git a/testing/internal/e2e/tests/base_with_postgres/target_tcp_connect_postgres_test.go b/testing/internal/e2e/tests/base_plus/target_tcp_connect_postgres_test.go similarity index 98% rename from testing/internal/e2e/tests/base_with_postgres/target_tcp_connect_postgres_test.go rename to testing/internal/e2e/tests/base_plus/target_tcp_connect_postgres_test.go index c4ddc99d9f..fd06e288bf 100644 --- a/testing/internal/e2e/tests/base_with_postgres/target_tcp_connect_postgres_test.go +++ b/testing/internal/e2e/tests/base_plus/target_tcp_connect_postgres_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package base_with_postgres_test +package base_plus_test import ( "bytes" diff --git a/testing/internal/e2e/tests/base_with_postgres/env_test.go b/testing/internal/e2e/tests/base_with_postgres/env_test.go deleted file mode 100644 index 8f4b5c4626..0000000000 --- a/testing/internal/e2e/tests/base_with_postgres/env_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package base_with_postgres_test - -import "github.com/kelseyhightower/envconfig" - -type config struct { - TargetAddress string `envconfig:"E2E_TARGET_ADDRESS" required:"true"` // e.g. 192.168.0.1 - TargetPort string `envconfig:"E2E_TARGET_PORT" required:"true"` - PostgresDbName string `envconfig:"E2E_POSTGRES_DB_NAME" required:"true"` - PostgresUser string `envconfig:"E2E_POSTGRES_USER" required:"true"` - PostgresPassword string `envconfig:"E2E_POSTGRES_PASSWORD" required:"true"` -} - -func loadTestConfig() (*config, error) { - var c config - err := envconfig.Process("", &c) - if err != nil { - return nil, err - } - - return &c, nil -}