From a9488d41bed25bd3150b2ba0d94740c0051df219 Mon Sep 17 00:00:00 2001 From: Yichun Ma Date: Fri, 18 Mar 2022 20:23:01 +0800 Subject: [PATCH] `r\iothub`: Add support for identity-based `file_upload` (#15874) --- internal/services/iothub/iothub_resource.go | 50 +++- .../services/iothub/iothub_resource_test.go | 258 +++++++++++++++++- website/docs/r/iothub.html.markdown | 8 + 3 files changed, 306 insertions(+), 10 deletions(-) diff --git a/internal/services/iothub/iothub_resource.go b/internal/services/iothub/iothub_resource.go index f325de2b8150..f851e19a6b89 100644 --- a/internal/services/iothub/iothub_resource.go +++ b/internal/services/iothub/iothub_resource.go @@ -184,6 +184,20 @@ func resourceIotHub() *pluginsdk.Resource { Type: pluginsdk.TypeString, Required: true, }, + "authentication_type": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(devices.AuthenticationTypeKeyBased), + ValidateFunc: validation.StringInSlice([]string{ + string(devices.AuthenticationTypeKeyBased), + string(devices.AuthenticationTypeIdentityBased), + }, false), + }, + "identity_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: msivalidate.UserAssignedIdentityID, + }, "notifications": { Type: pluginsdk.TypeBool, Optional: true, @@ -717,7 +731,7 @@ func resourceIotHubCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) err } } - storageEndpoints, messagingEndpoints, enableFileUploadNotifications := expandIoTHubFileUpload(d) + storageEndpoints, messagingEndpoints, enableFileUploadNotifications, err := expandIoTHubFileUpload(d) if err != nil { return fmt.Errorf("expanding `file_upload`: %+v", err) } @@ -1002,7 +1016,7 @@ func expandIoTHubEnrichments(d *pluginsdk.ResourceData) *[]devices.EnrichmentPro return &enrichmentProperties } -func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool) { +func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.StorageEndpointProperties, map[string]*devices.MessagingEndpointProperties, bool, error) { fileUploadList := d.Get("file_upload").([]interface{}) storageEndpointProperties := make(map[string]*devices.StorageEndpointProperties) @@ -1012,6 +1026,8 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor if len(fileUploadList) > 0 { fileUploadMap := fileUploadList[0].(map[string]interface{}) + authenticationType := devices.AuthenticationType(fileUploadMap["authentication_type"].(string)) + identityId := fileUploadMap["identity_id"].(string) connectionStr := fileUploadMap["connection_string"].(string) containerName := fileUploadMap["container_name"].(string) notifications = fileUploadMap["notifications"].(bool) @@ -1021,9 +1037,19 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor lockDuration := fileUploadMap["lock_duration"].(string) storageEndpointProperties["$default"] = &devices.StorageEndpointProperties{ - SasTTLAsIso8601: &sasTTL, - ConnectionString: &connectionStr, - ContainerName: &containerName, + SasTTLAsIso8601: &sasTTL, + AuthenticationType: authenticationType, + ConnectionString: &connectionStr, + ContainerName: &containerName, + } + + if identityId != "" { + if authenticationType != devices.AuthenticationTypeIdentityBased { + return nil, nil, false, fmt.Errorf("`identity_id` can only be specified when `authentication_type` is `identityBased`") + } + storageEndpointProperties["$default"].Identity = &devices.ManagedIdentity{ + UserAssignedIdentity: &identityId, + } } messagingEndpointProperties["fileNotifications"] = &devices.MessagingEndpointProperties{ @@ -1033,7 +1059,7 @@ func expandIoTHubFileUpload(d *pluginsdk.ResourceData) (map[string]*devices.Stor } } - return storageEndpointProperties, messagingEndpointProperties, notifications + return storageEndpointProperties, messagingEndpointProperties, notifications, nil } func expandIoTHubEndpoints(d *pluginsdk.ResourceData, subscriptionId string) (*devices.RoutingEndpoints, error) { @@ -1283,6 +1309,18 @@ func flattenIoTHubFileUpload(storageEndpoints map[string]*devices.StorageEndpoin output["sas_ttl"] = *sasTTLAsIso8601 } + authenticationType := string(devices.AuthenticationTypeKeyBased) + if v := string(storageEndpointProperties.AuthenticationType); v != "" { + authenticationType = v + } + output["authentication_type"] = authenticationType + + identityId := "" + if storageEndpointProperties.Identity != nil && storageEndpointProperties.Identity.UserAssignedIdentity != nil { + identityId = *storageEndpointProperties.Identity.UserAssignedIdentity + } + output["identity_id"] = identityId + if messagingEndpointProperties, ok := messagingEndpoints["fileNotifications"]; ok { if lockDurationAsIso8601 := messagingEndpointProperties.LockDurationAsIso8601; lockDurationAsIso8601 != nil { output["lock_duration"] = *lockDurationAsIso8601 diff --git a/internal/services/iothub/iothub_resource_test.go b/internal/services/iothub/iothub_resource_test.go index f27c48cadcec..4a19fd9ed025 100644 --- a/internal/services/iothub/iothub_resource_test.go +++ b/internal/services/iothub/iothub_resource_test.go @@ -178,7 +178,14 @@ func TestAccIotHub_fileUpload(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.fileUpload(data), + Config: r.fileUploadBasic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadUpdate(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("file_upload.#").HasValue("1"), @@ -189,6 +196,57 @@ func TestAccIotHub_fileUpload(t *testing.T) { }) } +func TestAccIotHub_fileUploadAuthenticationTypeUserAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.fileUploadAuthenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccIotHub_fileUploadAuthenticationTypeUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_iothub", "test") + r := IotHubResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.fileUploadAuthenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeUserAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.fileUploadAuthenticationTypeDefault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccIotHub_withDifferentEndpointResourceGroup(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -399,7 +457,7 @@ func TestAccIotHub_identityUpdate(t *testing.T) { }) } -func TestAccIotHub_AuthenticationTypeUserAssignedIdentity(t *testing.T) { +func TestAccIotHub_endpointAuthenticationTypeUserAssignedIdentity(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -414,7 +472,7 @@ func TestAccIotHub_AuthenticationTypeUserAssignedIdentity(t *testing.T) { }) } -func TestAccIotHub_AuthenticationTypeUpdate(t *testing.T) { +func TestAccIotHub_endpointAuthenticationTypeUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_iothub", "test") r := IotHubResource{} @@ -1057,7 +1115,50 @@ resource "azurerm_iothub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } -func (IotHubResource) fileUpload(data acceptance.TestData) string { +func (IotHubResource) fileUploadBasic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + +func (IotHubResource) fileUploadUpdate(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -1105,6 +1206,155 @@ resource "azurerm_iothub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) } +func (r IotHubResource) fileUploadAuthenticationTypeDefault(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (r IotHubResource) fileUploadAuthenticationTypeSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (r IotHubResource) fileUploadAuthenticationTypeUserAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_iothub" "test" { + name = "acctestIoTHub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "S1" + capacity = "1" + } + + file_upload { + connection_string = azurerm_storage_account.test.primary_blob_connection_string + container_name = azurerm_storage_container.test.name + + authentication_type = "identityBased" + identity_id = azurerm_user_assigned_identity.test.id + } + + identity { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.test.id, + ] + } + + depends_on = [ + azurerm_role_assignment.test_storage_blob_data_contrib_user, + ] +} +`, r.fileUploadAuthenticationTypeTemplate(data), data.RandomInteger) +} + +func (IotHubResource) fileUploadAuthenticationTypeTemplate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "test" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +resource "azurerm_user_assigned_identity" "test" { + name = "acctestuai-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_role_assignment" "test_storage_blob_data_contrib_user" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_user_assigned_identity.test.principal_id +} + +resource "azurerm_role_assignment" "test_storage_blob_data_contrib_system" { + role_definition_name = "Storage Blob Data Contributor" + scope = azurerm_storage_account.test.id + principal_id = azurerm_iothub.test.identity[0].principal_id +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString, data.RandomInteger) +} + func (IotHubResource) publicAccessEnabled(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/iothub.html.markdown b/website/docs/r/iothub.html.markdown index dccd6b8cca94..b5e384470dd1 100644 --- a/website/docs/r/iothub.html.markdown +++ b/website/docs/r/iothub.html.markdown @@ -294,6 +294,14 @@ A `fallback_route` block supports the following: A `file_upload` block supports the following: +* `authentication_type` - (Optional) The type used to authenticate against the storage account. Possible values are `keyBased` and `identityBased`. Defaults to `keyBased`. + +* `identity_id` - (Optional) The ID of the User Managed Identity used to authenticate against the storage account. + +-> **NOTE:** `identity_id` can only be specified when `authentication_type` is `identityBased`. It must be one of the `identity_ids` of the Iot Hub. If `identity_id`is omitted when `authentication_type` is `identityBased`, then the System Assigned Managed Identity of the Iot Hub will be used. + +~> **NOTE:** An IoT Hub can only be updated to use the System Assigned Managed Identity for `file_upload` since it is not possible to grant access to the endpoint until after creation. + * `connection_string` - (Required) The connection string for the Azure Storage account to which files are uploaded. * `container_name` - (Required) The name of the root container where you upload files. The container need not exist but should be creatable using the connection_string specified.