Skip to content

Commit

Permalink
Add support for CMPv2 configuration (#2330)
Browse files Browse the repository at this point in the history
* add resource and data source for CMPv2 config

* fix test

* add doc links and changelog

* Update website/docs/d/pki_secret_backend_config_cmpv2.html.md

Co-authored-by: Steven Clark <[email protected]>

* Update website/docs/d/pki_secret_backend_config_cmpv2.html.md

Co-authored-by: Steven Clark <[email protected]>

* Update website/docs/d/pki_secret_backend_config_cmpv2.html.md

Co-authored-by: Steven Clark <[email protected]>

* Update website/docs/d/pki_secret_backend_config_cmpv2.html.md

Co-authored-by: Steven Clark <[email protected]>

* add docs

* fix changelog

---------

Co-authored-by: Steven Clark <[email protected]>
  • Loading branch information
rculpepper and stevendpclark authored Dec 9, 2024
1 parent fabb0ac commit 4b62925
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ FEATURES:
* 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
150 changes: 150 additions & 0 deletions vault/data_source_pki_secret_backend_config_cmpv2.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 (
"context"
"errors"
"fmt"
"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"
"github.com/hashicorp/vault/api"
"strings"
)

func pkiSecretBackendConfigCMPV2DataSource() *schema.Resource {
return &schema.Resource{
Description: "Reads Vault PKI CMPv2 configuration",
ReadContext: provider.ReadContextWrapper(readPKISecretBackendConfigCMPV2),
Schema: map[string]*schema.Schema{
consts.FieldBackend: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Path where PKI engine is mounted",
},
consts.FieldEnabled: {
Type: schema.TypeBool,
Computed: true,
Description: "Specifies whether CMPv2 is enabled",
},
consts.FieldDefaultPathPolicy: {
Type: schema.TypeString,
Computed: true,
Description: "Can be sign-verbatim or a role given by role:<role_name>",
},
consts.FieldAuthenticators: {
Type: schema.TypeList,
Computed: true,
Description: "Lists the mount accessors CMPv2 should delegate authentication requests towards",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cert": {
Type: schema.TypeMap,
Optional: true,
Description: "The accessor and cert_role properties for cert auth backends",
},
},
},
},
consts.FieldEnableSentinelParsing: {
Type: schema.TypeBool,
Computed: true,
Description: "If set, parse out fields from the provided CSR making them available for Sentinel policies",
},
consts.FieldAuditFields: {
Type: schema.TypeList,
Computed: true,
Description: "Fields parsed from the CSR that appear in the audit and can be used by sentinel policies",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldLastUpdated: {
Type: schema.TypeString,
Computed: true,
Description: "A read-only timestamp representing the last time the configuration was updated",
},
},
}
}

func readPKISecretBackendConfigCMPV2(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if err := verifyPkiEstFeatureSupported(meta); err != nil {
return diag.FromErr(err)
}

client, err := provider.GetClient(d, meta)
if err != nil {
return diag.FromErr(fmt.Errorf("failed getting client: %w", err))
}

backend := d.Get(consts.FieldBackend).(string)
path := pkiSecretBackendConfigCMPV2Path(backend)

if err := readCMPV2Config(ctx, d, client, path); err != nil {
return diag.FromErr(err)
}

return nil
}

func readCMPV2Config(ctx context.Context, d *schema.ResourceData, client *api.Client, path string) error {
resp, err := client.Logical().ReadWithContext(ctx, path)
if err != nil {
return fmt.Errorf("error reading from Vault: %w", err)
}
if resp == nil {
return fmt.Errorf("got nil response from Vault from path: %q", path)
}

d.SetId(path)

keyComputedFields := []string{
consts.FieldEnabled,
consts.FieldDefaultPathPolicy,
consts.FieldEnableSentinelParsing,
consts.FieldAuditFields,
consts.FieldLastUpdated,
}

for _, k := range keyComputedFields {
if fieldVal, ok := resp.Data[k]; ok {
if err := d.Set(k, fieldVal); err != nil {
return fmt.Errorf("failed setting field [%s] with val [%s]: %w", k, fieldVal, err)
}
}
}

if authenticators, authOk := resp.Data[consts.FieldAuthenticators]; authOk {
if err := d.Set(consts.FieldAuthenticators, []interface{}{authenticators}); err != nil {
return fmt.Errorf("failed setting field [%s] with val [%s]: %w", consts.FieldAuthenticators, authenticators, err)
}
}

return nil
}

// verifyPkiCMPV2FeatureSupported verifies that we are talking to a Vault enterprise edition
// and its version 1.18.0 or higher, returns nil if the above is met, otherwise an error
func verifyPkiCMPV2FeatureSupported(meta interface{}) error {
currentVersion := meta.(*provider.ProviderMeta).GetVaultVersion()

minVersion := provider.VaultVersion118
if !provider.IsAPISupported(meta, minVersion) {
return fmt.Errorf("feature not enabled on current Vault version. min version required=%s; "+
"current vault version=%s", minVersion, currentVersion)
}

if !provider.IsEnterpriseSupported(meta) {
return errors.New("feature requires Vault Enterprise")
}
return nil
}

func pkiSecretBackendConfigCMPV2Path(backend string) string {
return strings.Trim(backend, "/") + "/config/cmp"
}
52 changes: 52 additions & 0 deletions vault/data_source_pki_secret_backend_config_cmpv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 TestAccDataSourcePKISecretConfigCMPV2(t *testing.T) {
backend := acctest.RandomWithPrefix("tf-test-pki-backend")
dataName := "data.vault_pki_secret_backend_config_cmpv2.test"
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
PreCheck: func() {
testutil.TestAccPreCheck(t)
testutil.TestEntPreCheck(t)
SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion118)
},
Steps: []resource.TestStep{
{
Config: testPKISecretEmptyCMPV2ConfigDataSource(backend),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataName, consts.FieldBackend, backend),
resource.TestCheckResourceAttrSet(dataName, consts.FieldEnabled),
resource.TestCheckResourceAttrSet(dataName, consts.FieldEnableSentinelParsing),
resource.TestCheckResourceAttrSet(dataName, consts.FieldLastUpdated),
),
},
},
})
}

func testPKISecretEmptyCMPV2ConfigDataSource(path string) string {
return fmt.Sprintf(`
resource "vault_mount" "test" {
path = "%s"
type = "pki"
description = "PKI secret engine mount"
}
data "vault_pki_secret_backend_config_cmpv2" "test" {
backend = vault_mount.test.path
}`, path)
}
8 changes: 8 additions & 0 deletions vault/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ var (
Resource: UpdateSchemaResource(raftAutopilotStateDataSource()),
PathInventory: []string{"/sys/storage/raft/autopilot/state"},
},
"vault_pki_secret_backend_config_cmpv2": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigCMPV2DataSource()),
PathInventory: []string{"/pki/config/cmp"},
},
"vault_pki_secret_backend_config_est": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigEstDataSource()),
PathInventory: []string{"/pki/config/est"},
Expand Down Expand Up @@ -591,6 +595,10 @@ var (
Resource: UpdateSchemaResource(pkiSecretBackendConfigClusterResource()),
PathInventory: []string{"/pki/config/cluster"},
},
"vault_pki_secret_backend_config_cmpv2": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigCMPV2Resource()),
PathInventory: []string{"/pki/config/cmp"},
},
"vault_pki_secret_backend_config_est": {
Resource: UpdateSchemaResource(pkiSecretBackendConfigEstResource()),
PathInventory: []string{"/pki/config/est"},
Expand Down
162 changes: 162 additions & 0 deletions vault/resource_pki_secret_backend_config_cmpv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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"
)

func pkiSecretBackendConfigCMPV2Resource() *schema.Resource {
return &schema.Resource{
Description: "Manages Vault PKI CMPv2 configuration",
CreateContext: provider.MountCreateContextWrapper(pkiSecretBackendConfigCMPV2Write, provider.VaultVersion118),
UpdateContext: pkiSecretBackendConfigCMPV2Write,
ReadContext: pkiSecretBackendConfigCMPV2Read,
DeleteContext: pkiSecretBackendConfigCMPV2Delete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
consts.FieldBackend: {
Type: schema.TypeString,
Required: true,
Description: "The PKI secret backend the resource belongs to",
ForceNew: true,
},
consts.FieldEnabled: {
Type: schema.TypeBool,
Optional: true,
Description: "Specifies whether CMPv2 is enabled",
},
consts.FieldDefaultPathPolicy: {
Type: schema.TypeString,
Optional: true,
Description: "Can be sign-verbatim or a role given by role:<role_name>",
},
consts.FieldAuthenticators: {
Type: schema.TypeList,
Optional: true,
Computed: true,
Description: "Lists the mount accessors CMPv2 should delegate authentication requests towards",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"cert": {
Type: schema.TypeMap,
Optional: true,
},
},
},
MaxItems: 1,
},
consts.FieldEnableSentinelParsing: {
Type: schema.TypeBool,
Optional: true,
Description: "If set, parse out fields from the provided CSR making them available for Sentinel policies",
},
consts.FieldAuditFields: {
Type: schema.TypeList,
Optional: true,
Computed: true,
Description: "Fields parsed from the CSR that appear in the audit and can be used by sentinel policies",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
consts.FieldLastUpdated: {
Type: schema.TypeString,
Computed: true, // read-only property
Description: "A read-only timestamp representing the last time the configuration was updated",
},
},
}
}

func pkiSecretBackendConfigCMPV2Write(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if err := verifyPkiCMPV2FeatureSupported(meta); err != nil {
return diag.FromErr(err)
}

client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

backend := d.Get(consts.FieldBackend).(string)
path := pkiSecretBackendConfigCMPV2Path(backend)

fieldsToSet := []string{
consts.FieldEnabled,
consts.FieldDefaultPathPolicy,
consts.FieldEnableSentinelParsing,
consts.FieldAuditFields,
}

data := map[string]interface{}{}
for _, field := range fieldsToSet {
if val, ok := d.GetOk(field); ok {
data[field] = val
}
}

if authenticatorsRaw, ok := d.GetOk(consts.FieldAuthenticators); ok {
authenticators := authenticatorsRaw.([]interface{})
var authenticator interface{}
if len(authenticators) > 0 {
authenticator = authenticators[0]
}

data[consts.FieldAuthenticators] = authenticator
}

log.Printf("[DEBUG] Updating CMPv2 config on PKI secret backend %q:\n%v", backend, data)
_, err := client.Logical().WriteWithContext(ctx, path, data)
if err != nil {
return diag.Errorf("error updating CMPv2 config for PKI secret backend %q: %s", backend, err)
}
log.Printf("[DEBUG] Updated CMPv2 config on PKI secret backend %q", backend)

d.SetId(path)

return pkiSecretBackendConfigCMPV2Read(ctx, d, meta)
}

func pkiSecretBackendConfigCMPV2Read(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
id := d.Id()
if id == "" {
return diag.FromErr(fmt.Errorf("no path set for import, id=%q", id))
}

backend := strings.TrimSuffix(id, "/config/cmp")
if err := d.Set("backend", backend); err != nil {
return diag.FromErr(fmt.Errorf("failed setting field [%s] with value [%v]: %w", "backend", backend, err))
}

if err := verifyPkiCMPV2FeatureSupported(meta); err != nil {
return diag.FromErr(err)
}

client, err := provider.GetClient(d, meta)
if err != nil {
return diag.FromErr(fmt.Errorf("failed getting client: %w", err))
}

if err := readCMPV2Config(ctx, d, client, id); err != nil {
return diag.FromErr(err)
}
return nil
}

func pkiSecretBackendConfigCMPV2Delete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
// There isn't any delete API for the CMPv2 config.
return nil
}
Loading

0 comments on commit 4b62925

Please sign in to comment.