diff --git a/internal/services/servicebus/registration.go b/internal/services/servicebus/registration.go index f20a54300764e..1f0b35cc6bf4f 100644 --- a/internal/services/servicebus/registration.go +++ b/internal/services/servicebus/registration.go @@ -44,6 +44,7 @@ func (r Registration) SupportedDataSources() map[string]*pluginsdk.Resource { func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { return map[string]*pluginsdk.Resource{ "azurerm_servicebus_namespace": resourceServiceBusNamespace(), + "azurerm_servicebus_namespace_customer_managed_key": resourceServiceBusNamespaceCustomerManagedKey(), "azurerm_servicebus_namespace_disaster_recovery_config": resourceServiceBusNamespaceDisasterRecoveryConfig(), "azurerm_servicebus_namespace_authorization_rule": resourceServiceBusNamespaceAuthorizationRule(), "azurerm_servicebus_namespace_network_rule_set": resourceServiceBusNamespaceNetworkRuleSet(), diff --git a/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource.go b/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource.go new file mode 100644 index 0000000000000..3e077441e4fe1 --- /dev/null +++ b/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource.go @@ -0,0 +1,202 @@ +package servicebus + +import ( + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/servicebus/mgmt/2021-06-01-preview/servicebus" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" + keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" + msiValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/msi/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicebus/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicebus/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/timeouts" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +func resourceServiceBusNamespaceCustomerManagedKey() *pluginsdk.Resource { + return &pluginsdk.Resource{ + Create: resourceServiceBusNamespaceCustomerManagedKeyCreateUpdate, + Read: resourceServiceBusNamespaceCustomerManagedKeyRead, + Update: resourceServiceBusNamespaceCustomerManagedKeyCreateUpdate, + Delete: resourceServiceBusNamespaceCustomerManagedKeyDelete, + + Timeouts: &pluginsdk.ResourceTimeout{ + Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Read: pluginsdk.DefaultTimeout(5 * time.Minute), + Update: pluginsdk.DefaultTimeout(30 * time.Minute), + Delete: pluginsdk.DefaultTimeout(30 * time.Minute), + }, + + Importer: pluginsdk.DefaultImporter(), + + Schema: map[string]*pluginsdk.Schema{ + "namespace_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.NamespaceID, + }, + + "key_vault_key_id": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + }, + + "identity_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: msiValidate.UserAssignedIdentityID, + }, + + "infrastructure_encryption_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + }, + } +} + +func resourceServiceBusNamespaceCustomerManagedKeyCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.NamespaceID(d.Get("namespace_id").(string)) + if err != nil { + return err + } + + locks.ByName(id.Name, "azurerm_servicebus_namespace") + defer locks.UnlockByName(id.Name, "azurerm_servicebus_namespace") + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + if d.IsNewResource() { + if resp.SBNamespaceProperties != nil && resp.SBNamespaceProperties.Encryption != nil { + return tf.ImportAsExistsError("azurerm_servicebus_namespace_customer_managed_key", id.ID()) + } + } + + keyId, err := keyVaultParse.ParseOptionallyVersionedNestedItemID(d.Get("key_vault_key_id").(string)) + if err != nil { + return err + } + props := resp + props.SBNamespaceProperties.Encryption = &servicebus.Encryption{ + KeyVaultProperties: &[]servicebus.KeyVaultProperties{ + { + KeyName: utils.String(keyId.Name), + KeyVersion: utils.String(keyId.Version), + KeyVaultURI: utils.String(keyId.KeyVaultBaseUrl), + Identity: &servicebus.UserAssignedIdentityProperties{ + UserAssignedIdentity: utils.String(d.Get("identity_id").(string)), + }, + }, + }, + KeySource: servicebus.KeySourceMicrosoftKeyVault, + RequireInfrastructureEncryption: utils.Bool(d.Get("infrastructure_encryption_enabled").(bool)), + } + + if _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, props); err != nil { + return fmt.Errorf("adding Customer Managed Key for %s: %+v", id, err) + } + + d.SetId(id.ID()) + + return resourceServiceBusNamespaceCustomerManagedKeyRead(d, meta) +} + +func resourceServiceBusNamespaceCustomerManagedKeyRead(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.NamespaceID(d.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + if resp.SBNamespaceProperties == nil || resp.SBNamespaceProperties.Encryption == nil { + d.SetId("") + return nil + } + + d.Set("namespace_id", id.ID()) + d.Set("infrastructure_encryption_enabled", resp.SBNamespaceProperties.Encryption.RequireInfrastructureEncryption) + if keyVaultProperties := resp.SBNamespaceProperties.Encryption.KeyVaultProperties; keyVaultProperties != nil && len(*keyVaultProperties) != 0 { + props := (*keyVaultProperties)[0] + keyVaultKeyId, err := keyVaultParse.NewNestedItemID(*props.KeyVaultURI, "keys", *props.KeyName, *props.KeyVersion) + if err != nil { + return fmt.Errorf("parsing `key_vault_key_id`: %+v", err) + } + d.Set("key_vault_key_id", keyVaultKeyId.ID()) + if props.Identity != nil { + d.Set("identity_id", props.Identity.UserAssignedIdentity) + } + } + + return nil +} + +func resourceServiceBusNamespaceCustomerManagedKeyDelete(d *pluginsdk.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.NamespaceID(d.Id()) + if err != nil { + return err + } + + locks.ByName(id.Name, "azurerm_servicebus_namespace") + defer locks.UnlockByName(id.Name, "azurerm_servicebus_namespace") + + resp, err := client.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + // Since this isn't a real object and it cannot be disabled once Customer Managed Key at rest has been enabled + // And it must keep at least one key once Customer Managed Key is enabled + // So for the delete operation, it has to recreate the EventHub Namespace with disabled Customer Managed Key + deleteFuture, err := client.Delete(ctx, id.ResourceGroup, id.Name) + if err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + if err = deleteFuture.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(deleteFuture.Response()) { + return fmt.Errorf("failed to wait for removal of %q: %+v", id, err) + } + } + + namespace := resp + namespace.Encryption = nil + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, namespace) + if err != nil { + return fmt.Errorf("creating/updating %s: %+v", id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for create/update of %s: %+v", id, err) + } + + return nil +} diff --git a/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource_test.go b/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource_test.go new file mode 100644 index 0000000000000..444f03ee734f1 --- /dev/null +++ b/internal/services/servicebus/servicebus_namespace_customer_managed_key_resource_test.go @@ -0,0 +1,201 @@ +package servicebus_test + +import ( + "context" + "fmt" + "testing" + + "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/services/servicebus/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type ServiceBusNamespaceCustomerManagedKeyResource struct{} + +func TestAccServiceBusNamespaceCustomerManagedKey_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_customer_managed_key", "test") + r := ServiceBusNamespaceCustomerManagedKeyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServiceBusNamespaceCustomerManagedKey_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_customer_managed_key", "test") + r := ServiceBusNamespaceCustomerManagedKeyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccServiceBusNamespaceCustomerManagedKey_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_customer_managed_key", "test") + r := ServiceBusNamespaceCustomerManagedKeyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r ServiceBusNamespaceCustomerManagedKeyResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.NamespaceID(state.ID) + if err != nil { + return nil, err + } + + resp, err := clients.ServiceBus.NamespacesClient.Get(ctx, id.ResourceGroup, id.Name) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %v", *id, err) + } + + if resp.SBNamespaceProperties == nil || resp.SBNamespaceProperties.Encryption == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil +} + +func (r ServiceBusNamespaceCustomerManagedKeyResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s +resource "azurerm_servicebus_namespace_customer_managed_key" "test" { + namespace_id = azurerm_servicebus_namespace.test.id + key_vault_key_id = azurerm_key_vault_key.test.id + identity_id = azurerm_user_assigned_identity.test.id +} +`, r.template(data)) +} + +func (r ServiceBusNamespaceCustomerManagedKeyResource) requiresImport(data acceptance.TestData) string { + template := r.basic(data) + return fmt.Sprintf(` +%s +resource "azurerm_servicebus_namespace_customer_managed_key" "import" { + namespace_id = azurerm_servicebus_namespace_customer_managed_key.test.namespace_id + key_vault_key_id = azurerm_servicebus_namespace_customer_managed_key.test.key_vault_key_id + identity_id = azurerm_servicebus_namespace_customer_managed_key.test.identity_id +} +`, template) +} + +func (r ServiceBusNamespaceCustomerManagedKeyResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s +resource "azurerm_servicebus_namespace_customer_managed_key" "test" { + namespace_id = azurerm_servicebus_namespace.test.id + key_vault_key_id = azurerm_key_vault_key.test.id + identity_id = azurerm_user_assigned_identity.test.id + infrastructure_encryption_enabled = true +} +`, r.template(data)) +} + +func (r ServiceBusNamespaceCustomerManagedKeyResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + } + } +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-servicebus-%[3]d" + location = "%[1]s" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + name = "%[2]s" +} + +resource "azurerm_servicebus_namespace" "test" { + name = "acctest-sb-%[3]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "Premium" + capacity = 1 + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } +} + +resource "azurerm_key_vault" "test" { + name = "acctestkv%[2]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 = azurerm_servicebus_namespace.test.identity.0.tenant_id + object_id = azurerm_servicebus_namespace.test.identity.0.principal_id + key_permissions = [ + "Get", "Create", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify" + ] + secret_permissions = [ + "Get", + ] + } + + 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%[2]s" + key_vault_id = azurerm_key_vault.test.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} +`, data.Locations.Primary, data.RandomString, data.RandomInteger) +} diff --git a/internal/services/servicebus/servicebus_namespace_resource.go b/internal/services/servicebus/servicebus_namespace_resource.go index 1ba41feb3f47a..3e5a8008bb39b 100644 --- a/internal/services/servicebus/servicebus_namespace_resource.go +++ b/internal/services/servicebus/servicebus_namespace_resource.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/features" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicebus/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicebus/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicebus/validate" @@ -138,6 +139,8 @@ func resourceServiceBusNamespaceCreateUpdate(d *pluginsdk.ResourceData, meta int t := d.Get("tags").(map[string]interface{}) resourceId := parse.NewNamespaceID(subscriptionId, d.Get("resource_group_name").(string), d.Get("name").(string)) + locks.ByName(resourceId.Name, "azurerm_servicebus_namespace") + defer locks.UnlockByName(resourceId.Name, "azurerm_servicebus_namespace") if d.IsNewResource() { existing, err := client.Get(ctx, resourceId.ResourceGroup, resourceId.Name) if err != nil { @@ -255,7 +258,8 @@ func resourceServiceBusNamespaceDelete(d *pluginsdk.ResourceData, meta interface if err != nil { return err } - + locks.ByName(id.Name, "azurerm_servicebus_namespace") + defer locks.UnlockByName(id.Name, "azurerm_servicebus_namespace") future, err := client.Delete(ctx, id.ResourceGroup, id.Name) if err != nil { return fmt.Errorf("deleting %s: %+v", id, err) diff --git a/website/docs/r/servicebus_namespace_customer_managed_key.html.markdown b/website/docs/r/servicebus_namespace_customer_managed_key.html.markdown new file mode 100644 index 0000000000000..83d5e09f188a5 --- /dev/null +++ b/website/docs/r/servicebus_namespace_customer_managed_key.html.markdown @@ -0,0 +1,135 @@ +--- +subcategory: "Messaging" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_servicebus_namespace_customer_managed_key" +description: |- + Manages a ServiceBus Namespace Customer Managed Key. +--- + +# azurerm_servicebus_namespace_customer_managed_key + +Manages a ServiceBus Namespace Customer Managed Key. + +## Example Usage + +```hcl + +data "azurerm_client_config" "current" {} + +resource "azurerm_resource_group" "example" { + name = "example" + location = "West Europe" +} + +resource "azurerm_user_assigned_identity" "example" { + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + name = "example" +} + +resource "azurerm_servicebus_namespace" "example" { + name = "example" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Premium" + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [azurerm_user_assigned_identity.example.id] + } +} + +resource "azurerm_key_vault" "example" { + name = "example" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_enabled = true + purge_protection_enabled = true + + access_policy { + tenant_id = azurerm_servicebus_namespace.example.identity.0.tenant_id + object_id = azurerm_servicebus_namespace.example.identity.0.principal_id + key_permissions = [ + "Get", "Create", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify" + ] + secret_permissions = [ + "Get", + ] + } + + 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.example.tenant_id + object_id = azurerm_user_assigned_identity.example.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" "example" { + name = "example" + key_vault_id = azurerm_key_vault.example.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_servicebus_namespace_customer_managed_key" "example" { + namespace_id = azurerm_servicebus_namespace.example.id + key_vault_key_id = azurerm_key_vault_key.example.id + identity_id = azurerm_user_assigned_identity.example.id + infrastructure_encryption_enabled = true +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `namespace_id` - (Required) The ID of the ServiceBus Namespace. Changing this forces a new ServiceBus Namespace Customer Managed Key to be created. + +* `key_vault_key_id` - (Required) The ID of the Key Vault Key which should be used to Encrypt the data in this ServiceBus Namespace. + +* `identity_id` - (Required) The ID of the User Assigned Identity that has access to the key. + +--- + +* `infrastructure_encryption_enabled` - (Optional) Used to specify whether enable Infrastructure Encryption (Double Encryption). + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the ServiceBus Namespace Customer Managed Key. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the ServiceBus Namespace Customer Managed Key. +* `read` - (Defaults to 5 minutes) Used when retrieving the ServiceBus Namespace Customer Managed Key. +* `update` - (Defaults to 30 minutes) Used when updating the ServiceBus Namespace Customer Managed Key. +* `delete` - (Defaults to 30 minutes) Used when deleting the ServiceBus Namespace Customer Managed Key. + +## Import + +ServiceBus Namespace Customer Managed Keys can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_servicebus_namespace_customer_managed_key.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/namespace1 +```