diff --git a/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go b/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go new file mode 100644 index 000000000000..ff14298f9b5b --- /dev/null +++ b/internal/customermanagedkeys/key_vault_or_managed_hsm_key.go @@ -0,0 +1,195 @@ +package customermanagedkeys + +import ( + "fmt" + + "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 +} + +func (k *KeyVaultOrManagedHSMKey) IsSet() bool { + return k != nil && (k.KeyVaultKeyId != nil || k.ManagedHSMKeyId != nil || k.ManagedHSMKeyVersionlessId != nil) +} + +func (k *KeyVaultOrManagedHSMKey) ID() string { + if k == nil { + return "" + } + + if k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.ID() + } + + if k.ManagedHSMKeyId != nil { + return k.ManagedHSMKeyId.ID() + } + + if k.ManagedHSMKeyVersionlessId != nil { + return k.ManagedHSMKeyVersionlessId.ID() + } + + return "" +} + +func (k *KeyVaultOrManagedHSMKey) KeyVaultKeyID() string { + if k != nil && k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.ID() + } + return "" +} + +func (k *KeyVaultOrManagedHSMKey) ManagedHSMKeyID() string { + if k != nil && k.ManagedHSMKeyId != nil { + return k.ManagedHSMKeyId.ID() + } + + if k != nil && k.ManagedHSMKeyVersionlessId != nil { + return k.ManagedHSMKeyVersionlessId.ID() + } + + return "" +} + +func (k *KeyVaultOrManagedHSMKey) BaseUri() string { + if k.KeyVaultKeyId != nil { + return k.KeyVaultKeyId.KeyVaultBaseUrl + } + + 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 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() + } + + 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) + } + + return versioned, versionless, err +} + +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) +} + +// 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 + if rd, ok := d.(*pluginsdk.ResourceData); ok { + if keyRaw, ok := rd.GetOk(keyVaultFieldName); ok { + vaultKeyStr = keyRaw.(string) + } else if keyRaw, ok = rd.GetOk(hsmFieldName); ok { + hsmKeyStr = keyRaw.(string) + } + } else if obj, ok := d.(map[string]interface{}); ok { + if keyRaw, ok := obj[keyVaultFieldName]; ok { + 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) + } + + switch { + case vaultKeyStr != "": + if key.KeyVaultKeyId, err = parseKeyvaultID(vaultKeyStr, requireVersion, keyVaultEnv); err != nil { + return nil, err + } + case hsmKeyStr != "": + if key.ManagedHSMKeyId, key.ManagedHSMKeyVersionlessId, err = parseManagedHSMKey(hsmKeyStr, requireVersion, hsmEnv); err != nil { + return nil, err + } + default: + return nil, nil + } + return key, err +} + +// FlattenKeyVaultOrManagedHSMID uses `KeyVaultOrManagedHSMKey.SetState()` to save the state, which this function is designed not to do. +func FlattenKeyVaultOrManagedHSMID(id string, hsmEnv environments.Api) (*KeyVaultOrManagedHSMKey, error) { + if id == "" { + return nil, nil + } + + key := &KeyVaultOrManagedHSMKey{} + var err error + key.KeyVaultKeyId, err = parse.ParseOptionallyVersionedNestedKeyID(id) + if err == nil { + return key, nil + } + + var domainSuffix *string + if hsmEnv != nil { + domainSuffix, _ = hsmEnv.DomainSuffix() + } + if key.ManagedHSMKeyId, err = hsmParse.ManagedHSMDataPlaneVersionedKeyID(id, domainSuffix); err == nil { + return key, nil + } + + if key.ManagedHSMKeyVersionlessId, err = hsmParse.ManagedHSMDataPlaneVersionlessKeyID(id, domainSuffix); err == nil { + return key, nil + } + + return nil, fmt.Errorf("cannot parse given id to key vault key nor managed hsm key: %s", id) +} diff --git a/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go b/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go new file mode 100644 index 000000000000..fe5fcd4b050c --- /dev/null +++ b/internal/customermanagedkeys/key_vault_or_managed_hsm_key_test.go @@ -0,0 +1,133 @@ +package customermanagedkeys_test + +import ( + "reflect" + "testing" + + "github.com/hashicorp/go-azure-sdk/sdk/environments" + "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" +) + +func buildData(keyVaultKey, keyVualtValue, hsmKey, hsmValue string) interface{} { + data := map[string]interface{}{} + if keyVaultKey != "" { + data[keyVaultKey] = keyVualtValue + } + + if hsmKey != "" { + data[hsmKey] = hsmValue + } + + return data +} + +func buildKeyVaultData(key, value string) interface{} { + return buildData(key, value, "", "") +} + +func buildHSMData(key, value string) interface{} { + return buildData("", "", key, value) +} + +func TestExpandKeyVaultOrManagedHSMKeyKey(t *testing.T) { + type args struct { + d interface{} + hasVersion customermanagedkeys.VersionType + keyVaultFieldName string + hsmFieldName string + hsmEnv environments.Api + } + tests := []struct { + name string + args args + 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: &customermanagedkeys.KeyVaultOrManagedHSMKey{ + KeyVaultKeyId: &parse.NestedItemId{ + KeyVaultBaseUrl: "https://test.keyvault.azure.net/", + NestedItemType: "keys", + Name: "test-key-name", + }, + }, + }, + { + 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", + }, + 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 versionless key vault id", + args: args{ + d: buildKeyVaultData("key_vault_key_id", "https://test.keyvault.azure.net/keys/test-key-versionless"), + keyVaultFieldName: "key_vault_key_id", + hasVersion: customermanagedkeys.VersionTypeVersionless, + }, + want: &customermanagedkeys.KeyVaultOrManagedHSMKey{ + KeyVaultKeyId: &parse.NestedItemId{ + KeyVaultBaseUrl: "https://test.keyvault.azure.net/", + NestedItemType: "keys", + Name: "test-key-versionless", + }, + }, + wantErr: false, + }, + { + 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: customermanagedkeys.VersionTypeVersionless, + }, + want: &customermanagedkeys.KeyVaultOrManagedHSMKey{ + ManagedHSMKeyVersionlessId: &hsmParse.ManagedHSMDataPlaneVersionlessKeyId{ + ManagedHSMName: "test", + DomainSuffix: "managedhsm.azure.net", + KeyName: "test-key-name", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t2 *testing.T) { + 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 + } + if !reflect.DeepEqual(got, tt.want) { + t2.Errorf("ExpandKeyVaultOrManagedHSMKeyWithCustomFieldKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/services/cosmos/cosmosdb_account_resource.go b/internal/services/cosmos/cosmosdb_account_resource.go index 23efd070ac4e..b37ac04ba0c5 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.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 fefcb5605d4e..29d0a42fec7e 100644 --- a/internal/services/cosmos/cosmosdb_account_resource_test.go +++ b/internal/services/cosmos/cosmosdb_account_resource_test.go @@ -6,11 +6,13 @@ package cosmos_test import ( "context" "fmt" + "os" "regexp" "strconv" "testing" "github.com/hashicorp/go-azure-sdk/resource-manager/cosmosdb/2024-08-15/cosmosdb" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" @@ -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,202 @@ 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 + // hsmTemplate := customermanagedkeys.ManagedHSMKeyTempalte(data.RandomInteger, data.RandomString, enablePurgeProtection, []string{"data.azuread_service_principal.cosmosdb.id"}) + raName1, _ := uuid.GenerateUUID() + raName2, _ := uuid.GenerateUUID() + raName3, _ := uuid.GenerateUUID() + 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" +} + +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" + ] + } + 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%[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 = true + 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 = "%[3]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 = "%[4]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_role_assignment" "racosmos" { + 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.encrypt-user.resource_manager_id + principal_id = data.azuread_service_principal.cosmosdb.object_id + + depends_on = [azurerm_key_vault_managed_hardware_security_module_key.test] +} + +resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { + name = "acctestHSMK-%[1]d" + 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 + ] +} + +resource "azurerm_cosmosdb_account" "test" { + name = "acc-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.racosmos] +} +`, data.RandomInteger, data.Locations.Primary, raName1, raName2, raName3) +} + func (CosmosDBAccountResource) systemAssignedUserAssignedIdentity(data acceptance.TestData, consistency cosmosdb.DefaultConsistencyLevel) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/cosmosdb_account.html.markdown b/website/docs/r/cosmosdb_account.html.markdown index 64b2e38ac673..229b497fc91c 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.