diff --git a/.teamcity/components/build_azure.kt b/.teamcity/components/build_azure.kt index d1e70485b1a9..6722c3d1e630 100644 --- a/.teamcity/components/build_azure.kt +++ b/.teamcity/components/build_azure.kt @@ -8,6 +8,9 @@ class ClientConfiguration(var clientId: String, val clientSecretAlt: String, val subscriptionIdAlt : String, val subscriptionIdDevTest : String, + val tenantIdAlt : String, + val subscriptionIdAltTenant : String, + val principalIdAltTenant : String, val vcsRootId : String, val enableTestTriggersGlobally : Boolean) { } @@ -16,10 +19,10 @@ class LocationConfiguration(var primary : String, var secondary : String, var te } fun ParametrizedWithType.ConfigureAzureSpecificTestParameters(environment: String, config: ClientConfiguration, locationsForEnv: LocationConfiguration, useAltSubscription: Boolean = false, useDevTestSubscription: Boolean = false) { - hiddenPasswordVariable("env.ARM_CLIENT_ID", config.clientId, "The ID of the Service Principal used for Testing") - hiddenPasswordVariable("env.ARM_CLIENT_ID_ALT", config.clientIdAlt, "The ID of the Alternate Service Principal used for Testing") - hiddenPasswordVariable("env.ARM_CLIENT_SECRET", config.clientSecret, "The Client Secret of the Service Principal used for Testing") - hiddenPasswordVariable("env.ARM_CLIENT_SECRET_ALT", config.clientSecretAlt, "The Client Secret of the Alternate Service Principal used for Testing") + hiddenPasswordVariable("env.ARM_CLIENT_ID", config.clientId, "The Client ID of the Application used for Testing") + hiddenPasswordVariable("env.ARM_CLIENT_ID_ALT", config.clientIdAlt, "The Client ID of the Alternate Application used for Testing") + hiddenPasswordVariable("env.ARM_CLIENT_SECRET", config.clientSecret, "The Client Secret of the Application used for Testing") + hiddenPasswordVariable("env.ARM_CLIENT_SECRET_ALT", config.clientSecretAlt, "The Client Secret of the Alternate Application used for Testing") hiddenVariable("env.ARM_ENVIRONMENT", environment, "The Azure Environment in which the tests are running") hiddenVariable("env.ARM_PROVIDER_DYNAMIC_TEST", "%b".format(locationsForEnv.rotate), "Should tests rotate between the supported regions?") if (useAltSubscription) { @@ -33,6 +36,9 @@ fun ParametrizedWithType.ConfigureAzureSpecificTestParameters(environment: Strin hiddenPasswordVariable("env.ARM_SUBSCRIPTION_ID_ALT", config.subscriptionIdAlt, "The ID of the Alternate Azure Subscription used for Testing") } hiddenPasswordVariable("env.ARM_TENANT_ID", config.tenantId, "The ID of the Azure Tenant used for Testing") + hiddenPasswordVariable("env.ARM_TENANT_ID_ALT", config.tenantIdAlt, "The ID of the Secondary Azure Tenant used for Testing") + hiddenPasswordVariable("env.ARM_SUBSCRIPTION_ID_ALT_TENANT", config.subscriptionIdAltTenant, "The ID of the Azure Subscription attached to the Secondary Azure Tenant used for Testing") + hiddenPasswordVariable("env.ARM_PRINCIPAL_ID_ALT_TENANT", config.principalIdAltTenant, "The Object ID of the Service Principal in the Secondary Azure Tenant representing the Application used for Testing") hiddenVariable("env.ARM_TEST_LOCATION", locationsForEnv.primary, "The Primary region which should be used for testing") hiddenVariable("env.ARM_TEST_LOCATION_ALT", locationsForEnv.secondary, "The Secondary region which should be used for testing") hiddenVariable("env.ARM_TEST_LOCATION_ALT2", locationsForEnv.tertiary, "The Tertiary region which should be used for testing") diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts index 444d1cf3be3d..a98686f58b27 100644 --- a/.teamcity/settings.kts +++ b/.teamcity/settings.kts @@ -11,9 +11,12 @@ var tenantId = DslContext.getParameter("tenantId", "") var environment = DslContext.getParameter("environment", "public") var clientIdAlt = DslContext.getParameter("clientIdAlt", "") var clientSecretAlt = DslContext.getParameter("clientSecretAlt", "") +var tenantIdAlt = DslContext.getParameter("tenantIdAlt", "") +var subscriptionIdAltTenant = DslContext.getParameter("subscriptionIdAltTenant", "") +var principalIdAltTenant = DslContext.getParameter("principalIdAltTenant", "") var vcsRootId = DslContext.getParameter("vcsRootId", "TF_HashiCorp_AzureRM_Repository") var enableTestTriggersGlobally = DslContext.getParameter("enableTestTriggersGlobally", "true").equals("true", ignoreCase = true) -var clientConfig = ClientConfiguration(clientId, clientSecret, subscriptionId, tenantId, clientIdAlt, clientSecretAlt, subscriptionIdAlt, subscriptionIdDevTest, vcsRootId, enableTestTriggersGlobally) +var clientConfig = ClientConfiguration(clientId, clientSecret, subscriptionId, tenantId, clientIdAlt, clientSecretAlt, subscriptionIdAlt, subscriptionIdDevTest, tenantIdAlt, subscriptionIdAltTenant, principalIdAltTenant, vcsRootId, enableTestTriggersGlobally) project(AzureRM(environment, clientConfig)) diff --git a/.teamcity/tests/helpers.kt b/.teamcity/tests/helpers.kt index 651944528435..bd8bdc0816ca 100644 --- a/.teamcity/tests/helpers.kt +++ b/.teamcity/tests/helpers.kt @@ -8,5 +8,5 @@ package tests import ClientConfiguration fun TestConfiguration() : ClientConfiguration { - return ClientConfiguration("clientId", "clientSecret", "subscriptionId", "tenantId", "clientIdAlt", "clientSecretAlt", "subscriptionIdAlt", "subscriptionIdDevTest", "vcsRootId", true) + return ClientConfiguration("clientId", "clientSecret", "subscriptionId", "tenantId", "clientIdAlt", "clientSecretAlt", "subscriptionIdAlt", "subscriptionIdDevTest", "tenantIdAlt", "subscriptionIdAltTenant", "principalIdAltTenant", "vcsRootId", true) } \ No newline at end of file diff --git a/internal/acceptance/testcase.go b/internal/acceptance/testcase.go index 7df339547d84..2425d7ea3cc1 100644 --- a/internal/acceptance/testcase.go +++ b/internal/acceptance/testcase.go @@ -136,7 +136,7 @@ func (td TestData) providers() map[string]func() (*schema.Provider, error) { func (td TestData) externalProviders() map[string]resource.ExternalProvider { return map[string]resource.ExternalProvider{ "azuread": { - VersionConstraint: "=2.8.0", + VersionConstraint: "=2.38.0", Source: "registry.terraform.io/hashicorp/azuread", }, "time": { diff --git a/internal/services/storage/storage_account_customer_managed_key_resource.go b/internal/services/storage/storage_account_customer_managed_key_resource.go index 8567501c251a..ced9489d0d1d 100644 --- a/internal/services/storage/storage_account_customer_managed_key_resource.go +++ b/internal/services/storage/storage_account_customer_managed_key_resource.go @@ -50,13 +50,22 @@ func resourceStorageAccountCustomerManagedKey() *pluginsdk.Resource { "key_vault_id": { // TODO: should this be split into two resources, since Key Vault and Managed HSM behave subtly differently in places already Type: pluginsdk.TypeString, - Required: true, + Optional: true, ValidateFunc: validation.Any( // Storage Account Customer Managed Keys support both Key Vault and Key Vault Managed HSM keys: // https://learn.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview commonids.ValidateKeyVaultID, managedhsms.ValidateManagedHSMID, ), + ExactlyOneOf: []string{"key_vault_id", "key_vault_uri"}, + }, + + "key_vault_uri": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsURLWithHTTPS, + ExactlyOneOf: []string{"key_vault_id", "key_vault_uri"}, + Computed: true, }, "key_name": { @@ -76,6 +85,13 @@ func resourceStorageAccountCustomerManagedKey() *pluginsdk.Resource { Optional: true, ValidateFunc: commonids.ValidateUserAssignedIdentityID, }, + + "federated_identity_client_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + RequiredWith: []string{"user_assigned_identity_id"}, + }, }, } } @@ -113,37 +129,46 @@ func resourceStorageAccountCustomerManagedKeyCreateUpdate(d *pluginsdk.ResourceD } } - keyVaultID, err := commonids.ParseKeyVaultID(d.Get("key_vault_id").(string)) - if err != nil { - return err - } - keyVault, err := vaultsClient.Get(ctx, *keyVaultID) - if err != nil { - return fmt.Errorf("retrieving Key Vault %q (Resource Group %q): %+v", keyVaultID.VaultName, keyVaultID.ResourceGroupName, err) - } + keyVaultURI := "" + if keyVaultURIRaw := d.Get("key_vault_uri").(string); keyVaultURIRaw != "" { + keyVaultURI = keyVaultURIRaw + } else { + keyVaultID, err := commonids.ParseKeyVaultID(d.Get("key_vault_id").(string)) + if err != nil { + return err + } + + keyVault, err := vaultsClient.Get(ctx, *keyVaultID) + if err != nil { + return fmt.Errorf("retrieving Key Vault %q (Resource Group %q): %+v", keyVaultID.VaultName, keyVaultID.ResourceGroupName, err) + } - softDeleteEnabled := false - purgeProtectionEnabled := false - if model := keyVault.Model; model != nil { - if esd := model.Properties.EnableSoftDelete; esd != nil { - softDeleteEnabled = *esd + softDeleteEnabled := false + purgeProtectionEnabled := false + if model := keyVault.Model; model != nil { + if esd := model.Properties.EnableSoftDelete; esd != nil { + softDeleteEnabled = *esd + } + if epp := model.Properties.EnablePurgeProtection; epp != nil { + purgeProtectionEnabled = *epp + } } - if epp := model.Properties.EnablePurgeProtection; epp != nil { - purgeProtectionEnabled = *epp + if !softDeleteEnabled || !purgeProtectionEnabled { + return fmt.Errorf("Key Vault %q (Resource Group %q) must be configured for both Purge Protection and Soft Delete", keyVaultID.VaultName, keyVaultID.ResourceGroupName) } - } - if !softDeleteEnabled || !purgeProtectionEnabled { - return fmt.Errorf("Key Vault %q (Resource Group %q) must be configured for both Purge Protection and Soft Delete", keyVaultID.VaultName, keyVaultID.ResourceGroupName) - } - keyVaultBaseURL, err := keyVaultsClient.BaseUriForKeyVault(ctx, *keyVaultID) - if err != nil { - return fmt.Errorf("looking up Key Vault URI from %s: %+v", *keyVaultID, err) + keyVaultBaseURL, err := keyVaultsClient.BaseUriForKeyVault(ctx, *keyVaultID) + if err != nil { + return fmt.Errorf("looking up Key Vault URI from %s: %+v", *keyVaultID, err) + } + + keyVaultURI = *keyVaultBaseURL } keyName := d.Get("key_name").(string) keyVersion := d.Get("key_version").(string) userAssignedIdentity := d.Get("user_assigned_identity_id").(string) + federatedIdentityClientID := d.Get("federated_identity_client_id").(string) props := storage.AccountUpdateParameters{ AccountPropertiesUpdateParameters: &storage.AccountPropertiesUpdateParameters{ @@ -163,12 +188,16 @@ func resourceStorageAccountCustomerManagedKeyCreateUpdate(d *pluginsdk.ResourceD KeyVaultProperties: &storage.KeyVaultProperties{ KeyName: utils.String(keyName), KeyVersion: utils.String(keyVersion), - KeyVaultURI: utils.String(*keyVaultBaseURL), + KeyVaultURI: utils.String(keyVaultURI), }, }, }, } + if federatedIdentityClientID != "" { + props.Encryption.EncryptionIdentity.EncryptionFederatedIdentityClientID = utils.String(federatedIdentityClientID) + } + if _, err = storageClient.Update(ctx, id.ResourceGroupName, id.StorageAccountName, props); err != nil { return fmt.Errorf("updating Customer Managed Key for %s: %+v", id, err) } @@ -226,28 +255,39 @@ func resourceStorageAccountCustomerManagedKeyRead(d *pluginsdk.ResourceData, met } userAssignedIdentity := "" + federatedIdentityClientID := "" if props := encryption.EncryptionIdentity; props != nil { if props.EncryptionUserAssignedIdentity != nil { userAssignedIdentity = *props.EncryptionUserAssignedIdentity } + if props.EncryptionFederatedIdentityClientID != nil { + federatedIdentityClientID = *props.EncryptionFederatedIdentityClientID + } } if keyVaultURI == "" { return fmt.Errorf("retrieving %s: `properties.encryption.keyVaultProperties.keyVaultURI` was nil", id) } - keyVaultID, err := keyVaultsClient.KeyVaultIDFromBaseUrl(ctx, resourcesClient, keyVaultURI) - if err != nil { - return fmt.Errorf("retrieving Key Vault ID from the Base URI %q: %+v", keyVaultURI, err) - } - // now we have the key vault uri we can look up the ID + // we can't look up the ID when using federated identity as the key will be under different tenant + keyVaultID := "" + if federatedIdentityClientID == "" { + tmpKeyVaultID, err := keyVaultsClient.KeyVaultIDFromBaseUrl(ctx, resourcesClient, keyVaultURI) + if err != nil { + return fmt.Errorf("retrieving Key Vault ID from the Base URI %q: %+v", keyVaultURI, err) + } + keyVaultID = *tmpKeyVaultID + } + d.Set("storage_account_id", id.ID()) d.Set("key_vault_id", keyVaultID) + d.Set("key_vault_uri", keyVaultURI) d.Set("key_name", keyName) d.Set("key_version", keyVersion) d.Set("user_assigned_identity_id", userAssignedIdentity) + d.Set("federated_identity_client_id", federatedIdentityClientID) return nil } diff --git a/internal/services/storage/storage_account_customer_managed_key_resource_test.go b/internal/services/storage/storage_account_customer_managed_key_resource_test.go index 5cd7a73a7d89..e75d6f08685c 100644 --- a/internal/services/storage/storage_account_customer_managed_key_resource_test.go +++ b/internal/services/storage/storage_account_customer_managed_key_resource_test.go @@ -6,6 +6,7 @@ package storage_test import ( "context" "fmt" + "os" "testing" "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-09-01/storage" // nolint: staticcheck @@ -132,6 +133,30 @@ func TestAccStorageAccountCustomerManagedKey_userAssignedIdentity(t *testing.T) }) } +func TestAccStorageAccountCustomerManagedKey_userAssignedIdentityWithFederatedIdentity(t *testing.T) { + // Multiple tenants are needed for this test + altTenantId := os.Getenv("ARM_TENANT_ID_ALT") + subscriptionIdAltTenant := os.Getenv("ARM_SUBSCRIPTION_ID_ALT_TENANT") + + if altTenantId == "" || subscriptionIdAltTenant == "" { + t.Skip("One of ARM_TENANT_ID_ALT, ARM_SUBSCRIPTION_ID_ALT_TENANT are not specified") + } + + data := acceptance.BuildTestData(t, "azurerm_storage_account_customer_managed_key", "test") + r := StorageAccountCustomerManagedKeyResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.federatedIdentity(data, altTenantId, subscriptionIdAltTenant), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("federated_identity_client_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + func (r StorageAccountCustomerManagedKeyResource) accountHasDefaultSettings(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) error { accountId, err := commonids.ParseStorageAccountID(state.Attributes["id"]) if err != nil { @@ -551,3 +576,145 @@ resource "azurerm_storage_account" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomString) } + +func (r StorageAccountCustomerManagedKeyResource) federatedIdentity(data acceptance.TestData, altTenantId, subscriptionIdAltTenant string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +provider "azurerm-alt" { + tenant_id = "%[1]s" + subscription_id = "%[2]s" + + features { + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + } + } +} + +provider "azuread" {} + +provider "azuread" { + alias = "alt" + tenant_id = "%[1]s" +} + +data "azurerm_client_config" "current" {} + +data "azurerm_client_config" "remote" { + provider = azurerm-alt +} + +data "azuread_client_config" "current" {} + +data "azuread_client_config" "remote" { + provider = azuread.alt +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[3]d" + location = "%[4]s" +} + +resource "azuread_application" "test" { + display_name = "acctestapp-%[5]s" + sign_in_audience = "AzureADMultipleOrgs" + owners = [data.azuread_client_config.current.object_id] +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestmi-%[5]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azuread_application_federated_identity_credential" "test" { + application_object_id = azuread_application.test.object_id + display_name = "acctestcred-%[5]s" + description = "Federated Identity Credential for CMK" + audiences = ["api://AzureADTokenExchange"] + issuer = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0" + subject = azurerm_user_assigned_identity.test.principal_id +} + +resource "azurerm_resource_group" "remotetest" { + provider = azurerm-alt + name = "acctestRG-alt-%[3]d" + location = "%[4]s" +} + +resource "azuread_service_principal" "remotetest" { + provider = azuread.alt + owners = [data.azuread_client_config.remote.object_id] + application_id = azuread_application.test.application_id +} + +resource "azurerm_key_vault" "remotetest" { + provider = azurerm-alt + + name = "acctestkv%[5]s" + location = azurerm_resource_group.remotetest.location + resource_group_name = azurerm_resource_group.remotetest.name + tenant_id = data.azurerm_client_config.remote.tenant_id + sku_name = "standard" + purge_protection_enabled = true + + access_policy { + tenant_id = data.azurerm_client_config.remote.tenant_id + object_id = data.azurerm_client_config.remote.object_id + + key_permissions = ["Get", "Create", "Delete", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify", "GetRotationPolicy"] + secret_permissions = ["Get"] + } + + access_policy { + tenant_id = data.azurerm_client_config.remote.tenant_id + object_id = azuread_service_principal.remotetest.object_id + + key_permissions = [ + "Get", "List", "UnwrapKey", "WrapKey", + ] + } + +} + +resource "azurerm_key_vault_key" "remotetest" { + provider = azurerm-alt + + name = "remote" + key_vault_id = azurerm_key_vault.remotetest.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[5]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + lifecycle { + ignore_changes = [customer_managed_key] + } +} + +resource "azurerm_storage_account_customer_managed_key" "test" { + storage_account_id = azurerm_storage_account.test.id + key_vault_uri = azurerm_key_vault.remotetest.vault_uri + key_name = azurerm_key_vault_key.remotetest.name + + user_assigned_identity_id = azurerm_user_assigned_identity.test.id + federated_identity_client_id = azuread_application.test.application_id +} +`, altTenantId, subscriptionIdAltTenant, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/website/docs/r/storage_account_customer_managed_key.html.markdown b/website/docs/r/storage_account_customer_managed_key.html.markdown index a4d5e3b0e66d..52d55e7c15d3 100644 --- a/website/docs/r/storage_account_customer_managed_key.html.markdown +++ b/website/docs/r/storage_account_customer_managed_key.html.markdown @@ -123,14 +123,21 @@ The following arguments are supported: * `storage_account_id` - (Required) The ID of the Storage Account. Changing this forces a new resource to be created. -* `key_vault_id` - (Required) The ID of the Key Vault. - * `key_name` - (Required) The name of Key Vault Key. +* `key_vault_id` - (Optional) The ID of the Key Vault. Exactly one of `key_vault_id`, or `key_vault_uri` must be specified. + +~> Note: When the principal running Terraform has access to the subscription containing the Key Vault, it's recommended to use the `key_vault_id` property for maximum compatibility, rather than the `key_vault_uri` property. + + +* `key_vault_uri` - (Optional) URI pointing at the Key Vault. Required when using `federated_identity_client_id`. Exactly one of `key_vault_id`, or `key_vault_uri` must be specified. + * `key_version` - (Optional) The version of Key Vault Key. Remove or omit this argument to enable Automatic Key Rotation. * `user_assigned_identity_id` - (Optional) The ID of a user assigned identity. +* `federated_identity_client_id` - (Optional) The Client ID of the multi-tenant application to be used in conjunction with the user-assigned identity for cross-tenant customer-managed-keys server-side encryption on the storage account. + ## Attributes Reference In addition to the Arguments listed above - the following Attributes are exported: