Files
opentf/internal/backend/remote-state/azure/auth/cli_auth.go
2025-09-15 19:22:17 +01:00

118 lines
3.4 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package auth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type AzureCLIAuthConfig struct {
CLIAuthEnabled bool
}
type azureCLICredentialAuth struct{}
var _ AuthMethod = &azureCLICredentialAuth{}
func (cred *azureCLICredentialAuth) Name() string {
return "Azure CLI Auth"
}
func (cred *azureCLICredentialAuth) Construct(_ context.Context, config *Config) (azcore.TokenCredential, error) {
// The SubscriptionID and TenantID can be empty, and the logic of this will still be okay
return azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{
Subscription: config.StorageAddresses.SubscriptionID,
TenantID: config.StorageAddresses.TenantID,
})
}
func (cred *azureCLICredentialAuth) Validate(ctx context.Context, config *Config) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if !config.CLIAuthEnabled {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid Azure Command Line Auth",
"Setting use_cli to false prevents the use of command-line auth (az).",
))
return diags
}
_, err := exec.LookPath("az")
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid Azure Command Line Auth",
"Error looking for command az in your PATH. Make sure the Azure Command Line tool is installed and executable.",
))
return diags
}
// Make sure the user is logged in by attempting to get the subscription
_, err = getCurrentSubscriptionInfo(ctx)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid Azure Command Line Auth",
fmt.Sprintf("Error using the az command: %s.", tfdiags.FormatError(err)),
))
}
return diags
}
func (cred *azureCLICredentialAuth) AugmentConfig(ctx context.Context, config *Config) (err error) {
if config.StorageAddresses.SubscriptionID == "" {
config.StorageAddresses.SubscriptionID, err = getCliAzureSubscriptionID(ctx)
if err != nil {
return err
}
}
return checkNamesForAccessKeyCredentials(config.StorageAddresses)
}
type Subscription struct {
Id string `json:"id"`
Name string `json:"name"`
IsDefault bool `json:"isDefault"`
}
// getCliAzureSubscriptionID obtains the subscription ID currently active in the
// Azure profile. This assumes the user has the Azure CLI installed on their machine.
func getCliAzureSubscriptionID(ctx context.Context) (string, error) {
rawSubscription, err := getCurrentSubscriptionInfo(ctx)
if err != nil {
return "", err
}
var subscription Subscription
err = json.Unmarshal(rawSubscription, &subscription)
if err != nil {
return "", fmt.Errorf("json error for azure subscription: %w", err)
}
return subscription.Id, nil
}
// getCurrentSubscriptionInfo is adapted from azure-sdk-for-go's CLI token retrieval
func getCurrentSubscriptionInfo(ctx context.Context) ([]byte, error) {
cliCmd := exec.CommandContext(ctx, "az", "account", "show", "-o", "json")
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
stdout, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
return nil, fmt.Errorf("error getting subscription info: error: %w\nmore information: %s", err, msg)
}
return stdout, nil
}