Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VAULT-8099 LDAP secrets engine #1859

Merged
merged 11 commits into from
May 18, 2023
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,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@v3
- name: Acceptance Tests
Expand All @@ -130,6 +140,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
28 changes: 28 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ 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"
FieldRequestTimeout = "request_timeout"
FieldSchema = "schema"
FieldPasswordPolicy = "password_policy"
FieldLength = "length"
FieldInsecureTLS = "insecure_tls"
FieldURL = "url"
FieldUserAttr = "userattr"
FieldUserDN = "userdn"
FieldRotationPeriod = "rotation_period"
FieldPath = "path"
FieldParameters = "parameters"
FieldMethod = "method"
Expand All @@ -26,15 +43,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 @@ -217,6 +238,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 @@ -272,6 +299,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