Skip to content

Commit

Permalink
Add new resource vault_pki_secret_backend_acme_eab to manage ACME EAB…
Browse files Browse the repository at this point in the history
… tokens
  • Loading branch information
stevendpclark committed Nov 19, 2024
1 parent 56aa746 commit 0e53264
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 0 deletions.
4 changes: 4 additions & 0 deletions internal/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ const (
FieldMaxRetries = "max_retries"
FieldSessionTags = "session_tags"
FieldSelfManagedPassword = "self_managed_password"
FieldsCreatedOn = "created_on"
FieldEabKey = "key"
FieldAcmeDirectory = "acme_directory"
FieldEabId = "eab_id"

/*
common environment variables
Expand Down
4 changes: 4 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,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
181 changes: 181 additions & 0 deletions vault/resource_pki_secret_backend_acme_eab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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)
}
}
147 changes: 147 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,147 @@
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 0e53264

Please sign in to comment.