Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for CMPv2 configuration #2330

Merged
merged 13 commits into from
Dec 9, 2024
Merged
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
Loading