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 }