github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/azure/arm_client.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package azure
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"log"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
    16  	armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-01-01/storage"
    17  	"github.com/Azure/go-autorest/autorest"
    18  	"github.com/Azure/go-autorest/autorest/azure"
    19  	"github.com/hashicorp/go-azure-helpers/authentication"
    20  	"github.com/hashicorp/go-azure-helpers/sender"
    21  	"github.com/manicminer/hamilton/environments"
    22  	"github.com/opentofu/opentofu/internal/httpclient"
    23  	"github.com/opentofu/opentofu/version"
    24  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs"
    25  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers"
    26  )
    27  
    28  type ArmClient struct {
    29  	// These Clients are only initialized if an Access Key isn't provided
    30  	groupsClient          *resources.GroupsClient
    31  	storageAccountsClient *armStorage.AccountsClient
    32  	containersClient      *containers.Client
    33  	blobsClient           *blobs.Client
    34  
    35  	// azureAdStorageAuth is only here if we're using AzureAD Authentication but is an Authorizer for Storage
    36  	azureAdStorageAuth *autorest.Authorizer
    37  
    38  	accessKey          string
    39  	environment        azure.Environment
    40  	resourceGroupName  string
    41  	storageAccountName string
    42  	sasToken           string
    43  }
    44  
    45  func buildArmClient(ctx context.Context, config BackendConfig) (*ArmClient, error) {
    46  	env, err := authentication.AzureEnvironmentByNameFromEndpoint(ctx, config.MetadataHost, config.Environment)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	client := ArmClient{
    52  		environment:        *env,
    53  		resourceGroupName:  config.ResourceGroupName,
    54  		storageAccountName: config.StorageAccountName,
    55  	}
    56  
    57  	// if we have an Access Key - we don't need the other clients
    58  	if config.AccessKey != "" {
    59  		client.accessKey = config.AccessKey
    60  		return &client, nil
    61  	}
    62  
    63  	// likewise with a SAS token
    64  	if config.SasToken != "" {
    65  		client.sasToken = config.SasToken
    66  		return &client, nil
    67  	}
    68  
    69  	builder := authentication.Builder{
    70  		ClientID:                      config.ClientID,
    71  		SubscriptionID:                config.SubscriptionID,
    72  		TenantID:                      config.TenantID,
    73  		CustomResourceManagerEndpoint: config.CustomResourceManagerEndpoint,
    74  		MetadataHost:                  config.MetadataHost,
    75  		Environment:                   config.Environment,
    76  		ClientSecretDocsLink:          "https://registry.opentofu.org/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret",
    77  
    78  		// Service Principal (Client Certificate)
    79  		ClientCertPassword: config.ClientCertificatePassword,
    80  		ClientCertPath:     config.ClientCertificatePath,
    81  
    82  		// Service Principal (Client Secret)
    83  		ClientSecret: config.ClientSecret,
    84  
    85  		// Managed Service Identity
    86  		MsiEndpoint: config.MsiEndpoint,
    87  
    88  		// OIDC
    89  		IDToken:             config.OIDCToken,
    90  		IDTokenFilePath:     config.OIDCTokenFilePath,
    91  		IDTokenRequestURL:   config.OIDCRequestURL,
    92  		IDTokenRequestToken: config.OIDCRequestToken,
    93  
    94  		// Feature Toggles
    95  		SupportsAzureCliToken:          true,
    96  		SupportsClientCertAuth:         true,
    97  		SupportsClientSecretAuth:       true,
    98  		SupportsManagedServiceIdentity: config.UseMsi,
    99  		SupportsOIDCAuth:               config.UseOIDC,
   100  		UseMicrosoftGraph:              true,
   101  	}
   102  	armConfig, err := builder.Build()
   103  	if err != nil {
   104  		return nil, fmt.Errorf("Error building ARM Config: %w", err)
   105  	}
   106  
   107  	oauthConfig, err := armConfig.BuildOAuthConfig(env.ActiveDirectoryEndpoint)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	hamiltonEnv, err := environments.EnvironmentFromString(config.Environment)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	sender := sender.BuildSender("backend/remote-state/azure")
   118  	log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Resource Manager..")
   119  	auth, err := armConfig.GetMSALToken(ctx, hamiltonEnv.ResourceManager, sender, oauthConfig, env.TokenAudience)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	if config.UseAzureADAuthentication {
   125  		log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Storage..")
   126  		storageAuth, err := armConfig.GetMSALToken(ctx, hamiltonEnv.Storage, sender, oauthConfig, env.ResourceIdentifiers.Storage)
   127  		if err != nil {
   128  			return nil, err
   129  		}
   130  		client.azureAdStorageAuth = &storageAuth
   131  	}
   132  
   133  	accountsClient := armStorage.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID)
   134  	client.configureClient(&accountsClient.Client, auth)
   135  	client.storageAccountsClient = &accountsClient
   136  
   137  	groupsClient := resources.NewGroupsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID)
   138  	client.configureClient(&groupsClient.Client, auth)
   139  	client.groupsClient = &groupsClient
   140  
   141  	return &client, nil
   142  }
   143  
   144  func (c ArmClient) getBlobClient(ctx context.Context) (*blobs.Client, error) {
   145  	if c.sasToken != "" {
   146  		log.Printf("[DEBUG] Building the Blob Client from a SAS Token")
   147  		storageAuth, err := autorest.NewSASTokenAuthorizer(c.sasToken)
   148  		if err != nil {
   149  			return nil, fmt.Errorf("Error building SAS Token Authorizer: %w", err)
   150  		}
   151  
   152  		blobsClient := blobs.NewWithEnvironment(c.environment)
   153  		c.configureClient(&blobsClient.Client, storageAuth)
   154  		return &blobsClient, nil
   155  	}
   156  
   157  	if c.azureAdStorageAuth != nil {
   158  		blobsClient := blobs.NewWithEnvironment(c.environment)
   159  		c.configureClient(&blobsClient.Client, *c.azureAdStorageAuth)
   160  		return &blobsClient, nil
   161  	}
   162  
   163  	accessKey := c.accessKey
   164  	if accessKey == "" {
   165  		log.Printf("[DEBUG] Building the Blob Client from an Access Token (using user credentials)")
   166  		keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName, "")
   167  		if err != nil {
   168  			return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %w", c.storageAccountName, err)
   169  		}
   170  
   171  		if keys.Keys == nil {
   172  			return nil, fmt.Errorf("Nil key returned for storage account %q", c.storageAccountName)
   173  		}
   174  
   175  		accessKeys := *keys.Keys
   176  		accessKey = *accessKeys[0].Value
   177  	}
   178  
   179  	storageAuth, err := autorest.NewSharedKeyAuthorizer(c.storageAccountName, accessKey, autorest.SharedKey)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("Error building Shared Key Authorizer: %w", err)
   182  	}
   183  
   184  	blobsClient := blobs.NewWithEnvironment(c.environment)
   185  	c.configureClient(&blobsClient.Client, storageAuth)
   186  	return &blobsClient, nil
   187  }
   188  
   189  func (c ArmClient) getContainersClient(ctx context.Context) (*containers.Client, error) {
   190  	if c.sasToken != "" {
   191  		log.Printf("[DEBUG] Building the Container Client from a SAS Token")
   192  		storageAuth, err := autorest.NewSASTokenAuthorizer(c.sasToken)
   193  		if err != nil {
   194  			return nil, fmt.Errorf("Error building SAS Token Authorizer: %w", err)
   195  		}
   196  
   197  		containersClient := containers.NewWithEnvironment(c.environment)
   198  		c.configureClient(&containersClient.Client, storageAuth)
   199  		return &containersClient, nil
   200  	}
   201  
   202  	if c.azureAdStorageAuth != nil {
   203  		containersClient := containers.NewWithEnvironment(c.environment)
   204  		c.configureClient(&containersClient.Client, *c.azureAdStorageAuth)
   205  		return &containersClient, nil
   206  	}
   207  
   208  	accessKey := c.accessKey
   209  	if accessKey == "" {
   210  		log.Printf("[DEBUG] Building the Container Client from an Access Token (using user credentials)")
   211  		keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName, "")
   212  		if err != nil {
   213  			return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %w", c.storageAccountName, err)
   214  		}
   215  
   216  		if keys.Keys == nil {
   217  			return nil, fmt.Errorf("Nil key returned for storage account %q", c.storageAccountName)
   218  		}
   219  
   220  		accessKeys := *keys.Keys
   221  		accessKey = *accessKeys[0].Value
   222  	}
   223  
   224  	storageAuth, err := autorest.NewSharedKeyAuthorizer(c.storageAccountName, accessKey, autorest.SharedKey)
   225  	if err != nil {
   226  		return nil, fmt.Errorf("Error building Shared Key Authorizer: %w", err)
   227  	}
   228  
   229  	containersClient := containers.NewWithEnvironment(c.environment)
   230  	c.configureClient(&containersClient.Client, storageAuth)
   231  	return &containersClient, nil
   232  }
   233  
   234  func (c *ArmClient) configureClient(client *autorest.Client, auth autorest.Authorizer) {
   235  	client.UserAgent = buildUserAgent()
   236  	client.Authorizer = auth
   237  	client.Sender = buildSender()
   238  	client.SkipResourceProviderRegistration = false
   239  	client.PollingDuration = 60 * time.Minute
   240  }
   241  
   242  func buildUserAgent() string {
   243  	userAgent := httpclient.OpenTofuUserAgent(version.Version)
   244  
   245  	// append the CloudShell version to the user agent if it exists
   246  	if azureAgent := os.Getenv("AZURE_HTTP_USER_AGENT"); azureAgent != "" {
   247  		userAgent = fmt.Sprintf("%s %s", userAgent, azureAgent)
   248  	}
   249  
   250  	return userAgent
   251  }