diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 9955e8e6475f..2d48ff56c194 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -91,9 +91,10 @@ func TestResourcesSupportCustomTimeouts(t *testing.T) { } else if *resource.Timeouts.Read > 5*time.Minute { exceptionResources := map[string]bool{ // The key vault item resources have longer read timeout for mitigating issue: https://github.com/hashicorp/terraform-provider-azurerm/issues/11059. - "azurerm_key_vault_key": true, - "azurerm_key_vault_secret": true, - "azurerm_key_vault_certificate": true, + "azurerm_key_vault_key": true, + "azurerm_key_vault_secret": true, + "azurerm_key_vault_certificate": true, + "azurerm_key_vault_managed_hardware_security_module_key": true, } if !exceptionResources[resourceName] { t.Fatalf("Read timeouts shouldn't be more than 5 minutes, this indicates a bug which needs to be fixed") diff --git a/internal/services/managedhsm/client/client.go b/internal/services/managedhsm/client/client.go index 0da9356e36c2..5139d667dd56 100644 --- a/internal/services/managedhsm/client/client.go +++ b/internal/services/managedhsm/client/client.go @@ -24,6 +24,7 @@ type Client struct { // Data Plane DataPlaneClient *dataplane.BaseClient + DataPlaneManagedHSMClient *dataplane.BaseClient DataPlaneRoleAssignmentsClient *dataplane.RoleAssignmentsClient DataPlaneRoleDefinitionsClient *dataplane.RoleDefinitionsClient DataPlaneSecurityDomainsClient *dataplane.HSMSecurityDomainClient @@ -39,6 +40,9 @@ func NewClient(o *common.ClientOptions) (*Client, error) { managementClient := dataplane.New() o.ConfigureClient(&managementClient.Client, o.KeyVaultAuthorizer) + managementHSMClient := dataplane.New() + o.ConfigureClient(&managementHSMClient.Client, o.ManagedHSMAuthorizer) + securityDomainClient := dataplane.NewHSMSecurityDomainClient() o.ConfigureClient(&securityDomainClient.Client, o.ManagedHSMAuthorizer) @@ -54,6 +58,7 @@ func NewClient(o *common.ClientOptions) (*Client, error) { // Data Plane DataPlaneClient: &managementClient, + DataPlaneManagedHSMClient: &managementHSMClient, DataPlaneSecurityDomainsClient: &securityDomainClient, DataPlaneRoleDefinitionsClient: &roleDefinitionsClient, DataPlaneRoleAssignmentsClient: &roleAssignmentsClient, diff --git a/internal/services/managedhsm/client/helpers.go b/internal/services/managedhsm/client/helpers.go new file mode 100644 index 000000000000..0c18d4abdc9b --- /dev/null +++ b/internal/services/managedhsm/client/helpers.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package client + +import ( + "context" + "fmt" + "net/url" + "strings" + "sync" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" +) + +type cacheItem struct { + ID managedhsms.ManagedHSMId + BaseURI string +} + +type localCache struct { + mux sync.Locker + nameToItem map[string]cacheItem +} + +var defaultCache = &localCache{ + mux: &sync.Mutex{}, + nameToItem: map[string]cacheItem{}, +} + +func cacheKey(name string) string { + return strings.ToLower(name) +} + +func AddToCache(id managedhsms.ManagedHSMId, baseURI string) { + defaultCache.add(id, baseURI) +} + +func (l *localCache) add(id managedhsms.ManagedHSMId, baseURI string) { + l.mux.Lock() + defer l.mux.Unlock() + + l.nameToItem[cacheKey(id.ManagedHSMName)] = cacheItem{ + ID: id, + BaseURI: baseURI, + } +} + +func (l *localCache) get(name string) (cacheItem, bool) { + l.mux.Lock() + defer l.mux.Unlock() + + item, ok := l.nameToItem[cacheKey(name)] + return item, ok +} + +func RemoveFromCache(name string) { + defaultCache.remove(name) +} + +func (l *localCache) remove(name string) { + l.mux.Lock() + defer l.mux.Unlock() + + delete(l.nameToItem, cacheKey(name)) +} + +func (c *Client) BaseUriForManagedHSM(ctx context.Context, id managedhsms.ManagedHSMId) (*string, error) { + item, ok := defaultCache.get(id.ManagedHSMName) + if ok { + return &item.BaseURI, nil + } + + resp, err := c.ManagedHsmClient.Get(ctx, id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return nil, fmt.Errorf("managedHSM %s was not found", id) + } + return nil, fmt.Errorf("retrieving managedHSM %s: %+v", id, err) + } + + vaultUri := "" + if model := resp.Model; model != nil { + if model.Properties.HsmUri != nil { + vaultUri = *model.Properties.HsmUri + } + } + if vaultUri == "" { + return nil, fmt.Errorf("retrieving %s: `properties.VaultUri` was nil", id) + } + + defaultCache.add(id, vaultUri) + return &vaultUri, nil +} + +func (c *Client) ManagedHSMIDFromBaseUri(ctx context.Context, subscriptionId commonids.SubscriptionId, uri string) (*managedhsms.ManagedHSMId, error) { + name, err := parseNameFromBaseUrl(uri) + if err != nil { + return nil, err + } + + item, ok := defaultCache.get(*name) + if ok { + return &item.ID, nil + } + // fetch all managedhsms + opts := managedhsms.DefaultListBySubscriptionOperationOptions() + results, err := c.ManagedHsmClient.ListBySubscriptionComplete(ctx, subscriptionId, opts) + if err != nil { + return nil, fmt.Errorf("listing the managed HSM within %s: %+v", subscriptionId, err) + } + for _, item := range results.Items { + if item.Id == nil || item.Properties.HsmUri == nil { + continue + } + + // Populate the managed HSM into the cache + managedHSMID, err := managedhsms.ParseManagedHSMIDInsensitively(*item.Id) + if err != nil { + return nil, fmt.Errorf("parsing %q as a managed HSM ID: %+v", *item.Id, err) + } + hsmUri := *item.Properties.HsmUri + defaultCache.add(*managedHSMID, hsmUri) + } + + // Now that the cache has been repopulated, check if we have the managed HSM or not + if v, ok := defaultCache.get(*name); ok { + return &v.ID, nil + } + return nil, fmt.Errorf("not implemented") +} + +func (c *Client) ManagedHSMExists(ctx context.Context, id managedhsms.ManagedHSMId) (bool, error) { + _, ok := defaultCache.get(id.ManagedHSMName) + if ok { + return true, nil + } + + resp, err := c.ManagedHsmClient.Get(ctx, id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return false, nil + } + return false, fmt.Errorf("retrieving managedHSM %s: %+v", id, err) + } + + vaultUri := "" + if model := resp.Model; model != nil { + if model.Properties.HsmUri != nil { + vaultUri = *model.Properties.HsmUri + } + } + if vaultUri == "" { + return false, fmt.Errorf("retrieving %s: `properties.VaultUri` was nil", id) + } + + defaultCache.add(id, vaultUri) + return true, nil +} + +func parseNameFromBaseUrl(input string) (*string, error) { + uri, err := url.Parse(input) + if err != nil { + return nil, err + } + + // https://the-hsm.managedhsm.azure.net + // https://the-hsm.managedhsm.microsoftazure.de + // https://the-hsm.managedhsm.usgovcloudapi.net + // https://the-hsm.managedhsm.cloudapi.microsoft + // https://the-hsm.managedhsm.azure.cn + + segments := strings.Split(uri.Host, ".") + if len(segments) < 3 || segments[1] != "managedhsm" { + return nil, fmt.Errorf("expected a URI in the format `the-managedhsm-name.managedhsm.**` but got %q", uri.Host) + } + return &segments[0], nil +} diff --git a/internal/services/managedhsm/custompollers/recover_key_poller.go b/internal/services/managedhsm/custompollers/recover_key_poller.go new file mode 100644 index 000000000000..e542a0680013 --- /dev/null +++ b/internal/services/managedhsm/custompollers/recover_key_poller.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package custompollers + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" +) + +var _ pollers.PollerType = &recoverKeyPoller{} + +func NewRecoverKeyPoller(uri string) pollers.PollerType { + return &recoverKeyPoller{ + uri: uri, + } +} + +type recoverKeyPoller struct { + uri string +} + +func (p *recoverKeyPoller) Poll(ctx context.Context) (*pollers.PollResult, error) { + + res := &pollers.PollResult{ + PollInterval: time.Second * 20, + Status: pollers.PollingStatusInProgress, + } + conn, err := http.Get(p.uri) + if err != nil { + log.Printf("[DEBUG] Didn't find KeyVault secret at %q", p.uri) + return res, fmt.Errorf("checking secret at %q: %s", p.uri, err) + } + + defer conn.Body.Close() + if response.WasNotFound(conn) { + res.Status = pollers.PollingStatusSucceeded + return res, nil + } + + res.Status = pollers.PollingStatusSucceeded + return res, nil +} diff --git a/internal/services/managedhsm/internal.go b/internal/services/managedhsm/internal.go new file mode 100644 index 000000000000..9b7869f1cff3 --- /dev/null +++ b/internal/services/managedhsm/internal.go @@ -0,0 +1,133 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedhsm + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type deleteAndPurgeNestedItem interface { + DeleteNestedItem(ctx context.Context) (*http.Response, error) + NestedItemHasBeenDeleted(ctx context.Context) (*http.Response, error) + PurgeNestedItem(ctx context.Context) (*http.Response, error) + NestedItemHasBeenPurged(ctx context.Context) (*http.Response, error) +} + +func deleteAndOptionallyPurge(ctx context.Context, description string, shouldPurge bool, helper deleteAndPurgeNestedItem) error { + timeout, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("context is missing a timeout") + } + + log.Printf("[DEBUG] Deleting %s..", description) + if resp, err := helper.DeleteNestedItem(ctx); err != nil { + if response.WasNotFound(resp) { + return nil + } + + return fmt.Errorf("deleting %s: %+v", description, err) + } + log.Printf("[DEBUG] Waiting for %s to finish deleting..", description) + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{"InProgress"}, + Target: []string{"NotFound"}, + Refresh: func() (interface{}, string, error) { + item, err := helper.NestedItemHasBeenDeleted(ctx) + if err != nil { + if response.WasNotFound(item) { + return item, "NotFound", nil + } + + return nil, "Error", err + } + + return item, "InProgress", nil + }, + ContinuousTargetOccurence: 3, + PollInterval: 5 * time.Second, + Timeout: time.Until(timeout), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for %s to be deleted: %+v", description, err) + } + log.Printf("[DEBUG] Deleted %s.", description) + + if !shouldPurge { + log.Printf("[DEBUG] Skipping purging of %s as opted-out..", description) + return nil + } + + log.Printf("[DEBUG] Purging %s..", description) + err := pluginsdk.Retry(time.Until(timeout), func() *pluginsdk.RetryError { + _, err := helper.PurgeNestedItem(ctx) + if err == nil { + return nil + } + if strings.Contains(err.Error(), "is currently being deleted") { + return pluginsdk.RetryableError(fmt.Errorf("%s is currently being deleted, retrying", description)) + } + return pluginsdk.NonRetryableError(fmt.Errorf("purging of %s : %+v", description, err)) + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Waiting for %s to finish purging..", description) + stateConf = &pluginsdk.StateChangeConf{ + Pending: []string{"InProgress"}, + Target: []string{"NotFound"}, + Refresh: func() (interface{}, string, error) { + item, err := helper.NestedItemHasBeenPurged(ctx) + if err != nil { + if response.WasNotFound(item) { + return item, "NotFound", nil + } + + return nil, "Error", err + } + + return item, "InProgress", nil + }, + ContinuousTargetOccurence: 3, + PollInterval: 5 * time.Second, + Timeout: time.Until(timeout), + } + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for %s to finish purging: %+v", description, err) + } + log.Printf("[DEBUG] Purged %s.", description) + + return nil +} + +func expandTags(tags map[string]string) map[string]*string { + if tags == nil { + return nil + } + res := map[string]*string{} + for k, v := range tags { + res[k] = &v + } + return res +} + +func flattenTags(tags map[string]*string) map[string]string { + if tags == nil { + return nil + } + res := map[string]string{} + for k, v := range tags { + res[k] = pointer.From(v) + } + return res +} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource.go new file mode 100644 index 000000000000..9dff342733c3 --- /dev/null +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource.go @@ -0,0 +1,865 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedhsm + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "log" + "math/big" + "net/http" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest/date" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-sdk/resource-manager/keyvault/2023-07-01/managedhsms" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + clientPackage "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/client" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/custompollers" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" + hsmValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tags" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" + "github.com/tombuildsstuff/kermit/sdk/keyvault/7.4/keyvault" + "golang.org/x/crypto/ssh" +) + +type AutomaticModel struct { + DurationAfterCreation string `tfschema:"duration_after_creation"` + DurationBeforeExpiry string `tfschema:"duration_before_expiry"` +} +type RotationPolicyModel struct { + Automatic []AutomaticModel `tfschema:"automatic"` + ExpireAfterDuration string `tfschema:"expire_after_duration"` + // NotifyBeforeExpiry string `tfschema:"notify_before_expiry"` +} +type KeyVaultManagedHardwareSecurityModuleKeyModel struct { + Curve string `tfschema:"curve"` + E string `tfschema:"e"` + ExpirationDate string `tfschema:"expiration_date"` + KeyOptions []string `tfschema:"key_options"` + KeySize int `tfschema:"key_size"` + KeyType string `tfschema:"key_type"` + ManagedHsmId string `tfschema:"managed_hsm_id"` + N string `tfschema:"n"` + Name string `tfschema:"name"` + NotUsableBeforeDate string `tfschema:"not_usable_before_date"` + PublicKeyOpenssh string `tfschema:"public_key_openssh"` + PublicKeyPem string `tfschema:"public_key_pem"` + ResourceId string `tfschema:"resource_id"` + ResourceVersionlessId string `tfschema:"resource_versionless_id"` + RotationPolicy []RotationPolicyModel `tfschema:"rotation_policy"` + Tags map[string]string `tfschema:"tags"` + Version string `tfschema:"version"` + VersionlessId string `tfschema:"versionless_id"` + X string `tfschema:"x"` + Y string `tfschema:"y"` +} + +type KeyVaultManagedHardwareSecurityModuleKeyResouece struct { +} + +// CustomizeDiff implements sdk.ResourceWithCustomizeDiff. +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: time.Minute * 30, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + if meta.ResourceData == nil { + return nil + } + + oldRaw, newRaw := meta.ResourceData.GetChange("expiration_date") + + // Parse old and new expiration dates + oldDate, err1 := time.Parse(time.RFC3339, oldRaw.(string)) + newDate, err2 := time.Parse(time.RFC3339, newRaw.(string)) + if err1 == nil || err2 == nil { + // Compare old and new expiration dates + if newDate.Before(oldDate) { + // If the new expiration date is before te old, force new resource + return meta.ResourceDiff.ForceNew("expiration_date") + } + } + + return nil + }, + } +} + +// CustomImporter implements sdk.ResourceWithCustomImporter. +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) CustomImporter() sdk.ResourceRunFunc { + return func(ctx context.Context, meta sdk.ResourceMetaData) error { + id, err := parse.ParseManagedHSMKeyID(meta.ResourceData.Id()) + if err != nil { + return err + } + + subscriptionResourceId := commonids.NewSubscriptionID(meta.Client.Account.SubscriptionId) + mHSMID, err := meta.Client.ManagedHSMs.ManagedHSMIDFromBaseUri(ctx, subscriptionResourceId, id.HSMBaseUrl) + if err != nil { + return fmt.Errorf("retrieving the Resource ID the managed HSM at URL %q: %s", id.HSMBaseUrl, err) + } + meta.ResourceData.Set("managed_hsm_id", mHSMID.ID()) + return nil + } +} + +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: hsmValidate.NestedItemName, + }, + + "managed_hsm_id": commonschema.ResourceIDReferenceRequiredForceNew(&managedhsms.ManagedHSMId{}), + + "key_type": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + // turns out Azure's *really* sensitive about the casing of these + // issue: https://github.com/Azure/azure-rest-api-specs/issues/1739 + ValidateFunc: validation.StringInSlice([]string{ + string(keyvault.JSONWebKeyTypeECHSM), + string(keyvault.JSONWebKeyTypeRSAHSM), + string(keyvault.JSONWebKeyTypeOctHSM), + }, false), + }, + + "key_size": { + Type: pluginsdk.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(0, 4096), + ConflictsWith: []string{"curve"}, + }, + + "key_options": { + // API Response order not stable + Type: pluginsdk.TypeSet, + Set: pluginsdk.HashString, + Required: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + // turns out Azure's *really* sensitive about the casing of these + // issue: https://github.com/Azure/azure-rest-api-specs/issues/1739 + ValidateFunc: validation.StringInSlice([]string{ + string(keyvault.JSONWebKeyOperationDecrypt), + string(keyvault.JSONWebKeyOperationEncrypt), + string(keyvault.JSONWebKeyOperationImport), + string(keyvault.JSONWebKeyOperationExport), + string(keyvault.JSONWebKeyOperationSign), + string(keyvault.JSONWebKeyOperationUnwrapKey), + string(keyvault.JSONWebKeyOperationVerify), + string(keyvault.JSONWebKeyOperationWrapKey), + }, false), + }, + }, + + "curve": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + DiffSuppressFunc: func(k, old, newVal string, d *pluginsdk.ResourceData) bool { + return old == "SECP256K1" && newVal == string(keyvault.JSONWebKeyCurveNameP256K) + }, + ValidateFunc: func() pluginsdk.SchemaValidateFunc { + out := []string{ + string(keyvault.JSONWebKeyCurveNameP256), + string(keyvault.JSONWebKeyCurveNameP256K), + string(keyvault.JSONWebKeyCurveNameP384), + string(keyvault.JSONWebKeyCurveNameP521), + } + return validation.StringInSlice(out, false) + }(), + // TODO: the curve name should probably be mandatory for EC in the future, + // but handle the diff so that we don't break existing configurations and + // imported EC keys + ConflictsWith: []string{"key_size"}, + }, + + "not_usable_before_date": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + }, + + "expiration_date": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsRFC3339Time, + }, + + "rotation_policy": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "expire_after_duration": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601DurationBetween("P28D", "P100Y"), + AtLeastOneOf: []string{ + "rotation_policy.0.expire_after_duration", + "rotation_policy.0.automatic", + }, + RequiredWith: []string{ + "rotation_policy.0.expire_after_duration", + }, + }, + + "automatic": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "duration_after_creation": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601Duration, + AtLeastOneOf: []string{ + "rotation_policy.0.automatic.0.duration_after_creation", + "rotation_policy.0.automatic.0.duration_before_expiry", + }, + }, + "duration_before_expiry": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validate.ISO8601Duration, + AtLeastOneOf: []string{ + "rotation_policy.0.automatic.0.duration_after_creation", + "rotation_policy.0.automatic.0.duration_before_expiry", + }, + }, + }, + }, + }, + }, + }, + }, + + "tags": tags.Schema(), + } +} + +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "version": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "versionless_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "n": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "e": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "x": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "y": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "public_key_pem": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "public_key_openssh": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "resource_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "resource_versionless_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) ResourceType() string { + return "azurerm_key_vault_managed_hardware_security_module_key" +} + +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) IDValidationFunc() func(interface{}, string) ([]string, []error) { + return hsmValidate.NestedItemId +} + +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) ModelObject() interface{} { + return &KeyVaultManagedHardwareSecurityModuleKeyModel{} +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: time.Minute * 30, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + managedHSMClient := meta.Client.ManagedHSMs + client := managedHSMClient.DataPlaneManagedHSMClient + + var model KeyVaultManagedHardwareSecurityModuleKeyModel + if err := meta.Decode(&model); err != nil { + return err + } + + name := model.Name + managedHSMID, err := managedhsms.ParseManagedHSMID(model.ManagedHsmId) + if err != nil { + return err + } + + managedHSMBaseUri, err := managedHSMClient.BaseUriForManagedHSM(ctx, *managedHSMID) + if err != nil { + return fmt.Errorf("looking up Key %q vault url from id %q: %+v", name, *managedHSMID, err) + } + + existing, err := client.GetKey(ctx, *managedHSMBaseUri, name, "") + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing Key %q (Managed HSM %q): %s", name, *managedHSMBaseUri, err) + } + } + + if existing.Key != nil && existing.Key.Kid != nil && *existing.Key.Kid != "" { + return tf.ImportAsExistsError("azurerm_key_vault_key", *existing.Key.Kid) + } + + keyType := model.KeyType + keyOptions := k.expandManagedHSMKeyOptions(&model) + + // TODO: support Importing Keys once this is fixed: + // https://github.com/Azure/azure-rest-api-specs/issues/1747 + parameters := keyvault.KeyCreateParameters{ + Kty: keyvault.JSONWebKeyType(keyType), + KeyOps: keyOptions, + KeyAttributes: &keyvault.KeyAttributes{ + Enabled: utils.Bool(true), + }, + + Tags: expandTags(model.Tags), + } + + if parameters.Kty == keyvault.JSONWebKeyTypeEC || parameters.Kty == keyvault.JSONWebKeyTypeECHSM { + curveName := model.Curve + parameters.Curve = keyvault.JSONWebKeyCurveName(curveName) + } else if parameters.Kty == keyvault.JSONWebKeyTypeRSA || parameters.Kty == keyvault.JSONWebKeyTypeRSAHSM { + parameters.KeySize = utils.Int32(int32(model.KeySize)) + } + + if v := model.NotUsableBeforeDate; v != "" { + notBeforeDate, _ := time.Parse(time.RFC3339, v) // validated by schema + notBeforeUnixTime := date.UnixTime(notBeforeDate) + parameters.KeyAttributes.NotBefore = ¬BeforeUnixTime + } + + if v := model.ExpirationDate; v != "" { + expirationDate, _ := time.Parse(time.RFC3339, v) // validated by schema + expirationUnixTime := date.UnixTime(expirationDate) + parameters.KeyAttributes.Expires = &expirationUnixTime + } + + if resp, err := client.CreateKey(ctx, *managedHSMBaseUri, name, parameters); err != nil { + if meta.Client.Features.KeyVault.RecoverSoftDeletedKeys && utils.ResponseWasConflict(resp.Response) { + recoveredKey, err := client.RecoverDeletedKey(ctx, *managedHSMBaseUri, name) + if err != nil { + return err + } + log.Printf("[DEBUG] Recovering Key %q with ID: %q", name, *recoveredKey.Key.Kid) + if kid := recoveredKey.Key.Kid; kid != nil { + recoveryPoller := custompollers.NewRecoverKeyPoller(*kid) + poller := pollers.NewPoller(recoveryPoller, time.Second*30, pollers.DefaultNumberOfDroppedConnectionsToAllow) + if err := poller.PollUntilDone(ctx); err != nil { + return fmt.Errorf("waiting for Managed HSM Secret %q to become available: %s", name, err) + } + } + } else { + return fmt.Errorf("creating Key: %+v", err) + } + } + + if len(model.RotationPolicy) > 0 { + policy := k.expandKeyVaultKeyRotationPolicy(model.RotationPolicy) + if respPolicy, err := client.UpdateKeyRotationPolicy(ctx, *managedHSMBaseUri, name, policy); err != nil { + if utils.ResponseWasForbidden(respPolicy.Response) { + return fmt.Errorf("current client lacks permissions to create Key Rotation Policy for Key %q (%q, Vault url: %q), please update this as described here: %s : %v", name, *managedHSMID, *managedHSMBaseUri, "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key#example-usage", err) + } + return fmt.Errorf("creating Key Rotation Policy: %+v", err) + } + } + + // "" indicates the latest version + read, err := client.GetKey(ctx, *managedHSMBaseUri, name, "") + if err != nil { + return err + } + + if read.Key == nil || read.Key.Kid == nil { + return fmt.Errorf("cannot read KeyVault Key '%s' (in Managed HSM '%s')", name, *managedHSMBaseUri) + } + keyId, err := parse.ParseManagedHSMKeyID(*read.Key.Kid) + if err != nil { + return err + } + meta.SetID(keyId) + + return nil + }, + } +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: time.Minute * 30, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + hsmsClient := meta.Client.ManagedHSMs + client := hsmsClient.DataPlaneManagedHSMClient + subscriptionId := meta.Client.Account.SubscriptionId + + id, err := parse.ParseManagedHSMKeyID(meta.ResourceData.Id()) + if err != nil { + return err + } + + subscriptionResourceId := commonids.NewSubscriptionID(subscriptionId) + managedHSMId, err := hsmsClient.ManagedHSMIDFromBaseUri(ctx, subscriptionResourceId, id.HSMBaseUrl) + if err != nil { + return fmt.Errorf("retrieving the Resource ID the Managed HSM at URL %q: %s", id.HSMBaseUrl, err) + } + if managedHSMId == nil { + log.Printf("[DEBUG] Unable to determine the Resource ID for the Managed HSM at URL %q - removing from state!", id.HSMBaseUrl) + return meta.MarkAsGone(id) + } + + ok, err := hsmsClient.ManagedHSMExists(ctx, *managedHSMId) + if err != nil { + return fmt.Errorf("checking if Managed HSM %q for Key %q in Vault at url %q exists: %v", *managedHSMId, id.Name, id.HSMBaseUrl, err) + } + if !ok { + log.Printf("[DEBUG] Key %q Managed HSM %q was not found in Managed HSM at URI %q - removing from state", id.Name, *managedHSMId, id.HSMBaseUrl) + return meta.MarkAsGone(id) + } + + resp, err := client.GetKey(ctx, id.HSMBaseUrl, id.Name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Key %q was not found in Managed HSM at URI %q - removing from state", id.Name, id.HSMBaseUrl) + return meta.MarkAsGone(id) + } + + return err + } + + var model KeyVaultManagedHardwareSecurityModuleKeyModel + model.Name = id.Name + model.ManagedHsmId = managedHSMId.ID() + + if key := resp.Key; key != nil { + model.KeyType = string(key.Kty) + model.KeyOptions = pointer.From(key.KeyOps) + + model.N = pointer.From(key.N) + model.E = pointer.From(key.N) + model.X = pointer.From(key.X) + model.Y = pointer.From(key.Y) + model.Curve = string(key.Crv) + + if key.N != nil { + nBytes, err := base64.RawURLEncoding.DecodeString(*key.N) + if err != nil { + return fmt.Errorf("could not decode N: %+v", err) + } + model.KeySize = len(nBytes) * 8 + } + } + + if attributes := resp.Attributes; attributes != nil { + if v := attributes.NotBefore; v != nil { + model.NotUsableBeforeDate = time.Time(*v).Format(time.RFC3339) + } + + if v := attributes.Expires; v != nil { + model.ExpirationDate = time.Time(*v).Format(time.RFC3339) + } + } + + // Computed + model.Version = id.Version + model.VersionlessId = id.VersionlessID() + if key := resp.Key; key != nil { + if key.Kty == keyvault.JSONWebKeyTypeRSA || key.Kty == keyvault.JSONWebKeyTypeRSAHSM { + nBytes, err := base64.RawURLEncoding.DecodeString(*key.N) + if err != nil { + return fmt.Errorf("failed to decode N: %+v", err) + } + eBytes, err := base64.RawURLEncoding.DecodeString(*key.E) + if err != nil { + return fmt.Errorf("failed to decode E: %+v", err) + } + publicKey := &rsa.PublicKey{ + N: big.NewInt(0).SetBytes(nBytes), + E: int(big.NewInt(0).SetBytes(eBytes).Uint64()), + } + err = k.readPublicKey(&model, publicKey) + if err != nil { + return fmt.Errorf("failed to read public key: %+v", err) + } + } else if key.Kty == keyvault.JSONWebKeyTypeEC || key.Kty == keyvault.JSONWebKeyTypeECHSM { + // do ec keys + xBytes, err := base64.RawURLEncoding.DecodeString(*key.X) + if err != nil { + return fmt.Errorf("failed to decode X: %+v", err) + } + yBytes, err := base64.RawURLEncoding.DecodeString(*key.Y) + if err != nil { + return fmt.Errorf("failed to decode Y: %+v", err) + } + publicKey := &ecdsa.PublicKey{ + X: big.NewInt(0).SetBytes(xBytes), + Y: big.NewInt(0).SetBytes(yBytes), + } + switch key.Crv { + case keyvault.JSONWebKeyCurveNameP256: + publicKey.Curve = elliptic.P256() + case keyvault.JSONWebKeyCurveNameP384: + publicKey.Curve = elliptic.P384() + case keyvault.JSONWebKeyCurveNameP521: + publicKey.Curve = elliptic.P521() + } + if publicKey.Curve != nil { + err = k.readPublicKey(&model, publicKey) + if err != nil { + return fmt.Errorf("failed to read public key: %+v", err) + } + } + } + } + + model.ResourceId = parse.NewKeyID(managedHSMId.SubscriptionId, managedHSMId.ResourceGroupName, managedHSMId.ManagedHSMName, id.Name, id.Version).ID() + + model.ResourceVersionlessId = parse.NewKeyVersionlessID(managedHSMId.SubscriptionId, managedHSMId.ResourceGroupName, managedHSMId.ManagedHSMName, id.Name).ID() + + respPolicy, err := client.GetKeyRotationPolicy(ctx, id.HSMBaseUrl, id.Name) + if err != nil { + if utils.ResponseWasForbidden(respPolicy.Response) { + + // If client is not authorized to access the policy: + return fmt.Errorf("current client lacks permissions to read Key Rotation Policy for Key %q (%q, Vault url: %q), please update this as described here: %s : %v", id.Name, *managedHSMId, id.HSMBaseUrl, "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key#example-usage", err) + } else if !utils.ResponseWasNotFound(respPolicy.Response) { + return err + } + } + + k.flattenKeyVaultKeyRotationPolicy(&model, respPolicy) + model.Tags = flattenTags(resp.Tags) + return meta.Encode(&model) + }, + } +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: time.Minute * 30, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + hsmsClient := meta.Client.ManagedHSMs + + id, err := parse.ParseManagedHSMKeyID(meta.ResourceData.Id()) + if err != nil { + return err + } + + var model KeyVaultManagedHardwareSecurityModuleKeyModel + if err = meta.Decode(&model); err != nil { + return err + } + + managedHSMId, err := managedhsms.ParseManagedHSMID(model.ManagedHsmId) + if err != nil { + return err + } + + clientPackage.AddToCache(*managedHSMId, id.HSMBaseUrl) + + ok, err := hsmsClient.ManagedHSMExists(ctx, *managedHSMId) + if err != nil { + return fmt.Errorf("checking if Managed HSM %q for Key %q in Vault at url %q exists: %v", *managedHSMId, id.Name, id.HSMBaseUrl, err) + } + if !ok { + log.Printf("[DEBUG] Key %q Managed HSM %q was not found in Managed HSM at URI %q - removing from state", id.Name, *managedHSMId, id.HSMBaseUrl) + return meta.MarkAsGone(id) + } + + parameters := keyvault.KeyUpdateParameters{} + + if meta.ResourceData.HasChange("key_options") { + keyOptions := k.expandManagedHSMKeyOptions(&model) + parameters.KeyOps = keyOptions + } + + if meta.ResourceData.HasChange("tags") { + parameters.Tags = expandTags(model.Tags) + } + + if meta.ResourceData.HasChanges("not_usable_before_date", "expiration_date") { + parameters.KeyAttributes = &keyvault.KeyAttributes{ + Enabled: pointer.To(true), + } + if v := model.NotUsableBeforeDate; v != "" { + notBeforeDate, _ := time.Parse(time.RFC3339, v) // validated by schema + notBeforeUnixTime := date.UnixTime(notBeforeDate) + parameters.KeyAttributes.NotBefore = ¬BeforeUnixTime + } + + if v := model.ExpirationDate; v != "" { + expirationDate, _ := time.Parse(time.RFC3339, v) // validated by schema + expirationUnixTime := date.UnixTime(expirationDate) + parameters.KeyAttributes.Expires = &expirationUnixTime + } + } + + if _, err = hsmsClient.DataPlaneManagedHSMClient.UpdateKey(ctx, id.HSMBaseUrl, id.Name, "", parameters); err != nil { + return fmt.Errorf("updating %q: %+v", id, err) + } + + if meta.ResourceData.HasChange("rotation_policy"); ok { + policy := k.expandKeyVaultKeyRotationPolicy(model.RotationPolicy) + if respPolicy, err := hsmsClient.DataPlaneManagedHSMClient.UpdateKeyRotationPolicy(ctx, id.HSMBaseUrl, id.Name, policy); err != nil { + if utils.ResponseWasForbidden(respPolicy.Response) { + return fmt.Errorf("current client lacks permissions to update Key Rotation Policy for Key %q (%q, Vault url: %q), please update this as described here: %s : %v", id.Name, *managedHSMId, id.HSMBaseUrl, "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key#example-usage", err) + } + return fmt.Errorf("creating Key Rotation Policy: %+v", err) + } + } + return nil + }, + } +} + +// Delete implements sdk.ResourceWithUpdate. +func (KeyVaultManagedHardwareSecurityModuleKeyResouece) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: time.Minute * 30, + Func: func(ctx context.Context, meta sdk.ResourceMetaData) error { + hsmsClient := meta.Client.ManagedHSMs + client := hsmsClient.DataPlaneManagedHSMClient + subscriptionId := meta.Client.Account.SubscriptionId + + id, err := parse.ParseManagedHSMKeyID(meta.ResourceData.Id()) + if err != nil { + return err + } + + subscriptionResourceId := commonids.NewSubscriptionID(subscriptionId) + managedHSMId, err := hsmsClient.ManagedHSMIDFromBaseUri(ctx, subscriptionResourceId, id.HSMBaseUrl) + if err != nil { + return fmt.Errorf("retrieving the Resource ID the Managed HSM at URL %q: %s", id.HSMBaseUrl, err) + } + if managedHSMId == nil { + return fmt.Errorf("unable to determine the Resource ID for the Managed HSM at URL %q", id.HSMBaseUrl) + } + + kv, err := hsmsClient.ManagedHsmClient.Get(ctx, *managedHSMId) + if err != nil { + if response.WasNotFound(kv.HttpResponse) { + log.Printf("[DEBUG] Key %q Managed HSM %q was not found in Managed HSM at URI %q - removing from state", id.Name, *managedHSMId, id.HSMBaseUrl) + return meta.MarkAsGone(id) + } + return fmt.Errorf("retrieving Managed HSM %q properties: %+v", *managedHSMId, err) + } + + shouldPurge := meta.Client.Features.KeyVault.PurgeSoftDeletedKeysOnDestroy + if shouldPurge && kv.Model != nil && utils.NormaliseNilableBool(kv.Model.Properties.EnablePurgeProtection) { + log.Printf("[DEBUG] cannot purge key %q because vault %q has purge protection enabled", id.Name, managedHSMId.String()) + shouldPurge = false + } + + description := fmt.Sprintf("Key %q (Managed HSM %q)", id.Name, id.HSMBaseUrl) + deleter := deleteAndPurgeKey{ + client: client, + mHSMUri: id.HSMBaseUrl, + name: id.Name, + } + if err := deleteAndOptionallyPurge(ctx, description, shouldPurge, deleter); err != nil { + return err + } + + return nil + }, + } +} + +var _ sdk.ResourceWithCustomImporter = KeyVaultManagedHardwareSecurityModuleKeyResouece{} +var _ sdk.ResourceWithUpdate = KeyVaultManagedHardwareSecurityModuleKeyResouece{} +var _ sdk.ResourceWithCustomizeDiff = KeyVaultManagedHardwareSecurityModuleKeyResouece{} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) expandManagedHSMKeyOptions(d *KeyVaultManagedHardwareSecurityModuleKeyModel) *[]keyvault.JSONWebKeyOperation { + results := make([]keyvault.JSONWebKeyOperation, 0) + + for _, option := range d.KeyOptions { + results = append(results, keyvault.JSONWebKeyOperation(option)) + } + + return &results +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) expandKeyVaultKeyRotationPolicy(v []RotationPolicyModel) keyvault.KeyRotationPolicy { + if len(v) == 0 { + return keyvault.KeyRotationPolicy{LifetimeActions: &[]keyvault.LifetimeActions{}} + } + + policy := v[0] + + var expiryTime *string = nil // needs to be set to nil if not set + if policy.ExpireAfterDuration != "" { + expiryTime = pointer.To(policy.ExpireAfterDuration) + } + + lifetimeActions := make([]keyvault.LifetimeActions, 0) + + if len(policy.Automatic) == 1 { + lifetimeActionRotate := keyvault.LifetimeActions{ + Action: &keyvault.LifetimeActionsType{ + Type: keyvault.ActionTypeRotate, + }, + Trigger: &keyvault.LifetimeActionsTrigger{}, + } + autoRotationRaw := policy.Automatic[0] + + if autoRotationRaw.DurationAfterCreation != "" { + lifetimeActionRotate.Trigger.TimeAfterCreate = &autoRotationRaw.DurationAfterCreation + } + + if autoRotationRaw.DurationBeforeExpiry != "" { + lifetimeActionRotate.Trigger.TimeBeforeExpiry = &autoRotationRaw.DurationBeforeExpiry + } + + lifetimeActions = append(lifetimeActions, lifetimeActionRotate) + } + + return keyvault.KeyRotationPolicy{ + LifetimeActions: &lifetimeActions, + Attributes: &keyvault.KeyRotationPolicyAttributes{ + ExpiryTime: expiryTime, + }, + } +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) flattenKeyVaultKeyRotationPolicy(model *KeyVaultManagedHardwareSecurityModuleKeyModel, input keyvault.KeyRotationPolicy) { + if input.LifetimeActions == nil && (input.Attributes == nil || pointer.From(input.Attributes.ExpiryTime) == "") { + return + } + + var policy RotationPolicyModel + if input.Attributes != nil { + policy.ExpireAfterDuration = pointer.From(input.Attributes.ExpiryTime) + } + + if input.LifetimeActions != nil { + for _, ltAction := range *input.LifetimeActions { + action := ltAction.Action + trigger := ltAction.Trigger + + if action != nil && trigger != nil { + if strings.EqualFold(string(action.Type), string(keyvault.ActionTypeRotate)) { + var autoRotation AutomaticModel + autoRotation.DurationAfterCreation = pointer.From(trigger.TimeAfterCreate) + autoRotation.DurationBeforeExpiry = pointer.From(trigger.TimeBeforeExpiry) + policy.Automatic = append(policy.Automatic, autoRotation) + } + } + + } + } + + model.RotationPolicy = []RotationPolicyModel{policy} +} + +var _ deleteAndPurgeNestedItem = deleteAndPurgeKey{} + +type deleteAndPurgeKey struct { + client *keyvault.BaseClient + mHSMUri string + name string +} + +func (d deleteAndPurgeKey) DeleteNestedItem(ctx context.Context) (*http.Response, error) { + resp, err := d.client.DeleteKey(ctx, d.mHSMUri, d.name) + return resp.Response.Response, err +} + +func (d deleteAndPurgeKey) NestedItemHasBeenDeleted(ctx context.Context) (*http.Response, error) { + resp, err := d.client.GetKey(ctx, d.mHSMUri, d.name, "") + return resp.Response.Response, err +} + +func (d deleteAndPurgeKey) PurgeNestedItem(ctx context.Context) (*http.Response, error) { + resp, err := d.client.PurgeDeletedKey(ctx, d.mHSMUri, d.name) + return resp.Response, err +} + +func (d deleteAndPurgeKey) NestedItemHasBeenPurged(ctx context.Context) (*http.Response, error) { + resp, err := d.client.GetDeletedKey(ctx, d.mHSMUri, d.name) + return resp.Response.Response, err +} + +func (k KeyVaultManagedHardwareSecurityModuleKeyResouece) readPublicKey(model *KeyVaultManagedHardwareSecurityModuleKeyModel, pubKey interface{}) error { + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return fmt.Errorf("failed to marshal public key error: %s", err) + } + pubKeyPemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + } + + model.PublicKeyPem = string(pem.EncodeToMemory(pubKeyPemBlock)) + + sshPubKey, err := ssh.NewPublicKey(pubKey) + if err == nil { + // Not all EC types can be SSH keys, so we'll produce this only + // if an appropriate type was selected. + sshPubKeyBytes := ssh.MarshalAuthorizedKey(sshPubKey) + model.PublicKeyOpenssh = string(sshPubKeyBytes) + } else { + model.PublicKeyOpenssh = "" + } + return nil +} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource_test.go new file mode 100644 index 000000000000..0296e8d2645d --- /dev/null +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_key_resource_test.go @@ -0,0 +1,245 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedhsm_test + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type KeyVaultManagedHSMKeyResource struct{} + +// real test nested in TestAccKeyVaultManagedHardwareSecurityModule, only provide Exists logic here +func (k KeyVaultManagedHSMKeyResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.ParseManagedHSMKeyID(state.ID) + if err != nil { + return nil, err + } + resp, err := client.ManagedHSMs.DataPlaneManagedHSMClient.GetKey(ctx, id.HSMBaseUrl, id.Name, id.Version) + if err != nil { + return nil, fmt.Errorf("retrieving Managed HSM Key %s: %+v", id, err) + } + return utils.Bool(resp.Attributes != nil), nil +} + +func (k KeyVaultManagedHSMKeyResource) template(data acceptance.TestData, purge bool) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_deleted_keys_on_destroy = "%[3]t" + recover_soft_deleted_key_vaults = true + } + } +} + +data "azurerm_client_config" "current" { +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-KV-%[1]d" + location = "%[2]s" +} + +resource "azurerm_key_vault" "test" { + name = "acc%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + certificate_permissions = [ + "Create", + "Delete", + "DeleteIssuers", + "Get", + "Purge", + "Update" + ] + } +} + +resource "azurerm_key_vault_certificate" "cert" { + count = 3 + name = "acchsmcert${count.index}" + key_vault_id = azurerm_key_vault.test.id + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + lifetime_action { + action { + action_type = "AutoRenew" + } + trigger { + days_before_expiry = 30 + } + } + secret_properties { + content_type = "application/x-pkcs12" + } + x509_certificate_properties { + extended_key_usage = [] + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + subject = "CN=hello-world" + validity_in_months = 12 + } + } +} +resource "azurerm_key_vault_managed_hardware_security_module" "test" { + name = "kvHsm%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku_name = "Standard_B1" + tenant_id = data.azurerm_client_config.current.tenant_id + admin_object_ids = [data.azurerm_client_config.current.object_id] + purge_protection_enabled = false + security_domain_key_vault_certificate_ids = [for cert in azurerm_key_vault_certificate.cert : cert.id] + security_domain_quorum = 2 +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "role_user" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = "21dbd100-6940-42c2-9190-5d6cb909625b" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "role_user" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = "706c03c7-69ad-33e5-2796-b3380d3a6e1a" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.role_user.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "role_officer" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = "515eb02d-2335-4d2d-92f2-b1cbdf9c3778" +} + +// need crypto officer role to purge keys +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "role_officer" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.test.hsm_uri + name = "d1a3242a-d521-11ee-9880-00155d316070" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.role_officer.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + `, data.RandomInteger, data.Locations.Primary, purge) +} + +func (k KeyVaultManagedHSMKeyResource) basic(data acceptance.TestData, purge bool) string { + + return fmt.Sprintf(` + +%s + +resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { + name = "key-%s" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + key_type = "EC-HSM" + + key_options = [ + "sign", + "verify", + ] + + depends_on = [ + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_user, + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_officer + ] +} +`, k.template(data, purge), data.RandomString) +} + +func (k KeyVaultManagedHSMKeyResource) update(data acceptance.TestData, purge bool) string { + + return fmt.Sprintf(` + + +%s + +resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { + name = "key-%s" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + key_type = "EC-HSM" + + key_options = [ + "sign", + "verify", + ] + + expiration_date = "2037-12-31T00:00:00Z" + tags = { + Env = "test" + } + + depends_on = [ + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_user, + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_officer + ] +} +`, k.template(data, purge), data.RandomString) +} + +func (k KeyVaultManagedHSMKeyResource) rotationPolicy(data acceptance.TestData, purge bool) string { + + return fmt.Sprintf(` + + +%s + +resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { + name = "key-%s" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + key_type = "RSA-HSM" + key_size = 2048 + + key_options = [ + "sign", + "verify", + "encrypt", + "decrypt", + ] + + rotation_policy { + automatic { + duration_before_expiry = "P30D" + } + + expire_after_duration = "P60D" + } + tags = { + Env = "test" + } + + depends_on = [ + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_user, + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_officer + ] +} +`, k.template(data, purge), data.RandomString) +} diff --git a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go index c9e602d10eb6..41544365dc10 100644 --- a/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go +++ b/internal/services/managedhsm/key_vault_managed_hardware_security_module_resource_test.go @@ -30,6 +30,7 @@ func TestAccKeyVaultManagedHardwareSecurityModule(t *testing.T) { "download": testAccKeyVaultManagedHardwareSecurityModule_download, "role_define": testAccKeyVaultManagedHardwareSecurityModule_roleDefinition, "role_assign": testAccKeyVaultManagedHardwareSecurityModule_roleAssignment, + "key": testAccKeyVaultManagedHardwareSecurityModule_keyBasic, }, }) } @@ -122,6 +123,45 @@ func testAccKeyVaultManagedHardwareSecurityModule_roleAssignment(t *testing.T) { }) } +func testAccKeyVaultManagedHardwareSecurityModule_keyBasic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module_key", "test") + r := KeyVaultManagedHSMKeyResource{} + + data.ResourceSequentialTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.template(data, false), + }, + { + Config: r.basic(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.rotationPolicy(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func testAccKeyVaultManagedHardwareSecurityModule_updateAndRequiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_key_vault_managed_hardware_security_module", "test") r := KeyVaultManagedHardwareSecurityModuleResource{} diff --git a/internal/services/managedhsm/parse/key.go b/internal/services/managedhsm/parse/key.go new file mode 100644 index 000000000000..e7904007415c --- /dev/null +++ b/internal/services/managedhsm/parse/key.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type KeyId struct { + SubscriptionId string + ResourceGroup string + ManagedHSMName string + Name string + VersionName string +} + +func NewKeyID(subscriptionId, resourceGroup, managedHSMName, name, versionName string) KeyId { + return KeyId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + ManagedHSMName: managedHSMName, + Name: name, + VersionName: versionName, + } +} + +func (id KeyId) String() string { + segments := []string{ + fmt.Sprintf("Version Name %q", id.VersionName), + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Managed H S M Name %q", id.ManagedHSMName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Key", segmentsStr) +} + +func (id KeyId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/managedHSMs/%s/keys/%s/versions/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.ManagedHSMName, id.Name, id.VersionName) +} + +// KeyID parses a Key ID into an KeyId struct +func KeyID(input string) (*KeyId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing %q as an Key ID: %+v", input, err) + } + + resourceId := KeyId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.ManagedHSMName, err = id.PopSegment("managedHSMs"); err != nil { + return nil, err + } + if resourceId.Name, err = id.PopSegment("keys"); err != nil { + return nil, err + } + if resourceId.VersionName, err = id.PopSegment("versions"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/managedhsm/parse/key_test.go b/internal/services/managedhsm/parse/key_test.go new file mode 100644 index 000000000000..7e8c690e75ce --- /dev/null +++ b/internal/services/managedhsm/parse/key_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = KeyId{} + +func TestKeyIDFormatter(t *testing.T) { + actual := NewKeyID("12345678-1234-9876-4563-123456789012", "resGroup1", "mhsm1", "key1", "version1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/version1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestKeyID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *KeyId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/", + Error: true, + }, + + { + // missing value for ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/", + Error: true, + }, + + { + // missing VersionName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/", + Error: true, + }, + + { + // missing value for VersionName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/version1", + Expected: &KeyId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ManagedHSMName: "mhsm1", + Name: "key1", + VersionName: "version1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.KEYVAULT/MANAGEDHSMS/MHSM1/KEYS/KEY1/VERSIONS/VERSION1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := KeyID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.ManagedHSMName != v.Expected.ManagedHSMName { + t.Fatalf("Expected %q but got %q for ManagedHSMName", v.Expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + if actual.VersionName != v.Expected.VersionName { + t.Fatalf("Expected %q but got %q for VersionName", v.Expected.VersionName, actual.VersionName) + } + } +} diff --git a/internal/services/managedhsm/parse/key_versionless.go b/internal/services/managedhsm/parse/key_versionless.go new file mode 100644 index 000000000000..b38edc8f845d --- /dev/null +++ b/internal/services/managedhsm/parse/key_versionless.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type KeyVersionlessId struct { + SubscriptionId string + ResourceGroup string + ManagedHSMName string + KeyName string +} + +func NewKeyVersionlessID(subscriptionId, resourceGroup, managedHSMName, keyName string) KeyVersionlessId { + return KeyVersionlessId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + ManagedHSMName: managedHSMName, + KeyName: keyName, + } +} + +func (id KeyVersionlessId) String() string { + segments := []string{ + fmt.Sprintf("Key Name %q", id.KeyName), + fmt.Sprintf("Managed H S M Name %q", id.ManagedHSMName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Key Versionless", segmentsStr) +} + +func (id KeyVersionlessId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/managedHSMs/%s/keys/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.ManagedHSMName, id.KeyName) +} + +// KeyVersionlessID parses a KeyVersionless ID into an KeyVersionlessId struct +func KeyVersionlessID(input string) (*KeyVersionlessId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("parsing %q as an KeyVersionless ID: %+v", input, err) + } + + resourceId := KeyVersionlessId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.ManagedHSMName, err = id.PopSegment("managedHSMs"); err != nil { + return nil, err + } + if resourceId.KeyName, err = id.PopSegment("keys"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/managedhsm/parse/key_versionless_test.go b/internal/services/managedhsm/parse/key_versionless_test.go new file mode 100644 index 000000000000..457699949cd5 --- /dev/null +++ b/internal/services/managedhsm/parse/key_versionless_test.go @@ -0,0 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = KeyVersionlessId{} + +func TestKeyVersionlessIDFormatter(t *testing.T) { + actual := NewKeyVersionlessID("12345678-1234-9876-4563-123456789012", "resGroup1", "mhsm1", "key1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestKeyVersionlessID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *KeyVersionlessId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/", + Error: true, + }, + + { + // missing value for ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/", + Error: true, + }, + + { + // missing KeyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/", + Error: true, + }, + + { + // missing value for KeyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1", + Expected: &KeyVersionlessId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + ManagedHSMName: "mhsm1", + KeyName: "key1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.KEYVAULT/MANAGEDHSMS/MHSM1/KEYS/KEY1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := KeyVersionlessID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.ManagedHSMName != v.Expected.ManagedHSMName { + t.Fatalf("Expected %q but got %q for ManagedHSMName", v.Expected.ManagedHSMName, actual.ManagedHSMName) + } + if actual.KeyName != v.Expected.KeyName { + t.Fatalf("Expected %q but got %q for KeyName", v.Expected.KeyName, actual.KeyName) + } + } +} diff --git a/internal/services/managedhsm/parse/managed_hsm_key_id.go b/internal/services/managedhsm/parse/managed_hsm_key_id.go new file mode 100644 index 000000000000..31e07bf4fe6f --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_key_id.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = ManagedHSMKeyID{} + +type ManagedHSMKeyID struct { + HSMBaseUrl string + Name string + Version string +} + +func NewManagedHSMKeyID(keyVaultBaseUrl string, name, version string) (*ManagedHSMKeyID, error) { + keyVaultUrl, err := url.Parse(keyVaultBaseUrl) + if err != nil || keyVaultBaseUrl == "" { + return nil, fmt.Errorf("parsing %q: %+v", keyVaultBaseUrl, err) + } + // (@jackofallops) - Log Analytics service adds the port number to the API returns, so we strip it here + if hostParts := strings.Split(keyVaultUrl.Host, ":"); len(hostParts) > 1 { + keyVaultUrl.Host = hostParts[0] + } + + if !strings.Contains(strings.ToLower(keyVaultBaseUrl), ".managedhsm.") { + return nil, fmt.Errorf("internal-error: only support Managed HSM IDs as Nested Items") + } + + return &ManagedHSMKeyID{ + HSMBaseUrl: keyVaultUrl.String(), + Name: name, + Version: version, + }, nil +} + +func (id ManagedHSMKeyID) ID() string { + // example: https://tharvey-managedhsm.managedhsm.azure.net/keys/key-bird/fdf067c93bbb4b22bff4d8b7a9a56217 + segments := []string{ + strings.TrimSuffix(id.HSMBaseUrl, "/"), + "keys", + id.Name, + } + if id.Version != "" { + segments = append(segments, id.Version) + } + return strings.TrimSuffix(strings.Join(segments, "/"), "/") +} + +func (id ManagedHSMKeyID) String() string { + components := []string{ + fmt.Sprintf("Base Url %q", id.HSMBaseUrl), + fmt.Sprintf("Type %q", "keys"), + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Version %q", id.Version), + } + return fmt.Sprintf("Managed HSM Nested Item %s", strings.Join(components, " / ")) +} + +func (id ManagedHSMKeyID) VersionlessID() string { + // example: https://tharvey-managedhsm.managedhsm.azure.net/keys/key-bird + segments := []string{ + strings.TrimSuffix(id.HSMBaseUrl, "/"), + "keys", + id.Name, + } + return strings.TrimSuffix(strings.Join(segments, "/"), "/") +} + +// ParseManagedHSMKeyID parses a managed HSM Nested Item ID (such as a Certificate, Key or Secret) +// containing a version into a NestedItemId object +func ParseManagedHSMKeyID(input string) (*ManagedHSMKeyID, error) { + item, err := parseNestedItemId(input) + if err != nil { + return nil, err + } + + if item.Version == "" { + return nil, fmt.Errorf("expected a managed HSM versioned ID but no version information was found in: %q", input) + } + + return item, nil +} + +// ParseOptionallyVersionedMangedHSMKeyID parses a managed HSM Nested Item ID (such as a Certificate, Key or Secret) +// optionally containing a version into a NestedItemId object +func ParseOptionallyVersionedMangedHSMKeyID(input string) (*ManagedHSMKeyID, error) { + return parseNestedItemId(input) +} + +func parseNestedItemId(id string) (*ManagedHSMKeyID, error) { + if !strings.Contains(strings.ToLower(id), ".managedhsm.") { + return nil, fmt.Errorf("internal-error: only support Managed HSM IDs as managed HSM Nested Items") + } + // versioned example: https://tharvey-managedhsm.managedhsm.azure.net/keys/bird/fdf067c93bbb4b22bff4d8b7a9a56217 + // versionless example: https://tharvey-managedhsm.managedhsm.azure.net/keys/bird/ + idURL, err := url.ParseRequestURI(id) + if err != nil { + return nil, fmt.Errorf("cannot parse azure managed HSM child ID: %s", err) + } + + path := idURL.Path + + path = strings.TrimPrefix(path, "/") + path = strings.TrimSuffix(path, "/") + + components := strings.Split(path, "/") + + if len(components) != 2 && len(components) != 3 { + return nil, fmt.Errorf("managed HSM nested item should contain 2 or 3 segments, found %d segment(s) in %q", len(components), id) + } + + if components[0] != "keys" { + return nil, fmt.Errorf("expected a managed HSM nested item of type 'keys' but got %q", components[0]) + } + + version := "" + if len(components) == 3 { + version = components[2] + } + + childId := ManagedHSMKeyID{ + HSMBaseUrl: fmt.Sprintf("https://%s/", idURL.Host), + Name: components[1], + Version: version, + } + + return &childId, nil +} diff --git a/internal/services/managedhsm/parse/managed_hsm_key_id_test.go b/internal/services/managedhsm/parse/managed_hsm_key_id_test.go new file mode 100644 index 000000000000..19f18919ab1e --- /dev/null +++ b/internal/services/managedhsm/parse/managed_hsm_key_id_test.go @@ -0,0 +1,219 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import "testing" + +func TestNewNestedItemID(t *testing.T) { + childName := "test" + childVersion := "testVersionString" + cases := []struct { + Scenario string + keyVaultBaseUrl string + Expected string + ExpectError bool + }{ + { + Scenario: "empty values", + keyVaultBaseUrl: "", + Expected: "", + ExpectError: true, + }, + { + Scenario: "valid, no port", + keyVaultBaseUrl: "https://test.managedhsm.azure.net", + Expected: "https://test.managedhsm.azure.net/keys/test/testVersionString", + ExpectError: false, + }, + { + Scenario: "valid, with port", + keyVaultBaseUrl: "https://test.managedhsm.azure.net:443", + Expected: "https://test.managedhsm.azure.net/keys/test/testVersionString", + ExpectError: false, + }, + { + Scenario: "mhsm valid, with port", + keyVaultBaseUrl: "https://test.managedhsm.azure.net:443", + Expected: "https://test.managedhsm.azure.net/keys/test/testVersionString", + ExpectError: true, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing %q", tc.keyVaultBaseUrl) + + id, err := NewManagedHSMKeyID(tc.keyVaultBaseUrl, childName, childVersion) + if err != nil { + if !tc.ExpectError { + t.Fatalf("Got error for New Resource ID '%s': %+v", tc.keyVaultBaseUrl, err) + return + } + t.Logf("[DEBUG] --> [Received Expected Error]: %+v", err) + continue + } + if id.ID() != tc.Expected { + t.Fatalf("Expected id for %q to be %q, got %q", tc.keyVaultBaseUrl, tc.Expected, id) + } + t.Logf("[DEBUG] --> [Valid Value]: %+v", tc.keyVaultBaseUrl) + } +} + +func TestParseNestedItemID(t *testing.T) { + cases := []struct { + Input string + Expected ManagedHSMKeyID + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https", + ExpectError: true, + }, + { + Input: "https://", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/invalidNestedItemObjectType/bird/fdf067c93bbb4b22bff4d8b7a9a56217", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/keys/castle/1492", + ExpectError: false, + Expected: ManagedHSMKeyID{ + Name: "castle", + HSMBaseUrl: "https://my-keyvault.managedhsm.azure.net/", + Version: "1492", + }, + }, + } + + for _, tc := range cases { + t.Logf("[DEBUG] Testing %q", tc.Input) + + secretId, err := ParseManagedHSMKeyID(tc.Input) + if err != nil { + if tc.ExpectError { + t.Logf("[DEBUG] --> [Received Expected Error]: %+v", err) + continue + } + + t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) + } + + if secretId == nil { + t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) + } + + if tc.Expected.HSMBaseUrl != secretId.HSMBaseUrl { + t.Fatalf("Expected 'HSMBaseUrl' to be '%s', got '%s' for ID '%s'", tc.Expected.HSMBaseUrl, secretId.HSMBaseUrl, tc.Input) + } + + if tc.Expected.Name != secretId.Name { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Name, secretId.Name, tc.Input) + } + + if tc.Expected.Version != secretId.Version { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Version, secretId.Version, tc.Input) + } + + if tc.Input != secretId.ID() { + t.Fatalf("Expected 'ID()' to be '%s', got '%s'", tc.Input, secretId.ID()) + } + t.Logf("[DEBUG] --> [Valid Value]: %+v", tc.Input) + } +} + +func TestParseOptionallyVersionedNestedItemID(t *testing.T) { + cases := []struct { + Input string + Expected ManagedHSMKeyID + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/secrets", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/invalidNestedItemObjectType/hello/world", + ExpectError: true, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/keys/bird", + ExpectError: false, + Expected: ManagedHSMKeyID{ + Name: "bird", + HSMBaseUrl: "https://my-keyvault.managedhsm.azure.net/", + Version: "", + }, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/keys/bird/fdf067c93bbb4b22bff4d8b7a9a56217", + ExpectError: false, + Expected: ManagedHSMKeyID{ + Name: "bird", + HSMBaseUrl: "https://my-keyvault.managedhsm.azure.net/", + Version: "fdf067c93bbb4b22bff4d8b7a9a56217", + }, + }, + { + Input: "https://my-keyvault.managedhsm.azure.net/keys/castle/1492", + ExpectError: false, + Expected: ManagedHSMKeyID{ + Name: "castle", + HSMBaseUrl: "https://my-keyvault.managedhsm.azure.net/", + Version: "1492", + }, + }, + } + + for _, tc := range cases { + t.Logf("[DEBUG] Testing %q", tc.Input) + + secretId, err := ParseOptionallyVersionedMangedHSMKeyID(tc.Input) + if err != nil { + if tc.ExpectError { + t.Logf("[DEBUG] --> [Received Expected Error]: %+v", err) + continue + } + + t.Fatalf("Got error for ID '%s': %+v", tc.Input, err) + } + + if secretId == nil { + t.Fatalf("Expected a SecretID to be parsed for ID '%s', got nil.", tc.Input) + } + + if tc.Expected.HSMBaseUrl != secretId.HSMBaseUrl { + t.Fatalf("Expected 'KeyVaultBaseUrl' to be '%s', got '%s' for ID '%s'", tc.Expected.HSMBaseUrl, secretId.HSMBaseUrl, tc.Input) + } + + if tc.Expected.Name != secretId.Name { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Name, secretId.Name, tc.Input) + } + + if tc.Expected.Version != secretId.Version { + t.Fatalf("Expected 'Version' to be '%s', got '%s' for ID '%s'", tc.Expected.Version, secretId.Version, tc.Input) + } + + if tc.Input != secretId.ID() { + t.Fatalf("Expected 'ID()' to be '%s', got '%s'", tc.Input, secretId.ID()) + } + t.Logf("[DEBUG] --> [Valid Value]: %+v", tc.Input) + } +} diff --git a/internal/services/managedhsm/registration.go b/internal/services/managedhsm/registration.go index 61c233c2b596..3c7e99271821 100644 --- a/internal/services/managedhsm/registration.go +++ b/internal/services/managedhsm/registration.go @@ -56,5 +56,6 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ KeyVaultMHSMRoleDefinitionResource{}, KeyVaultManagedHSMRoleAssignmentResource{}, + KeyVaultManagedHardwareSecurityModuleKeyResouece{}, } } diff --git a/internal/services/managedhsm/resourceids.go b/internal/services/managedhsm/resourceids.go new file mode 100644 index 000000000000..b42aeb0a12e8 --- /dev/null +++ b/internal/services/managedhsm/resourceids.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package managedhsm + +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=Key -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/version1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=KeyVersionless -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1 diff --git a/internal/services/managedhsm/validate/key_id.go b/internal/services/managedhsm/validate/key_id.go new file mode 100644 index 000000000000..813c239e33db --- /dev/null +++ b/internal/services/managedhsm/validate/key_id.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +func KeyID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.KeyID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/managedhsm/validate/key_id_test.go b/internal/services/managedhsm/validate/key_id_test.go new file mode 100644 index 000000000000..147f5320d807 --- /dev/null +++ b/internal/services/managedhsm/validate/key_id_test.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestKeyID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/", + Valid: false, + }, + + { + // missing value for ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/", + Valid: false, + }, + + { + // missing VersionName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/", + Valid: false, + }, + + { + // missing value for VersionName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1/versions/version1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.KEYVAULT/MANAGEDHSMS/MHSM1/KEYS/KEY1/VERSIONS/VERSION1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := KeyID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/internal/services/managedhsm/validate/key_versionless_id.go b/internal/services/managedhsm/validate/key_versionless_id.go new file mode 100644 index 000000000000..4705c42fe055 --- /dev/null +++ b/internal/services/managedhsm/validate/key_versionless_id.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" +) + +func KeyVersionlessID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.KeyVersionlessID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/managedhsm/validate/key_versionless_id_test.go b/internal/services/managedhsm/validate/key_versionless_id_test.go new file mode 100644 index 000000000000..7ce3e435917d --- /dev/null +++ b/internal/services/managedhsm/validate/key_versionless_id_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestKeyVersionlessID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/", + Valid: false, + }, + + { + // missing value for ManagedHSMName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/", + Valid: false, + }, + + { + // missing KeyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/", + Valid: false, + }, + + { + // missing value for KeyName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.KeyVault/managedHSMs/mhsm1/keys/key1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.KEYVAULT/MANAGEDHSMS/MHSM1/KEYS/KEY1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := KeyVersionlessID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/internal/services/managedhsm/validate/nested_item_name.go b/internal/services/managedhsm/validate/nested_item_name.go new file mode 100644 index 000000000000..72d00974c3ec --- /dev/null +++ b/internal/services/managedhsm/validate/nested_item_name.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + "regexp" +) + +func NestedItemName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + if matched := regexp.MustCompile(`^[0-9a-zA-Z-]+$`).Match([]byte(value)); !matched { + errors = append(errors, fmt.Errorf("%q may only contain alphanumeric characters and dashes", k)) + } + + return warnings, errors +} diff --git a/internal/services/managedhsm/validate/nested_item_name_test.go b/internal/services/managedhsm/validate/nested_item_name_test.go new file mode 100644 index 000000000000..cf9f6cb83863 --- /dev/null +++ b/internal/services/managedhsm/validate/nested_item_name_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import "testing" + +func TestNestedItemName(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "hello", + ExpectError: false, + }, + { + Input: "hello-world", + ExpectError: false, + }, + { + Input: "hello-world-21", + ExpectError: false, + }, + { + Input: "hello_world_21", + ExpectError: true, + }, + { + Input: "Hello-World", + ExpectError: false, + }, + { + Input: "20202020", + ExpectError: false, + }, + { + Input: "ABC123!@£", + ExpectError: true, + }, + } + + for _, tc := range cases { + _, errors := NestedItemName(tc.Input, "") + + hasError := len(errors) > 0 + + if tc.ExpectError && !hasError { + t.Fatalf("Expected the Key Vault Nested Item Name to trigger a validation error for '%s'", tc.Input) + } + } +} diff --git a/website/docs/r/key_vault_managed_hardware_security_module_key.html.markdown b/website/docs/r/key_vault_managed_hardware_security_module_key.html.markdown new file mode 100644 index 000000000000..24f1794832d6 --- /dev/null +++ b/website/docs/r/key_vault_managed_hardware_security_module_key.html.markdown @@ -0,0 +1,261 @@ +--- +subcategory: "Key Vault" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_key_vault_managed_hardware_security_module_key" +description: |- + Manages a Managed Hardware Security Module Key. + +--- + +# azurerm_key_vault_managed_hardware_security_module_key + +Manages a Managed Hardware Security Module Key. + +## Example Usage + +~> **Note:** To use this resource, your client should have RBAC roles with permissions like `Managed HSM Crypto Officer` or `Managed HSM Administrator` See [built-in-roles](https://learn.microsoft.com/en-us/azure/key-vault/managed-hsm/built-in-roles). + +~> **Note:** The Azure Provider includes a Feature Toggle which will purge a Managed Hardware Security Module Key resource on destroy, rather than the default soft-delete. See [`purge_soft_deleted_keys_on_destroy`](https://registry.terraform.io/providers/hashicorp/azurerm/l.example.docs/guides/features-block#purge_soft_deleted_keys_on_destroy) for more information. + +## Example Usage + +```hcl +provider "azurerm" { + features { + key_vault { + purge_soft_deleted_keys_on_destroy = true + recover_soft_deleted_keys = true + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + + +resource "azurerm_key_vault" "example" { + name = "acc240226165403061820" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Create", + "Delete", + "Get", + "Purge", + "Recover", + "Update", + "GetRotationPolicy", + ] + secret_permissions = [ + "Delete", + "Get", + "Set", + ] + certificate_permissions = [ + "Create", + "Delete", + "DeleteIssuers", + "Get", + "Purge", + "Update" + ] + } +} + +resource "azurerm_key_vault_certificate" "cert" { + count = 3 + name = "hsmcertexample${count.index}" + key_vault_id = azurerm_key_vault.example.id + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + lifetime_action { + action { + action_type = "AutoRenew" + } + trigger { + days_before_expiry = 30 + } + } + secret_properties { + content_type = "application/x-pkcs12" + } + x509_certificate_properties { + extended_key_usage = [] + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + subject = "CN=hello-world" + validity_in_months = 12 + } + } +} + +resource "azurerm_key_vault_managed_hardware_security_module" example { + name = "hsmexample" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku_name = "Standard_B1" + tenant_id = data.azurerm_client_config.current.tenant_id + admin_object_ids = [data.azurerm_client_config.current.object_id] + purge_protection_enabled = false + + security_domain_key_vault_certificate_ids = [for cert in azurerm_key_vault_certificate.cert : cert.id] + security_domain_quorum = 2 +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "role_user" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.example.hsm_uri + name = "21dbd100-6940-42c2-9190-5d6cb909625b" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "role_user" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.example.hsm_uri + name = "706c03c7-69ad-33e5-2796-b3380d3a6e1a" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.role_user.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "role_officer" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.example.hsm_uri + name = "515eb02d-2335-4d2d-92f2-b1cbdf9c3778" +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "role_officer" { + vault_base_url = azurerm_key_vault_managed_hardware_security_module.example.hsm_uri + name = "d1a3242a-d521-11ee-9880-00155d316070" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.role_officer.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + + +resource "azurerm_key_vault_managed_hardware_security_module_key" "example" { + name = "key-example" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.example.id + key_type = "EC-HSM" + + key_opts = [ + "sign", + "verify", + ] + rotation_policy { + expire_after_duration = "P66D" + + automatic { + duration_before_expiry = "P30D" + } + } + + depends_on = [ + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_user, + azurerm_key_vault_managed_hardware_security_module_role_assignment.role_officer + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the Managed Hardware Security Module Key. Changing this forces a new resource to be created. + +* `managed_hsm_id` - (Required) The ID of the Managed Hardware Security Module where the Key should be created. Changing this forces a new resource to be created. + +* `key_type` - (Required) Specifies the Key Type to use for this Managed Hardware Security Module Key. Possible values are `EC-HSM`, `RSA-HSM` and `oct-HSM`. Changing this forces a new resource to be created. + +* `key_size` - (Optional) Specifies the Size of the RSA key to create in bytes. For example, `1024` or `2048`. *Note*: This field is required if `key_type` is `RSA-HSM`. Changing this forces a new resource to be created. + +* `curve` - (Optional) Specifies the curve to use when creating an `EC` key. Possible values are `P-256`, `P-256K`, `P-384`, and `P-521`. This field will be required in a future release if `key_type` is `EC-HSM`. The API will default to `P-256` if nothing is specified. Changing this forces a new resource to be created. + +* `key_options` - (Required) A list of JSON web key operations. Possible values are `decrypt`, `encrypt`, `import`, `export`, `sign`, `unwrapKey`, `verify` and `wrapKey`. Please note these values are case sensitive. + +* `not_usable_before_date` - (Optional) Key not usable before the provided UTC datetime (Y-m-d'T'H:M:S'Z'). + +* `expiration_date` - (Optional) Expiration UTC datetime (Y-m-d'T'H:M:S'Z'). + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +* `rotation_policy` - (Optional) A `rotation_policy` block as defined below. + +--- + +A `rotation_policy` block supports the following: + +* `expire_after_duration` - (Optional) Expire a Managed Hardware Security Module Key after given duration as an [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). + +* `automatic` - (Optional) An `automatic` block as defined below. + +--- + +An `automatic` block supports the following: + +* `duration_after_creation` - (Optional) Rotate automatically at a duration after create as an [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). + +* `duration_before_expiry` - (Optional) Rotate automatically at a duration before expiry as an [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations). + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The Managed Hardware Security Module Key ID. + +* `resource_id` - The (Versioned) ID for this Managed Hardware Security Module Key. This property points to a specific version of a Managed Hardware Security Module Key, as such using this won't auto-rotate values if used in other Azure Services. + +* `resource_versionless_id` - The Versionless ID of the Managed Hardware Security Module Key. This property allows other Azure Services (that support it) to auto-rotate their value when the Managed Hardware Security Module Key is updated. + +* `version` - The current version of the Managed Hardware Security Module Key. + +* `versionless_id` - The Base ID of the Managed Hardware Security Module Key. + +* `n` - The RSA modulus of this Managed Hardware Security Module Key. + +* `e` - The RSA public exponent of this Managed Hardware Security Module Key. + +* `x` - The EC X component of this Managed Hardware Security Module Key. + +* `y` - The EC Y component of this Managed Hardware Security Module Key. + +* `public_key_pem` - The PEM encoded public key of this Managed Hardware Security Module Key. + +* `public_key_openssh` - The OpenSSH encoded public key of this Managed Hardware Security Module Key. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Managed Hardware Security Module Key. +* `update` - (Defaults to 30 minutes) Used when updating the Managed Hardware Security Module Key. +* `read` - (Defaults to 30 minutes) Used when retrieving the Managed Hardware Security Module Key. +* `delete` - (Defaults to 30 minutes) Used when deleting the Managed Hardware Security Module Key. + +## Import + +Managed Hardware Security Module Key which is Enabled can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_key_vault_managed_hardware_security_module_key.example "https://example-mhsm.managedhsm.azure.net/keys/key-example/versionofthekey" +```