diff --git a/docs/resources/credential_library_vault_ssh_certificate.md b/docs/resources/credential_library_vault_ssh_certificate.md new file mode 100644 index 00000000..843e1c62 --- /dev/null +++ b/docs/resources/credential_library_vault_ssh_certificate.md @@ -0,0 +1,111 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_credential_library_vault_ssh_certificate Resource - terraform-provider-boundary" +subcategory: "" +description: |- + The credential library for Vault resource allows you to configure a Boundary credential library for Vault. +--- + +# boundary_credential_library_vault_ssh_certificate (Resource) + +The credential library for Vault resource allows you to configure a Boundary credential library for Vault. + +## Example Usage + +```terraform +resource "boundary_scope" "org" { + name = "organization_one" + description = "My first scope!" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_vault" "foo" { + name = "foo" + description = "My first Vault credential store!" + address = "http://127.0.0.1:8200" # change to Vault address + token = "s.0ufRo6XEGU2jOqnIr7OlFYP5" # change to valid Vault token + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_library_vault_ssh_certificate" "foo" { + name = "foo" + description = "My first Vault SSH certificate credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/sign/foo" # change to correct Vault endpoint and role + username = "foo" # change to valid username +} + +resource "boundary_credential_library_vault_ssh_certificate" "bar" { + name = "bar" + description = "My second Vault SSH certificate credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/sign/foo" # change to correct Vault endpoint and role + username = "foo" + key_type = "ecdsa" + key_bits = 384 + + extensions = { + permit-pty = "" + } +} + +resource "boundary_credential_library_vault_ssh_certificate" "baz" { + name = "baz" + description = "vault " + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/issue/foo" # change to correct Vault endpoint and role + username = "foo" + key_type = "rsa" + key_bits = 4096 + + extensions = { + permit-pty = "" + permit-X11-forwarding = "" + } + + critical_options = { + force-command = "/bin/some_script" + } +} +``` + + +## Schema + +### Required + +- `credential_store_id` (String) The ID of the credential store that this library belongs to. +- `path` (String) The path in Vault to request credentials from. +- `username` (String) The username to use with the certificate returned by the library. + +### Optional + +- `critical_options` (Map of String) Specifies a map of the critical options that the certificate should be signed for. +- `description` (String) The Vault credential library description. +- `extensions` (Map of String) Specifies a map of the extensions that the certificate should be signed for. +- `key_bits` (Number) Specifies the number of bits to use for the generated keys. +- `key_id` (String) Specifies the key id a certificate should have. +- `key_type` (String) Specifies the desired key type; must be ed25519, ecdsa, or rsa. +- `name` (String) The Vault credential library name. Defaults to the resource name. +- `ttl` (String) Specifies the requested time to live for a certificate returned from the library. + +### Read-Only + +- `id` (String) The ID of the Vault credential library. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import boundary_credential_library_vault_ssh_certificate.foo +``` diff --git a/examples/resources/boundary_credential_library_vault_ssh_certificate/import.sh b/examples/resources/boundary_credential_library_vault_ssh_certificate/import.sh new file mode 100644 index 00000000..332ee5e1 --- /dev/null +++ b/examples/resources/boundary_credential_library_vault_ssh_certificate/import.sh @@ -0,0 +1 @@ +terraform import boundary_credential_library_vault_ssh_certificate.foo diff --git a/examples/resources/boundary_credential_library_vault_ssh_certificate/resource.tf b/examples/resources/boundary_credential_library_vault_ssh_certificate/resource.tf new file mode 100644 index 00000000..15d45376 --- /dev/null +++ b/examples/resources/boundary_credential_library_vault_ssh_certificate/resource.tf @@ -0,0 +1,63 @@ +resource "boundary_scope" "org" { + name = "organization_one" + description = "My first scope!" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_vault" "foo" { + name = "foo" + description = "My first Vault credential store!" + address = "http://127.0.0.1:8200" # change to Vault address + token = "s.0ufRo6XEGU2jOqnIr7OlFYP5" # change to valid Vault token + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_library_vault_ssh_certificate" "foo" { + name = "foo" + description = "My first Vault SSH certificate credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/sign/foo" # change to correct Vault endpoint and role + username = "foo" # change to valid username +} + +resource "boundary_credential_library_vault_ssh_certificate" "bar" { + name = "bar" + description = "My second Vault SSH certificate credential library!" + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/sign/foo" # change to correct Vault endpoint and role + username = "foo" + key_type = "ecdsa" + key_bits = 384 + + extensions = { + permit-pty = "" + } +} + +resource "boundary_credential_library_vault_ssh_certificate" "baz" { + name = "baz" + description = "vault " + credential_store_id = boundary_credential_store_vault.foo.id + path = "ssh/issue/foo" # change to correct Vault endpoint and role + username = "foo" + key_type = "rsa" + key_bits = 4096 + + extensions = { + permit-pty = "" + permit-X11-forwarding = "" + } + + critical_options = { + force-command = "/bin/some_script" + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2256d62f..78e264db 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -71,33 +71,34 @@ func New() *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "boundary_account": resourceAccount(), - "boundary_account_password": resourceAccountPassword(), - "boundary_account_oidc": resourceAccountOidc(), - "boundary_auth_method": resourceAuthMethod(), - "boundary_auth_method_password": resourceAuthMethodPassword(), - "boundary_auth_method_oidc": resourceAuthMethodOidc(), - "boundary_credential_library_vault": resourceCredentialLibraryVault(), - "boundary_credential_store_vault": resourceCredentialStoreVault(), - "boundary_credential_store_static": resourceCredentialStoreStatic(), - "boundary_credential_username_password": resourceCredentialUsernamePassword(), - "boundary_credential_ssh_private_key": resourceCredentialSshPrivateKey(), - "boundary_credential_json": resourceCredentialJson(), - "boundary_managed_group": resourceManagedGroup(), - "boundary_group": resourceGroup(), - "boundary_host": resourceHost(), - "boundary_host_static": resourceHostStatic(), - "boundary_host_catalog": resourceHostCatalog(), - "boundary_host_catalog_static": resourceHostCatalogStatic(), - "boundary_host_catalog_plugin": resourceHostCatalogPlugin(), - "boundary_host_set": resourceHostSet(), - "boundary_host_set_static": resourceHostSetStatic(), - "boundary_host_set_plugin": resourceHostSetPlugin(), - "boundary_role": resourceRole(), - "boundary_scope": resourceScope(), - "boundary_target": resourceTarget(), - "boundary_user": resourceUser(), - "boundary_worker": resourceWorker(), + "boundary_account": resourceAccount(), + "boundary_account_password": resourceAccountPassword(), + "boundary_account_oidc": resourceAccountOidc(), + "boundary_auth_method": resourceAuthMethod(), + "boundary_auth_method_password": resourceAuthMethodPassword(), + "boundary_auth_method_oidc": resourceAuthMethodOidc(), + "boundary_credential_library_vault": resourceCredentialLibraryVault(), + "boundary_credential_library_vault_ssh_certificate": resourceCredentialLibraryVaultSshCertificate(), + "boundary_credential_store_vault": resourceCredentialStoreVault(), + "boundary_credential_store_static": resourceCredentialStoreStatic(), + "boundary_credential_username_password": resourceCredentialUsernamePassword(), + "boundary_credential_ssh_private_key": resourceCredentialSshPrivateKey(), + "boundary_credential_json": resourceCredentialJson(), + "boundary_managed_group": resourceManagedGroup(), + "boundary_group": resourceGroup(), + "boundary_host": resourceHost(), + "boundary_host_static": resourceHostStatic(), + "boundary_host_catalog": resourceHostCatalog(), + "boundary_host_catalog_static": resourceHostCatalogStatic(), + "boundary_host_catalog_plugin": resourceHostCatalogPlugin(), + "boundary_host_set": resourceHostSet(), + "boundary_host_set_static": resourceHostSetStatic(), + "boundary_host_set_plugin": resourceHostSetPlugin(), + "boundary_role": resourceRole(), + "boundary_scope": resourceScope(), + "boundary_target": resourceTarget(), + "boundary_user": resourceUser(), + "boundary_worker": resourceWorker(), }, } diff --git a/internal/provider/resource_credential_library_vault_ssh_certificate.go b/internal/provider/resource_credential_library_vault_ssh_certificate.go new file mode 100644 index 00000000..70056553 --- /dev/null +++ b/internal/provider/resource_credential_library_vault_ssh_certificate.go @@ -0,0 +1,404 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + credentialLibraryVaultSshCertificateType = "vault-ssh-certificate" + credentialLibraryVaultSshCertificatePathKey = "path" + credentialLibraryVaultSshCertificateUsernameKey = "username" + credentialLibraryVaultSshCertificateKeyTypeKey = "key_type" + credentialLibraryVaultSshCertificateKeyBitsKey = "key_bits" + credentialLibraryVaultSshCertificateTtlKey = "ttl" + credentialLibraryVaultSshCertificateKeyIdKey = "key_id" + credentialLibraryVaultSshCertificateCriticalOptionsKey = "critical_options" + credentialLibraryVaultSshCertificateExtensionsKey = "extensions" +) + +var libraryVaultSshCertificateAttrs = []string{ + credentialLibraryVaultSshCertificatePathKey, + credentialLibraryVaultSshCertificateUsernameKey, + credentialLibraryVaultSshCertificateKeyTypeKey, + credentialLibraryVaultSshCertificateKeyBitsKey, + credentialLibraryVaultSshCertificateTtlKey, + credentialLibraryVaultSshCertificateKeyIdKey, + credentialLibraryVaultSshCertificateCriticalOptionsKey, + credentialLibraryVaultSshCertificateExtensionsKey, +} + +func resourceCredentialLibraryVaultSshCertificate() *schema.Resource { + return &schema.Resource{ + Description: "The credential library for Vault resource allows you to configure a Boundary credential library for Vault.", + + CreateContext: resourceCredentialLibraryCreateVaultSshCertificate, + ReadContext: resourceCredentialLibraryReadVaultSshCertificate, + UpdateContext: resourceCredentialLibraryUpdateVaultSshCertificate, + DeleteContext: resourceCredentialLibraryDeleteVaultSshCertificate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of the Vault credential library.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The Vault credential library name. Defaults to the resource name.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The Vault credential library description.", + Type: schema.TypeString, + Optional: true, + }, + credentialStoreIdKey: { + Description: "The ID of the credential store that this library belongs to.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + credentialLibraryVaultSshCertificatePathKey: { + Description: "The path in Vault to request credentials from.", + Type: schema.TypeString, + Required: true, + }, + credentialLibraryVaultSshCertificateUsernameKey: { + Description: "The username to use with the certificate returned by the library.", + Type: schema.TypeString, + Required: true, + }, + credentialLibraryVaultSshCertificateKeyTypeKey: { + Description: "Specifies the desired key type; must be ed25519, ecdsa, or rsa.", + Type: schema.TypeString, + Optional: true, + }, + credentialLibraryVaultSshCertificateKeyBitsKey: { + Description: "Specifies the number of bits to use for the generated keys.", + Type: schema.TypeInt, + Optional: true, + }, + credentialLibraryVaultSshCertificateTtlKey: { + Description: "Specifies the requested time to live for a certificate returned from the library.", + Type: schema.TypeString, + Optional: true, + }, + credentialLibraryVaultSshCertificateKeyIdKey: { + Description: "Specifies the key id a certificate should have.", + Type: schema.TypeString, + Optional: true, + }, + credentialLibraryVaultSshCertificateCriticalOptionsKey: { + Description: "Specifies a map of the critical options that the certificate should be signed for.", + Type: schema.TypeMap, + Optional: true, + }, + credentialLibraryVaultSshCertificateExtensionsKey: { + Description: "Specifies a map of the extensions that the certificate should be signed for.", + Type: schema.TypeMap, + Optional: true, + }, + }, + } +} + +func setFromVaultSshCertificateCredentialLibraryResponseMap(d *schema.ResourceData, raw map[string]interface{}) error { + if err := d.Set(NameKey, raw[NameKey]); err != nil { + return err + } + if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil { + return err + } + if err := d.Set(credentialStoreIdKey, raw[credentialStoreIdKey]); err != nil { + return err + } + + if attrsVal, ok := raw["attributes"]; ok { + attrs := attrsVal.(map[string]interface{}) + for _, v := range libraryVaultSshCertificateAttrs { + if err := d.Set(v, attrs[v]); err != nil { + return err + } + } + } + + d.SetId(raw["id"].(string)) + + return nil +} + +func resourceCredentialLibraryCreateVaultSshCertificate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + + var opts []credentiallibraries.Option + if v, ok := d.GetOk(NameKey); ok { + opts = append(opts, credentiallibraries.WithName(v.(string))) + } + if v, ok := d.GetOk(DescriptionKey); ok { + opts = append(opts, credentiallibraries.WithDescription(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificatePathKey); ok { + opts = append(opts, credentiallibraries.WithVaultCredentialLibraryPath(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateUsernameKey); ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryUsername(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyTypeKey); ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyType(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyBitsKey); ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyBits(uint32(v.(int)))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateTtlKey); ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryTtl(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyIdKey); ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyId(v.(string))) + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateCriticalOptionsKey); ok { + if vv, ok := v.(map[string]any); ok { + co := make(map[string]string, len(vv)) + for k, vvv := range vv { + co[k] = vvv.(string) + } + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryCriticalOptions(co)) + } + } + if v, ok := d.GetOk(credentialLibraryVaultSshCertificateExtensionsKey); ok { + if vv, ok := v.(map[string]any); ok { + e := make(map[string]string, len(vv)) + for k, vvv := range vv { + e[k] = vvv.(string) + } + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryExtensions(e)) + } + } + + var credentialStoreId string + cid, ok := d.GetOk(credentialStoreIdKey) + if ok { + credentialStoreId = cid.(string) + } else { + return diag.Errorf("no credential store ID is set") + } + + client := credentiallibraries.NewClient(md.client) + cr, err := client.Create(ctx, credentialLibraryVaultSshCertificateType, credentialStoreId, opts...) + if err != nil { + return diag.Errorf("error creating credential library: %v", err) + } + if cr == nil { + return diag.Errorf("nil credential library after create") + } + + if err := setFromVaultSshCertificateCredentialLibraryResponseMap(d, cr.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceCredentialLibraryReadVaultSshCertificate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + cr, err := client.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error reading credential library: %v", err) + } + if cr == nil { + return diag.Errorf("credential library nil after read") + } + + if err := setFromVaultSshCertificateCredentialLibraryResponseMap(d, cr.GetResponse().Map); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceCredentialLibraryUpdateVaultSshCertificate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + var opts []credentiallibraries.Option + if d.HasChange(NameKey) { + opts = append(opts, credentiallibraries.DefaultName()) + nameVal, ok := d.GetOk(NameKey) + if ok { + opts = append(opts, credentiallibraries.WithName(nameVal.(string))) + } + } + if d.HasChange(DescriptionKey) { + opts = append(opts, credentiallibraries.DefaultDescription()) + descVal, ok := d.GetOk(DescriptionKey) + if ok { + opts = append(opts, credentiallibraries.WithDescription(descVal.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificatePathKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificatePathKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultCredentialLibraryPath(v.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateUsernameKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificateUsernameKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryUsername(v.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateKeyTypeKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyTypeKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyType(v.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateKeyBitsKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyBitsKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyBits(uint32(v.(int)))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateTtlKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificateTtlKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryTtl(v.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateKeyIdKey) { + v, ok := d.GetOk(credentialLibraryVaultSshCertificateKeyIdKey) + if ok { + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryKeyId(v.(string))) + } + } + if d.HasChange(credentialLibraryVaultSshCertificateCriticalOptionsKey) { + var oldCriticalOptions, newCriticalOptions map[string]interface{} + old, new := d.GetChange(credentialLibraryVaultSshCertificateCriticalOptionsKey) + + if old == nil { + old = map[string]interface{}{} + } + oldCriticalOptions = old.(map[string]interface{}) + + if new == nil { + new = map[string]interface{}{} + } + newCriticalOptions = new.(map[string]interface{}) + + newAttrs := []string{} + for k := range newCriticalOptions { + newAttrs = append(newAttrs, k) + } + + for oldAttr := range oldCriticalOptions { + isRemoved := true + for _, newAttr := range newAttrs { + if oldAttr == newAttr { + isRemoved = false + break + } + } + if isRemoved { + newCriticalOptions[oldAttr] = nil + } + } + + co := make(map[string]string, 0) + for k, v := range newCriticalOptions { + if v != nil { + co[k] = v.(string) + } + } + if len(co) == 0 { + co = nil + } + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryCriticalOptions(co)) + + } + if d.HasChange(credentialLibraryVaultSshCertificateExtensionsKey) { + var oldExtensions, newExtensions map[string]interface{} + old, new := d.GetChange(credentialLibraryVaultSshCertificateExtensionsKey) + + if old == nil { + old = map[string]interface{}{} + } + oldExtensions = old.(map[string]interface{}) + + if new == nil { + new = map[string]interface{}{} + } + newExtensions = new.(map[string]interface{}) + + newAttrs := []string{} + for k := range newExtensions { + newAttrs = append(newAttrs, k) + } + + for oldAttr := range oldExtensions { + isRemoved := true + for _, newAttr := range newAttrs { + if oldAttr == newAttr { + isRemoved = false + break + } + } + if isRemoved { + newExtensions[oldAttr] = nil + } + } + + e := make(map[string]string, 0) + for k, v := range newExtensions { + if v != nil { + e[k] = v.(string) + } + } + if len(e) == 0 { + e = nil + } + opts = append(opts, credentiallibraries.WithVaultSSHCertificateCredentialLibraryExtensions(e)) + } + + if len(opts) > 0 { + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + aur, err := client.Update(ctx, d.Id(), 0, opts...) + if err != nil { + return diag.Errorf("error updating credential library: %v", err) + } + + if err := setFromVaultSshCertificateCredentialLibraryResponseMap(d, aur.GetResponse().Map); err != nil { + return diag.Errorf("error setting credential library from response: %v", err) + } + } + + return nil +} + +func resourceCredentialLibraryDeleteVaultSshCertificate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentiallibraries.NewClient(md.client) + + _, err := client.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting credential library: %v", err) + } + + return nil +} diff --git a/internal/provider/resource_credential_library_vault_ssh_certificate_test.go b/internal/provider/resource_credential_library_vault_ssh_certificate_test.go new file mode 100644 index 00000000..0882468e --- /dev/null +++ b/internal/provider/resource_credential_library_vault_ssh_certificate_test.go @@ -0,0 +1,267 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/boundary/testing/vault" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + vaultSshCertCredResc = "boundary_credential_library_vault_ssh_certificate.example" + vaultSshCertCredExtCOResc = "boundary_credential_library_vault_ssh_certificate.ext_co_example" + vaultSshCertCredLibName = "foo" + vaultSshCertCredLibDesc = "the foo" + vaultSshCertCredLibPath = "/ssh/sign/foo" + vaultSshCertCredUsername = "bar" + vaultSshCertCredLibStringUpdate = "_random" + vaultSshCertCredKeyType = "ecdsa" + vaultSshCertCredKeyBits = 256 +) + +var vaultSshCertCredLibResource = fmt.Sprintf(` +resource "boundary_credential_library_vault_ssh_certificate" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" + username = "%s" + key_type = "ed25519" + +}`, vaultSshCertCredLibName, + vaultSshCertCredLibDesc, + vaultSshCertCredLibPath, + vaultSshCertCredUsername) + +var vaultSshCertCredLibResourceUpdate = fmt.Sprintf(` +resource "boundary_credential_library_vault_ssh_certificate" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" + username = "%s" + key_type = "%s" + key_bits = %d +}`, vaultSshCertCredLibName+vaultSshCertCredLibStringUpdate, + vaultSshCertCredLibDesc+vaultSshCertCredLibStringUpdate, + vaultSshCertCredLibPath+vaultSshCertCredLibStringUpdate, + vaultSshCertCredUsername+vaultSshCertCredLibStringUpdate, + vaultSshCertCredKeyType, + vaultSshCertCredKeyBits) + +var vaultSshCertCredLibResourceExtensionsCriticalOpts = fmt.Sprintf(` +resource "boundary_credential_library_vault_ssh_certificate" "ext_co_example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" + username = "%s" + key_type = "ed25519" + + extensions = { + permit-pty = "" + } + + critical_options = { + force-command = "/bin/foo" + } +}`, vaultSshCertCredLibName, + vaultSshCertCredLibDesc, + vaultSshCertCredLibPath, + vaultSshCertCredUsername) + +var vaultSshCertCredLibResourceExtensionsCriticalOptsUpdate = fmt.Sprintf(` +resource "boundary_credential_library_vault_ssh_certificate" "ext_co_example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" + username = "%s" + key_type = "ed25519" + + extensions = { + permit-pty = "" + permit-X11-forwarding = "" + } +}`, vaultSshCertCredLibName, + vaultSshCertCredLibDesc, + vaultSshCertCredLibPath, + vaultSshCertCredUsername) + +var vaultSshCertCredLibResourceExtensionsCriticalOptsUpdate2 = fmt.Sprintf(` +resource "boundary_credential_library_vault_ssh_certificate" "ext_co_example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_vault.example.id + path = "%s" + username = "%s" + key_type = "ed25519" + + extensions = { + permit-pty = "" + } + + critical_options = { + force-command = "/bin/foo" + source-address = "10.10.0.0/16" + } +}`, vaultSshCertCredLibName, + vaultSshCertCredLibDesc, + vaultSshCertCredLibPath, + vaultSshCertCredUsername) + +func TestAccCredentialLibraryVaultSshCertificate(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + vc := vault.NewTestVaultServer(t) + _, token := vc.CreateToken(t) + credStoreRes := vaultCredStoreResource(vc, + vaultCredStoreName, + vaultCredStoreDesc, + vaultCredStoreNamespace, + "www.original.com", + token, + true) + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialLibraryVaultSshCertificateResourceDestroy(t, provider), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultSshCertCredLibResource), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultSshCertCredResc, NameKey, vaultSshCertCredLibName), + resource.TestCheckResourceAttr(vaultSshCertCredResc, DescriptionKey, vaultSshCertCredLibDesc), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultPathKey, vaultSshCertCredLibPath), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultSshCertificateUsernameKey, vaultSshCertCredUsername), + + testAccCheckCredentialLibraryVaultSshCertificateResourceExists(provider, vaultSshCertCredResc), + ), + }, + importStep(vaultSshCertCredResc), + { + // update + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultSshCertCredLibResourceUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultSshCertCredResc, NameKey, vaultSshCertCredLibName+vaultSshCertCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultSshCertCredResc, DescriptionKey, vaultSshCertCredLibDesc+vaultSshCertCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultPathKey, vaultSshCertCredLibPath+vaultSshCertCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultSshCertificateUsernameKey, vaultSshCertCredUsername+vaultSshCertCredLibStringUpdate), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultSshCertificateKeyTypeKey, vaultSshCertCredKeyType), + resource.TestCheckResourceAttr(vaultSshCertCredResc, credentialLibraryVaultSshCertificateKeyBitsKey, strconv.Itoa(vaultSshCertCredKeyBits)), + + testAccCheckCredentialLibraryVaultSshCertificateResourceExists(provider, vaultSshCertCredResc), + ), + }, + importStep(vaultSshCertCredResc), + { + // create with extensions and critical options + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultSshCertCredLibResourceExtensionsCriticalOpts), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, NameKey, vaultSshCertCredLibName), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, DescriptionKey, vaultSshCertCredLibDesc), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultPathKey, vaultSshCertCredLibPath), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateUsernameKey, vaultSshCertCredUsername), + + testAccCheckCredentialLibraryVaultSshCertificateResourceExists(provider, vaultSshCertCredExtCOResc), + ), + }, + importStep(vaultSshCertCredExtCOResc), + { + // update with extensions and remove critical options + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultSshCertCredLibResourceExtensionsCriticalOptsUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, NameKey, vaultSshCertCredLibName), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, DescriptionKey, vaultSshCertCredLibDesc), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultPathKey, vaultSshCertCredLibPath), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateUsernameKey, vaultSshCertCredUsername), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateCriticalOptionsKey+".%", "0"), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateExtensionsKey+".%", "2"), + + testAccCheckCredentialLibraryVaultSshCertificateResourceExists(provider, vaultSshCertCredExtCOResc), + ), + }, + importStep(vaultSshCertCredExtCOResc), + { + // update with extensions and remove critical options + Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, vaultSshCertCredLibResourceExtensionsCriticalOptsUpdate2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, NameKey, vaultSshCertCredLibName), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, DescriptionKey, vaultSshCertCredLibDesc), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultPathKey, vaultSshCertCredLibPath), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateUsernameKey, vaultSshCertCredUsername), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateCriticalOptionsKey+".%", "2"), + resource.TestCheckResourceAttr(vaultSshCertCredExtCOResc, credentialLibraryVaultSshCertificateExtensionsKey+".%", "1"), + + testAccCheckCredentialLibraryVaultSshCertificateResourceExists(provider, vaultSshCertCredExtCOResc), + ), + }, + importStep(vaultSshCertCredExtCOResc), + }, + }) +} + +func testAccCheckCredentialLibraryVaultSshCertificateResourceExists(testProvider *schema.Provider, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + md := testProvider.Meta().(*metaData) + c := credentiallibraries.NewClient(md.client) + if _, err := c.Read(context.Background(), id); err != nil { + return fmt.Errorf("got an error reading %q: %w", id, err) + } + + return nil + } +} + +func testAccCheckCredentialLibraryVaultSshCertificateResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + if testProvider.Meta() == nil { + t.Fatal("got nil provider metadata") + } + md := testProvider.Meta().(*metaData) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "boundary_credential_library_vault": + id := rs.Primary.ID + + c := credentiallibraries.NewClient(md.client) + _, err := c.Read(context.Background(), id) + if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound { + return fmt.Errorf("didn't get a 404 when reading destroyed vault credential library %q: %v", id, err) + } + + default: + continue + } + } + return nil + } +}