diff --git a/internal/services/cognitive/cognitive_account_customer_managed_key_resource_test.go b/internal/services/cognitive/cognitive_account_customer_managed_key_resource_test.go index 940101a0264a..e8d46d372c5b 100644 --- a/internal/services/cognitive/cognitive_account_customer_managed_key_resource_test.go +++ b/internal/services/cognitive/cognitive_account_customer_managed_key_resource_test.go @@ -178,6 +178,10 @@ resource "azurerm_cognitive_account" "test" { type = "SystemAssigned, UserAssigned" identity_ids = [azurerm_user_assigned_identity.test.id] } + + lifecycle { + ignore_changes = ["customer_managed_key"] + } } resource "azurerm_key_vault" "test" { diff --git a/internal/services/cognitive/cognitive_account_resource.go b/internal/services/cognitive/cognitive_account_resource.go index ad3ae85214c9..f7251d601967 100644 --- a/internal/services/cognitive/cognitive_account_resource.go +++ b/internal/services/cognitive/cognitive_account_resource.go @@ -20,6 +20,8 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cognitive/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/cognitive/validate" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" "github.com/hashicorp/terraform-provider-azurerm/internal/services/network" networkParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/parse" storageValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/validate" @@ -118,6 +120,27 @@ func resourceCognitiveAccount() *pluginsdk.Resource { ValidateFunc: validation.StringIsNotEmpty, }, + "customer_managed_key": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "key_vault_key_id": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + }, + + "identity_client_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + }, + }, + }, + }, + "fqdns": { Type: pluginsdk.TypeList, Optional: true, @@ -352,6 +375,7 @@ func resourceCognitiveAccountCreate(d *pluginsdk.ResourceData, meta interface{}) UserOwnedStorage: expandCognitiveAccountStorage(d.Get("storage").([]interface{})), RestrictOutboundNetworkAccess: utils.Bool(d.Get("outbound_network_access_restricted").(bool)), DisableLocalAuth: utils.Bool(!d.Get("local_auth_enabled").(bool)), + Encryption: expandCognitiveAccountCustomerManagedKey(d.Get("customer_managed_key").([]interface{})), }, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } @@ -435,6 +459,7 @@ func resourceCognitiveAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) UserOwnedStorage: expandCognitiveAccountStorage(d.Get("storage").([]interface{})), RestrictOutboundNetworkAccess: utils.Bool(d.Get("outbound_network_access_restricted").(bool)), DisableLocalAuth: utils.Bool(!d.Get("local_auth_enabled").(bool)), + Encryption: expandCognitiveAccountCustomerManagedKey(d.Get("customer_managed_key").([]interface{})), }, Tags: tags.Expand(d.Get("tags").(map[string]interface{})), } @@ -549,6 +574,15 @@ func resourceCognitiveAccountRead(d *pluginsdk.ResourceData, meta interface{}) e localAuthEnabled = !*props.DisableLocalAuth } d.Set("local_auth_enabled", localAuthEnabled) + + customerManagedKey, err := flattenCognitiveAccountCustomerManagedKey(id, props.Encryption) + if err != nil { + return err + } + + if err := d.Set("customer_managed_key", customerManagedKey); err != nil { + return fmt.Errorf("setting `customer_managed_key`: %+v", err) + } } return tags.FlattenAndSet(d, model.Tags) @@ -797,3 +831,54 @@ func flattenCognitiveAccountStorage(input *[]cognitiveservicesaccounts.UserOwned } return results } + +func expandCognitiveAccountCustomerManagedKey(input []interface{}) *cognitiveservicesaccounts.Encryption { + if len(input) == 0 || input[0] == nil { + return nil + } + + v := input[0].(map[string]interface{}) + keyId, _ := keyVaultParse.ParseOptionallyVersionedNestedItemID(v["key_vault_key_id"].(string)) + keySource := cognitiveservicesaccounts.KeySourceMicrosoftPointKeyVault + + var identity string + if value := v["identity_client_id"]; value != nil && value != "" { + identity = value.(string) + } + + return &cognitiveservicesaccounts.Encryption{ + KeySource: &keySource, + KeyVaultProperties: &cognitiveservicesaccounts.KeyVaultProperties{ + KeyName: utils.String(keyId.Name), + KeyVersion: utils.String(keyId.Version), + KeyVaultUri: utils.String(keyId.KeyVaultBaseUrl), + IdentityClientId: utils.String(identity), + }, + } +} + +func flattenCognitiveAccountCustomerManagedKey(cognitiveAccountId *cognitiveservicesaccounts.AccountId, input *cognitiveservicesaccounts.Encryption) ([]interface{}, error) { + if input == nil { + return []interface{}{}, nil + } + + var keyId string + var identityClientId string + if props := input.KeyVaultProperties; props != nil { + keyVaultKeyId, err := keyVaultParse.NewNestedItemID(*props.KeyVaultUri, "keys", *props.KeyName, *props.KeyVersion) + if err != nil { + return nil, fmt.Errorf("parsing `key_vault_key_id`: %+v", err) + } + keyId = keyVaultKeyId.ID() + if props.IdentityClientId != nil { + identityClientId = *props.IdentityClientId + } + } + + return []interface{}{ + map[string]interface{}{ + "key_vault_key_id": keyId, + "identity_client_id": identityClientId, + }, + }, nil +} diff --git a/internal/services/cognitive/cognitive_account_resource_test.go b/internal/services/cognitive/cognitive_account_resource_test.go index 9554d235cf48..aa6a8be45aaf 100644 --- a/internal/services/cognitive/cognitive_account_resource_test.go +++ b/internal/services/cognitive/cognitive_account_resource_test.go @@ -340,6 +340,23 @@ func TestAccCognitiveAccount_metricsAdvisor(t *testing.T) { }) } +func TestAccCognitiveAccount_customerManagedKey(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_cognitive_account", "test") + r := CognitiveAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.customerManagedKey(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("customer_managed_key.0.key_vault_key_id").Exists(), + check.That(data.ResourceName).Key("customer_managed_key.0.identity_client_id").IsUUID(), + ), + }, + data.ImportStep(), + }) +} + func (t CognitiveAccountResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := cognitiveservicesaccounts.ParseAccountID(state.ID) if err != nil { @@ -934,3 +951,89 @@ resource "azurerm_subnet" "test_b" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) } + +func (CognitiveAccountResource) customerManagedKey(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-cognitive-%d" + location = "%s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%s" +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%s" + 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" + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify" + ] + secret_permissions = [ + "Get", + ] + } + + access_policy { + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + key_permissions = [ + "Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify" + ] + secret_permissions = [ + "Get", + ] + } +} + +resource "azurerm_key_vault_key" "test" { + name = "acctestkvkey%s" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_cognitive_account" "test" { + name = "acctest-cogacc-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + kind = "SpeechServices" + sku_name = "S0" + custom_subdomain_name = "acctest-cogacc-%d" + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id + ] + } + + customer_managed_key { + key_vault_key_id = azurerm_key_vault_key.test.id + identity_client_id = azurerm_user_assigned_identity.test.client_id + } +} +`, data.RandomInteger, data.Locations.Secondary, data.RandomString, data.RandomString, data.RandomString, data.RandomInteger, data.RandomInteger) +} diff --git a/website/docs/r/cognitive_account.html.markdown b/website/docs/r/cognitive_account.html.markdown index 1c44211c59fc..62f419e16c7e 100644 --- a/website/docs/r/cognitive_account.html.markdown +++ b/website/docs/r/cognitive_account.html.markdown @@ -54,6 +54,8 @@ The following arguments are supported: * `custom_subdomain_name` - (Required) The subdomain name used for token-based authentication. Changing this forces a new resource to be created. +* `customer_managed_key` (Optional) A `customer_managed_key` block as documented below. + * `fqdns` - (Optional) List of FQDNs allowed for the Cognitive Account. * `identity` - (Optional) An `identity` block as defined below. @@ -106,6 +108,14 @@ A `virtual_network_rules` block supports the following: --- +A `customer_managed_key` block supports the following: + +* `key_vault_key_id` - (Required) The ID of the Key Vault Key which should be used to Encrypt the data in this Cognitive Account. + +* `identity_client_id` - (Optional) The Client ID of the User Assigned Identity that has access to the key. This property only needs to be specified when there're multiple identities attached to the Cognitive Account. + +--- + A `identity` block supports the following: * `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this Cognitive Account. Possible values are `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned` (to enable both). diff --git a/website/docs/r/cognitive_account_customer_managed_key.html.markdown b/website/docs/r/cognitive_account_customer_managed_key.html.markdown index 888e57b424f5..64c616d8a49b 100644 --- a/website/docs/r/cognitive_account_customer_managed_key.html.markdown +++ b/website/docs/r/cognitive_account_customer_managed_key.html.markdown @@ -10,6 +10,8 @@ description: |- Manages a Customer Managed Key for a Cognitive Services Account. +~> **NOTE:** It's possible to define a Customer Managed Key both within [the `azurerm_cognitive_account` resource](cognitive_account.html) via the `customer_managed_key` block and by using [the `azurerm_cognitive_account_customer_managed_key` resource](cognitive_account_customer_managed_key.html). However it's not possible to use both methods to manage a Customer Managed Key for a Cognitive Account, since there'll be conflicts. + ## Example Usage ```hcl