Skip to content

Commit

Permalink
Merge branch 'main' into rculpepper/vault-28619
Browse files Browse the repository at this point in the history
  • Loading branch information
rculpepper authored Dec 9, 2024
2 parents 0cb435f + fabb0ac commit d454195
Show file tree
Hide file tree
Showing 18 changed files with 906 additions and 6 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ FEATURES:

* Update `vault_database_secret_backend_connection`to support `password_authentication` for PostgreSQL, allowing to encrypt password before being passed to PostgreSQL ([#2371](https://github.com/hashicorp/terraform-provider-vault/pull/2371))
* Add support for `external_id` field for the `vault_aws_auth_backend_sts_role` resource ([#2370](https://github.com/hashicorp/terraform-provider-vault/pull/2370))
* Add support for ACME configuration with the `vault_pki_secret_backend_config_acme` resource. Requires Vault 1.14+ ([#2157](https://github.com/hashicorp/terraform-provider-vault/pull/2157)).
* Update `vault_pki_secret_backend_role` to support the `cn_validations` role field ([#1820](https://github.com/hashicorp/terraform-provider-vault/pull/1820)).
* Add new resource `vault_pki_secret_backend_acme_eab` to manage PKI ACME external account binding tokens. Requires Vault 1.14+. ([#2367](https://github.com/hashicorp/terraform-provider-vault/pull/2367))
* Add new data source and resource `vault_pki_secret_backend_config_cmpv2`. Requires Vault 1.18+. *Available only for Vault Enterprise* ([#2330](https://github.com/hashicorp/terraform-provider-vault/pull/2330))

## 4.5.0 (Nov 19, 2024)
Expand Down Expand Up @@ -52,8 +55,10 @@ FEATURES:

IMPROVEMENTS:
* return a useful error when delete fails for the `vault_jwt_auth_backend_role` resource: ([#2232](https://github.com/hashicorp/terraform-provider-vault/pull/2232))
BUGS:
* Remove dependency on `github.com/hashicorp/vault` package: ([#2251](https://github.com/hashicorp/terraform-provider-vault/pull/2251))
* Add missing `custom_tags` and `secret_name_template` fields to `vault_secrets_sync_azure_destination` resource ([#2247](https://github.com/hashicorp/terraform-provider-vault/pull/2247))
* Fix handling of 0 value within field `max_path_length` in `vault_pki_secret_backend_root_cert` and `vault_pki_secret_backend_root_sign_intermediate` resources ([#2253](https://github.com/hashicorp/terraform-provider-vault/pull/2253))

## 4.2.0 (Mar 27, 2024)

Expand Down
12 changes: 11 additions & 1 deletion internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,17 @@ const (
FieldMaxRetries = "max_retries"
FieldSessionTags = "session_tags"
FieldSelfManagedPassword = "self_managed_password"

FieldAllowedIssuers = "allowed_issuers"
FieldAllowedRoles = "allowed_roles"
FieldAllowRoleExtKeyUsage = "allow_role_ext_key_usage"
FieldDefaultDirectoryPolicy = "default_directory_policy"
FieldDnsResolver = "dns_resolver"
FieldEabPolicy = "eab_policy"
FieldCnValidations = "cn_validations"
FieldsCreatedOn = "created_on"
FieldEabKey = "key"
FieldAcmeDirectory = "acme_directory"
FieldEabId = "eab_id"
/*
common environment variables
*/
Expand Down
8 changes: 8 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@ var (
Resource: UpdateSchemaResource(pkiSecretBackendCrlConfigResource()),
PathInventory: []string{"/pki/config/crl"},
},
"vault_pki_secret_backend_config_acme": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigACMEResource()),
PathInventory: []string{"/pki/config/acme"},
},
"vault_pki_secret_backend_config_ca": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigCAResource()),
PathInventory: []string{"/pki/config/ca"},
Expand Down Expand Up @@ -639,6 +643,10 @@ var (
Resource: UpdateSchemaResource(pkiSecretBackendConfigIssuers()),
PathInventory: []string{"/pki/config/issuers"},
},
"vault_pki_secret_backend_acme_eab": {
Resource: UpdateSchemaResource(pkiSecretBackendAcmeEabResource()),
PathInventory: []string{"/pki/acme/new-eab"},
},
"vault_quota_lease_count": {
Resource: UpdateSchemaResource(quotaLeaseCountResource()),
PathInventory: []string{"/sys/quotas/lease-count/{name}"},
Expand Down
184 changes: 184 additions & 0 deletions vault/resource_pki_secret_backend_acme_eab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
"context"
"fmt"
"log"
"strings"

"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"
)

var requiredEabDataKeys = []string{"id", "key_type", "acme_directory", "key"}

/*
ACME EAB (External Account Binding) tokens, that restrict ACME accounts from
being created anonymously
*/
func pkiSecretBackendAcmeEabResource() *schema.Resource {
return &schema.Resource{
Description: "Manages Vault PKI ACME EAB bindings",
CreateContext: provider.MountCreateContextWrapper(pkiSecretBackendCreateAcmeEab, provider.VaultVersion114),
ReadContext: pkiSecretBackendReadAcmeEab,
DeleteContext: pkiSecretBackendDeleteAcmeEab,
// There is no UpdateContext or ImportContext for EAB tokens as there is no read API available

Schema: map[string]*schema.Schema{
consts.FieldBackend: {
Type: schema.TypeString,
Required: true,
Description: "The PKI secret backend the resource belongs to",
ForceNew: true,
},
consts.FieldIssuer: {
Type: schema.TypeString,
Optional: true,
Description: "Specifies the issuer reference to use for directory path",
ForceNew: true, // If this is changed/set a new directory path will need to be calculated
},
consts.FieldRole: {
Type: schema.TypeString,
Optional: true,
Description: "Specifies the role to use for directory path",
ForceNew: true, // If this is changed/set a new directory path will need to be calculated
},
// Response fields for EAB
consts.FieldEabId: {
Type: schema.TypeString,
Computed: true,
Description: "The identifier of a specific ACME EAB token",
},
consts.FieldKeyType: {
Type: schema.TypeString,
Computed: true,
Description: "The key type of the EAB key",
},
consts.FieldAcmeDirectory: {
Type: schema.TypeString,
Computed: true,
Description: "The ACME directory to which the key belongs",
},
consts.FieldEabKey: {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
Description: "The ACME EAB token",
},
consts.FieldsCreatedOn: {
Type: schema.TypeString,
Computed: true,
Description: "An RFC3339 formatted date time when the EAB token was created",
},
},
}
}

func pkiSecretBackendCreateAcmeEab(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return pkiSecretBackendReadAcmeEab(ctx, d, meta)
}

func pkiSecretBackendReadAcmeEab(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

var issuer, role string
// These are optional inputs that will determine our path to use to create the EAB token
if issuerVal, ok := d.GetOk(consts.FieldIssuer); ok {
issuer = issuerVal.(string)
}
if roleVal, ok := d.GetOk(consts.FieldRole); ok {
role = roleVal.(string)
}

backend := d.Get(consts.FieldBackend).(string)
path := pkiSecretBackendComputeAcmeDirectoryPath(backend, issuer, role)

log.Printf("[DEBUG] Creating new ACME EAB token on PKI secret backend %q at path: %q", backend, path)
secret, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{})
if err != nil || secret == nil {
return diag.Errorf("error creating new ACME EAB on PKI secret backend %q at path %q: %s", backend, path, err)
}

for _, reqKey := range requiredEabDataKeys {
if rawVal, ok := secret.Data[reqKey]; !ok {
return diag.Errorf("eab response missing required field: %q", reqKey)
} else {
if val, ok := rawVal.(string); !ok {
return diag.Errorf("eab response field: %q was not a string", reqKey)
} else if len(val) == 0 {
return diag.Errorf("eab response has empty required field: %q", reqKey)
}
}
}

log.Printf("[DEBUG] Successfully created new ACME EAB token on backend %q at path %q", backend, path)
eabId := secret.Data["id"].(string)
fieldsToSet := map[string]string{
consts.FieldEabId: eabId,
consts.FieldKeyType: secret.Data["key_type"].(string),
consts.FieldAcmeDirectory: secret.Data["acme_directory"].(string),
consts.FieldEabKey: secret.Data["key"].(string),
consts.FieldsCreatedOn: secret.Data["created_on"].(string),
}

d.SetId(fmt.Sprintf("acme-eab:%s:%s", path, eabId))
for key, val := range fieldsToSet {
if err := d.Set(key, val); err != nil {
return diag.FromErr(fmt.Errorf("failed setting field %q: %w", key, err))
}
}

return nil
}

func pkiSecretBackendDeleteAcmeEab(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(data, meta)
if e != nil {
return diag.FromErr(e)
}

backend := data.Get(consts.FieldBackend).(string)
eabId := data.Get(consts.FieldEabId).(string)
acmeDirectory := data.Get(consts.FieldAcmeDirectory).(string)

log.Printf("[DEBUG] Deleting EAB token %q under path %q if unused", eabId, acmeDirectory)
path := pkiSecretBackendComputeAcmeEabPath(backend, eabId)
resp, err := client.Logical().DeleteWithContext(ctx, path)
if err != nil {
return diag.FromErr(err)
}
_ = resp
log.Printf("[DEBUG] Deleted EAB token %q if it was unused", eabId)
return nil
}

func pkiSecretBackendComputeAcmeEabPath(backend, eabId string) string {
trimmedBackend := strings.TrimPrefix(strings.TrimSpace(backend), "/")
trimmedEabId := strings.TrimSpace(eabId)
return fmt.Sprintf("%s/eab/%s", trimmedBackend, trimmedEabId)
}

func pkiSecretBackendComputeAcmeDirectoryPath(backend string, issuer string, role string) string {
trimmedBackend := strings.TrimPrefix(strings.TrimSpace(backend), "/")
trimmedIssuer := strings.TrimSpace(issuer)
trimmedRole := strings.TrimSpace(role)

switch {
case len(trimmedIssuer) > 0 && len(trimmedRole) > 0:
return fmt.Sprintf("%s/issuer/%s/roles/%s/acme/new-eab", trimmedBackend, trimmedIssuer, trimmedRole)
case len(trimmedIssuer) > 0:
return fmt.Sprintf("%s/issuer/%s/acme/new-eab", trimmedBackend, trimmedIssuer)
case len(trimmedRole) > 0:
return fmt.Sprintf("%s/roles/%s/acme/new-eab", trimmedBackend, trimmedRole)
default:
return fmt.Sprintf("%s/acme/new-eab", trimmedBackend)
}
}
150 changes: 150 additions & 0 deletions vault/resource_pki_secret_backend_acme_eab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 TestAccPKISecretBackendAcmeEab(t *testing.T) {
t.Parallel()

backend := acctest.RandomWithPrefix("tf-test-pki")
resourceType := "vault_pki_secret_backend_acme_eab"
resourceBackend := resourceType + ".test"

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
PreCheck: func() {
testutil.TestAccPreCheck(t)
SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion114)
},
CheckDestroy: testCheckMountDestroyed(resourceType, consts.MountTypePKI, consts.FieldBackend),
Steps: []resource.TestStep{
{
// Test EAB creation without a role and issuer
Config: testAccPKISecretBackendEAB(backend, "", ""),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, ""),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, ""),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "acme/directory"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn),
),
},
{
// Test EAB creation with role and issuer
Config: testAccPKISecretBackendEAB(backend, "test-issuer", "test-role"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, "test-issuer"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, "test-role"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "issuer/test-issuer/roles/test-role/acme/directory"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn),
),
},
{
// Test EAB creation with just an issuer
Config: testAccPKISecretBackendEAB(backend, "test-issuer", ""),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, "test-issuer"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, ""),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "issuer/test-issuer/acme/directory"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn),
),
},
{
// Test EAB creation with just a role
Config: testAccPKISecretBackendEAB(backend, "", "test-role"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceBackend, consts.FieldBackend, backend),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldIssuer, ""),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldRole, "test-role"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabId),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldKeyType, "hs"),
resource.TestCheckResourceAttr(resourceBackend, consts.FieldAcmeDirectory, "roles/test-role/acme/directory"),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldEabKey),
resource.TestCheckResourceAttrSet(resourceBackend, consts.FieldsCreatedOn),
),
},
{
// Test EAB deletion
Config: testAccPKISecretBackendEAB(backend, "", ""),
Destroy: true,
},
},
})

}

func testAccPKISecretBackendEAB(path, issuer, role string) string {
return fmt.Sprintf(`
resource "vault_mount" "test" {
path = "%s"
type = "pki"
description = "PKI secret engine mount"
}
resource "vault_pki_secret_backend_role" "test" {
backend = vault_mount.test.path
name = "test-role"
}
resource "vault_pki_secret_backend_root_cert" "test" {
backend = vault_mount.test.path
type = "internal"
common_name = "test"
ttl = "86400"
issuer_name = "test-issuer"
}
resource "vault_pki_secret_backend_acme_eab" "test" {
backend = vault_mount.test.path
issuer = "%s"
role = "%s"
}
`, path, issuer, role)
}

func Test_pkiSecretBackendComputeAcmeDirectoryPath(t *testing.T) {
type args struct {
backend string
issuer string
role string
}
tests := []struct {
name string
args args
want string
}{
{"issuer-with-role", args{"pki", "my-issuer", "my-role"}, "pki/issuer/my-issuer/roles/my-role/acme/new-eab"},
{"only-issuer", args{"pki", "my-issuer", ""}, "pki/issuer/my-issuer/acme/new-eab"},
{"only-role", args{"pki", "", "my-role"}, "pki/roles/my-role/acme/new-eab"},
{"only-backend", args{"pki", "", ""}, "pki/acme/new-eab"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := pkiSecretBackendComputeAcmeDirectoryPath(tt.args.backend, tt.args.issuer, tt.args.role); got != tt.want {
t.Errorf("pkiSecretBackendComputeAcmeDirectoryPath() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit d454195

Please sign in to comment.