From d7466ef28751f0a91bedc68301807152ee0f796e Mon Sep 17 00:00:00 2001 From: wuxu92 Date: Mon, 23 Sep 2024 10:25:45 +0800 Subject: [PATCH] add managed hsm key id support for cosmos db account --- .../hsm_key_acceptance_template.go | 158 +++++++++++++++++ .../key_vault_or_managed_hsm_key.go | 161 +++++++++++------- .../key_vault_or_managed_hsm_key_test.go | 34 ++-- .../cosmos/cosmosdb_account_resource.go | 55 +++--- .../cosmos/cosmosdb_account_resource_test.go | 85 +++++++++ .../netapp_account_encryption_resource.go | 9 +- website/docs/r/cosmosdb_account.html.markdown | 20 ++- 7 files changed, 419 insertions(+), 103 deletions(-) create mode 100644 internal/customermanagedkeys/hsm_key_acceptance_template.go diff --git a/internal/customermanagedkeys/hsm_key_acceptance_template.go b/internal/customermanagedkeys/hsm_key_acceptance_template.go new file mode 100644 index 0000000000000..f78eb62ff2b3a --- /dev/null +++ b/internal/customermanagedkeys/hsm_key_acceptance_template.go @@ -0,0 +1,158 @@ +package customermanagedkeys + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-uuid" +) + +// ManagedHSMKeyTempalte: Helper function to generate a template for HSM key acceptance tests +// Ensure `azurerm_client_config.current` datasource is defined before using this template. +// Verify there are no resource address conflicts in the caller of this template. +func ManagedHSMKeyTempalte(randomInteger int, randomString string, purgeProtectionEnabled bool, principalRefs []string) string { + roleAssignes := []string{} + for idx, principal := range principalRefs { + randomUUID, _ := uuid.GenerateUUID() + roleAssignes = append(roleAssignes, fmt.Sprintf(` +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "ra%[1]d" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = "%[3]s" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.encrypt-user.resource_manager_id + principal_id = %[2]s + + depends_on = [azurerm_key_vault_managed_hardware_security_module_key.test] +} + `, idx, principal, randomUUID)) + } + roleAssigneName1, _ := uuid.GenerateUUID() + roleAssigneName2, _ := uuid.GenerateUUID() + + return fmt.Sprintf(` +resource "azurerm_key_vault" "test" { + name = "acc%[2]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" + ] + } + tags = { + environment = "Production" + } +} + +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%[2]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 = %[6]t + soft_delete_retention_days = 7 + + security_domain_key_vault_certificate_ids = [for cert in azurerm_key_vault_certificate.cert : cert.id] + security_domain_quorum = 3 +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "crypto-officer" { + name = "515eb02d-2335-4d2d-92f2-b1cbdf9c3778" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "crypto-user" { + name = "21dbd100-6940-42c2-9190-5d6cb909625b" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id +} + +data "azurerm_key_vault_managed_hardware_security_module_role_definition" "encrypt-user" { + name = "33413926-3206-4cdd-b39a-83574fe37a17" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "client1" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = "%[4]s" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.crypto-officer.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_key_vault_managed_hardware_security_module_role_assignment" "client2" { + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + name = "%[5]s" + scope = "/keys" + role_definition_id = data.azurerm_key_vault_managed_hardware_security_module_role_definition.crypto-user.resource_manager_id + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { + name = "acctestHSMK-%[1]s" + managed_hsm_id = azurerm_key_vault_managed_hardware_security_module.test.id + key_type = "RSA-HSM" + key_size = 2048 + key_opts = ["unwrapKey", "wrapKey"] + + depends_on = [ + azurerm_key_vault_managed_hardware_security_module_role_assignment.client1, + azurerm_key_vault_managed_hardware_security_module_role_assignment.client2 + ] +} + +%[3]s +`, randomString, randomInteger, strings.Join(roleAssignes, "\n\n"), roleAssigneName1, roleAssigneName2, purgeProtectionEnabled) +} diff --git a/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go b/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go index 1279b140c46e0..2ce89464331ff 100644 --- a/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go +++ b/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go @@ -3,17 +3,28 @@ package customermanagedkeys import ( "fmt" - "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-sdk/sdk/environments" "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" hsmParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" ) +type VersionType int + +const ( + VersionTypeAny VersionType = iota + VersionTypeVersioned + VersionTypeVersionless +) + type KeyVaultOrManagedHSMKey struct { - KeyVaultKeyID *parse.NestedItemId - ManagedHSMKeyID *hsmParse.ManagedHSMDataPlaneVersionedKeyId - ManagedHSMKeyVersionlessID *hsmParse.ManagedHSMDataPlaneVersionlessKeyId + KeyVaultKeyId *parse.NestedItemId + ManagedHSMKeyId *hsmParse.ManagedHSMDataPlaneVersionedKeyId + ManagedHSMKeyVersionlessId *hsmParse.ManagedHSMDataPlaneVersionlessKeyId +} + +func (k *KeyVaultOrManagedHSMKey) IsSet() bool { + return k != nil && (k.KeyVaultKeyId != nil || k.ManagedHSMKeyId != nil || k.ManagedHSMKeyVersionlessId != nil) } func (k *KeyVaultOrManagedHSMKey) ID() string { @@ -21,91 +32,105 @@ func (k *KeyVaultOrManagedHSMKey) ID() string { return "" } - if k.KeyVaultKeyID != nil { - return k.KeyVaultKeyID.ID() + if k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.ID() } - if k.ManagedHSMKeyID != nil { - return k.ManagedHSMKeyID.ID() + if k.ManagedHSMKeyId != nil { + return k.ManagedHSMKeyId.ID() } - if k.ManagedHSMKeyVersionlessID != nil { - return k.ManagedHSMKeyVersionlessID.ID() + if k.ManagedHSMKeyVersionlessId != nil { + return k.ManagedHSMKeyVersionlessId.ID() } return "" } -func (k *KeyVaultOrManagedHSMKey) BaseUri() string { - if k.KeyVaultKeyID != nil { - return k.KeyVaultKeyID.KeyVaultBaseUrl +func (k *KeyVaultOrManagedHSMKey) KeyVaultKeyID() string { + if k != nil && k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.ID() } + return "" +} - if k.ManagedHSMKeyID != nil { - return k.ManagedHSMKeyID.BaseUri() +func (k *KeyVaultOrManagedHSMKey) ManagedHSMKeyID() string { + if k != nil && k.ManagedHSMKeyId != nil { + return k.ManagedHSMKeyId.ID() } - if k.ManagedHSMKeyVersionlessID != nil { - return k.ManagedHSMKeyVersionlessID.BaseUri() + if k != nil && k.ManagedHSMKeyVersionlessId != nil { + return k.ManagedHSMKeyVersionlessId.ID() } return "" } -func expandKeyvauleID(keyRaw string, hasVersion *bool) (*parse.NestedItemId, error) { - if pointer.From(hasVersion) { - if keyID, err := parse.ParseNestedKeyID(keyRaw); err == nil { - return keyID, nil - } else { - return nil, err - } +func (k *KeyVaultOrManagedHSMKey) BaseUri() string { + if k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.KeyVaultBaseUrl } - if keyID, err := parse.ParseOptionallyVersionedNestedKeyID(keyRaw); err == nil { - return keyID, nil - } else { + if k.ManagedHSMKeyId != nil { + return k.ManagedHSMKeyId.BaseUri() + } + + if k.ManagedHSMKeyVersionlessId != nil { + return k.ManagedHSMKeyVersionlessId.BaseUri() + } + + return "" +} + +func parseKeyvaultID(keyRaw string, requireVersion VersionType, _ environments.Api) (*parse.NestedItemId, error) { + keyID, err := parse.ParseOptionallyVersionedNestedKeyID(keyRaw) + if err != nil { return nil, err } + + if requireVersion == VersionTypeVersioned && keyID.Version == "" { + return nil, fmt.Errorf("expected a key vault versioned ID but no version information was found in: %q", keyRaw) + } + + if requireVersion == VersionTypeVersionless && keyID.Version != "" { + return nil, fmt.Errorf("expected a key vault versionless ID but version information was found in: %q", keyRaw) + } + + return keyID, nil } -func expandManagedHSMKey(keyRaw string, hasVersion *bool, hsmEnv environments.Api) (*hsmParse.ManagedHSMDataPlaneVersionedKeyId, *hsmParse.ManagedHSMDataPlaneVersionlessKeyId, error) { +func parseManagedHSMKey(keyRaw string, requireVersion VersionType, hsmEnv environments.Api) ( + versioned *hsmParse.ManagedHSMDataPlaneVersionedKeyId, versionless *hsmParse.ManagedHSMDataPlaneVersionlessKeyId, err error) { // if specified with hasVersion == True, then it has to be parsed as versionedKeyID var domainSuffix *string if hsmEnv != nil { domainSuffix, _ = hsmEnv.DomainSuffix() } - if hasVersion == nil || *hasVersion { - versioned, err := hsmParse.ManagedHSMDataPlaneVersionedKeyID(keyRaw, domainSuffix) - if err == nil { - return versioned, nil, nil - } - // if required versioned but got error - if pointer.From(hasVersion) { - return nil, nil, err - } - } - // versionless or optional version - if versionless, err := hsmParse.ManagedHSMDataPlaneVersionlessKeyID(keyRaw, domainSuffix); err == nil { - return nil, versionless, nil - } else { - return nil, nil, err + switch requireVersion { + case VersionTypeAny: + if versioned, err = hsmParse.ManagedHSMDataPlaneVersionedKeyID(keyRaw, domainSuffix); err != nil { + if versionless, err = hsmParse.ManagedHSMDataPlaneVersionlessKeyID(keyRaw, domainSuffix); err != nil { + return nil, nil, fmt.Errorf("parse Managed HSM both versionedID and versionlessID err for %s", keyRaw) + } + } + case VersionTypeVersioned: + versioned, err = hsmParse.ManagedHSMDataPlaneVersionedKeyID(keyRaw, domainSuffix) + case VersionTypeVersionless: + versionless, err = hsmParse.ManagedHSMDataPlaneVersionlessKeyID(keyRaw, domainSuffix) } -} -// hasVersion: -// - nil: both versioned or versionless are ok -// - true: must have version -// - false: must not have vesrion -func ExpandKeyVaultOrManagedHSMOptionallyVersionedKey(d interface{}, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { - return ExpandKeyVaultOrManagedHSMKey(d, nil, hsmEnv) + return versioned, versionless, err } -func ExpandKeyVaultOrManagedHSMKey(d interface{}, hasVersion *bool, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { - return ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(d, hasVersion, "key_vault_key_id", "managed_hsm_key_id", hsmEnv) +func ExpandKeyVaultOrManagedHSMKey(d interface{}, requireVersion VersionType, keyVaultEnv, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { + return ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(d, requireVersion, "key_vault_key_id", "managed_hsm_key_id", keyVaultEnv, hsmEnv) } -func ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(d interface{}, hasVersion *bool, keyVaultFieldName, hsmFieldName string, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { +// ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey +// d: should be one of *pluginsdk.ResourceData or map[string]interface{} +// if return nil, nil, it means no key_vault_key_id or managed_hsm_key_id is specified +func ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(d interface{}, requireVersion VersionType, keyVaultFieldName, hsmFieldName string, keyVaultEnv, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { key := &KeyVaultOrManagedHSMKey{} var err error var vaultKeyStr, hsmKeyStr string @@ -117,36 +142,40 @@ func ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(d interface{}, hasVersion * } } else if obj, ok := d.(map[string]interface{}); ok { if keyRaw, ok := obj[keyVaultFieldName]; ok { - vaultKeyStr = keyRaw.(string) - } else if keyRaw, ok = obj[hsmFieldName]; ok { - hsmKeyStr = keyRaw.(string) + vaultKeyStr, _ = keyRaw.(string) + } + if keyRaw, ok := obj[hsmFieldName]; ok { + hsmKeyStr, _ = keyRaw.(string) } } else { return nil, fmt.Errorf("not supported data type to parse CMK: %T", d) } - if vaultKeyStr != "" { - if key.KeyVaultKeyID, err = expandKeyvauleID(vaultKeyStr, hasVersion); err != nil { + switch { + case vaultKeyStr != "": + if key.KeyVaultKeyId, err = parseKeyvaultID(vaultKeyStr, requireVersion, keyVaultEnv); err != nil { return nil, err } - } else if hsmKeyStr != "" { - if key.ManagedHSMKeyID, key.ManagedHSMKeyVersionlessID, err = expandManagedHSMKey(hsmKeyStr, hasVersion, hsmEnv); err != nil { + case hsmKeyStr != "": + if key.ManagedHSMKeyId, key.ManagedHSMKeyVersionlessId, err = parseManagedHSMKey(hsmKeyStr, requireVersion, hsmEnv); err != nil { return nil, err } - } else { - return nil, fmt.Errorf("at least one of `%s` or `%s` should be specified", keyVaultFieldName, hsmFieldName) + default: + return nil, nil } return key, err } -func FlattenKeyVaultOrManagedHSMID(id string, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { +// FlattenKeyVaultOrManagedHSMID uses `KeyVaultOrManagedHSMKey.SetState()` to save the state, which this function is designed not to do. +func FlattenKeyVaultOrManagedHSMID(id string, keyVaultEnv, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { + _ = keyVaultEnv if id == "" { return nil, nil } key := &KeyVaultOrManagedHSMKey{} var err error - key.KeyVaultKeyID, err = parse.ParseOptionallyVersionedNestedItemID(id) + key.KeyVaultKeyId, err = parse.ParseOptionallyVersionedNestedKeyID(id) if err == nil { return key, nil } @@ -155,11 +184,11 @@ func FlattenKeyVaultOrManagedHSMID(id string, hsmEnv environments.Api) (*KeyVaul if hsmEnv != nil { domainSuffix, _ = hsmEnv.DomainSuffix() } - if key.ManagedHSMKeyID, err = hsmParse.ManagedHSMDataPlaneVersionedKeyID(id, domainSuffix); err == nil { + if key.ManagedHSMKeyId, err = hsmParse.ManagedHSMDataPlaneVersionedKeyID(id, domainSuffix); err == nil { return key, nil } - if key.ManagedHSMKeyVersionlessID, err = hsmParse.ManagedHSMDataPlaneVersionlessKeyID(id, domainSuffix); err == nil { + if key.ManagedHSMKeyVersionlessId, err = hsmParse.ManagedHSMDataPlaneVersionlessKeyID(id, domainSuffix); err == nil { return key, nil } diff --git a/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go b/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go index 0e6c3ece8032d..c8cc93d1a3e9d 100644 --- a/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go +++ b/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go @@ -4,9 +4,8 @@ import ( "reflect" "testing" - "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-sdk/sdk/environments" - cmk "github.com/hashicorp/terraform-provider-azurerm/internal/customermanagedkeys" + "github.com/hashicorp/terraform-provider-azurerm/internal/customermanagedkeys" "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" hsmParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" ) @@ -35,7 +34,7 @@ func buildHSMData(key, value string) interface{} { func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { type args struct { d interface{} - hasVersion *bool + hasVersion customermanagedkeys.VersionType keyVaultFieldName string hsmFieldName string hsmEnv environments.Api @@ -43,16 +42,17 @@ func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { tests := []struct { name string args args - want *cmk.KeyVaultOrManagedHSMKey + want *customermanagedkeys.KeyVaultOrManagedHSMKey wantErr bool }{ { + name: "success with key_vault_key_id", args: args{ d: buildKeyVaultData("key_vault_key_id", "https://test.keyvault.azure.net/keys/test-key-name"), keyVaultFieldName: "key_vault_key_id", }, - want: &cmk.KeyVaultOrManagedHSMKey{ - KeyVaultKeyID: &parse.NestedItemId{ + want: &customermanagedkeys.KeyVaultOrManagedHSMKey{ + KeyVaultKeyId: &parse.NestedItemId{ KeyVaultBaseUrl: "https://test.keyvault.azure.net/", NestedItemType: "keys", Name: "test-key-name", @@ -60,6 +60,7 @@ func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { }, }, { + name: "fail with wrong item type: cert", args: args{ d: buildKeyVaultData("key_vault_key_id", "https://test.keyvault.azure.net/certs/test-key-name"), keyVaultFieldName: "key_vault_key_id", @@ -67,20 +68,33 @@ func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { wantErr: true, }, { + name: "fail with wrong field name", args: args{ d: buildKeyVaultData("key_vault_key_url", "https://test.keyvault.azure.net/keys/test-key-name"), keyVaultFieldName: "key_vault_key_id", }, + want: nil, + wantErr: false, + }, + { + name: "fail with no version provided", + args: args{ + d: buildKeyVaultData("key_vault_key_id", "https://test.keyvault.azure.net/keys/test-key-name3"), + keyVaultFieldName: "key_vault_key_id", + hasVersion: customermanagedkeys.VersionTypeVersioned, + }, + want: nil, wantErr: true, }, { + name: "success with managed_hsm_key_id", args: args{ d: buildHSMData("managed_hsm_key_id", "https://test.managedhsm.azure.net/keys/test-key-name"), hsmFieldName: "managed_hsm_key_id", - hasVersion: pointer.To(false), + hasVersion: customermanagedkeys.VersionTypeVersionless, }, - want: &cmk.KeyVaultOrManagedHSMKey{ - ManagedHSMKeyVersionlessID: &hsmParse.ManagedHSMDataPlaneVersionlessKeyId{ + want: &customermanagedkeys.KeyVaultOrManagedHSMKey{ + ManagedHSMKeyVersionlessId: &hsmParse.ManagedHSMDataPlaneVersionlessKeyId{ ManagedHSMName: "test", DomainSuffix: "managedhsm.azure.net", KeyName: "test-key-name", @@ -90,7 +104,7 @@ func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t2 *testing.T) { - got, err := cmk.ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(tt.args.d, tt.args.hasVersion, tt.args.keyVaultFieldName, tt.args.hsmFieldName, tt.args.hsmEnv) + got, err := customermanagedkeys.ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey(tt.args.d, tt.args.hasVersion, tt.args.keyVaultFieldName, tt.args.hsmFieldName, nil, tt.args.hsmEnv) if (err != nil) != tt.wantErr { t2.Errorf("ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/services/cosmos/cosmosdb_account_resource.go b/internal/services/cosmos/cosmosdb_account_resource.go index 718ee3fd644d5..9ebc898247ec0 100644 --- a/internal/services/cosmos/cosmosdb_account_resource.go +++ b/internal/services/cosmos/cosmosdb_account_resource.go @@ -25,13 +25,14 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/customermanagedkeys" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/common" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/validate" - keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" keyVaultSuppress "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/suppress" keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + managedHsmValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" @@ -336,6 +337,15 @@ func resourceCosmosDbAccount() *pluginsdk.Resource { ForceNew: true, DiffSuppressFunc: keyVaultSuppress.DiffSuppressIgnoreKeyVaultKeyVersion, ValidateFunc: keyVaultValidate.VersionlessNestedItemId, + ConflictsWith: []string{"managed_hsm_key_id"}, + }, + + "managed_hsm_key_id": { + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: managedHsmValidate.ManagedHSMDataPlaneVersionlessKeyID, + ConflictsWith: []string{"key_vault_key_id"}, }, "consistency_policy": { @@ -764,7 +774,8 @@ func resourceCosmosDbAccount() *pluginsdk.Resource { func resourceCosmosDbAccountCreate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Cosmos.CosmosDBClient databaseClient := meta.(*clients.Client).Cosmos.DatabaseClient - subscriptionId := meta.(*clients.Client).Account.SubscriptionId + accountClient := meta.(*clients.Client).Account + subscriptionId := accountClient.SubscriptionId ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() log.Printf("[INFO] Preparing arguments for AzureRM Cosmos DB Account creation") @@ -902,12 +913,10 @@ func resourceCosmosDbAccountCreate(d *pluginsdk.ResourceData, meta interface{}) return fmt.Errorf("`create_mode` only works when `backup.type` is `Continuous`") } - if keyVaultKeyIDRaw, ok := d.GetOk("key_vault_key_id"); ok { - keyVaultKey, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(keyVaultKeyIDRaw.(string)) - if err != nil { - return fmt.Errorf("could not parse Key Vault Key ID: %+v", err) - } - account.Properties.KeyVaultKeyUri = pointer.To(keyVaultKey.ID()) + if key, err := customermanagedkeys.ExpandKeyVaultOrManagedHSMKey(d, customermanagedkeys.VersionTypeAny, accountClient.Environment.KeyVault, accountClient.Environment.ManagedHSM); err != nil { + return fmt.Errorf("parse key vault key id: %+v", err) + } else if key != nil { + account.Properties.KeyVaultKeyUri = pointer.To(key.ID()) } // additional validation on MaxStalenessPrefix as it varies depending on if the DB is multi region or not @@ -942,6 +951,7 @@ func resourceCosmosDbAccountCreate(d *pluginsdk.ResourceData, meta interface{}) func resourceCosmosDbAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Cosmos.CosmosDBClient + apiEnvs := meta.(*clients.Client).Account.Environment // subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -1049,7 +1059,7 @@ func resourceCosmosDbAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) // TODO Post 4.0 remove `enable_automatic_failover` from this list if d.HasChanges("consistency_policy", "virtual_network_rule", "cors_rule", "access_key_metadata_writes_enabled", "network_acl_bypass_for_azure_services", "network_acl_bypass_ids", "analytical_storage", - "capacity", "create_mode", "restore", "key_vault_key_id", "mongo_server_version", + "capacity", "create_mode", "restore", "key_vault_key_id", "managed_hsm_key_id", "mongo_server_version", "public_network_access_enabled", "ip_range_filter", "offer_type", "is_virtual_network_filter_enabled", "kind", "tags", "enable_automatic_failover", "automatic_failover_enabled", "analytical_storage_enabled", "local_authentication_disabled", "partition_merge_enabled", "minimal_tls_version", "burst_capacity_enabled") { @@ -1106,12 +1116,10 @@ func resourceCosmosDbAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) Tags: t, } - if keyVaultKeyIDRaw, ok := d.GetOk("key_vault_key_id"); ok { - keyVaultKey, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(keyVaultKeyIDRaw.(string)) - if err != nil { - return fmt.Errorf("could not parse Key Vault Key ID: %+v", err) - } - account.Properties.KeyVaultKeyUri = pointer.To(keyVaultKey.ID()) + if key, err := customermanagedkeys.ExpandKeyVaultOrManagedHSMKey(d, customermanagedkeys.VersionTypeAny, apiEnvs.KeyVault, apiEnvs.ManagedHSM); err != nil { + return err + } else if key != nil { + account.Properties.KeyVaultKeyUri = pointer.To(key.ID()) } // 'default_identity_type' will always have a value since it now has a default value of "FirstPartyIdentity" per the API documentation. @@ -1390,16 +1398,25 @@ func resourceCosmosDbAccountRead(d *pluginsdk.ResourceData, meta interface{}) er d.Set("partition_merge_enabled", pointer.From(props.EnablePartitionMerge)) d.Set("burst_capacity_enabled", pointer.From(props.EnableBurstCapacity)) - if v := existing.Model.Properties.IsVirtualNetworkFilterEnabled; v != nil { + if v := props.IsVirtualNetworkFilterEnabled; v != nil { d.Set("is_virtual_network_filter_enabled", props.IsVirtualNetworkFilterEnabled) } - if v := existing.Model.Properties.EnableAutomaticFailover; v != nil { + if v := props.EnableAutomaticFailover; v != nil { d.Set("automatic_failover_enabled", props.EnableAutomaticFailover) } - if v := existing.Model.Properties.KeyVaultKeyUri; v != nil { - d.Set("key_vault_key_id", props.KeyVaultKeyUri) + if v := props.KeyVaultKeyUri; v != nil { + envs := meta.(*clients.Client).Account.Environment + if key, err := customermanagedkeys.FlattenKeyVaultOrManagedHSMID(*v, envs.KeyVault, envs.ManagedHSM); err != nil { + return fmt.Errorf("flatten key vault uri: %+v", err) + } else if key.IsSet() { + if key.KeyVaultKeyId != nil { + d.Set("key_vault_key_id", key.KeyVaultKeyId.ID()) + } else { + d.Set("managed_hsm_key_id", key.ManagedHSMKeyID()) + } + } } if v := existing.Model.Properties.EnableMultipleWriteLocations; v != nil { diff --git a/internal/services/cosmos/cosmosdb_account_resource_test.go b/internal/services/cosmos/cosmosdb_account_resource_test.go index 34835ba66495e..1805e7aa36c5f 100644 --- a/internal/services/cosmos/cosmosdb_account_resource_test.go +++ b/internal/services/cosmos/cosmosdb_account_resource_test.go @@ -6,6 +6,7 @@ package cosmos_test import ( "context" "fmt" + "os" "regexp" "strconv" "testing" @@ -15,6 +16,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/customermanagedkeys" "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cosmos/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" @@ -121,6 +123,26 @@ func TestAccCosmosDBAccount_keyVaultUri(t *testing.T) { }) } +func TestAccCosmosDBAccount_ManagedHSMUri(t *testing.T) { + if os.Getenv("ARM_TEST_HSM_KEY") == "" { + t.Skip("Skipping as ARM_TEST_HSM_KEY is not specified") + return + } + + data := acceptance.BuildTestData(t, "azurerm_cosmosdb_account", "test") + r := CosmosDBAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.managedHSMKey(data), + Check: acceptance.ComposeAggregateTestCheckFunc( + checkAccCosmosDBAccount_basic(data, cosmosdb.DefaultConsistencyLevelStrong, 1), + ), + }, + data.ImportStep(), + }) +} + func TestAccCosmosDBAccount_customerManagedKeyWithIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_cosmosdb_account", "test") r := CosmosDBAccountResource{} @@ -3361,6 +3383,69 @@ resource "azurerm_cosmosdb_account" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString, data.RandomInteger, string(kind), string(consistency)) } +func (CosmosDBAccountResource) managedHSMKey(data acceptance.TestData) string { + // Purge Protection must be enabled to configure Managed HSM Key: https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-customer-managed-keys-mhsm#configure-your-azure-managed-hsm-key-vault + const enablePurgeProtection = true + hsmTemplate := customermanagedkeys.ManagedHSMKeyTempalte(data.RandomInteger, data.RandomString, enablePurgeProtection, []string{"data.azuread_service_principal.cosmosdb.id"}) + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +provider "azuread" {} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cosmos-%[1]d" + location = "%[2]s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "acctest-user-example" +} + +data "azuread_service_principal" "cosmosdb" { + display_name = "Azure Cosmos DB" +} + +%[3]s + +resource "azurerm_cosmosdb_account" "test" { + name = "acctest-ca-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + offer_type = "Standard" + kind = "MongoDB" + managed_hsm_key_id = azurerm_key_vault_managed_hardware_security_module_key.test.id + + capabilities { + name = "EnableMongo" + } + + consistency_policy { + consistency_level = "Strong" + } + + geo_location { + location = azurerm_resource_group.test.location + failover_priority = 0 + } + + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + depends_on = [azurerm_key_vault_managed_hardware_security_module_role_assignment.ra0] +} +`, data.RandomInteger, data.Locations.Primary, hsmTemplate) +} + func (CosmosDBAccountResource) systemAssignedUserAssignedIdentity(data acceptance.TestData, consistency cosmosdb.DefaultConsistencyLevel) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/internal/services/netapp/netapp_account_encryption_resource.go b/internal/services/netapp/netapp_account_encryption_resource.go index bc2f114526171..479ccd1a5521c 100644 --- a/internal/services/netapp/netapp_account_encryption_resource.go +++ b/internal/services/netapp/netapp_account_encryption_resource.go @@ -70,7 +70,14 @@ func (r NetAppAccountEncryptionResource) Arguments() map[string]*pluginsdk.Schem "encryption_key": { Type: pluginsdk.TypeString, - Required: true, + Optional: true, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + Description: "The versionless encryption key url.", + }, + + "managed_hsm_key_id": { + Type: pluginsdk.TypeString, + Optional: true, ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, Description: "The versionless encryption key url.", }, diff --git a/website/docs/r/cosmosdb_account.html.markdown b/website/docs/r/cosmosdb_account.html.markdown index 64b2e38ac673e..229b497fc91c6 100644 --- a/website/docs/r/cosmosdb_account.html.markdown +++ b/website/docs/r/cosmosdb_account.html.markdown @@ -164,6 +164,12 @@ The following arguments are supported: ~> **Note:** In order to use a `Custom Key` from Key Vault for encryption you must grant Azure Cosmos DB Service access to your key vault. For instructions on how to configure your Key Vault correctly please refer to the [product documentation](https://docs.microsoft.com/azure/cosmos-db/how-to-setup-cmk#add-an-access-policy-to-your-azure-key-vault-instance) +* `managed_hsm_key_id` - (Optional) A versionless Managed HSM Key ID for CMK encryption. Changing this forces a new resource to be created. + +~> **Note:** When referencing an `azurerm_key_vault_managed_hardware_security_module_key` resource, use `id` instead of `versioned_id` + +~> **Note:** In order to use a `Custom Key` from Managed HSM for encryption you must grant Azure Cosmos DB Service access to your Managed HSM. For instructions on how to configure your Key Vault correctly please refer to the [product documentation](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-customer-managed-keys-mhsm) + * `virtual_network_rule` - (Optional) Specifies a `virtual_network_rule` block as defined below, used to define which subnets are allowed to access this CosmosDB account. * `multiple_write_locations_enabled` - (Optional) Enable multiple write locations for this Cosmos DB account. @@ -193,9 +199,9 @@ The following arguments are supported: The `consistency_policy` block Configures the database consistency and supports the following: * `consistency_level` - (Required) The Consistency Level to use for this CosmosDB Account - can be either `BoundedStaleness`, `Eventual`, `Session`, `Strong` or `ConsistentPrefix`. - + * `max_interval_in_seconds` - (Optional) When used with the Bounded Staleness consistency level, this value represents the time amount of staleness (in seconds) tolerated. The accepted range for this value is `5` - `86400` (1 day). Defaults to `5`. Required when `consistency_level` is set to `BoundedStaleness`. - + * `max_staleness_prefix` - (Optional) When used with the Bounded Staleness consistency level, this value represents the number of stale requests tolerated. The accepted range for this value is `10` – `2147483647`. Defaults to `100`. Required when `consistency_level` is set to `BoundedStaleness`. ~> **Note:** `max_interval_in_seconds` and `max_staleness_prefix` can only be set to values other than default when the `consistency_level` is set to `BoundedStaleness`. @@ -205,9 +211,9 @@ The `consistency_policy` block Configures the database consistency and supports The `geo_location` block Configures the geographic locations the data is replicated to and supports the following: * `location` - (Required) The name of the Azure region to host replicated data. - + * `failover_priority` - (Required) The failover priority of the region. A failover priority of `0` indicates a write region. The maximum value for a failover priority = (total number of regions - 1). Failover priority values must be unique for each of the regions in which the database account exists. Changing this causes the location to be re-provisioned and cannot be changed for the location with failover priority `0`. - + * `zone_redundant` - (Optional) Should zone redundancy be enabled for this region? Defaults to `false`. --- @@ -216,7 +222,7 @@ A `capabilities` block Configures the capabilities to be enabled for this Cosmos * `name` - (Required) The capability to enable - Possible values are `AllowSelfServeUpgradeToMongo36`, `DisableRateLimitingResponses`, `EnableAggregationPipeline`, `EnableCassandra`, `EnableGremlin`, `EnableMongo`, `EnableMongo16MBDocumentSupport`, `EnableMongoRetryableWrites`, `EnableMongoRoleBasedAccessControl`, `EnableNoSQLVectorSearch`, `EnablePartialUniqueIndex`, `EnableServerless`, `EnableTable`, `EnableTtlOnCustomPath`, `EnableUniqueCompoundNestedDocs`, `MongoDBv3.4` and `mongoEnableDocLevelTTL`. -~> **Note:** Setting `MongoDBv3.4` also requires setting `EnableMongo`. +~> **Note:** Setting `MongoDBv3.4` also requires setting `EnableMongo`. ~> **Note:** Only `AllowSelfServeUpgradeToMongo36`, `DisableRateLimitingResponses`, `EnableAggregationPipeline`, `MongoDBv3.4`, `EnableMongoRetryableWrites`, `EnableMongoRoleBasedAccessControl`, `EnableUniqueCompoundNestedDocs`, `EnableMongo16MBDocumentSupport`, `mongoEnableDocLevelTTL`, `EnableTtlOnCustomPath` and `EnablePartialUniqueIndex` can be added to an existing Cosmos DB account. @@ -245,7 +251,7 @@ A `capacity` block supports the following: A `backup` block supports the following: -* `type` - (Required) The type of the `backup`. Possible values are `Continuous` and `Periodic`. +* `type` - (Required) The type of the `backup`. Possible values are `Continuous` and `Periodic`. ~> **Note:** Migration of `Periodic` to `Continuous` is one-way, changing `Continuous` to `Periodic` forces a new resource to be created. @@ -339,7 +345,7 @@ In addition to the Arguments listed above - the following Attributes are exporte * `primary_readonly_sql_connection_string` - Primary readonly SQL connection string for the CosmosDB Account. -* `secondary_readonly_sql_connection_string` - Secondary readonly SQL connection string for the CosmosDB Account. +* `secondary_readonly_sql_connection_string` - Secondary readonly SQL connection string for the CosmosDB Account. * `primary_mongodb_connection_string` - Primary Mongodb connection string for the CosmosDB Account.