From ec8a058bab70319fa0f17abecb337523a96ca2bf Mon Sep 17 00:00:00 2001 From: "Benjamin M. Hughes" Date: Mon, 15 May 2023 19:33:51 +0100 Subject: [PATCH] Add LDAP secret backup support - Add ldap_secret_backend resource - Add ldap_secret_backend static and dynamic role resources - Add ldap_secret_backend library set resource --- internal/consts/consts.go | 1 + vault/provider.go | 16 + vault/resource_ldap_secret_backend.go | 481 ++++++++++++++++++ ...source_ldap_secret_backend_dynamic_role.go | 230 +++++++++ ...e_ldap_secret_backend_dynamic_role_test.go | 121 +++++ ...esource_ldap_secret_backend_library_set.go | 214 ++++++++ ...ce_ldap_secret_backend_library_set_test.go | 126 +++++ ...esource_ldap_secret_backend_static_role.go | 201 ++++++++ ...ce_ldap_secret_backend_static_role_test.go | 121 +++++ vault/resource_ldap_secret_backend_test.go | 92 ++++ 10 files changed, 1603 insertions(+) create mode 100644 vault/resource_ldap_secret_backend.go create mode 100644 vault/resource_ldap_secret_backend_dynamic_role.go create mode 100644 vault/resource_ldap_secret_backend_dynamic_role_test.go create mode 100644 vault/resource_ldap_secret_backend_library_set.go create mode 100644 vault/resource_ldap_secret_backend_library_set_test.go create mode 100644 vault/resource_ldap_secret_backend_static_role.go create mode 100644 vault/resource_ldap_secret_backend_static_role_test.go create mode 100644 vault/resource_ldap_secret_backend_test.go diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 2fe12efcf4..2cade2ac08 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -276,6 +276,7 @@ const ( MountTypeAD = "ad" MountTypeConsul = "consul" MountTypeTerraform = "terraform" + MountTypeLDAP = "ldap" /* Vault version constants diff --git a/vault/provider.go b/vault/provider.go index a2d643799c..094a800de5 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -831,6 +831,22 @@ var ( Resource: UpdateSchemaResource(managedKeysResource()), PathInventory: []string{"/sys/managed-keys/{type}/{name}"}, }, + "vault_ldap_secret_backend": { + Resource: UpdateSchemaResource(ldapSecretBackendResource()), + PathInventory: []string{"/ldap"}, + }, + "vault_ldap_secret_backend_dynamic_role": { + Resource: UpdateSchemaResource(ldapSecretBackendDynamicRoleResource()), + PathInventory: []string{"/ldap/role/{role_name}"}, + }, + "vault_ldap_secret_backend_static_role": { + Resource: UpdateSchemaResource(ldapSecretBackendStaticRoleResource()), + PathInventory: []string{"/ldap/static-role/{role_name}"}, + }, + "vault_ldap_secret_backend_library_set": { + Resource: UpdateSchemaResource(ldapSecretBackendLibrarySetResource()), + PathInventory: []string{"/ldap/library/{set_name}"}, + }, } ) diff --git a/vault/resource_ldap_secret_backend.go b/vault/resource_ldap_secret_backend.go new file mode 100644 index 0000000000..487e07a85e --- /dev/null +++ b/vault/resource_ldap_secret_backend.go @@ -0,0 +1,481 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/util" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/hashicorp/vault/api" +) + +func ldapSecretBackendResource() *schema.Resource { + return &schema.Resource{ + CreateContext: ldapSecretBackendCreate, + UpdateContext: ldapSecretBackendUpdate, + ReadContext: ReadContextWrapper(ldapSecretBackendRead), + DeleteContext: deleteLdapSecretBackend, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + consts.FieldPath: { + Type: schema.TypeString, + Default: consts.MountTypeLDAP, + Optional: true, + ForceNew: true, + Description: `The mount path for a backend, for example, the path given in "$ vault auth enable -path=ldap ldap".`, + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "anonymous_group_search": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `Use anonymous binds when performing LDAP group searches (if true the initial credentials will still be used for the initial connection test).`, + }, + "binddn": { + Type: schema.TypeString, + Required: true, + Description: `Distinguished name (DN) of object to bind for managing user entries. For example, cn=vault,ou=Users,dc=hashicorp,dc=com.`, + }, + "bindpass": { + Type: schema.TypeString, + Computed: false, + Required: true, + Sensitive: true, + Description: `Password to use along with binddn for managing user entries.`, + }, + "case_sensitive_names": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `If true, case sensitivity will be used when comparing usernames and groups for matching policies.`, + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + Description: `CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded.`, + }, + "deny_null_bind": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `Denies an unauthenticated LDAP bind request if the user's password is empty.`, + }, + "discoverdn": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `Use anonymous bind to discover the bind DN of a user.`, + }, + "groupattr": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `The attribute field name used to perform group search in library management and static roles.`, + }, + "groupdn": { + Type: schema.TypeString, + Optional: true, + Description: `The base DN under which to perform group search in library management and static roles.`, + }, + "groupfilter": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Go template for querying group membership of user.`, + }, + "insecure_tls": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, skips LDAP server SSL certificate verification - insecure, use with caution!`, + }, + "request_timeout": { + Type: schema.TypeInt, + Computed: true, + Optional: true, + Description: `Timeout, in seconds, for the connection when making requests against the server before returning back an error.`, + }, + "schema": { + Type: schema.TypeString, + Required: true, + Description: `The LDAP schema to use when storing entry passwords. Valid schemas include openldap, ad, and racf.`, + ValidateFunc: validation.StringInSlice([]string{"tls10", "tls11", "tls12", "tls13"}, false), + }, + "starttls": { + Type: schema.TypeBool, + Optional: true, + Description: `If true, issues a StartTLS command after establishing an unencrypted connection.`, + }, + "upndomain": { + Type: schema.TypeString, + Optional: true, + Description: ` The domain (userPrincipalDomain) used to construct a UPN string for authentication.`, + }, + "url": { + Type: schema.TypeString, + Required: true, + Description: `Password to use along with binddn for managing user entries.`, + }, + "use_token_groups": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `If true, use the Active Directory tokenGroups constructed attribute of the user to find the group memberships. This will find all security groups including nested ones.`, + }, + "password_policy": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `The name of the password policy to use to generate passwords. Note that this accepts the name of the policy, not the policy itself.`, + }, + "userattr": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `The attribute field name used to perform user search in library management and static roles.`, + }, + "userdn": { + Type: schema.TypeString, + Optional: true, + Description: `The base DN under which to perform user search in library management and static roles.`, + }, + "userfilter": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Go template for LDAP user search filter`, + }, + "username_as_alias": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `If true, sets the alias name to the username`, + }, + + "tls_min_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `Minimum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'.`, + ValidateFunc: validation.StringInSlice([]string{"tls10", "tls11", "tls12", "tls13"}, false), + }, + "tls_max_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `Maximum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'.`, + ValidateFunc: validation.StringInSlice([]string{"tls10", "tls11", "tls12", "tls13"}, false), + }, + + "connection_timeout": { + Type: schema.TypeInt, + Computed: true, + Optional: true, + Description: `Timeout, in seconds, when attempting to connect to the LDAP server before trying the next URL in the configuration.`, + }, + "client_tls_cert": { + Type: schema.TypeString, + Optional: true, + Description: `Client certificate to provide to the LDAP server, must be x509 PEM encoded.`, + }, + "client_tls_key": { + Type: schema.TypeString, + Optional: true, + Description: `Client key to provide to the LDAP server, must be x509 PEM encoded.`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `Mount description.`, + }, + "default_lease_ttl_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Default lease duration for secrets in seconds", + }, + "max_lease_ttl_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum possible lease duration for secrets in seconds.", + }, + "last_bind_password_rotation": { + Type: schema.TypeString, + Computed: true, + Description: "Time the bind password was last rotated.", + }, + }, + } +} + +func ldapSecretBackendCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Get(consts.FieldPath).(string) + + info := &api.MountInput{ + Type: consts.MountTypeLDAP, + Description: d.Get("description").(string), + Local: false, + Config: api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")), + MaxLeaseTTL: fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds")), + }, + } + + log.Printf("[DEBUG] Mounting LDAP backend at %q", path) + if err := client.Sys().Mount(path, info); err != nil { + return diag.FromErr(err) + } + + data := map[string]interface{}{} + configFields := []string{ + "anonymous_group_search", + "binddn", + "bindpass", + "case_sensitive_names", + "certificate", + "deny_null_bind", + "discoverdn", + "groupattr", + "groupdn", + "groupfilter", + "insecure_tls", + "password_policy", + "request_timeout", + "schema", + "starttls", + "tls_max_version", + "tls_min_version", + "upndomain", + "url", + "use_token_groups", + "userattr", + "userdn", + "userfilter", + "username_as_alias", + } + for _, k := range configFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + configPath := path + "/config" + log.Printf("[DEBUG] Writing LDAP configuration to %q", configPath) + + if _, err := client.Logical().Write(configPath, data); err != nil { + return diag.FromErr(err) + } + + d.SetId(path) + + return ldapSecretBackendRead(ctx, d, meta) +} + +func ldapSecretBackendRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + diags := diag.Diagnostics{} + + path := d.Id() + log.Printf("[DEBUG] Reading LDAP backend mount %q from Vault", path) + + mounts, err := client.Sys().ListMounts() + if err != nil { + return diag.Errorf("error reading mount %q: %s", path, err) + } + + // path can have a trailing slash, but doesn't need to have one + // this standardises on having a trailing slash, which is how the + // API always responds. + mount, ok := mounts[strings.Trim(path, "/")+"/"] + if !ok { + log.Printf("[WARN] Mount %q not found, removing from state.", path) + d.SetId("") + return nil + } + + mountConfig, err := client.Sys().MountConfig(path) + if err != nil { + return diag.Errorf("error reading config from Vault: %s", err) + } + if mountConfig == nil { + log.Printf("[WARN] config (%s) not found, removing from state", path) + d.SetId("") + return nil + } + + d.Set(consts.FieldPath, d.Id()) + d.Set("description", mount.Description) + d.Set("default_lease_ttl_seconds", mountConfig.DefaultLeaseTTL) + d.Set("max_lease_ttl_seconds", mountConfig.MaxLeaseTTL) + + configPath := fmt.Sprintf("%s/config", d.Id()) + log.Printf("[DEBUG] Reading %s from Vault", configPath) + + config, err := client.Logical().Read(configPath) + if err != nil { + return diag.Errorf("error reading config from mount %q: %s", path, err) + } + if config != nil { + // log.Printf("[WARN] config (%s) not found, removing from state", path) + // d.SetId("") + // return nil + + configFields := []string{ + "anonymous_group_search", + "binddn", + "case_sensitive_names", + "certificate", + "deny_null_bind", + "discoverdn", + "groupattr", + "groupdn", + "groupfilter", + "insecure_tls", + "last_bind_password_rotation", + "password_policy", + "request_timeout", + "schema", + "starttls", + "tls_max_version", + "tls_min_version", + "upndomain", + "url", + "use_token_groups", + "userattr", + "userdn", + "userfilter", + "username_as_alias", + } + + for _, k := range configFields { + if err := d.Set(k, config.Data[k]); err != nil { + return diag.FromErr(err) + } + } + } + + return diags +} + +func ldapSecretBackendUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + backend := d.Id() + + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + tune := api.MountConfigInput{} + + if d.HasChange("description") { + description := d.Get("description").(string) + tune.Description = &description + } + if d.HasChange("default_lease_ttl_seconds") { + tune.DefaultLeaseTTL = fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds").(int)) + } + if d.HasChange("max_lease_ttl_seconds") { + tune.MaxLeaseTTL = fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds").(int)) + } + + if d.HasChanges("description", "default_lease_ttl_seconds", "max_lease_ttl_seconds") { + err := client.Sys().TuneMount(backend, tune) + if err != nil { + return diag.FromErr(err) + } + } + + path := fmt.Sprintf("%s/config", backend) + data := map[string]interface{}{} + + configFields := []string{ + "anonymous_group_search", + "binddn", + "case_sensitive_names", + "certificate", + "deny_null_bind", + "discoverdn", + "groupattr", + "groupdn", + "groupfilter", + "insecure_tls", + "password_policy", + "request_timeout", + "schema", + "starttls", + "tls_max_version", + "tls_min_version", + "upndomain", + "url", + "use_token_groups", + "userattr", + "userdn", + "userfilter", + "username_as_alias", + } + for _, k := range configFields { + if d.HasChange(k) { + data[k] = d.Get(k) + } + } + data["schema"] = d.Get("schema") // Schema always reverts to openldap if we don't specify it when modifying for some reason + + if len(data) > 0 { + log.Printf("[DEBUG] Updating %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.Errorf("error writing config to mount %q: %s", path, err) + } + log.Printf("[DEBUG] Updated %q", path) + } else { + log.Printf("[DEBUG] Nothing to update for %q", path) + } + + return ldapSecretBackendRead(ctx, d, meta) +} + +func deleteLdapSecretBackend(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := d.Id() + log.Printf("[DEBUG] Unmounting LDAP backend %q", path) + + err := client.Sys().Unmount(path) + if err != nil && util.Is404(err) { + log.Printf("[WARN] %q not found, removing from state", path) + d.SetId("") + return diag.FromErr(err) + } else if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Unmounted LDAP backend %q", path) + + 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 0000000000..dbde5461bf --- /dev/null +++ b/vault/resource_ldap_secret_backend_dynamic_role.go @@ -0,0 +1,230 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func ldapSecretBackendDynamicRoleResource() *schema.Resource { + return &schema.Resource{ + CreateContext: ldapSecretBackendDynamicRoleCreate, + UpdateContext: ldapSecretBackendDynamicRoleUpdate, + ReadContext: ReadContextWrapper(ldapSecretBackendDynamicRoleRead), + DeleteContext: deleteLdapSecretBackendDynamicRole, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + consts.FieldPath: { + Type: schema.TypeString, + Required: true, + Description: `The mount path for a backend, for example, the path given in "$ vault auth enable -path=ldap ldap".`, + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "role_name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the dynamic role.`, + }, + "creation_ldif": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `A templatized LDIF string used to create a user account. This may contain multiple LDIF entries.`, + }, + "deletion_ldif": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `A templatized LDIF string used to delete the user account once its TTL has expired.`, + }, + "rollback_ldif": { + Type: schema.TypeString, + Optional: true, + ForceNew: 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.`, + }, + "username_template": { + Type: schema.TypeString, + Default: "v_{{.DisplayName}}_{{.RoleName}}_{{random 10}}_{{unix_time}}", + 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.`, + }, + "default_ttl": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Specifies the TTL for the leases associated with this role. Accepts duration format strings. Defaults to system/engine default TTL time.`, + StateFunc: func(v interface{}) string { + duration, _ := time.ParseDuration(v.(string)) + return fmt.Sprintf("%.0f", duration.Seconds()) + }, + }, + "max_ttl": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: `Specifies the maximum TTL for the leases associated with this role. Accepts duration format strings.`, + StateFunc: func(v interface{}) string { + duration, _ := time.ParseDuration(v.(string)) + return fmt.Sprintf("%.0f", duration.Seconds()) + }, + }, + }, + } +} + +func ldapSecretBackendDynamicRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role_name := d.Get("role_name").(string) + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Creating dynamic role %q on LDAP backend %q", role_name, mountPath) + + path := mountPath + "/role/" + role_name + + data := map[string]interface{}{} + configFields := []string{ + "role_name", + "creation_ldif", + "deletion_ldif", + "rollback_ldif", + "username_template", + "default_ttl", + "max_ttl", + } + for _, k := range configFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + log.Printf("[DEBUG] Writing dynamic role %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.FromErr(err) + } + + d.SetId(role_name) + + return ldapSecretBackendDynamicRoleRead(ctx, d, meta) +} + +func ldapSecretBackendDynamicRoleRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + diags := diag.Diagnostics{} + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Reading dynamic role %q from LDAP backend %q", role_name, mountPath) + + path := mountPath + "/role/" + role_name + config, err := client.Logical().Read(path) + if err != nil { + return diag.Errorf("error reading dynamic role from %q: %s", path, err) + } + if config == nil { + log.Printf("[WARN] config (%q) not found, removing from state", path) + d.SetId("") + return nil + } + + configFields := []string{ + "role_name", + "creation_ldif", + "deletion_ldif", + "rollback_ldif", + "username_template", + "default_ttl", + "max_ttl", + } + for _, k := range configFields { + if err := d.Set(k, config.Data[k]); err != nil { + return diag.FromErr(err) + } + } + + return diags +} + +func ldapSecretBackendDynamicRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Updating dynamic role %q for LDAP backend %q", role_name, mountPath) + + path := mountPath + "/role/" + role_name + data := map[string]interface{}{} + + configFields := []string{ + "role_name", + "creation_ldif", + "deletion_ldif", + "rollback_ldif", + "username_template", + "default_ttl", + "max_ttl", + } + for _, k := range configFields { + if d.HasChange(k) { + data[k] = d.Get(k) + } + } + + if len(data) > 0 { + log.Printf("[DEBUG] Updating %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.Errorf("error writing config to dynamic role %q: %s", path, err) + } + log.Printf("[DEBUG] Updated %q", path) + } else { + log.Printf("[DEBUG] Nothing to update for %q", path) + } + + return ldapSecretBackendDynamicRoleRead(ctx, d, meta) +} + +func deleteLdapSecretBackendDynamicRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Deleting dynamic role %q from LDAP backend %q", role_name, mountPath) + + path := mountPath + "/role/" + role_name + if _, err := client.Logical().Delete(path); err != nil { + return diag.Errorf("error deleting role %q from mount %q: %s", role_name, mountPath, err) + } + + log.Printf("[DEBUG] Deleted dynamic role %q from LDAP backend %q", role_name, mountPath) + + 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 0000000000..48961ac6d3 --- /dev/null +++ b/vault/resource_ldap_secret_backend_dynamic_role_test.go @@ -0,0 +1,121 @@ +// 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-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAccLdapSecretBackendRole_basic(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-ad") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccLdapSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendRoleConfig(path, bindDN, bindPass, url, "bob", "Bob", 60), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vault_ldap_secret_backend_dynamic_role.role", "password_last_set"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "role", "bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "service_account_name", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "ttl", "60"), + ), + }, + { + Config: testLdapSecretBackendRoleConfig(path, bindDN, bindPass, url, "bob", "Bob", 120), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vault_ldap_secret_backend_dynamic_role.role", "password_last_set"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "role", "bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "service_account_name", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "ttl", "120"), + ), + }, + }, + }) +} + +func TestAccLdapSecretBackendRole_import(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-ad") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + role := "bob" + serviceAccountName := "Bob" + ttl := 60 + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccLdapSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendRoleConfig(backend, bindDN, bindPass, url, role, serviceAccountName, ttl), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "role", role), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "service_account_name", serviceAccountName), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_dynamic_role.role", "ttl", fmt.Sprintf("%d", ttl)), + ), + }, + { + ResourceName: "vault_ldap_secret_backend_dynamic_role.role", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccLdapSecretBackendRoleCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_ldap_secret_backend_dynamic_role" { + continue + } + + client, e := provider.GetClient(rs.Primary, testProvider.Meta()) + if e != nil { + return e + } + + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return err + } + if secret != nil { + return fmt.Errorf("role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testLdapSecretBackendRoleConfig(path, bindDN, bindPass, url, role, serviceAccountName string, ttl int) string { + return fmt.Sprintf(` +resource "vault_ad_secret_backend" "config" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + binddn = "%s" + bindpass = "%s" + url = "%s" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_backend_dynamic_role" "role" { + path = vault_ad_secret_backend.config.backend + role = "%s" + service_account_name = "%s" + ttl = %d +} +`, path, bindDN, bindPass, url, role, serviceAccountName, ttl) +} diff --git a/vault/resource_ldap_secret_backend_library_set.go b/vault/resource_ldap_secret_backend_library_set.go new file mode 100644 index 0000000000..641fd64352 --- /dev/null +++ b/vault/resource_ldap_secret_backend_library_set.go @@ -0,0 +1,214 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func ldapSecretBackendLibrarySetResource() *schema.Resource { + return &schema.Resource{ + CreateContext: ldapSecretBackendLibrarySetCreate, + UpdateContext: ldapSecretBackendLibrarySetUpdate, + ReadContext: ReadContextWrapper(ldapSecretBackendLibrarySetRead), + DeleteContext: deleteLdapSecretBackendLibrarySet, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + consts.FieldPath: { + Type: schema.TypeString, + Required: true, + Description: `The mount path for a backend, for example, the path given in "$ vault auth enable -path=ldap ldap".`, + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "set_name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the set of service accounts.`, + }, + "service_account_names": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + Description: `The names of all the service accounts that can be checked out from this set.`, + }, + "ttl": { + Type: schema.TypeString, + Computed: true, + Optional: true, + StateFunc: func(v interface{}) string { + duration, _ := time.ParseDuration(v.(string)) + return fmt.Sprintf("%.0f", duration.Seconds()) + }, + Description: `The maximum amount of time a single check-out lasts before Vault automatically checks it back in.`, + }, + "max_ttl": { + Type: schema.TypeString, + Computed: true, + Optional: true, + StateFunc: func(v interface{}) string { + duration, _ := time.ParseDuration(v.(string)) + return fmt.Sprintf("%.0f", duration.Seconds()) + }, + Description: `Specifies the maximum TTL for the leases associated with this role. Accepts duration format strings.`, + }, + "disable_check_in_enforcement": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `Specifies the maximum TTL for the leases associated with this role. Accepts duration format strings.`, + }, + }, + } +} + +func ldapSecretBackendLibrarySetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + set_name := d.Get("set_name").(string) + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Creating library set %q on LDAP backend %q", set_name, mountPath) + + path := mountPath + "/library/" + set_name + + data := map[string]interface{}{} + data["name"] = set_name + if v, ok := d.GetOk("service_account_names"); ok { + data["service_account_names"] = v.(*schema.Set).List() + } + configFields := []string{ + "ttl", + "max_ttl", + "disable_check_in_enforcement", + } + for _, k := range configFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + log.Printf("[DEBUG] Writing dynamic role %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.FromErr(err) + } + + d.SetId(set_name) + + return ldapSecretBackendLibrarySetRead(ctx, d, meta) +} + +func ldapSecretBackendLibrarySetRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + diags := diag.Diagnostics{} + + set_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Reading library set %q from LDAP backend %q", set_name, mountPath) + + path := mountPath + `/library/` + set_name + config, err := client.Logical().Read(path) + if err != nil { + return diag.Errorf("error reading library set from %q: %s", path, err) + } + if config == nil { + log.Printf("[WARN] config (%q) not found, removing from state", path) + d.SetId("") + return nil + } + + configFields := []string{ + "service_account_names", + "ttl", + "max_ttl", + "disable_check_in_enforcement", + } + for _, k := range configFields { + if err := d.Set(k, config.Data[k]); err != nil { + return diag.FromErr(err) + } + } + + return diags +} + +func ldapSecretBackendLibrarySetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + set_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Updating library set %q for LDAP backend %q", set_name, mountPath) + + path := mountPath + `/library/` + set_name + data := map[string]interface{}{} + + configFields := []string{ + "service_account_names", + "ttl", + "max_ttl", + "disable_check_in_enforcement", + } + for _, k := range configFields { + if d.HasChange(k) { + data[k] = d.Get(k) + } + } + + if len(data) > 0 { + log.Printf("[DEBUG] Updating %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.Errorf("error writing config to library set %q: %s", path, err) + } + log.Printf("[DEBUG] Updated %q", path) + } else { + log.Printf("[DEBUG] Nothing to update for %q", path) + } + + return ldapSecretBackendLibrarySetRead(ctx, d, meta) +} + +func deleteLdapSecretBackendLibrarySet(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + set_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Deleting dynamic role %q from LDAP backend %q", set_name, mountPath) + + path := mountPath + `/library/` + set_name + if _, err := client.Logical().Delete(path); err != nil { + return diag.Errorf("error deleting role %q from mount %q: %s", set_name, mountPath, err) + } + + log.Printf("[DEBUG] Deleted dynamic role %q from LDAP backend %q", set_name, mountPath) + + return nil +} diff --git a/vault/resource_ldap_secret_backend_library_set_test.go b/vault/resource_ldap_secret_backend_library_set_test.go new file mode 100644 index 0000000000..1404827c1f --- /dev/null +++ b/vault/resource_ldap_secret_backend_library_set_test.go @@ -0,0 +1,126 @@ +// 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-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAccLdapSecretBackendLibrarySet_basic(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-ldap") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccLdapSecretBackendLibrarySetCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendLibrarySetConfig(path, bindDN, bindPass, url, "qa", `"Bob","Mary"`, 60, 120, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "disable_check_in_enforcement", "false"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "max_ttl", "120"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.0", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.1", "Mary"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.#", "2"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "ttl", "60"), + ), + }, + { + Config: testADSecretBackendLibraryConfig(path, bindDN, bindPass, url, "qa", `"Bob"`, 120, 240, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "disable_check_in_enforcement", "true"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "max_ttl", "240"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.0", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.#", "1"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "ttl", "120"), + ), + }, + }, + }) +} + +func TestAccLdapSecretBackendLibrarySet_import(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-ldap") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccADSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendLibrarySetConfig(backend, bindDN, bindPass, url, "qa", `"Bob","Mary"`, 60, 120, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "disable_check_in_enforcement", "false"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "max_ttl", "120"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.0", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.1", "Mary"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "service_account_names.#", "2"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_library_set.test", "ttl", "60"), + ), + }, + { + ResourceName: "vault_ldap_secret_backend_library_set.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccLdapSecretBackendLibrarySetCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_ldap_secret_backend_library_set" { + continue + } + + client, e := provider.GetClient(rs.Primary, testProvider.Meta()) + if e != nil { + return e + } + + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return err + } + if secret != nil { + return fmt.Errorf("library %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testLdapSecretBackendLibrarySetConfig(path, bindDN, bindPass, url, name, serviceAccountNames string, ttl, maxTTL int, disable bool) string { + return fmt.Sprintf(` +resource "vault_ldap_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + binddn = "%s" + bindpass = "%s" + url = "%s" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_backend_library_set" "test" { + backend = vault_ldap_secret_backend.path + name = "%s" + service_account_names = [%s] + ttl = %d + max_ttl = %d + disable_check_in_enforcement = %t +} +`, path, bindDN, bindPass, url, name, serviceAccountNames, ttl, maxTTL, disable) +} diff --git a/vault/resource_ldap_secret_backend_static_role.go b/vault/resource_ldap_secret_backend_static_role.go new file mode 100644 index 0000000000..2ed3cacb6c --- /dev/null +++ b/vault/resource_ldap_secret_backend_static_role.go @@ -0,0 +1,201 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func ldapSecretBackendStaticRoleResource() *schema.Resource { + return &schema.Resource{ + CreateContext: ldapSecretBackendStaticRoleCreate, + UpdateContext: ldapSecretBackendStaticRoleUpdate, + ReadContext: ReadContextWrapper(ldapSecretBackendStaticRoleRead), + DeleteContext: deleteLdapSecretBackendStaticRole, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + consts.FieldPath: { + Type: schema.TypeString, + Required: true, + Description: `The mount path for a backend, for example, the path given in "$ vault auth enable -path=ldap ldap".`, + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "role_name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the dynamic role.`, + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The username of the existing LDAP entry to manage password rotation for.`, + }, + "dn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: `Distinguished name (DN) of the existing LDAP entry to manage password rotation for.`, + }, + "rotation_period": { + Type: schema.TypeString, + Required: true, + Description: `How often Vault should rotate the password of the user entry.`, + StateFunc: func(v interface{}) string { + duration, _ := time.ParseDuration(v.(string)) + return fmt.Sprintf("%.0f", duration.Seconds()) + }, + }, + "last_vault_rotation": { + Type: schema.TypeString, + Computed: true, + Description: ``, + }, + }, + } +} + +func ldapSecretBackendStaticRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role := d.Get("role_name").(string) + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Creating static role %q on LDAP backend %q", role, mountPath) + + path := mountPath + "/static-role/" + role + + data := map[string]interface{}{} + configFields := []string{ + "username", + "dn", + "rotation_period", + } + for _, k := range configFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + log.Printf("[DEBUG] Writing static role %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.FromErr(err) + } + + d.SetId(role) + + return ldapSecretBackendStaticRoleRead(ctx, d, meta) +} + +func ldapSecretBackendStaticRoleRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + diags := diag.Diagnostics{} + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Reading static role %q from LDAP backend %q", role_name, mountPath) + + path := mountPath + "/static-role/" + role_name + config, err := client.Logical().Read(path) + if err != nil { + return diag.Errorf("error reading static role from %q: %s", path, err) + } + if config == nil { + log.Printf("[WARN] config (%q) not found, removing from state", path) + d.SetId("") + return nil + } + + configFields := []string{ + "username", + "dn", + "rotation_period", + "last_vault_rotation", + } + for _, k := range configFields { + if err := d.Set(k, config.Data[k]); err != nil { + return diag.FromErr(err) + } + } + + return diags +} + +func ldapSecretBackendStaticRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Updating static role %q for LDAP backend %q", role_name, mountPath) + + path := mountPath + "/static-role/" + role_name + data := map[string]interface{}{} + + configFields := []string{ + "username", + "dn", + "rotation_period", + } + for _, k := range configFields { + if d.HasChange(k) { + data[k] = d.Get(k) + } + } + + if len(data) > 0 { + log.Printf("[DEBUG] Updating %q", path) + + if _, err := client.Logical().Write(path, data); err != nil { + return diag.Errorf("error writing config to static role %q: %s", path, err) + } + log.Printf("[DEBUG] Updated %q", path) + } else { + log.Printf("[DEBUG] Nothing to update for %q", path) + } + + return ldapSecretBackendStaticRoleRead(ctx, d, meta) +} + +func deleteLdapSecretBackendStaticRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + role_name := d.Id() + mountPath := d.Get(consts.FieldPath).(string) + log.Printf("[DEBUG] Deleting static role %q from LDAP backend %q", role_name, mountPath) + + path := mountPath + "/static-role/" + role_name + if _, err := client.Logical().Delete(path); err != nil { + return diag.Errorf("error deleting role %q from mount %q: %s", role_name, mountPath, err) + } + + log.Printf("[DEBUG] Deleted static role %q from LDAP backend %q", role_name, mountPath) + + return nil +} diff --git a/vault/resource_ldap_secret_backend_static_role_test.go b/vault/resource_ldap_secret_backend_static_role_test.go new file mode 100644 index 0000000000..036c2778b9 --- /dev/null +++ b/vault/resource_ldap_secret_backend_static_role_test.go @@ -0,0 +1,121 @@ +// 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-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAccLdapSecretBackendStaticRole_basic(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-ldap") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccLdapSecretBackendStaticRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendStaticRoleConfig(path, bindDN, bindPass, url, "bob", "Bob", 60), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vault_ldap_secret_backend_static_role.role", "password_last_set"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "role", "bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "service_account_name", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "ttl", "60"), + ), + }, + { + Config: testLdapSecretBackendStaticRoleConfig(path, bindDN, bindPass, url, "bob", "Bob", 120), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("vault_ldap_secret_backend_static_role.role", "password_last_set"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "role", "bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "service_account_name", "Bob"), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "ttl", "120"), + ), + }, + }, + }) +} + +func TestAccLdapSecretBackendStaticRole_import(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-ldap") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + role := "bob" + serviceAccountName := "Bob" + ttl := 60 + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testAccLdapSecretBackendStaticRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackendStaticRoleConfig(backend, bindDN, bindPass, url, role, serviceAccountName, ttl), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "role", role), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "service_account_name", serviceAccountName), + resource.TestCheckResourceAttr("vault_ldap_secret_backend_static_role.role", "ttl", fmt.Sprintf("%d", ttl)), + ), + }, + { + ResourceName: "vault_ldap_secret_backend_static_role.role", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccLdapSecretBackendStaticRoleCheckDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_ldap_secret_backend_static_role" { + continue + } + + client, e := provider.GetClient(rs.Primary, testProvider.Meta()) + if e != nil { + return e + } + + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return err + } + if secret != nil { + return fmt.Errorf("role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testLdapSecretBackendStaticRoleConfig(path, bindDN, bindPass, url, role, serviceAccountName string, ttl int) string { + return fmt.Sprintf(` +resource "vault_ldap_secret_backend" "config" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + binddn = "%s" + bindpass = "%s" + url = "%s" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} + +resource "vault_ldap_secret_backend_static_role" "role" { + backend = vault_ad_secret_backend.path + role = "%s" + service_account_name = "%s" + ttl = %d +} +`, path, bindDN, bindPass, url, role, serviceAccountName, ttl) +} diff --git a/vault/resource_ldap_secret_backend_test.go b/vault/resource_ldap_secret_backend_test.go new file mode 100644 index 0000000000..d80ef10abd --- /dev/null +++ b/vault/resource_ldap_secret_backend_test.go @@ -0,0 +1,92 @@ +// 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/testutil" +) + +func TestAccLdapSecretBackend(t *testing.T) { + path := acctest.RandomWithPrefix("tf-test-ad") + bindDN, bindPass, url := testutil.GetTestADCreds(t) + + resourceType := "vault_ldap_secret_backend.test" + resourceName := resourceType + ".test" + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testutil.TestAccPreCheck(t) }, + CheckDestroy: testCheckMountDestroyed(resourceType, consts.MountTypeLDAP, consts.FieldPath), + Steps: []resource.TestStep{ + { + Config: testLdapSecretBackend_createConfig(path, bindDN, bindPass, url), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldPath, path), + resource.TestCheckResourceAttr(resourceName, "description", "test description"), + resource.TestCheckResourceAttr(resourceName, "default_lease_ttl_seconds", "3600"), + resource.TestCheckResourceAttr(resourceName, "max_lease_ttl_seconds", "7200"), + resource.TestCheckResourceAttr(resourceName, "binddn", bindDN), + resource.TestCheckResourceAttr(resourceName, "bindpass", bindPass), + resource.TestCheckResourceAttr(resourceName, "url", url), + resource.TestCheckResourceAttr(resourceName, "insecure_tls", "true"), + resource.TestCheckResourceAttr(resourceName, "userdn", "CN=Users,DC=corp,DC=example,DC=net"), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil, "bindpass", "description", "disable_remount"), + { + Config: testLdapSecretBackend_updateConfig(path, bindDN, bindPass, url), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, consts.FieldPath, path), + resource.TestCheckResourceAttr(resourceName, "description", "test description"), + resource.TestCheckResourceAttr(resourceName, "default_lease_ttl_seconds", "7200"), + resource.TestCheckResourceAttr(resourceName, "max_lease_ttl_seconds", "14400"), + resource.TestCheckResourceAttr(resourceName, "binddn", bindDN), + resource.TestCheckResourceAttr(resourceName, "bindpass", bindPass), + resource.TestCheckResourceAttr(resourceName, "url", url), + resource.TestCheckResourceAttr(resourceName, "insecure_tls", "false"), + resource.TestCheckResourceAttr(resourceName, "userdn", "CN=Users,DC=corp,DC=hashicorp,DC=com"), + ), + }, + }, + }) +} + +func testLdapSecretBackend_createConfig(path, bindDN, bindPass, url string) string { + return fmt.Sprintf(` +resource "vault_ldap_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + binddn = "%s" + bindpass = "%s" + url = "%s" + insecure_tls = "true" + userdn = "CN=Users,DC=corp,DC=example,DC=net" +} +`, path, bindDN, bindPass, url) +} + +func testLdapSecretBackend_updateConfig(path, bindDN, bindPass, url string) string { + return fmt.Sprintf(` +resource "vault_ad_secret_backend" "test" { + path = "%s" + description = "test description" + default_lease_ttl_seconds = "7200" + max_lease_ttl_seconds = "14400" + binddn = "%s" + bindpass = "%s" + url = "%s" + insecure_tls = "false" + userdn = "CN=Users,DC=corp,DC=hashicorp,DC=com" +} +`, path, bindDN, bindPass, url) +}