Password Credential support (#758)

* feat(password credential): Add password credential type support

* build(deps): upgrade Boundary to v0.21.0 and update dependencies

* chore: update go.mod and go.sum from main

* Dependency cleanup
This commit is contained in:
Bharath Gajjala authored and GitHub committed 2025-12-16 22:29:42 -05:00
1 parent 8ebe3fba5e
commit d738671047
11 files changed
+473 -5

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_credential_password Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The password credential resource allows you to configure a credential using a password.
---
# boundary_credential_password (Resource)
The password credential resource allows you to configure a credential using a password.
## Example Usage
```terraform
resource "boundary_scope" "org" {
name = "organization_one"
description = "global 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_static" "example" {
name = "example_static_credential_store"
description = "My first static credential store!"
scope_id = boundary_scope.project.id
}
resource "boundary_credential_password" "example" {
name = "example_password"
description = "My first password credential!"
credential_store_id = boundary_credential_store_static.example.id
password = "my-password"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `credential_store_id` (String) The credential store in which to save this password credential.
- `password` (String, Sensitive) The password of this password credential.
### Optional
- `description` (String) The description of this password credential.
- `name` (String) The name of this password credential. Defaults to the resource name.
### Read-Only
- `id` (String) The ID of this password credential.
- `password_hmac` (String) The password hmac.
## 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_password.example <my-id>
```
+1 -1
View File
@@ -147,7 +147,7 @@ resource "boundary_target" "rdp_foo" {
injected_application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
egress_worker_filter = "\"egress\" in \"/tags/type\""
egress_worker_filter = "\"egress\" in \"/tags/type\""
}
resource "boundary_target" "address_foo" {
@@ -0,0 +1 @@
terraform import boundary_credential_password.example <my-id>
@@ -0,0 +1,27 @@
resource "boundary_scope" "org" {
name = "organization_one"
description = "global 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_static" "example" {
name = "example_static_credential_store"
description = "My first static credential store!"
scope_id = boundary_scope.project.id
}
resource "boundary_credential_password" "example" {
name = "example_password"
description = "My first password credential!"
credential_store_id = boundary_credential_store_static.example.id
password = "my-password"
}
@@ -132,7 +132,7 @@ resource "boundary_target" "rdp_foo" {
injected_application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
egress_worker_filter = "\"egress\" in \"/tags/type\""
egress_worker_filter = "\"egress\" in \"/tags/type\""
}
resource "boundary_target" "address_foo" {
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.25.1
require (
github.com/YakDriver/regexache v0.25.0
github.com/hashicorp/boundary v0.21.0
github.com/hashicorp/boundary/api v0.0.58
github.com/hashicorp/boundary/api v0.0.59
github.com/hashicorp/boundary/sdk v0.0.56
github.com/hashicorp/cap v0.12.0
github.com/hashicorp/cap/ldap v0.0.0-20240206183135-ed8f24513744
+2 -2
View File
@@ -199,8 +199,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLW
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/boundary v0.21.0 h1:unHXCGVormSQUGhog391K9cb3YKUtN8AgkvkBWxMtsQ=
github.com/hashicorp/boundary v0.21.0/go.mod h1:3jUWByzIdYg5RSyUOPc8Rydd3auk5sdU95xZ1Vh337Y=
github.com/hashicorp/boundary/api v0.0.58 h1:TuvCJ73N1UHvLWNLGXff81TD89GJ0mxuKK4vG6RDunM=
github.com/hashicorp/boundary/api v0.0.58/go.mod h1:44IFIfNlAbtH6X23GvTsXYO4ae+W9b85Ex2GMPHn/rE=
github.com/hashicorp/boundary/api v0.0.59 h1:rs9lwDtJw1pof0LXPPhTRDm9VtKWaThbqnepqpH0kNU=
github.com/hashicorp/boundary/api v0.0.59/go.mod h1:qhJRAJ+CUuWI2bvFZTHKQ8OgmzDEkBTQ4MTQNujpqnA=
github.com/hashicorp/boundary/sdk v0.0.56 h1:A/Wdd1A4qENjdnkvWQ20Igahs/3BSdbTPPL+A09BGMc=
github.com/hashicorp/boundary/sdk v0.0.56/go.mod h1:cnudX5/Su+aPPcsK5vrQ6Y2t0OsERgT5oweQ7x0wNTs=
github.com/hashicorp/cap v0.12.0 h1:z1PU5j8iqLpyortK8JatvPVqEGSl3eN2vxZUWtOgdZU=
+1
View File
@@ -109,6 +109,7 @@ func New() *schema.Provider {
"boundary_credential_store_static": resourceCredentialStoreStatic(),
"boundary_credential_username_password": resourceCredentialUsernamePassword(),
"boundary_credential_username_password_domain": resourceCredentialUsernamePasswordDomain(),
"boundary_credential_password": resourceCredentialPassword(),
"boundary_credential_ssh_private_key": resourceCredentialSshPrivateKey(),
"boundary_credential_json": resourceCredentialJson(),
"boundary_managed_group": resourceManagedGroup(),
@@ -144,6 +144,7 @@ const (
sshPrivateKeyCredentialType credentialType = "boundary_credential_ssh_private_key"
usernamePasswordCredentialType credentialType = "boundary_credential_username_password"
usernamePasswordDomainCredentialType credentialType = "boundary_credential_username_password_domain"
passwordCredentialType credentialType = "boundary_credential_password"
)
func testAccCheckCredentialResourceDestroy(t *testing.T, testProvider *schema.Provider, typ credentialType) resource.TestCheckFunc {
@@ -0,0 +1,222 @@
// 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/credentials"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
credentialPasswordKey = "password"
credentialPasswordHmacKey = "password_hmac"
credentialPasswordCredentialType = "password"
)
func resourceCredentialPassword() *schema.Resource {
return &schema.Resource{
Description: "The password credential resource allows you to configure a credential using a password.",
CreateContext: resourceCredentialPasswordCreate,
ReadContext: resourceCredentialPasswordRead,
UpdateContext: resourceCredentialPasswordUpdate,
DeleteContext: resourceCredentialPasswordDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of this password credential.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The name of this password credential. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The description of this password credential.",
Type: schema.TypeString,
Optional: true,
},
credentialStoreIdKey: {
Description: "The credential store in which to save this password credential.",
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
credentialPasswordKey: {
Description: "The password of this password credential.",
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
credentialPasswordHmacKey: {
Description: "The password hmac.",
Type: schema.TypeString,
Computed: true,
},
},
}
}
func setFromCredentialPasswordResponseMap(d *schema.ResourceData, raw map[string]interface{}, fromRead bool) 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{})
statePasswordHmac := d.Get(credentialPasswordHmacKey)
boundaryPasswordHmac := attrs[credentialPasswordHmacKey].(string)
if statePasswordHmac.(string) != boundaryPasswordHmac && fromRead {
// PasswordHmac has changed in Boundary, therefore the password has changed.
// Update password value to force tf to attempt update.
if err := d.Set(credentialPasswordKey, "(changed in Boundary)"); err != nil {
return err
}
}
if err := d.Set(credentialPasswordHmacKey, boundaryPasswordHmac); err != nil {
return err
}
}
d.SetId(raw["id"].(string))
return nil
}
func resourceCredentialPasswordCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var opts []credentials.Option
if v, ok := d.GetOk(NameKey); ok {
opts = append(opts, credentials.WithName(v.(string)))
}
if v, ok := d.GetOk(DescriptionKey); ok {
opts = append(opts, credentials.WithDescription(v.(string)))
}
if v, ok := d.GetOk(credentialPasswordKey); ok {
opts = append(opts, credentials.WithPasswordCredentialPassword(v.(string)))
}
var credentialStoreId string
retrievedStoreId, ok := d.GetOk(credentialStoreIdKey)
if ok {
credentialStoreId = retrievedStoreId.(string)
} else {
return diag.Errorf("credential store id is unset")
}
client := credentials.NewClient(md.client)
cr, err := client.Create(ctx, credentialPasswordCredentialType, credentialStoreId, opts...)
if err != nil {
return diag.Errorf("error creating credential: %v", err)
}
if cr == nil {
return diag.Errorf("nil credential after create")
}
if err := setFromCredentialPasswordResponseMap(d, cr.GetResponse().Map, false); err != nil {
return diag.Errorf("error generating credential from response map: %v", err)
}
return nil
}
func resourceCredentialPasswordRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := credentials.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: %v", err)
}
if cr == nil {
return diag.Errorf("credential nil after read")
}
if err := setFromCredentialPasswordResponseMap(d, cr.GetResponse().Map, true); err != nil {
return diag.Errorf("error generating credential from response map: %v", err)
}
return nil
}
func resourceCredentialPasswordUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := credentials.NewClient(md.client)
var opts []credentials.Option
if d.HasChange(NameKey) {
opts = append(opts, credentials.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
opts = append(opts, credentials.WithName(nameVal.(string)))
}
}
if d.HasChange(DescriptionKey) {
opts = append(opts, credentials.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
opts = append(opts, credentials.WithDescription(descVal.(string)))
}
}
if d.HasChange(credentialPasswordKey) {
passwordVal, ok := d.GetOk(credentialPasswordKey)
if ok {
opts = append(opts, credentials.WithPasswordCredentialPassword(passwordVal.(string)))
}
}
if len(opts) > 0 {
opts = append(opts, credentials.WithAutomaticVersioning(true))
crUpdate, err := client.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating credential: %v", err)
}
if crUpdate == nil {
return diag.Errorf("credential nil after update")
}
if err = setFromCredentialPasswordResponseMap(d, crUpdate.GetResponse().Map, false); err != nil {
return diag.Errorf("error generating credential from response map: %v", err)
}
}
return nil
}
func resourceCredentialPasswordDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := credentials.NewClient(md.client)
_, err := client.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting credential: %v", err)
}
return nil
}
@@ -0,0 +1,145 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/boundary/api/credentials"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
passwordCredResc = "boundary_credential_password.example"
passwordCredName = "foo"
passwordCredDesc = "the foo"
passwordCredPassword = "default_password"
passwordCredUpdate = "_random"
)
func passwordCredResource(name, description, password string) string {
return fmt.Sprintf(`
resource "boundary_credential_store_static" "example" {
name = "static store name"
description = "static store description"
scope_id = boundary_scope.proj1.id
depends_on = [boundary_role.proj1_admin]
}
resource "boundary_credential_password" "example" {
name = "%s"
description = "%s"
credential_store_id = boundary_credential_store_static.example.id
password = "%s"
}`, name,
description,
password)
}
func TestAccCredentialPassword(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
res := passwordCredResource(
passwordCredName,
passwordCredDesc,
passwordCredPassword,
)
resUpdate := passwordCredResource(
passwordCredName,
passwordCredDesc,
passwordCredPassword+passwordCredUpdate,
)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, passwordCredentialType),
Steps: []resource.TestStep{
{
// create
Config: testConfig(url, fooOrg, firstProjectFoo, res),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(passwordCredResc, NameKey, passwordCredName),
resource.TestCheckResourceAttr(passwordCredResc, DescriptionKey, passwordCredDesc),
resource.TestCheckResourceAttr(passwordCredResc, credentialPasswordKey, passwordCredPassword),
testAccCheckCredentialPasswordHmac(),
testAccCheckCredentialResourceExists(provider, passwordCredResc),
),
},
importStep(passwordCredResc, credentialPasswordKey),
{
// update
Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(passwordCredResc, NameKey, passwordCredName),
resource.TestCheckResourceAttr(passwordCredResc, DescriptionKey, passwordCredDesc),
resource.TestCheckResourceAttr(passwordCredResc, credentialPasswordKey, passwordCredPassword+passwordCredUpdate),
testAccCheckCredentialPasswordHmac(),
testAccCheckCredentialResourceExists(provider, passwordCredResc),
),
},
importStep(passwordCredResc, credentialPasswordKey),
{
// Run a plan only update and verify no changes
PlanOnly: true,
Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate),
},
importStep(passwordCredResc, credentialPasswordKey),
{
// update again but apply a preConfig to externally update resource
PreConfig: func() { passwordCredExternalUpdate(t, provider) },
Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate),
},
importStep(passwordCredResc, credentialPasswordKey),
},
})
}
func passwordCredExternalUpdate(t *testing.T, testProvider *schema.Provider) {
if storeId == "" {
t.Fatal("storeId must be set before testing an external update")
}
md := testProvider.Meta().(*metaData)
c := credentials.NewClient(md.client)
cr, err := c.Read(context.Background(), storeId)
if err != nil {
t.Fatal(fmt.Errorf("got an error reading %q: %w", storeId, err))
}
// update credential options
var opts []credentials.Option
opts = append(opts, credentials.WithDescription("this is an updated description, my guy"))
_, err = c.Update(context.Background(), cr.Item.Id, cr.Item.Version, opts...)
if err != nil {
t.Fatal(fmt.Errorf("got an error updating %q: %w", cr.Item.Id, err))
}
}
func testAccCheckCredentialPasswordHmac() resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[passwordCredResc]
if !ok {
return fmt.Errorf("not found: %s", passwordCredResc)
}
computed := rs.Primary.Attributes["password_hmac"]
if len(computed) != 43 {
return fmt.Errorf("Computed password hmac not the expected length of 43 characters. hmac: %q", computed)
}
return nil
}
}