diff --git a/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource.go b/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource.go index 9da39b79619e..d0775a2bd01d 100644 --- a/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource.go +++ b/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" "github.com/hashicorp/go-azure-sdk/resource-manager/eventhub/2022-01-01-preview/namespaces" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" @@ -84,6 +85,12 @@ func resourceEventHubNamespaceCustomerManagedKey() *pluginsdk.Resource { Default: false, ForceNew: true, }, + + "user_assigned_identity_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: commonids.ValidateUserAssignedIdentityID, + }, }, } } @@ -116,7 +123,6 @@ func resourceEventHubNamespaceCustomerManagedKeyCreateUpdate(d *pluginsdk.Resour } namespace := resp.Model - keySource := namespaces.KeySourceMicrosoftPointKeyVault namespace.Properties.Encryption = &namespaces.Encryption{ KeySource: &keySource, @@ -126,6 +132,34 @@ func resourceEventHubNamespaceCustomerManagedKeyCreateUpdate(d *pluginsdk.Resour if err != nil { return err } + + userAssignedIdentity := d.Get("user_assigned_identity_id").(string) + if userAssignedIdentity != "" && keyVaultProps != nil { + + // this provides a more helpful error message than the API response + if namespace.Identity == nil { + return fmt.Errorf("user assigned identity '%s' must also be assigned to the parent event hub - currently no user assigned identities are assigned to the parent event hub", userAssignedIdentity) + } + + isIdentityAssignedToParent := false + for item := range namespace.Identity.IdentityIds { + if item == userAssignedIdentity { + isIdentityAssignedToParent = true + } + } + + // this provides a more helpful error message than the API response + if !isIdentityAssignedToParent { + return fmt.Errorf("user assigned identity '%s' must also be assigned to the parent event hub", userAssignedIdentity) + } + + for i := 0; i < len(*keyVaultProps); i++ { + (*keyVaultProps)[i].Identity = &namespaces.UserAssignedIdentityProperties{ + UserAssignedIdentity: &userAssignedIdentity, + } + } + } + namespace.Properties.Encryption.KeyVaultProperties = keyVaultProps namespace.Properties.Encryption.RequireInfrastructureEncryption = utils.Bool(d.Get("infrastructure_encryption_enabled").(bool)) @@ -174,14 +208,30 @@ func resourceEventHubNamespaceCustomerManagedKeyRead(d *pluginsdk.ResourceData, d.Set("key_vault_key_ids", keyVaultKeyIds) d.Set("infrastructure_encryption_enabled", props.Encryption.RequireInfrastructureEncryption) + + if kvprops := props.Encryption.KeyVaultProperties; kvprops != nil { + // we can only have a single user managed id for N number of keys, azure portal only allows setting a single one and then applies it to each key + for _, item := range *kvprops { + if item.Identity != nil && item.Identity.UserAssignedIdentity != nil { + userAssignedId, err := commonids.ParseUserAssignedIdentityIDInsensitively(*item.Identity.UserAssignedIdentity) + if err != nil { + return fmt.Errorf("parsing `user_assigned_identity_id`: %+v", err) + } + if err := d.Set("user_assigned_identity_id", userAssignedId.ID()); err != nil { + return fmt.Errorf("setting `user_assigned_identity_id`: %+v", err) + } + + break + } + } + } } return nil } func resourceEventHubNamespaceCustomerManagedKeyDelete(d *pluginsdk.ResourceData, meta interface{}) error { - log.Printf(`[INFO] Customer Managed Keys cannot be removed from EventHub Namespaces once added. To remove the Customer Managed Key delete and recreate the parent EventHub Namespace") -`) + log.Printf(`[INFO] Customer Managed Keys cannot be removed from EventHub Namespaces once added. To remove the Customer Managed Key delete and recreate the parent EventHub Namespace`) return nil } @@ -208,8 +258,8 @@ func expandEventHubNamespaceKeyVaultKeyIds(input []interface{}) (*[]namespaces.K return &results, nil } -func flattenEventHubNamespaceKeyVaultKeyIds(input *namespaces.Encryption) ([]interface{}, error) { - results := make([]interface{}, 0) +func flattenEventHubNamespaceKeyVaultKeyIds(input *namespaces.Encryption) ([]string, error) { + results := make([]string, 0) if input == nil || input.KeyVaultProperties == nil { return results, nil } diff --git a/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource_test.go b/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource_test.go index bbb556f4e073..c661e4e06589 100644 --- a/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource_test.go +++ b/internal/services/eventhub/eventhub_namespace_customer_managed_key_resource_test.go @@ -33,6 +33,21 @@ func TestAccEventHubNamespaceCustomerManagedKey_basic(t *testing.T) { }) } +func TestAccEventHubNamespaceCustomerManagedKey_withUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_eventhub_namespace_customer_managed_key", "test") + r := EventHubNamespaceCustomerManagedKeyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccEventHubNamespaceCustomerManagedKey_requiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_eventhub_namespace_customer_managed_key", "test") r := EventHubNamespaceCustomerManagedKeyResource{} @@ -129,6 +144,18 @@ resource "azurerm_eventhub_namespace_customer_managed_key" "test" { `, r.template(data)) } +func (r EventHubNamespaceCustomerManagedKeyResource) withUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_eventhub_namespace_customer_managed_key" "test" { + eventhub_namespace_id = azurerm_eventhub_namespace.test.id + key_vault_key_ids = [azurerm_key_vault_key.test.id] + user_assigned_identity_id = azurerm_user_assigned_identity.test.id +} +`, r.templateWithUserAssignedIdentity(data)) +} + func (r EventHubNamespaceCustomerManagedKeyResource) update(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -262,3 +289,95 @@ resource "azurerm_key_vault_key" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomString, data.RandomString) } + +func (r EventHubNamespaceCustomerManagedKeyResource) templateWithUserAssignedIdentity(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 + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-namespacecmk-%d" + location = "%s" +} + +resource "azurerm_eventhub_cluster" "test" { + name = "acctest-cluster-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku_name = "Dedicated_1" +} + +resource "azurerm_user_assigned_identity" "test" { + location = azurerm_resource_group.test.location + name = "acctest-identity-%s" + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_eventhub_namespace" "test" { + name = "acctest-namespace-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "Standard" + dedicated_cluster_id = azurerm_eventhub_cluster.test.id + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } +} + +data "azurerm_client_config" "current" {} + +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 +} + +resource "azurerm_key_vault_access_policy" "test" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + + key_permissions = ["Get", "UnwrapKey", "WrapKey", "GetRotationPolicy"] +} + +resource "azurerm_key_vault_access_policy" "test2" { + key_vault_id = azurerm_key_vault.test.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Delete", + "Get", + "List", + "Purge", + "Recover", + "GetRotationPolicy" + ] +} + +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"] + + depends_on = [ + azurerm_key_vault_access_policy.test, + azurerm_key_vault_access_policy.test2, + ] +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomString, data.RandomInteger, data.RandomString, data.RandomString) +} diff --git a/website/docs/r/eventhub_namespace_customer_managed_key.html.markdown b/website/docs/r/eventhub_namespace_customer_managed_key.html.markdown index 3b7250fa11ee..79af548e8cf4 100644 --- a/website/docs/r/eventhub_namespace_customer_managed_key.html.markdown +++ b/website/docs/r/eventhub_namespace_customer_managed_key.html.markdown @@ -12,7 +12,7 @@ Manages a Customer Managed Key for a EventHub Namespace. !> **Note:** In 2.x versions of the Azure Provider during deletion this resource will **delete and recreate the parent EventHub Namespace which may involve data loss** as it's not possible to remove the Customer Managed Key from the EventHub Namespace once it's been added. Version 3.0 of the Azure Provider will change this so that the Delete operation is a noop, requiring the parent EventHub Namespace is deleted/recreated to remove the Customer Managed Key. -## Example Usage +## Example Usage with System Assigned Identity ```hcl resource "azurerm_resource_group" "example" { @@ -93,6 +93,99 @@ resource "azurerm_eventhub_namespace_customer_managed_key" "example" { } ``` +## Example Usage with User Assigned Identity + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_eventhub_cluster" "example" { + name = "example-cluster" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku_name = "Dedicated_1" +} + +resource "azurerm_user_assigned_identity" "example" { + location = azurerm_resource_group.example.location + name = "example" + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_eventhub_namespace" "example" { + name = "example-namespace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Standard" + dedicated_cluster_id = azurerm_eventhub_cluster.example.id + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.example.id] + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "example" { + name = "examplekv" + 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" + purge_protection_enabled = true +} + +resource "azurerm_key_vault_access_policy" "example" { + key_vault_id = azurerm_key_vault.example.id + tenant_id = azurerm_user_assigned_identity.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + + key_permissions = ["Get", "UnwrapKey", "WrapKey"] +} + +resource "azurerm_key_vault_access_policy" "example2" { + key_vault_id = azurerm_key_vault.example.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Delete", + "Get", + "List", + "Purge", + "Recover", + "GetRotationPolicy", + ] +} + +resource "azurerm_key_vault_key" "example" { + name = "examplekvkey" + key_vault_id = azurerm_key_vault.example.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] + + depends_on = [ + azurerm_key_vault_access_policy.example, + azurerm_key_vault_access_policy.example2, + ] +} + +resource "azurerm_eventhub_namespace_customer_managed_key" "example" { + eventhub_namespace_id = azurerm_eventhub_namespace.example.id + key_vault_key_ids = [azurerm_key_vault_key.example.id] + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.example.id] + } +} +``` + ## Arguments Reference The following arguments are supported: @@ -103,6 +196,12 @@ The following arguments are supported: * `infrastructure_encryption_enabled` - (Optional) Whether to enable Infrastructure Encryption (Double Encryption). Changing this forces a new resource to be created. +* `user_assigned_identity_id` - (Optional) The ID of a User Managed Identity that will be used to access Key Vaults that contain the encryption keys. + +~> **Note:** If using `user_assigned_identity_id`, ensure the User Assigned Identity is also assigned to the parent Event Hub. + +~> **Note:** If using `user_assigned_identity_id`, make sure to assign the identity the appropriate permissions to access the Key Vault key. Failure to grant `Get, UnwrapKey, and WrapKey` will cause this resource to fail to apply. + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: