Skip to content

Commit

Permalink
VAULT-8099 LDAP secrets engine (#1859)
Browse files Browse the repository at this point in the history
  • Loading branch information
raymonstah authored May 18, 2023
1 parent 926c9a6 commit d8ba8b4
Show file tree
Hide file tree
Showing 33 changed files with 2,299 additions and 15 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ jobs:
--health-interval 1s
--health-timeout 5s
--health-retries 5
openldap:
image: docker.io/bitnami/openldap:2.6
ports:
- '1389:1389'
- '1636:1636'
env:
LDAP_ADMIN_USERNAME: "admin"
LDAP_ADMIN_PASSWORD: "adminpassword"
LDAP_USERS: "alice,bob,foo"
LDAP_PASSWORDS: "password1,password2,password3"
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Acceptance Tests
Expand All @@ -128,6 +138,9 @@ jobs:
COUCHBASE_PASSWORD: password
CONSUL_HTTP_ADDR: "consul:8500"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
LDAP_BINDDN: "cn=admin,dc=example,dc=org"
LDAP_BINDPASS: "adminpassword"
LDAP_URL: "ldap://openldap:1389"
run: |
make testacc-ent TESTARGS='-test.v -test.parallel=10' SKIP_MSSQL_MULTI_CI=true SKIP_RAFT_TESTS=true SKIP_VAULT_NEXT_TESTS=true
- name: "Generate Vault API Path Coverage Report"
Expand Down
13 changes: 12 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@ services:
"DOCKER_INFLUXDB_INIT_USERNAME": "admin"
"DOCKER_INFLUXDB_INIT_PASSWORD": "password"
"DOCKER_INFLUXDB_INIT_ORG": "test"
"DOCKER_INFLUXDB_INIT_BUCKET": "test"
"DOCKER_INFLUXDB_INIT_BUCKET": "test"

openldap:
image: docker.io/bitnami/openldap:2.6
ports:
- '1389:1389'
- '1636:1636'
environment:
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=adminpassword
- LDAP_USERS=alice,bob,foo
- LDAP_PASSWORDS=password1,password2,password3
29 changes: 29 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ const (
/*
common field names
*/
FieldBindDN = "binddn"
FieldBindPass = "bindpass"
FieldCertificate = "certificate"
FieldClientTLSCert = "client_tls_cert"
FieldClientTLSKey = "client_tls_key"
FieldDistinguishedNames = "distinguished_names"
FieldUPNDomain = "upndomain"
FieldStartTLS = "starttls"
FieldConnectionTimeout = "connection_timeout"
FieldRequestTimeout = "request_timeout"
FieldSchema = "schema"
FieldPasswordPolicy = "password_policy"
FieldLength = "length"
FieldInsecureTLS = "insecure_tls"
FieldURL = "url"
FieldUserAttr = "userattr"
FieldUserDN = "userdn"
FieldRotationPeriod = "rotation_period"
FieldPath = "path"
FieldPaths = "paths"
FieldParameters = "parameters"
Expand All @@ -27,15 +45,19 @@ const (
FieldLeaseRenewable = "lease_renewable"
FieldDepth = "depth"
FieldDataJSON = "data_json"
FieldDN = "dn"
FieldRole = "role"
FieldRoles = "roles"
FieldDescription = "description"
FieldTTL = "ttl"
FieldMaxTTL = "max_ttl"
FieldDefaultLeaseTTL = "default_lease_ttl_seconds"
FieldDefaultTTL = "default_ttl"
FieldMaxLeaseTTL = "max_lease_ttl_seconds"
FieldAuditNonHMACRequestKeys = "audit_non_hmac_request_keys"
FieldAuditNonHMACResponseKeys = "audit_non_hmac_response_keys"
FieldLastPassword = "last_password"
FieldLastVaultRotation = "last_vault_rotation"
FieldLocal = "local"
FieldSealWrap = "seal_wrap"
FieldExternalEntropyAccess = "external_entropy_access"
Expand Down Expand Up @@ -219,6 +241,12 @@ const (
FieldIPAddresses = "ip_addresses"
FieldCIDRBlocks = "cidr_blocks"
FieldProjectRoles = "project_roles"
FieldCreationLDIF = "creation_ldif"
FieldDeletionLDIF = "deletion_ldif"
FieldRollbackLDIF = "rollback_ldif"
FieldUsernameTemplate = "username_template"
FieldServiceAccountNames = "service_account_names"
FieldDisableCheckInEnforcement = "disable_check_in_enforcement"

/*
common environment variables
Expand Down Expand Up @@ -274,6 +302,7 @@ const (
MountTypeAzure = "azure"
MountTypeGitHub = "github"
MountTypeAD = "ad"
MountTypeLDAP = "ldap"
MountTypeConsul = "consul"
MountTypeTerraform = "terraform"

Expand Down
5 changes: 5 additions & 0 deletions testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ func GetTestADCreds(t *testing.T) (string, string, string) {
return v[0], v[1], v[2]
}

func GetTestLDAPCreds(t *testing.T) (string, string, string) {
v := SkipTestEnvUnset(t, "LDAP_BINDDN", "LDAP_BINDPASS", "LDAP_URL")
return v[0], v[1], v[2]
}

func GetTestNomadCreds(t *testing.T) (string, string) {
v := SkipTestEnvUnset(t, "NOMAD_ADDR", "NOMAD_TOKEN")
return v[0], v[1]
Expand Down
3 changes: 2 additions & 1 deletion vault/data_source_ad_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (

func adAccessCredentialsDataSource() *schema.Resource {
return &schema.Resource{
Read: ReadWrapper(readCredsResource),
DeprecationMessage: `This data source is replaced by "vault_ldap_static_credentials" and will be removed in the next major release.`,
Read: ReadWrapper(readCredsResource),
Schema: map[string]*schema.Schema{
"backend": {
Type: schema.TypeString,
Expand Down
147 changes: 147 additions & 0 deletions vault/data_source_ldap_dynamic_role_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
"context"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/vault/api"

"github.com/hashicorp/terraform-provider-vault/internal/consts"
"github.com/hashicorp/terraform-provider-vault/internal/provider"
)

func ldapDynamicCredDataSource() *schema.Resource {
return &schema.Resource{
ReadContext: ReadContextWrapper(readLDAPDynamicCreds),
Schema: map[string]*schema.Schema{
consts.FieldMount: {
Type: schema.TypeString,
Required: true,
Description: "LDAP Secret Backend to read credentials from.",
},
consts.FieldRoleName: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of the role.",
},
consts.FieldLeaseID: {
Type: schema.TypeString,
Computed: true,
Description: "Lease identifier assigned by Vault.",
},
consts.FieldLeaseDuration: {
Type: schema.TypeInt,
Computed: true,
Description: "Lease duration in seconds.",
},
consts.FieldLeaseRenewable: {
Type: schema.TypeBool,
Computed: true,
Description: "True if the duration of this lease can be extended through renewal.",
},
consts.FieldDistinguishedNames: {
Type: schema.TypeList,
Computed: true,
Description: "List of the distinguished names (DN) created.",
Elem: &schema.Schema{Type: schema.TypeString},
},
consts.FieldPassword: {
Type: schema.TypeString,
Computed: true,
Description: "Password for the dynamic role.",
Sensitive: true,
},
consts.FieldUsername: {
Type: schema.TypeString,
Computed: true,
Description: "Name of the dynamic role.",
},
},
}
}

func readLDAPDynamicCreds(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, err := provider.GetClient(d, meta)
if err != nil {
return diag.FromErr(err)
}

mount := d.Get(consts.FieldMount).(string)
role := d.Get(consts.FieldRoleName).(string)
fullPath := fmt.Sprintf("%s/creds/%s", mount, role)

secret, err := client.Logical().ReadWithContext(ctx, fullPath)
if err != nil {
return diag.FromErr(fmt.Errorf("error reading from Vault: %s", err))
}
log.Printf("[DEBUG] Read %q from Vault", fullPath)
if secret == nil {
return diag.FromErr(fmt.Errorf("no role found at %q", fullPath))
}

response, err := parseLDAPDynamicCredSecret(secret)
if err != nil {
return diag.FromErr(err)
}

d.SetId(secret.LeaseID)
if err := d.Set(consts.FieldLeaseID, secret.LeaseID); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldLeaseDuration, secret.LeaseDuration); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldLeaseRenewable, secret.Renewable); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldDistinguishedNames, response.distinguishedNames); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldPassword, response.password); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldUsername, response.username); err != nil {
return diag.FromErr(err)
}
return nil
}

type lDAPDynamicCredResponse struct {
distinguishedNames []string
password string
username string
}

func parseLDAPDynamicCredSecret(secret *api.Secret) (lDAPDynamicCredResponse, error) {
var (
distinguishedNames []string
)
if distinguishedNamesRaw, ok := secret.Data[consts.FieldDistinguishedNames]; ok {
for _, dnRaw := range distinguishedNamesRaw.([]interface{}) {
distinguishedNames = append(distinguishedNames, dnRaw.(string))
}
}

username := secret.Data[consts.FieldUsername].(string)
if username == "" {
return lDAPDynamicCredResponse{}, fmt.Errorf("username is not set in response")
}

password := secret.Data[consts.FieldPassword].(string)
if password == "" {
return lDAPDynamicCredResponse{}, fmt.Errorf("password is not set in response")
}

return lDAPDynamicCredResponse{
distinguishedNames: distinguishedNames,
password: password,
username: username,
}, nil
}
73 changes: 73 additions & 0 deletions vault/data_source_ldap_dynamic_role_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"

"github.com/hashicorp/terraform-provider-vault/internal/consts"
"github.com/hashicorp/terraform-provider-vault/internal/provider"
"github.com/hashicorp/terraform-provider-vault/testutil"
)

func TestAccDataSourceLDAPDynamicRoleCredentials(t *testing.T) {
path := acctest.RandomWithPrefix("tf-test-ldap-dynamic-role-credentials")
bindDN, bindPass, url := testutil.GetTestLDAPCreds(t)
dataName := "data.vault_ldap_dynamic_credentials.creds"
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
PreCheck: func() {
testutil.TestAccPreCheck(t)
SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion112)
},
Steps: []resource.TestStep{
{
Config: testLDAPDynamicRoleDataSource(path, path, bindDN, bindPass, url, "100", "100"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(dataName, consts.FieldPassword),
resource.TestCheckResourceAttrSet(dataName, consts.FieldUsername),
resource.TestCheckResourceAttrSet(dataName, consts.FieldLeaseID),
resource.TestCheckResourceAttrSet(dataName, consts.FieldLeaseDuration),
resource.TestCheckResourceAttrSet(dataName, consts.FieldLeaseRenewable),
),
},
},
})
}
func testLDAPDynamicRoleDataSource(path, roleName, bindDN, bindPass, url, defaultTTL, maxTTL string) string {
return fmt.Sprintf(`
resource "vault_ldap_secret_backend" "test" {
path = "%s"
description = "test description"
binddn = "%s"
bindpass = "%s"
url = "%s"
}
resource "vault_ldap_secret_backend_dynamic_role" "role" {
mount = vault_ldap_secret_backend.test.path
role_name = "%s"
creation_ldif = <<EOT
%s
EOT
deletion_ldif = <<EOT
%s
EOT
rollback_ldif = <<EOT
%s
EOT
default_ttl = %s
max_ttl = %s
}
data "vault_ldap_dynamic_credentials" "creds" {
mount = vault_ldap_secret_backend.test.path
role_name = vault_ldap_secret_backend_dynamic_role.role.role_name
}
`, path, bindDN, bindPass, url, roleName, creationLDIF, deletionLDIF, rollbackLDIF, defaultTTL, maxTTL)
}
Loading

0 comments on commit d8ba8b4

Please sign in to comment.