diff --git a/azurerm/internal/clients/builder.go b/azurerm/internal/clients/builder.go index 555cfbe227a8..8e3e05be01df 100644 --- a/azurerm/internal/clients/builder.go +++ b/azurerm/internal/clients/builder.go @@ -17,6 +17,7 @@ type ClientBuilder struct { DisableTerraformPartnerID bool PartnerId string SkipProviderRegistration bool + StorageUseAzureAD bool TerraformVersion string Features features.UserFeatures } @@ -89,6 +90,7 @@ func Build(ctx context.Context, builder ClientBuilder) (*Client, error) { DisableTerraformPartnerID: builder.DisableTerraformPartnerID, Environment: *env, Features: builder.Features, + StorageUseAzureAD: builder.StorageUseAzureAD, } if err := client.Build(ctx, o); err != nil { diff --git a/azurerm/internal/common/client_options.go b/azurerm/internal/common/client_options.go index cc0375ac2321..dd211a5c7063 100644 --- a/azurerm/internal/common/client_options.go +++ b/azurerm/internal/common/client_options.go @@ -33,6 +33,7 @@ type ClientOptions struct { DisableTerraformPartnerID bool Environment azure.Environment Features features.UserFeatures + StorageUseAzureAD bool // TODO: remove me in 2.0 PollingDuration time.Duration diff --git a/azurerm/internal/provider/provider.go b/azurerm/internal/provider/provider.go index b59f0af295a3..628499e4069b 100644 --- a/azurerm/internal/provider/provider.go +++ b/azurerm/internal/provider/provider.go @@ -175,6 +175,13 @@ func AzureProvider() terraform.ResourceProvider { DefaultFunc: schema.EnvDefaultFunc("ARM_SKIP_PROVIDER_REGISTRATION", false), Description: "Should the AzureRM Provider skip registering all of the Resource Providers that it supports, if they're not already registered?", }, + + "storage_use_azuread": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_STORAGE_USE_AZUREAD", false), + Description: "Should the AzureRM Provider use AzureAD to access the Storage Data Plane API's?", + }, }, DataSourcesMap: dataSources, @@ -244,6 +251,7 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { DisableCorrelationRequestID: d.Get("disable_correlation_request_id").(bool), DisableTerraformPartnerID: d.Get("disable_terraform_partner_id").(bool), Features: expandFeatures(d.Get("features").([]interface{})), + StorageUseAzureAD: d.Get("storage_use_azuread").(bool), } client, err := clients.Build(p.StopContext(), clientBuilder) if err != nil { diff --git a/azurerm/internal/services/storage/client/client.go b/azurerm/internal/services/storage/client/client.go index 9e0af9c918bf..eccc3759166c 100644 --- a/azurerm/internal/services/storage/client/client.go +++ b/azurerm/internal/services/storage/client/client.go @@ -24,7 +24,9 @@ type Client struct { FileSystemsClient *filesystems.Client ManagementPoliciesClient storage.ManagementPoliciesClient BlobServicesClient storage.BlobServicesClient - environment az.Environment + + environment az.Environment + storageAuth *autorest.Authorizer } func NewClient(options *common.ClientOptions) *Client { @@ -32,7 +34,7 @@ func NewClient(options *common.ClientOptions) *Client { options.ConfigureClient(&accountsClient.Client, options.ResourceManagerAuthorizer) fileSystemsClient := filesystems.NewWithEnvironment(options.Environment) - fileSystemsClient.Authorizer = options.StorageAuthorizer + options.ConfigureClient(&fileSystemsClient.Client, options.StorageAuthorizer) managementPoliciesClient := storage.NewManagementPoliciesClientWithBaseURI(options.ResourceManagerEndpoint, options.SubscriptionId) options.ConfigureClient(&managementPoliciesClient.Client, options.ResourceManagerAuthorizer) @@ -42,16 +44,28 @@ func NewClient(options *common.ClientOptions) *Client { // TODO: switch Storage Containers to using the storage.BlobContainersClient // (which should fix #2977) when the storage clients have been moved in here - return &Client{ + client := Client{ AccountsClient: &accountsClient, FileSystemsClient: &fileSystemsClient, ManagementPoliciesClient: managementPoliciesClient, BlobServicesClient: blobServicesClient, environment: options.Environment, } + + if options.StorageUseAzureAD { + client.storageAuth = &options.StorageAuthorizer + } + + return &client } func (client Client) BlobsClient(ctx context.Context, account accountDetails) (*blobs.Client, error) { + if client.storageAuth != nil { + blobsClient := blobs.NewWithEnvironment(client.environment) + blobsClient.Client.Authorizer = *client.storageAuth + return &blobsClient, nil + } + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -68,6 +82,12 @@ func (client Client) BlobsClient(ctx context.Context, account accountDetails) (* } func (client Client) ContainersClient(ctx context.Context, account accountDetails) (*containers.Client, error) { + if client.storageAuth != nil { + containersClient := containers.NewWithEnvironment(client.environment) + containersClient.Client.Authorizer = *client.storageAuth + return &containersClient, nil + } + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -84,6 +104,8 @@ func (client Client) ContainersClient(ctx context.Context, account accountDetail } func (client Client) FileShareDirectoriesClient(ctx context.Context, account accountDetails) (*directories.Client, error) { + // NOTE: Files do not support AzureAD Authentication + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -100,6 +122,8 @@ func (client Client) FileShareDirectoriesClient(ctx context.Context, account acc } func (client Client) FileSharesClient(ctx context.Context, account accountDetails) (*shares.Client, error) { + // NOTE: Files do not support AzureAD Authentication + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -110,12 +134,18 @@ func (client Client) FileSharesClient(ctx context.Context, account accountDetail return nil, fmt.Errorf("Error building Authorizer: %+v", err) } - directoriesClient := shares.NewWithEnvironment(client.environment) - directoriesClient.Client.Authorizer = storageAuth - return &directoriesClient, nil + sharesClient := shares.NewWithEnvironment(client.environment) + sharesClient.Client.Authorizer = storageAuth + return &sharesClient, nil } func (client Client) QueuesClient(ctx context.Context, account accountDetails) (*queues.Client, error) { + if client.storageAuth != nil { + queueAuth := queues.NewWithEnvironment(client.environment) + queueAuth.Client.Authorizer = *client.storageAuth + return &queueAuth, nil + } + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -132,6 +162,8 @@ func (client Client) QueuesClient(ctx context.Context, account accountDetails) ( } func (client Client) TableEntityClient(ctx context.Context, account accountDetails) (*entities.Client, error) { + // NOTE: Table Entity does not support AzureAD Authentication + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) @@ -148,6 +180,8 @@ func (client Client) TableEntityClient(ctx context.Context, account accountDetai } func (client Client) TablesClient(ctx context.Context, account accountDetails) (*tables.Client, error) { + // NOTE: Tables do not support AzureAD Authentication + accountKey, err := account.AccountKey(ctx, client) if err != nil { return nil, fmt.Errorf("Error retrieving Account Key: %s", err) diff --git a/azurerm/internal/services/storage/tests/resource_arm_storage_blob_test.go b/azurerm/internal/services/storage/tests/resource_arm_storage_blob_test.go index 391699c86d6a..ddc5438f401b 100644 --- a/azurerm/internal/services/storage/tests/resource_arm_storage_blob_test.go +++ b/azurerm/internal/services/storage/tests/resource_arm_storage_blob_test.go @@ -108,6 +108,30 @@ func TestAccAzureRMStorageBlob_blockEmpty(t *testing.T) { }) } +func TestAccAzureRMStorageBlob_blockEmptyAzureADAuth(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_blob", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMStorageBlobDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMStorageBlob_blockEmptyAzureADAuth(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMStorageBlobExists(data.ResourceName), + ), + }, + { + ResourceName: data.ResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"attempts", "parallelism", "size", "type"}, + }, + }, + }) +} + func TestAccAzureRMStorageBlob_blockEmptyMetaData(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_storage_blob", "test") @@ -781,6 +805,17 @@ resource "azurerm_storage_blob" "test" { `, template) } +func testAccAzureRMStorageBlob_blockEmptyAzureADAuth(data acceptance.TestData) string { + template := testAccAzureRMStorageBlob_blockEmpty(data) + return fmt.Sprintf(` +provider "azurerm" { + storage_use_azuread = true +} + +%s +`, template) +} + func testAccAzureRMStorageBlob_blockEmptyMetaData(data acceptance.TestData) string { template := testAccAzureRMStorageBlob_template(data, "private") return fmt.Sprintf(` diff --git a/azurerm/internal/services/storage/tests/resource_arm_storage_container_test.go b/azurerm/internal/services/storage/tests/resource_arm_storage_container_test.go index 4680c462413e..45753cc73f35 100644 --- a/azurerm/internal/services/storage/tests/resource_arm_storage_container_test.go +++ b/azurerm/internal/services/storage/tests/resource_arm_storage_container_test.go @@ -33,6 +33,25 @@ func TestAccAzureRMStorageContainer_basic(t *testing.T) { }) } +func TestAccAzureRMStorageContainer_basicAzureADAuth(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_container", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMStorageContainerDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMStorageContainer_basicAzureADAuth(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMStorageContainerExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + func TestAccAzureRMStorageContainer_requiresImport(t *testing.T) { if !features.ShouldResourcesBeImported() { t.Skip("Skipping since resources aren't required to be imported") @@ -299,6 +318,17 @@ resource "azurerm_storage_container" "test" { `, template) } +func testAccAzureRMStorageContainer_basicAzureADAuth(data acceptance.TestData) string { + template := testAccAzureRMStorageContainer_basic(data) + return fmt.Sprintf(` +provider "azurerm" { + storage_use_azuread = true +} + +%s +`, template) +} + func testAccAzureRMStorageContainer_requiresImport(data acceptance.TestData) string { template := testAccAzureRMStorageContainer_basic(data) return fmt.Sprintf(` diff --git a/azurerm/internal/services/storage/tests/resource_arm_storage_queue_test.go b/azurerm/internal/services/storage/tests/resource_arm_storage_queue_test.go index b845f092b2d7..c72d4adf9e51 100644 --- a/azurerm/internal/services/storage/tests/resource_arm_storage_queue_test.go +++ b/azurerm/internal/services/storage/tests/resource_arm_storage_queue_test.go @@ -74,6 +74,25 @@ func TestAccAzureRMStorageQueue_basic(t *testing.T) { }) } +func TestAccAzureRMStorageQueue_basicAzureADAuth(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_queue", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMStorageQueueDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMStorageQueue_basicAzureADAuth(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMStorageQueueExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + func TestAccAzureRMStorageQueue_requiresImport(t *testing.T) { if !features.ShouldResourcesBeImported() { t.Skip("Skipping since resources aren't required to be imported") @@ -211,6 +230,17 @@ resource "azurerm_storage_queue" "test" { `, template, data.RandomInteger) } +func testAccAzureRMStorageQueue_basicAzureADAuth(data acceptance.TestData) string { + template := testAccAzureRMStorageQueue_basic(data) + return fmt.Sprintf(` +provider "azurerm" { + storage_use_azuread = true +} + +%s +`, template) +} + func testAccAzureRMStorageQueue_requiresImport(data acceptance.TestData) string { template := testAccAzureRMStorageQueue_basic(data) return fmt.Sprintf(` diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 5732fa2caeac..2cd82048cf09 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -135,6 +135,12 @@ For some advanced scenarios, such as where more granular permissions are necessa -> By default, Terraform will attempt to register any Resource Providers that it supports, even if they're not used in your configurations to be able to display more helpful error messages. If you're running in an environment with restricted permissions, or wish to manage Resource Provider Registration outside of Terraform you may wish to disable this flag; however please note that the error messages returned from Azure may be confusing as a result (example: `API version 2019-01-01 was not found for Microsoft.Foo`). +* `storage_use_azuread` - (Optional) Should the AzureRM Provider use AzureAD to connect to the Storage Blob & Queue API's, rather than the SharedKey from the Storage Account? This can also be sourced from the `ARM_STORAGE_USE_AZUREAD` Environment Variable. Defaults to `false`. + +~> **Note:** This requires that the User/Service Principal being used has the associated `Storage` roles - which are added to new Contributor/Owner role-assignments, but **have not** been backported by Azure to existing role-assignments. + +~> **Note:** The Files & Table Storage API's do not support authenticating via AzureAD and will continue to use a SharedKey to access the API's. + It's also possible to use multiple Provider blocks within a single Terraform configuration, for example to work with resources across multiple Subscriptions - more information can be found [in the documentation for Providers](https://www.terraform.io/docs/configuration/providers.html#multiple-provider-instances). ---