github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/azure/arm_client.go (about)

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