From ffc88e48e471fef46c6a50843031e858885dada1 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney <109917432+anGaffney@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:36:20 -0500 Subject: [PATCH] feat(provider): Implement Vault Ldap Resource (#778) --- .../credential_library_vault_ldap.md | 79 +++++++ .../import.sh | 1 + .../resource.tf | 36 ++++ internal/provider/provider.go | 1 + .../resource_credential_library_vault_ldap.go | 204 ++++++++++++++++++ ...urce_credential_library_vault_ldap_test.go | 90 ++++++++ .../resource_credential_library_vault_test.go | 1 + 7 files changed, 412 insertions(+) create mode 100644 docs/resources/credential_library_vault_ldap.md create mode 100644 examples/resources/boundary_credential_library_vault_ldap/import.sh create mode 100644 examples/resources/boundary_credential_library_vault_ldap/resource.tf create mode 100644 internal/provider/resource_credential_library_vault_ldap.go create mode 100644 internal/provider/resource_credential_library_vault_ldap_test.go diff --git a/docs/resources/credential_library_vault_ldap.md b/docs/resources/credential_library_vault_ldap.md new file mode 100644 index 00000000..b420f58c --- /dev/null +++ b/docs/resources/credential_library_vault_ldap.md @@ -0,0 +1,79 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_credential_library_vault_ldap Resource - terraform-provider-boundary" +subcategory: "" +description: |- + The credential library for Vault LDAP resource allows you to configure a Boundary credential library for Vault LDAP. +--- + +# boundary_credential_library_vault_ldap (Resource) + +The credential library for Vault LDAP resource allows you to configure a Boundary credential library for Vault LDAP. + +## Example Usage + +```terraform +resource "boundary_scope" "org" { + name = "organization_one" + description = "My first scope!" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_vault" "foo" { + name = "foo" + description = "My first Vault credential store!" + address = "http://127.0.0.1:8200" # change to Vault address + token = "s.0ufRo6XEGU2jOqnIr7OlFYP5" # change to valid Vault token + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_library_vault_ldap" "foo-static" { + name = "foo" + description = "My first static Vault LDAP credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ldap/static-cred/foo" # change to Vault backend path +} + +resource "boundary_credential_library_vault_ldap" "bar-dynamic" { + name = "bar" + description = "My first dynamic Vault LDAP credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ldap/creds/bar" # change to Vault backend path +} +``` + + +## Schema + +### Required + +- `credential_store_id` (String) The ID of the credential store that this library belongs to. +- `path` (String) The path in Vault to request credentials from. + +### Optional + +- `description` (String) The Vault LDAP credential library description. +- `name` (String) The Vault LDAP credential library name. Defaults to the resource name. + +### Read-Only + +- `id` (String) The ID of the Vault LDAP credential library. + +## Import + +Import is supported using the following syntax: + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +```shell +terraform import boundary_credential_library_vault_ldap.foo +``` diff --git a/examples/resources/boundary_credential_library_vault_ldap/import.sh b/examples/resources/boundary_credential_library_vault_ldap/import.sh new file mode 100644 index 00000000..b494f936 --- /dev/null +++ b/examples/resources/boundary_credential_library_vault_ldap/import.sh @@ -0,0 +1 @@ +terraform import boundary_credential_library_vault_ldap.foo diff --git a/examples/resources/boundary_credential_library_vault_ldap/resource.tf b/examples/resources/boundary_credential_library_vault_ldap/resource.tf new file mode 100644 index 00000000..29ebac12 --- /dev/null +++ b/examples/resources/boundary_credential_library_vault_ldap/resource.tf @@ -0,0 +1,36 @@ +resource "boundary_scope" "org" { + name = "organization_one" + description = "My first scope!" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_vault" "foo" { + name = "foo" + description = "My first Vault credential store!" + address = "http://127.0.0.1:8200" # change to Vault address + token = "s.0ufRo6XEGU2jOqnIr7OlFYP5" # change to valid Vault token + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_library_vault_ldap" "foo-static" { + name = "foo" + description = "My first static Vault LDAP credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ldap/static-cred/foo" # change to Vault backend path +} + +resource "boundary_credential_library_vault_ldap" "bar-dynamic" { + name = "bar" + description = "My first dynamic Vault LDAP credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ldap/creds/bar" # change to Vault backend path +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dd9b3832..71910830 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -105,6 +105,7 @@ func New() *schema.Provider { "boundary_auth_method_ldap": resourceAuthMethodLdap(), "boundary_credential_library_vault": resourceCredentialLibraryVault(), "boundary_credential_library_vault_ssh_certificate": resourceCredentialLibraryVaultSshCertificate(), + "boundary_credential_library_vault_ldap": resourceCredentialLibraryVaultLdap(), "boundary_credential_store_vault": resourceCredentialStoreVault(), "boundary_credential_store_static": resourceCredentialStoreStatic(), "boundary_credential_username_password": resourceCredentialUsernamePassword(), diff --git a/internal/provider/resource_credential_library_vault_ldap.go b/internal/provider/resource_credential_library_vault_ldap.go new file mode 100644 index 00000000..50eb9d34 --- /dev/null +++ b/internal/provider/resource_credential_library_vault_ldap.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + credentialLibraryVaultLdapType = "vault-ldap" + credentialLibraryVaultLdapPathKey = "path" +) + +var libraryVaultLdapAttrs = []string{ + credentialLibraryVaultLdapPathKey, +} + +func resourceCredentialLibraryVaultLdap() *schema.Resource { + return &schema.Resource{ + Description: "The credential library for Vault LDAP resource allows you to configure a Boundary credential library for Vault LDAP.", + + CreateContext: resourceCredentialLibraryCreateVaultLdap, + ReadContext: resourceCredentialLibraryReadVaultLdap, + UpdateContext: resourceCredentialLibraryUpdateVaultLdap, + DeleteContext: resourceCredentialLibraryDeleteVaultLdap, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the Vault LDAP credential library.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The Vault LDAP credential library name. Defaults to the resource name.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The Vault LDAP credential library description.", + Type: schema.TypeString, + Optional: true, + }, + credentialStoreIdKey: { + Description: "The ID of the credential store that this library belongs to.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + credentialLibraryVaultLdapPathKey: { + Description: "The path in Vault to request credentials from.", + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func setFromVaultLdapCredentialLibraryResponseMap(d *schema.ResourceData, raw map[string]interface{}) error { + if err := d.Set(NameKey, raw[NameKey]); err != nil { + return err + } + if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil { + return err + } + if err := d.Set(credentialStoreIdKey, raw[credentialStoreIdKey]); err != nil { + return err + } + + if attrsVal, ok := raw["attributes"]; ok { + attrs := attrsVal.(map[string]interface{}) + for _, v := range libraryVaultLdapAttrs { + if err := d.Set(v, attrs[v]); err != nil { + return err + } + } + } + + d.SetId(raw["id"].(string)) + + return nil +} + +func resourceCredentialLibraryCreateVaultLdap(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + + var opts []credentiallibraries.Option + if v, ok := d.GetOk(NameKey); ok { + opts = append(opts, credentiallibraries.WithName(v.(string))) + } + if v, ok := d.GetOk(DescriptionKey); ok { + opts = append(opts, credentiallibraries.WithDescription(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultLdapPathKey); ok { + opts = append(opts, credentiallibraries.WithVaultLdapCredentialLibraryPath(v.(string))) + } + + var credentialStoreId string + cid, ok := d.GetOk(credentialStoreIdKey) + if ok { + credentialStoreId = cid.(string) + } else { + return diag.Errorf("no credential store ID is set") + } + + client := credentiallibraries.NewClient(md.client) + cr, err := client.Create(ctx, credentialLibraryVaultLdapType, credentialStoreId, opts...) + if err != nil { + return diag.Errorf("error creating credential library: %v", err) + } + if cr == nil { + return diag.Errorf("nil credential library after create") + } + + if err := setFromVaultLdapCredentialLibraryResponseMap(d, cr.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceCredentialLibraryReadVaultLdap(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + cr, err := client.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error reading credential library: %v", err) + } + if cr == nil { + return diag.Errorf("credential library nil after read") + } + + if err := setFromVaultLdapCredentialLibraryResponseMap(d, cr.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceCredentialLibraryUpdateVaultLdap(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + var opts []credentiallibraries.Option + if d.HasChange(NameKey) { + opts = append(opts, credentiallibraries.DefaultName()) + nameVal, ok := d.GetOk(NameKey) + if ok { + opts = append(opts, credentiallibraries.WithName(nameVal.(string))) + } + } + if d.HasChange(DescriptionKey) { + opts = append(opts, credentiallibraries.DefaultDescription()) + descVal, ok := d.GetOk(DescriptionKey) + if ok { + opts = append(opts, credentiallibraries.WithDescription(descVal.(string))) + } + } + if d.HasChange(credentialLibraryVaultLdapPathKey) { + v, ok := d.GetOk(credentialLibraryVaultLdapPathKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultLdapCredentialLibraryPath(v.(string))) + } + } + if len(opts) > 0 { + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + aur, err := client.Update(ctx, d.Id(), 0, opts...) + if err != nil { + return diag.Errorf("error updating credential library: %v", err) + } + + if err := setFromVaultLdapCredentialLibraryResponseMap(d, aur.GetResponse().Map); err != nil { + return diag.Errorf("error setting credential library from response: %v", err) + } + } + + return nil +} + +func resourceCredentialLibraryDeleteVaultLdap(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + _, err := client.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting credential library: %v", err) + } + + return nil +} diff --git a/internal/provider/resource_credential_library_vault_ldap_test.go b/internal/provider/resource_credential_library_vault_ldap_test.go new file mode 100644 index 00000000..622f94bb --- /dev/null +++ b/internal/provider/resource_credential_library_vault_ldap_test.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/boundary/testing/vault" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + vaultLdapCredResc = "boundary_credential_library_vault_ldap.example" + vaultLdapCredLibName = "foo" + vaultLdapCredLibDesc = "the foo" + vaultLdapCredLibPath = "/ldap/static-cred/foo" + vaultLdapCredLibStringUpdate = "_random" +) + +var vaultLdapCredLibResource = fmt.Sprintf(` +resource "boundary_credential_library_vault_ldap" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" +}`, vaultLdapCredLibName, + vaultLdapCredLibDesc, + vaultLdapCredLibPath) + +var vaultLdapCredLibResourceUpdate = fmt.Sprintf(` +resource "boundary_credential_library_vault_ldap" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" +}`, vaultLdapCredLibName+vaultLdapCredLibStringUpdate, + vaultLdapCredLibDesc+vaultLdapCredLibStringUpdate, + vaultLdapCredLibPath+vaultLdapCredLibStringUpdate) + +func TestAccCredentialLibraryVaultLdap(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + vc := vault.NewTestVaultServer(t) + _, token := vc.CreateToken(t) + credStoreRes := vaultCredStoreResource(vc, + vaultCredStoreName, + vaultCredStoreDesc, + vaultCredStoreNamespace, + "www.original.com", + token, + true) + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialLibraryVaultResourceDestroy(t, provider, ldapVaultCredentialLibraryType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultLdapCredLibResource), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultLdapCredResc, NameKey, vaultLdapCredLibName), + resource.TestCheckResourceAttr(vaultLdapCredResc, DescriptionKey, vaultLdapCredLibDesc), + resource.TestCheckResourceAttr(vaultLdapCredResc, credentialLibraryVaultLdapPathKey, vaultLdapCredLibPath), + + testAccCheckCredentialLibraryResourceExists(provider, vaultLdapCredResc), + ), + }, + importStep(vaultLdapCredResc), + { + // update + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultLdapCredLibResourceUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultLdapCredResc, NameKey, vaultLdapCredLibName+vaultLdapCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultLdapCredResc, DescriptionKey, vaultLdapCredLibDesc+vaultLdapCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultLdapCredResc, credentialLibraryVaultLdapPathKey, vaultLdapCredLibPath+vaultLdapCredLibStringUpdate), + testAccCheckCredentialLibraryResourceExists(provider, vaultLdapCredResc), + ), + }, + importStep(vaultLdapCredResc), + }, + }) +} diff --git a/internal/provider/resource_credential_library_vault_test.go b/internal/provider/resource_credential_library_vault_test.go index 5205f211..d122e336 100644 --- a/internal/provider/resource_credential_library_vault_test.go +++ b/internal/provider/resource_credential_library_vault_test.go @@ -302,6 +302,7 @@ type vaultCredentialLibraryType string const ( baseVaultCredentialLibraryType vaultCredentialLibraryType = "boundary_credential_library_vault" sshCertVaultCredentialLibraryType vaultCredentialLibraryType = "boundary_credential_library_vault_ssh_certificate" + ldapVaultCredentialLibraryType vaultCredentialLibraryType = "boundary_credential_library_vault_ldap" ) func testAccCheckCredentialLibraryVaultResourceDestroy(t *testing.T, testProvider *schema.Provider, typ vaultCredentialLibraryType) resource.TestCheckFunc {