-
Notifications
You must be signed in to change notification settings - Fork 548
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pki): Add support for ACME configuration (#2157)
* feat(pki): Add support for ACME configuration * imprv(pki): Remove ValidateFunc, check for 1.14.1. Simplify StateContext. Return error message if response is nil, nil
- Loading branch information
Showing
7 changed files
with
418 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package vault | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"regexp" | ||
"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 ( | ||
pkiSecretBackendFromConfigACMERegex = regexp.MustCompile("^(.+)/config/acme$") | ||
pkiAcmeFields = []string{ | ||
consts.FieldEnabled, | ||
consts.FieldDefaultDirectoryPolicy, | ||
consts.FieldAllowedRoles, | ||
consts.FieldAllowRoleExtKeyUsage, | ||
consts.FieldAllowedIssuers, | ||
consts.FieldEabPolicy, | ||
consts.FieldDnsResolver, | ||
} | ||
) | ||
|
||
func pkiSecretBackendConfigACMEResource() *schema.Resource { | ||
return &schema.Resource{ | ||
CreateContext: provider.MountCreateContextWrapper(pkiSecretBackendConfigACMECreate, provider.VaultVersion113), | ||
ReadContext: provider.ReadContextWrapper(pkiSecretBackendConfigACMERead), | ||
UpdateContext: pkiSecretBackendConfigACMEUpdate, | ||
DeleteContext: pkiSecretBackendConfigACMEDelete, | ||
Importer: &schema.ResourceImporter{ | ||
StateContext: schema.ImportStatePassthroughContext, | ||
}, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
consts.FieldBackend: { | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
Description: "Full path where PKI backend is mounted.", | ||
// standardise on no beginning or trailing slashes | ||
StateFunc: func(v interface{}) string { | ||
return strings.Trim(v.(string), "/") | ||
}, | ||
}, | ||
consts.FieldEnabled: { | ||
Type: schema.TypeBool, | ||
Required: true, | ||
Description: "Specifies whether ACME is enabled.", | ||
}, | ||
consts.FieldDefaultDirectoryPolicy: { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Computed: true, | ||
Description: "Specifies the policy to be used for non-role-qualified ACME requests.", | ||
}, | ||
consts.FieldAllowedRoles: { | ||
Type: schema.TypeList, | ||
Optional: true, | ||
Computed: true, | ||
Description: "Specifies which roles are allowed for use with ACME.", | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
consts.FieldAllowRoleExtKeyUsage: { | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
Description: "Specifies whether the ExtKeyUsage field from a role is used.", | ||
}, | ||
consts.FieldAllowedIssuers: { | ||
Type: schema.TypeList, | ||
Optional: true, | ||
Computed: true, | ||
Description: "Specifies which issuers are allowed for use with ACME.", | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
consts.FieldEabPolicy: { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Computed: true, | ||
Description: "Specifies the policy to use for external account binding behaviour.", | ||
}, | ||
consts.FieldDnsResolver: { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "DNS resolver to use for domain resolution on this mount. " + | ||
"Must be in the format <host>:<port>, with both parts mandatory.", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func pkiSecretBackendConfigACMECreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client, e := provider.GetClient(d, meta) | ||
if e != nil { | ||
return diag.FromErr(e) | ||
} | ||
|
||
backend := d.Get(consts.FieldBackend).(string) | ||
path := fmt.Sprintf("%s/config/acme", backend) | ||
|
||
resp, err := client.Logical().ReadWithContext(ctx, path) | ||
if err != nil { | ||
return diag.Errorf("error reading acme config at %s, err=%s", path, err) | ||
} | ||
|
||
if resp == nil { | ||
return diag.Errorf("no acme config found at path %s", path) | ||
} | ||
|
||
d.SetId(path) | ||
|
||
return pkiSecretBackendConfigACMEUpdate(ctx, d, meta) | ||
} | ||
|
||
func pkiSecretBackendConfigACMEUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client, e := provider.GetClient(d, meta) | ||
if e != nil { | ||
return diag.FromErr(e) | ||
} | ||
|
||
path := d.Id() | ||
|
||
var patchRequired bool | ||
data := map[string]interface{}{} | ||
for _, k := range pkiAcmeFields { | ||
if d.HasChange(k) { | ||
data[k] = d.Get(k) | ||
patchRequired = true | ||
} | ||
} | ||
|
||
if patchRequired { | ||
_, err := client.Logical().WriteWithContext(ctx, path, data) | ||
if err != nil { | ||
return diag.Errorf("error writing data to %q, err=%s", path, err) | ||
} | ||
} | ||
|
||
return pkiSecretBackendConfigACMERead(ctx, d, meta) | ||
} | ||
|
||
func pkiSecretBackendConfigACMERead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client, e := provider.GetClient(d, meta) | ||
if e != nil { | ||
return diag.FromErr(e) | ||
} | ||
|
||
path := d.Id() | ||
if path == "" { | ||
return diag.Errorf("no path set, id=%q", d.Id()) | ||
} | ||
|
||
// get backend from full path | ||
backend, err := pkiSecretBackendFromConfigACME(path) | ||
if err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
log.Printf("[DEBUG] Reading %s from Vault", path) | ||
resp, err := client.Logical().ReadWithContext(ctx, path) | ||
if err != nil { | ||
return diag.Errorf("error reading from Vault: %s", err) | ||
} | ||
|
||
if resp == nil { | ||
return diag.Errorf("got nil response from Vault from path: %q", path) | ||
} | ||
|
||
// set backend and issuerRef | ||
if err := d.Set(consts.FieldBackend, backend); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
for _, k := range pkiAcmeFields { | ||
if err := d.Set(k, resp.Data[k]); err != nil { | ||
return diag.Errorf("error setting state key %q for acme config, err=%s", | ||
k, err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func pkiSecretBackendConfigACMEDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
return nil | ||
} | ||
|
||
func pkiSecretBackendFromConfigACME(path string) (string, error) { | ||
if !pkiSecretBackendFromConfigACMERegex.MatchString(path) { | ||
return "", fmt.Errorf("no backend found") | ||
} | ||
res := pkiSecretBackendFromConfigACMERegex.FindStringSubmatch(path) | ||
if len(res) != 2 { | ||
return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) | ||
} | ||
return res[1], nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// 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 TestPkiSecretBackendConfigACME_basic(t *testing.T) { | ||
backend := acctest.RandomWithPrefix("pki-root") | ||
resourceType := "vault_pki_secret_backend_config_acme" | ||
resourceName := 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{ | ||
{ | ||
Config: testPkiSecretBackendConfigACME(backend, "sign-verbatim", "*", "*", "not-required", "", | ||
false, false), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "false"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "sign-verbatim"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "*"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "not-required"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, ""), | ||
), | ||
}, | ||
{ | ||
Config: testPkiSecretBackendConfigACME(backend, "forbid", "test", "*", "new-account-required", | ||
"1.1.1.1:8443", true, false), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "true"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "forbid"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "test"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "new-account-required"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, "1.1.1.1:8443"), | ||
), | ||
}, | ||
{ | ||
Config: testPkiSecretBackendConfigACME(backend, "role:test", "*", "*", "always-required", "", | ||
true, true), | ||
Check: resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldBackend, backend), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEnabled, "true"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDefaultDirectoryPolicy, "role:test"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedRoles+".0", "*"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowRoleExtKeyUsage, "true"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldAllowedIssuers+".0", "*"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldEabPolicy, "always-required"), | ||
resource.TestCheckResourceAttr(resourceName, consts.FieldDnsResolver, ""), | ||
), | ||
}, | ||
testutil.GetImportTestStep(resourceName, false, nil), | ||
}, | ||
}) | ||
} | ||
|
||
func testPkiSecretBackendConfigACME(path, default_directory_policy, allowed_roles, allowed_issuers, | ||
eab_policy, dns_resolver string, enabled, allow_role_ext_key_usage bool) string { | ||
return fmt.Sprintf(` | ||
resource "vault_mount" "test" { | ||
path = "%s" | ||
type = "pki" | ||
} | ||
resource "vault_pki_secret_backend_root_cert" "test" { | ||
backend = vault_mount.test.path | ||
type = "internal" | ||
common_name = "test" | ||
ttl = "86400" | ||
} | ||
resource "vault_pki_secret_backend_config_cluster" "test" { | ||
backend = vault_mount.test.path | ||
path = "http://127.0.0.1:8200/v1/${vault_mount.test.path}" | ||
aia_path = "http://127.0.0.1:8200/v1/${vault_mount.test.path}" | ||
} | ||
resource "vault_pki_secret_backend_role" "test" { | ||
backend = vault_pki_secret_backend_root_cert.test.backend | ||
name = "test" | ||
} | ||
resource "vault_pki_secret_backend_config_acme" "test" { | ||
backend = vault_mount.test.path | ||
enabled = "%t" | ||
allowed_issuers = ["%s"] | ||
allowed_roles = ["%s"] | ||
allow_role_ext_key_usage = "%t" | ||
default_directory_policy = "%s" | ||
dns_resolver = "%s" | ||
eab_policy = "%s" | ||
}`, path, enabled, allowed_issuers, allowed_roles, allow_role_ext_key_usage, | ||
default_directory_policy, dns_resolver, eab_policy) | ||
} |
Oops, something went wrong.