diff --git a/azurerm/internal/services/storage/storage_account_resource.go b/azurerm/internal/services/storage/storage_account_resource.go index ba3c153654f6..102d10769b8d 100644 --- a/azurerm/internal/services/storage/storage_account_resource.go +++ b/azurerm/internal/services/storage/storage_account_resource.go @@ -17,6 +17,8 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" + msiparse "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/msi/parse" + msiValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/msi/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/storage/migration" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/storage/validate" @@ -324,6 +326,8 @@ func resourceStorageAccount() *pluginsdk.Resource { DiffSuppressFunc: suppress.CaseDifference, ValidateFunc: validation.StringInSlice([]string{ string(storage.IdentityTypeSystemAssigned), + string(storage.IdentityTypeSystemAssignedUserAssigned), + string(storage.IdentityTypeUserAssigned), }, true), }, "principal_id": { @@ -334,6 +338,15 @@ func resourceStorageAccount() *pluginsdk.Resource { Type: pluginsdk.TypeString, Computed: true, }, + "identity_ids": { + Type: pluginsdk.TypeSet, + Optional: true, + MinItems: 1, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: msiValidate.UserAssignedIdentityID, + }, + }, }, }, }, @@ -834,10 +847,11 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e parameters.AccountPropertiesCreateParameters.MinimumTLSVersion = storage.MinimumTLSVersion(minimumTLSVersion) } - if _, ok := d.GetOk("identity"); ok { - storageAccountIdentity := expandAzureRmStorageAccountIdentity(d) - parameters.Identity = storageAccountIdentity + storageAccountIdentity, err := expandAzureRmStorageAccountIdentity(d.Get("identity").([]interface{})) + if err != nil { + return err } + parameters.Identity = storageAccountIdentity if v, ok := d.GetOk("azure_files_authentication"); ok { expandAADFilesAuthentication, err := expandArmStorageAccountAzureFilesAuthentication(v.([]interface{})) @@ -1179,12 +1193,16 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e } if d.HasChange("identity") { + storageAccountIdentity, err := expandAzureRmStorageAccountIdentity(d.Get("identity").([]interface{})) + if err != nil { + return err + } opts := storage.AccountUpdateParameters{ - Identity: expandAzureRmStorageAccountIdentity(d), + Identity: storageAccountIdentity, } if _, err := client.Update(ctx, resourceGroupName, storageAccountName, opts); err != nil { - return fmt.Errorf("Error updating Azure Storage Account identity %q: %+v", storageAccountName, err) + return fmt.Errorf("updating Azure Storage Account identity %q: %+v", storageAccountName, err) } } @@ -1482,7 +1500,10 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err d.Set("secondary_access_key", storageAccountKeys[1].Value) } - identity := flattenAzureRmStorageAccountIdentity(resp.Identity) + identity, err := flattenAzureRmStorageAccountIdentity(resp.Identity) + if err != nil { + return err + } if err := d.Set("identity", identity); err != nil { return err } @@ -2439,32 +2460,76 @@ func flattenStorageAccountBypass(input storage.Bypass) []interface{} { return bypass } -func expandAzureRmStorageAccountIdentity(d *pluginsdk.ResourceData) *storage.Identity { - identities := d.Get("identity").([]interface{}) - identity := identities[0].(map[string]interface{}) - identityType := identity["type"].(string) - return &storage.Identity{ - Type: storage.IdentityType(identityType), +func expandAzureRmStorageAccountIdentity(vs []interface{}) (*storage.Identity, error) { + if len(vs) == 0 { + return &storage.Identity{ + Type: storage.IdentityTypeNone, + }, nil } -} -func flattenAzureRmStorageAccountIdentity(identity *storage.Identity) []interface{} { - if identity == nil { - return make([]interface{}, 0) + v := vs[0].(map[string]interface{}) + identity := storage.Identity{ + Type: storage.IdentityType(v["type"].(string)), + } + + var identityIdSet []interface{} + if identityIds, exists := v["identity_ids"]; exists { + identityIdSet = identityIds.(*pluginsdk.Set).List() } - result := make(map[string]interface{}) - if identity.Type != "" { - result["type"] = string(identity.Type) + // If type contains `UserAssigned`, `identity_ids` must be specified and have at least 1 element + if identity.Type == storage.IdentityTypeUserAssigned || identity.Type == storage.IdentityTypeSystemAssignedUserAssigned { + if len(identityIdSet) == 0 { + return nil, fmt.Errorf("`identity_ids` must have at least 1 element when `type` includes `UserAssigned`") + } + + userAssignedIdentities := make(map[string]*storage.UserAssignedIdentity) + for _, id := range identityIdSet { + userAssignedIdentities[id.(string)] = &storage.UserAssignedIdentity{} + } + + identity.UserAssignedIdentities = userAssignedIdentities + } else if len(identityIdSet) > 0 { + // If type does _not_ contain `UserAssigned` (i.e. is set to `SystemAssigned` or defaulted to `None`), `identity_ids` is not allowed + return nil, fmt.Errorf("`identity_ids` can only be specified when `type` includes `UserAssigned`; but `type` is currently %q", identity.Type) + } + + return &identity, nil +} + +func flattenAzureRmStorageAccountIdentity(identity *storage.Identity) ([]interface{}, error) { + if identity == nil || identity.Type == storage.IdentityTypeNone { + return make([]interface{}, 0), nil } + + var principalId, tenantId string if identity.PrincipalID != nil { - result["principal_id"] = *identity.PrincipalID + principalId = *identity.PrincipalID } + if identity.TenantID != nil { - result["tenant_id"] = *identity.TenantID + tenantId = *identity.TenantID } - return []interface{}{result} + identityIds := make([]interface{}, 0) + if identity.UserAssignedIdentities != nil { + for key := range identity.UserAssignedIdentities { + parsedId, err := msiparse.UserAssignedIdentityID(key) + if err != nil { + return nil, err + } + identityIds = append(identityIds, parsedId.ID()) + } + } + + return []interface{}{ + map[string]interface{}{ + "type": string(identity.Type), + "principal_id": principalId, + "tenant_id": tenantId, + "identity_ids": pluginsdk.NewSet(pluginsdk.HashString, identityIds), + }, + }, nil } func getBlobConnectionString(blobEndpoint *string, acctName *string, acctKey *string) string { diff --git a/azurerm/internal/services/storage/storage_account_resource_test.go b/azurerm/internal/services/storage/storage_account_resource_test.go index fa5fe414d676..17b144692316 100644 --- a/azurerm/internal/services/storage/storage_account_resource_test.go +++ b/azurerm/internal/services/storage/storage_account_resource_test.go @@ -419,13 +419,13 @@ func TestAccStorageAccount_NonStandardCasing(t *testing.T) { }) } -func TestAccStorageAccount_enableIdentity(t *testing.T) { +func TestAccStorageAccount_systemAssignedIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.identity(data), + Config: r.systemAssignedIdentity(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), @@ -436,23 +436,65 @@ func TestAccStorageAccount_enableIdentity(t *testing.T) { }) } +func TestAccStorageAccount_userAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") + r := StorageAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.userAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccStorageAccount_systemAssignedUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") + r := StorageAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.systemAssignedUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.0.principal_id").MatchesRegex(validate.UUIDRegExp), + check.That(data.ResourceName).Key("identity.0.tenant_id").MatchesRegex(validate.UUIDRegExp), + ), + }, + }) +} + func TestAccStorageAccount_updateResourceByEnablingIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.basic(data), + Config: r.identityDisabled(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("identity.#").HasValue("0"), ), }, { - Config: r.identity(data), + Config: r.systemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("identity.0.principal_id").MatchesRegex(validate.UUIDRegExp), + check.That(data.ResourceName).Key("identity.0.tenant_id").MatchesRegex(validate.UUIDRegExp), + ), + }, + { + Config: r.userAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.systemAssignedUserAssignedIdentity(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("identity.0.type").HasValue("SystemAssigned"), check.That(data.ResourceName).Key("identity.0.principal_id").MatchesRegex(validate.UUIDRegExp), check.That(data.ResourceName).Key("identity.0.tenant_id").MatchesRegex(validate.UUIDRegExp), ), @@ -932,7 +974,7 @@ resource "azurerm_storage_account" "test" { account_replication_type = "LRS" tags = { - %s + %s } } `, data.RandomInteger, data.Locations.Primary, data.RandomString, tags) @@ -1523,7 +1565,7 @@ resource "azurerm_storage_account" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString) } -func (r StorageAccountResource) identity(data acceptance.TestData) string { +func (r StorageAccountResource) identityTemplate(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -1534,6 +1576,34 @@ resource "azurerm_resource_group" "test" { location = "%s" } +resource "azurerm_user_assigned_identity" "test" { + name = "acctestUAI-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (r StorageAccountResource) identityDisabled(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + +} +`, r.identityTemplate(data), data.RandomString) +} + +func (r StorageAccountResource) systemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + resource "azurerm_storage_account" "test" { name = "unlikely23exst2acct%s" resource_group_name = azurerm_resource_group.test.name @@ -1545,12 +1615,52 @@ resource "azurerm_storage_account" "test" { identity { type = "SystemAssigned" } +} +`, r.identityTemplate(data), data.RandomString) +} - tags = { - environment = "production" +func (r StorageAccountResource) userAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%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, + ] } } -`, data.RandomInteger, data.Locations.Primary, data.RandomString) +`, r.identityTemplate(data), data.RandomString) +} + +func (r StorageAccountResource) systemAssignedUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + + identity { + type = "SystemAssigned,UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } +} +`, r.identityTemplate(data), data.RandomString) } func (r StorageAccountResource) networkRulesTemplate(data acceptance.TestData) string { diff --git a/website/docs/r/storage_account.html.markdown b/website/docs/r/storage_account.html.markdown index 004cd9567525..926c7f876f94 100644 --- a/website/docs/r/storage_account.html.markdown +++ b/website/docs/r/storage_account.html.markdown @@ -205,10 +205,14 @@ A `hour_metrics` block supports the following: A `identity` block supports the following: -* `type` - (Required) Specifies the identity type of the Storage Account. At this time the only allowed value is `SystemAssigned`. +* `type` - (Required) Specifies the identity type of the Storage Account. Possible values are `SystemAssigned`, `UserAssigned`, `SystemAssigned,UserAssigned` (to enable both). ~> The assigned `principal_id` and `tenant_id` can be retrieved after the identity `type` has been set to `SystemAssigned` and Storage Account has been created. More details are available below. +* `identity_ids` - (Optional) A list of IDs for User Assigned Managed Identity resources to be assigned. + +~> **NOTE:** This is required when `type` is set to `UserAssigned` or `SystemAssigned, UserAssigned`. + --- A `logging` block supports the following: