diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b302147b9..36209ade6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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" diff --git a/docker-compose.yaml b/docker-compose.yaml index 5d693909c..2144d0d13 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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" \ No newline at end of file + "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 diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 311ae6ede..c493febda 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -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" FieldParameters = "parameters" FieldMethod = "method" @@ -26,15 +44,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" @@ -217,6 +239,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 @@ -272,6 +300,7 @@ const ( MountTypeAzure = "azure" MountTypeGitHub = "github" MountTypeAD = "ad" + MountTypeLDAP = "ldap" MountTypeConsul = "consul" MountTypeTerraform = "terraform" diff --git a/testutil/testutil.go b/testutil/testutil.go index 55f1dc5e5..915aba057 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -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] diff --git a/vault/data_source_ad_credentials.go b/vault/data_source_ad_credentials.go index 713d8bfff..00627a6bf 100644 --- a/vault/data_source_ad_credentials.go +++ b/vault/data_source_ad_credentials.go @@ -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, diff --git a/vault/data_source_ldap_dynamic_role_credentials.go b/vault/data_source_ldap_dynamic_role_credentials.go new file mode 100644 index 000000000..9994983ce --- /dev/null +++ b/vault/data_source_ldap_dynamic_role_credentials.go @@ -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 +} diff --git a/vault/data_source_ldap_dynamic_role_credentials_test.go b/vault/data_source_ldap_dynamic_role_credentials_test.go new file mode 100644 index 000000000..f762f6004 --- /dev/null +++ b/vault/data_source_ldap_dynamic_role_credentials_test.go @@ -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 = <= 1.5.", + Description: "The desired length of passwords that Vault generates.", + ConflictsWith: []string{consts.FieldPasswordPolicy}, + }, + consts.FieldPasswordPolicy: { + Type: schema.TypeString, + Optional: true, + Description: "Name of the password policy to use to generate passwords.", + ConflictsWith: []string{consts.FieldLength}, + }, + consts.FieldSchema: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The LDAP schema to use when storing entry passwords. Valid schemas include openldap, ad, and racf.", + }, + consts.FieldConnectionTimeout: { + Type: schema.TypeInt, + Optional: true, + Default: 30, + Description: "Timeout, in seconds, when attempting to connect to the LDAP server before trying the next URL in the configuration.", + }, + consts.FieldRequestTimeout: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Timeout, in seconds, for the connection when making requests against the server before returning back an error.", + }, + consts.FieldStartTLS: { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Issue a StartTLS command after establishing unencrypted connection.", + }, + consts.FieldUPNDomain: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Enables userPrincipalDomain login with [username]@UPNDomain.", + }, + consts.FieldURL: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order.", + }, + consts.FieldUserAttr: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Attribute used for users (default: cn)", + }, + consts.FieldUserDN: { + Type: schema.TypeString, + Optional: true, + Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)", + }, + } + resource := provider.MustAddMountMigrationSchema(&schema.Resource{ + CreateContext: MountCreateContextWrapper(createUpdateLDAPConfigResource, provider.VaultVersion112), + UpdateContext: createUpdateLDAPConfigResource, + ReadContext: ReadContextWrapper(readLDAPConfigResource), + DeleteContext: deleteLDAPConfigResource, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + CustomizeDiff: getMountCustomizeDiffFunc(consts.FieldPath), + Schema: fields, + }) + + // Add common mount schema to the resource + provider.MustAddSchema(resource, getMountSchema("path", "type")) + return resource +} + +func createUpdateLDAPConfigResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := provider.GetClient(d, meta) + if err != nil { + return diag.FromErr(err) + } + + path := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Mounting LDAP mount at %q", path) + if d.IsNewResource() { + if err := createMount(d, client, path, consts.MountTypeLDAP); err != nil { + return diag.FromErr(err) + } + } else { + if err := updateMount(d, meta, true); err != nil { + return diag.FromErr(err) + } + } + + log.Printf("[DEBUG] Mounted LDAP mount at %q", path) + d.SetId(path) + + data := map[string]interface{}{} + fields := []string{ + consts.FieldBindDN, + consts.FieldBindPass, + consts.FieldCertificate, + consts.FieldConnectionTimeout, + consts.FieldClientTLSCert, + consts.FieldClientTLSKey, + consts.FieldLength, + consts.FieldPasswordPolicy, + consts.FieldRequestTimeout, + consts.FieldSchema, + consts.FieldUPNDomain, + consts.FieldURL, + consts.FieldUserAttr, + consts.FieldUserDN, + } + + booleanFields := []string{ + consts.FieldInsecureTLS, + consts.FieldStartTLS, + } + + // use d.Get() for boolean fields + for _, field := range booleanFields { + data[field] = d.Get(field) + } + + for _, field := range fields { + if v, ok := d.GetOk(field); ok { + data[field] = v + } + } + + configPath := fmt.Sprintf("%s/config", path) + log.Printf("[DEBUG] Writing %q", configPath) + if _, err := client.Logical().Write(configPath, data); err != nil { + return diag.FromErr(fmt.Errorf("error writing %q: %s", configPath, err)) + } + log.Printf("[DEBUG] Wrote %q", configPath) + return readLDAPConfigResource(ctx, d, meta) +} + +func readLDAPConfigResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := provider.GetClient(d, meta) + if err != nil { + return diag.FromErr(err) + } + + path := d.Id() + configPath := fmt.Sprintf("%s/config", path) + log.Printf("[DEBUG] Reading %q", configPath) + + resp, err := client.Logical().ReadWithContext(ctx, configPath) + if err != nil { + return diag.FromErr(fmt.Errorf("error reading %q: %s", configPath, err)) + } + log.Printf("[DEBUG] Read %q", configPath) + if resp == nil { + log.Printf("[WARN] %q not found, removing from state", configPath) + d.SetId("") + return nil + } + + fields := []string{ + consts.FieldBindDN, + consts.FieldConnectionTimeout, + consts.FieldClientTLSCert, + consts.FieldClientTLSKey, + consts.FieldInsecureTLS, + consts.FieldLength, + consts.FieldPasswordPolicy, + consts.FieldRequestTimeout, + consts.FieldSchema, + consts.FieldStartTLS, + consts.FieldUPNDomain, + consts.FieldURL, + consts.FieldUserAttr, + consts.FieldUserDN, + } + + for _, field := range fields { + if val, ok := resp.Data[field]; ok { + if err := d.Set(field, val); err != nil { + return diag.FromErr(fmt.Errorf("error setting state key '%s': %s", field, err)) + } + } + } + + if err := readMount(d, meta, true); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func deleteLDAPConfigResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := provider.GetClient(d, meta) + if err != nil { + return diag.FromErr(err) + } + + vaultPath := d.Id() + log.Printf("[DEBUG] Unmounting LDAP backend %q", vaultPath) + + err = client.Sys().UnmountWithContext(ctx, vaultPath) + if err != nil { + if util.Is404(err) { + log.Printf("[WARN] %q not found, removing from state", vaultPath) + d.SetId("") + return nil + } + return diag.FromErr(fmt.Errorf("error unmounting LDAP backend from %q: %s", vaultPath, err)) + } + log.Printf("[DEBUG] Unmounted LDAP backend %q", vaultPath) + return nil +} diff --git a/vault/resource_ldap_secret_backend_dynamic_role.go b/vault/resource_ldap_secret_backend_dynamic_role.go new file mode 100644 index 000000000..d3d63cfb2 --- /dev/null +++ b/vault/resource_ldap_secret_backend_dynamic_role.go @@ -0,0 +1,156 @@ +// 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/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/util" +) + +func ldapSecretBackendDynamicRoleResource() *schema.Resource { + fields := map[string]*schema.Schema{ + consts.FieldMount: { + Type: schema.TypeString, + Default: consts.MountTypeLDAP, + Optional: true, + Description: "The path where the LDAP secrets backend is mounted.", + ValidateFunc: provider.ValidateNoLeadingTrailingSlashes, + }, + consts.FieldRoleName: { + Type: schema.TypeString, + Required: true, + Description: "Name of the role.", + ForceNew: true, + }, + consts.FieldCreationLDIF: { + Type: schema.TypeString, + Required: true, + Description: "A templatized LDIF string used to create a user account. May contain multiple entries.", + }, + consts.FieldDeletionLDIF: { + Type: schema.TypeString, + Required: true, + Description: "A templatized LDIF string used to delete the user account once its TTL has expired. This may contain multiple LDIF entries.", + }, + consts.FieldRollbackLDIF: { + Type: schema.TypeString, + Optional: true, + Description: "A templatized LDIF string used to attempt to rollback any changes in the event that execution of the creation_ldif results in an error. This may contain multiple LDIF entries.", + }, + consts.FieldUsernameTemplate: { + Type: schema.TypeString, + Optional: true, + Description: "A template used to generate a dynamic username. This will be used to fill in the .Username field within the creation_ldif string.", + }, + consts.FieldDefaultTTL: { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the TTL for the leases associated with this role.", + }, + consts.FieldMaxTTL: { + Type: schema.TypeString, + Optional: true, + Description: "Specifies the maximum TTL for the leases associated with this role.", + }, + } + return &schema.Resource{ + CreateContext: createUpdateLDAPDynamicRoleResource, + UpdateContext: createUpdateLDAPDynamicRoleResource, + ReadContext: ReadContextWrapper(readLDAPDynamicRoleResource), + DeleteContext: deleteLDAPDynamicRoleResource, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: fields, + } +} + +var ldapSecretBackendDynamicRoleFields = []string{ + consts.FieldCreationLDIF, + consts.FieldDeletionLDIF, + consts.FieldRollbackLDIF, + consts.FieldUsernameTemplate, + consts.FieldDefaultTTL, + consts.FieldMaxTTL, +} + +func createUpdateLDAPDynamicRoleResource(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) + rolePath := fmt.Sprintf("%s/role/%s", mount, role) + log.Printf("[DEBUG] Creating LDAP dynamic role at %q", rolePath) + data := map[string]interface{}{} + for _, field := range ldapSecretBackendDynamicRoleFields { + if v, ok := d.GetOk(field); ok { + data[field] = v + } + } + + if _, err := client.Logical().WriteWithContext(ctx, rolePath, data); err != nil { + return diag.FromErr(fmt.Errorf("error writing %q: %s", rolePath, err)) + } + + d.SetId(rolePath) + log.Printf("[DEBUG] Wrote %q", rolePath) + return readLDAPDynamicRoleResource(ctx, d, meta) +} + +func readLDAPDynamicRoleResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := provider.GetClient(d, meta) + if err != nil { + return diag.FromErr(err) + } + + rolePath := d.Id() + log.Printf("[DEBUG] Reading %q", rolePath) + + resp, err := client.Logical().ReadWithContext(ctx, rolePath) + if resp == nil { + log.Printf("[WARN] %q not found, removing from state", rolePath) + d.SetId("") + return nil + } + + for _, field := range ldapSecretBackendDynamicRoleFields { + if val, ok := resp.Data[field]; ok { + if err := d.Set(field, val); err != nil { + return diag.FromErr(fmt.Errorf("error setting state key '%s': %s", field, err)) + } + } + } + + return nil +} + +func deleteLDAPDynamicRoleResource(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := provider.GetClient(d, meta) + if err != nil { + return diag.FromErr(err) + } + + rolePath := d.Id() + _, err = client.Logical().DeleteWithContext(ctx, rolePath) + if err != nil { + if util.Is404(err) { + d.SetId("") + return nil + } + + return diag.FromErr(fmt.Errorf("error deleting dynamic role %q: %w", rolePath, err)) + } + + return nil +} diff --git a/vault/resource_ldap_secret_backend_dynamic_role_test.go b/vault/resource_ldap_secret_backend_dynamic_role_test.go new file mode 100644 index 000000000..4482ce97a --- /dev/null +++ b/vault/resource_ldap_secret_backend_dynamic_role_test.go @@ -0,0 +1,120 @@ +// 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" +) + +var ( + creationLDIF = `dn: cn={{.Username}},ou=users,dc=example,dc=org +objectClass: person +objectClass: top +cn: learn +sn: {{.Password | utf16le | base64}} +userPassword: {{.Password}}` + deletionLDIF = `dn: cn={{.Username}},ou=users,dc=example,dc=org +changetype: delete` + rollbackLDIF = deletionLDIF +) + +func TestAccLDAPSecretBackendDynamicRole(t *testing.T) { + roleName := acctest.RandomWithPrefix("tf-test-ldap-dynamic-role") + bindDN, bindPass, _ := testutil.GetTestLDAPCreds(t) + resourceType := "vault_ldap_secret_backend_dynamic_role" + resourceName := resourceType + ".role" + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion112) + }, + CheckDestroy: testCheckMountDestroyed(resourceType, consts.MountTypeLDAP, consts.FieldMount), + Steps: []resource.TestStep{ + { + Config: testLDAPSecretBackendDynamicRoleConfig_defaults(roleName, bindDN, bindPass), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldCreationLDIF, creationLDIF+"\n"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDeletionLDIF, deletionLDIF+"\n"), + resource.TestCheckResourceAttr(resourceName, consts.FieldRollbackLDIF, ""), + resource.TestCheckResourceAttr(resourceName, consts.FieldUsernameTemplate, ""), + resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultTTL, "10"), + resource.TestCheckResourceAttr(resourceName, consts.FieldMaxTTL, "20"), + ), + }, + { + Config: testLDAPSecretBackendDynamicRoleConfig(roleName, bindDN, bindPass, "20", "40"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldCreationLDIF, creationLDIF+"\n"), + resource.TestCheckResourceAttr(resourceName, consts.FieldDeletionLDIF, deletionLDIF+"\n"), + resource.TestCheckResourceAttr(resourceName, consts.FieldRollbackLDIF, rollbackLDIF+"\n"), + resource.TestCheckResourceAttr(resourceName, consts.FieldUsernameTemplate, ""), + resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultTTL, "20"), + resource.TestCheckResourceAttr(resourceName, consts.FieldMaxTTL, "40"), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil, consts.FieldMount, consts.FieldRoleName), + }, + }) +} + +// testLDAPSecretBackendDynamicRoleConfig_defaults sets up default and required +// fields. +func testLDAPSecretBackendDynamicRoleConfig_defaults(roleName, bindDN, bindPass string) string { + return fmt.Sprintf(` +resource "vault_ldap_secret_backend" "test" { + description = "test description" + binddn = "%s" + bindpass = "%s" +} + +resource "vault_ldap_secret_backend_dynamic_role" "role" { + mount = vault_ldap_secret_backend.test.path + role_name = "%s" + creation_ldif = < **Note** This data source is replaced by "vault_ldap_static_credentials" and +will be removed in the next major release. + Reads Active Directory credentials from an AD secret backend in Vault. ~> **Important** All data retrieved from Vault will be diff --git a/website/docs/d/ldap_dynamic_role_credentials.html.md b/website/docs/d/ldap_dynamic_role_credentials.html.md new file mode 100644 index 000000000..cdb469604 --- /dev/null +++ b/website/docs/d/ldap_dynamic_role_credentials.html.md @@ -0,0 +1,94 @@ +--- +layout: "vault" +page_title: "Vault: vault_ldap_dynamic_credentials data source" +sidebar_current: "docs-vault-datasource-ldap-dynamic_credentials" +description: |- + Reads dynamic role credentials from an LDAP secret backend in Vault +--- + +# vault\_ldap\_dynamic\_credentials + +Reads dynamic role credentials from an LDAP secret backend in Vault + +~> **Important** All data retrieved from Vault will be +written in cleartext to state file generated by Terraform, will appear in +the console output when Terraform runs, and may be included in plan files +if secrets are interpolated into any resource attributes. +Protect these artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_ldap_secret_backend" "test" { + binddn = "..." + bindpass = "..." + url = "..." +} + +resource "vault_ldap_secret_backend_dynamic_role" "role" { + mount = vault_ldap_secret_backend.test.path + role_name = "%s" + creation_ldif = < **Important** All data retrieved from Vault will be +written in cleartext to state file generated by Terraform, will appear in +the console output when Terraform runs, and may be included in plan files +if secrets are interpolated into any resource attributes. +Protect these artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_ldap_secret_backend" "test" { + binddn = "..." + bindpass = "..." + url = "..." +} + +resource "vault_ldap_secret_backend_static_role" "role" { + mount = vault_ldap_secret_backend.test.path + username = "alice" + role_name = "alice" + rotation_period = 60 +} + +data "vault_ldap_static_credentials" "creds" { + mount = vault_ldap_secret_backend.test.path + role_name = vault_ldap_secret_backend_static_role.role.role_name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace of the target resource. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + *Available only for Vault Enterprise*. + +* `mount` - (Required) The path to the LDAP secret backend to +read credentials from, with no leading or trailing `/`s. + +* `role_name` - (Required) The name of the LDAP secret backend static role to read +credentials from, with no leading or trailing `/`s. + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `dn` - Distinguished name (DN) of the existing LDAP entry to manage password rotation for. + +* `last_vault_rotation` - Last time Vault rotated this static role's password. + +* `password` - The current set password for the static role. + +* `last_password` - The last known password for the static role. + +* `rotation_period` - How often Vault should rotate the password of the user entry. + +* `ttl` - Duration in seconds after which the issued credential should expire. + +* `username` - The name of the static role. diff --git a/website/docs/r/ad_secret_backend.html.md b/website/docs/r/ad_secret_backend.html.md index 9e9d1aead..e3e489422 100644 --- a/website/docs/r/ad_secret_backend.html.md +++ b/website/docs/r/ad_secret_backend.html.md @@ -8,6 +8,9 @@ description: |- # vault\_ad\_secret\_backend +~> **Note** This resource is replaced by "vault_ldap_secret_backend" and will +be removed in the next major release. + Creates an Active Directory Secret Backend for Vault. Active Directory secret backend rotates existing Active Directory service account passwords based on the TTL of the role. diff --git a/website/docs/r/ad_secret_backend_library.html.md b/website/docs/r/ad_secret_backend_library.html.md index 863234834..ab7ae4e4f 100644 --- a/website/docs/r/ad_secret_backend_library.html.md +++ b/website/docs/r/ad_secret_backend_library.html.md @@ -8,6 +8,9 @@ description: |- # vault\_ad\_secret\_backend\_library +~> **Note** This resource is replaced by "vault_ldap_secret_backend_library_set" +and will be removed in the next major release. + Creates a library on an Active Directory Secret Backend for Vault. Libraries create a pool of existing Active Directory service accounts which can be checked out by users. diff --git a/website/docs/r/ad_secret_role.html.md b/website/docs/r/ad_secret_role.html.md index f59816552..d82e64f2d 100644 --- a/website/docs/r/ad_secret_role.html.md +++ b/website/docs/r/ad_secret_role.html.md @@ -8,6 +8,9 @@ description: |- # vault\_ad\_secret\_role +~> **Note** This resource is replaced by "vault_ldap_secret_backend_static_role" +and will be removed in the next major release. + Creates a role on an Active Directory Secret Backend for Vault. Roles are used to map credentials to existing Active Directory service accounts. diff --git a/website/docs/r/ldap_secret_backend.html.md b/website/docs/r/ldap_secret_backend.html.md new file mode 100644 index 000000000..b9efe5c5b --- /dev/null +++ b/website/docs/r/ldap_secret_backend.html.md @@ -0,0 +1,101 @@ +--- +layout: "vault" +page_title: "Vault: vault_ldap_secret_backend resource" +sidebar_current: "docs-vault-resource-ldap-secret-backend" +description: |- + Creates a LDAP secret backend for Vault. +--- + +# vault\_ldap\_secret\_backend + +Creates a LDAP Secret Backend for Vault. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl + +resource "vault_ldap_secret_backend" "config" { + path = "my-custom-ldap" + binddn = "CN=Administrator,CN=Users,DC=corp,DC=example,DC=net" + bindpass = "SuperSecretPassw0rd" + url = "ldaps://localhost" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + *Available only for Vault Enterprise*. + +* `path` - (Optional) The unique path this backend should be mounted at. Must + not begin or end with a `/`. Defaults to `ldap`. + +* `binddn` - (Required) Distinguished name of object to bind when performing user and group search. + +* `bindpass` - (Required) Password to use along with binddn when performing user search. + +* `certificate` - (Optional) CA certificate to use when verifying LDAP server certificate, must be + x509 PEM encoded. + +* `connection_timeout` - (Optional) Timeout, in seconds, when attempting to connect to the LDAP server before trying + the next URL in the configuration. + +* `client_tls_cert` - (Optional) Client certificate to provide to the LDAP server, must be x509 PEM encoded. + +* `client_tls_key` - (Optional) Client certificate key to provide to the LDAP server, must be x509 PEM encoded. + +* `default_lease_ttl_seconds` - (Optional) Default lease duration for secrets in seconds. + +* `description` - (Optional) Human-friendly description of the mount for the Active Directory backend. + +* `insecure_tls` - (Optional) Skip LDAP server SSL Certificate verification. This is not recommended for production. + Defaults to `false`. + +* `length` - (Optional) **Deprecated** use `password_policy`. The desired length of passwords that Vault generates. + *Mutually exclusive with `password_policy` on vault-1.11+* + +* `local` - (Optional) Mark the secrets engine as local-only. Local engines are not replicated or removed by + replication.Tolerance duration to use when checking the last rotation time. + +* `max_lease_ttl_seconds` - (Optional) Maximum possible lease duration for secrets in seconds. + +* `password_policy` - (Optional) Name of the password policy to use to generate passwords. + +* `request_timeout` - (Optional) Timeout, in seconds, for the connection when making requests against the server + before returning back an error. + +* `starttls` - (Optional) Issue a StartTLS command after establishing unencrypted connection. + +* `upndomain` - (Optional) Enables userPrincipalDomain login with [username]@UPNDomain. + +* `url` - (Required) LDAP URL to connect to. Multiple URLs can be specified by concatenating + them with commas; they will be tried in-order. Defaults to `ldap://127.0.0.1`. + +* `userattr` - (Optional) Attribute used when searching users. Defaults to `cn`. + +* `userdn` - (Optional) LDAP domain to use for users (eg: ou=People,dc=example,dc=org)`. + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +LDAP secret backend can be imported using the `${mount}/config`, e.g. + +``` +$ terraform import vault_ldap_secret_backend.config ldap/config +``` diff --git a/website/docs/r/ldap_secret_backend_dynamic_role.html.md b/website/docs/r/ldap_secret_backend_dynamic_role.html.md new file mode 100644 index 000000000..7ed013303 --- /dev/null +++ b/website/docs/r/ldap_secret_backend_dynamic_role.html.md @@ -0,0 +1,110 @@ +--- +layout: "vault" +page_title: "Vault: vault_ldap_secret_backend_dynamic_role resource" +sidebar_current: "docs-vault-resource-ldap-secret-backend-dynamic-role" +description: |- + Creates a dynamic role for the LDAP secret backend for Vault. +--- + +# vault\_ldap\_secret\_backend\_dynamic\_role + +Creates a dynamic role for LDAP Secret Backend for Vault. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl + +resource "vault_ldap_secret_backend" "config" { + path = "my-custom-ldap" + binddn = "CN=Administrator,CN=Users,DC=corp,DC=example,DC=net" + bindpass = "SuperSecretPassw0rd" + url = "ldaps://localhost" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_backend_dynamic_role" "role" { + mount = vault_ldap_secret_backend.config.path + role_name = "alice" + creation_ldif = </dynamic-role/` e.g. + +``` +$ terraform import vault_ldap_secret_backend_dynamic_role.role ldap/role/dynamic-role +``` diff --git a/website/docs/r/ldap_secret_backend_library_set.html.md b/website/docs/r/ldap_secret_backend_library_set.html.md new file mode 100644 index 000000000..5566b4e32 --- /dev/null +++ b/website/docs/r/ldap_secret_backend_library_set.html.md @@ -0,0 +1,77 @@ +--- +layout: "vault" +page_title: "Vault: vault_ldap_secret_backend_library resource" +sidebar_current: "docs-vault-resource-ldap-secret-backend-library" +description: |- + Creates a library on the LDAP Secret Backend for Vault. +--- + +# vault\_ldap\_secret\_backend\_library + +Creates a library on an LDAP Secret Backend for Vault. Libraries create +a pool of existing LDAP service accounts which can be checked out +by users. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_ldap_secret_backend" "config" { + path = "ldap" + binddn = "CN=Administrator,CN=Users,DC=corp,DC=example,DC=net" + bindpass = "SuperSecretPassw0rd" + url = "ldaps://localhost" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_library" "qa" { + mount = vault_ldap_secret_backend.config.path + name = "qa" + service_account_names = ["Bob", "Mary"] + ttl = 60 + disable_check_in_enforcement = true + max_ttl = 120 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + *Available only for Vault Enterprise*. + +* `path` - (Required) The path the LDAP secret backend is mounted at, + with no leading or trailing `/`s. + +* `name` - (Required) The name to identify this set of service accounts. + Must be unique within the backend. + +* `service_account_names` - (Required) Specifies the slice of service accounts mapped to this set. + +* `ttl` - (Optional) The password time-to-live in seconds. Defaults to the configuration + ttl if not provided. + +* `max_ttl` - (Optional) The maximum password time-to-live in seconds. Defaults + to the configuration max_ttl if not provided. + +* `disable_check_in_enforcement` - (Optional) Disable enforcing that service + accounts must be checked in by the entity or client token that checked them + out. Defaults to false. + +## Import + +LDAP secret backend libraries can be imported using the `path`, e.g. + +``` +$ terraform import vault_ldap_secret_backend_library.set ldap/library/bob +``` diff --git a/website/docs/r/ldap_secret_backend_static_role.html.md b/website/docs/r/ldap_secret_backend_static_role.html.md new file mode 100644 index 000000000..36dc2a6a7 --- /dev/null +++ b/website/docs/r/ldap_secret_backend_static_role.html.md @@ -0,0 +1,75 @@ +--- +layout: "vault" +page_title: "Vault: vault_ldap_secret_backend_static_role resource" +sidebar_current: "docs-vault-resource-ldap-secret-backend-static-role" +description: |- + Creates a static role for the LDAP secret backend for Vault. +--- + +# vault\_ldap\_secret\_backend\_static\_role + +Creates a static role for LDAP Secret Backend for Vault. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl + +resource "vault_ldap_secret_backend" "config" { + path = "my-custom-ldap" + binddn = "CN=Administrator,CN=Users,DC=corp,DC=example,DC=net" + bindpass = "SuperSecretPassw0rd" + url = "ldaps://localhost" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_backend_static_role" "role" { + mount = vault_ldap_secret_backend.config.path + username = "alice" + dn = "cn=alice,ou=Users,DC=corp,DC=example,DC=net" + role_name = "alice" + rotation_period = 60 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + *Available only for Vault Enterprise*. + +* `mount` - (Optional) The unique path this backend should be mounted at. Must + not begin or end with a `/`. Defaults to `ldap`. + +* `role_name` - (Required) Name of the role. + +* `username` - (Required) The username of the existing LDAP entry to manage password rotation for. + +* `dn` - (Optional) Distinguished name (DN) of the existing LDAP entry to manage + password rotation for. If given, it will take precedence over `username` for the LDAP + search performed during password rotation. Cannot be modified after creation. + +* `rotation_period` - (Required) How often Vault should rotate the password of the user entry. + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +LDAP secret backend static role can be imported using the full path to the role +of the form: `/static-role/` e.g. + +``` +$ terraform import vault_ldap_secret_backend_static_role.role ldap/static-role/example-role +``` diff --git a/website/vault.erb b/website/vault.erb index 7391aeb24..b9b79ccf9 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -101,6 +101,14 @@ vault_kubernetes_auth_backend_role + > + vault_ldap_dynamic_credentials + + + > + vault_ldap_static_credentials + + > vault_policy_document @@ -421,6 +429,22 @@ vault_ldap_auth_backend_group + > + vault_ldap_secret_backend + + + > + vault_ldap_secret_backend_static_role + + + > + vault_ldap_secret_backend_static_role + + + > + vault_ldap_secret_backend_library_set + + > vault_mongodbatlas_secret_backend