github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/azure/credentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  
    11  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    12  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    13  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    14  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    15  	"github.com/juju/errors"
    16  
    17  	"github.com/juju/juju/cloud"
    18  	"github.com/juju/juju/environs"
    19  	"github.com/juju/juju/provider/azure/internal/azureauth"
    20  	"github.com/juju/juju/provider/azure/internal/azurecli"
    21  )
    22  
    23  const (
    24  	credAttrAppId                 = "application-id"
    25  	credAttrApplicationObjectId   = "application-object-id"
    26  	credAttrSubscriptionId        = "subscription-id"
    27  	credAttrManagedSubscriptionId = "managed-subscription-id"
    28  	credAttrAppPassword           = "application-password"
    29  
    30  	// clientCredentialsAuthType is the auth-type for the
    31  	// "client credentials" OAuth flow, which requires a
    32  	// service principal with a password.
    33  	clientCredentialsAuthType cloud.AuthType = "service-principal-secret"
    34  
    35  	// deviceCodeAuthType is the auth-type for the interactive
    36  	// "device code" OAuth flow.
    37  	deviceCodeAuthType cloud.AuthType = cloud.InteractiveAuthType
    38  )
    39  
    40  type ServicePrincipalCreator interface {
    41  	InteractiveCreate(sdkCtx context.Context, stderr io.Writer, params azureauth.ServicePrincipalParams) (appid, spid, password string, _ error)
    42  	Create(sdkCtx context.Context, params azureauth.ServicePrincipalParams) (appid, spid, password string, _ error)
    43  }
    44  
    45  type AzureCLI interface {
    46  	ListAccounts() ([]azurecli.Account, error)
    47  	FindAccountsWithCloudName(name string) ([]azurecli.Account, error)
    48  	ShowAccount(subscription string) (*azurecli.Account, error)
    49  	ListClouds() ([]azurecli.Cloud, error)
    50  }
    51  
    52  // environPoviderCredentials is an implementation of
    53  // environs.ProviderCredentials for the Azure Resource
    54  // Manager cloud provider.
    55  type environProviderCredentials struct {
    56  	servicePrincipalCreator ServicePrincipalCreator
    57  	azureCLI                AzureCLI
    58  	transporter             policy.Transporter
    59  }
    60  
    61  // CredentialSchemas is part of the environs.ProviderCredentials interface.
    62  func (c environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema {
    63  	interactiveSchema := cloud.CredentialSchema{{
    64  		credAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"},
    65  	}}
    66  	if _, err := c.azureCLI.ShowAccount(""); err == nil {
    67  		// If az account show returns successfully then we can
    68  		// use that to get at least some login details, otherwise
    69  		// we need the user to supply their subscription ID.
    70  		interactiveSchema[0].CredentialAttr.Optional = true
    71  	}
    72  	return map[cloud.AuthType]cloud.CredentialSchema{
    73  		// deviceCodeAuthType is the interactive device-code oauth
    74  		// flow. This is only supported on the client side; it will
    75  		// be used to generate a service principal, and transformed
    76  		// into clientCredentialsAuthType.
    77  		deviceCodeAuthType: interactiveSchema,
    78  
    79  		// clientCredentialsAuthType is the "client credentials"
    80  		// oauth flow, which requires a service principal with a
    81  		// password.
    82  		clientCredentialsAuthType: {
    83  			{
    84  				credAttrAppId, cloud.CredentialAttr{Description: "Azure Active Directory application ID"},
    85  			}, {
    86  				credAttrApplicationObjectId, cloud.CredentialAttr{
    87  					Description: "Azure Active Directory application Object ID",
    88  					Optional:    true,
    89  				},
    90  			}, {
    91  				credAttrSubscriptionId, cloud.CredentialAttr{Description: "Azure subscription ID"},
    92  			}, {
    93  				credAttrManagedSubscriptionId, cloud.CredentialAttr{
    94  					Description: "Azure managed subscription ID",
    95  					Optional:    true,
    96  				},
    97  			}, {
    98  				credAttrAppPassword, cloud.CredentialAttr{
    99  					Description: "Azure Active Directory application password",
   100  					Hidden:      true,
   101  				},
   102  			},
   103  		},
   104  	}
   105  }
   106  
   107  // DetectCredentials is part of the environs.ProviderCredentials
   108  // interface. It attempts to detect subscription IDs from accounts
   109  // configured in the Azure CLI.
   110  func (c environProviderCredentials) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) {
   111  	// Attempt to get accounts from az.
   112  	accounts, err := c.azureCLI.ListAccounts()
   113  	if err != nil {
   114  		logger.Debugf("error getting accounts from az: %s", err)
   115  		return nil, errors.NotFoundf("credentials")
   116  	}
   117  	if len(accounts) < 1 {
   118  		return nil, errors.NotFoundf("credentials")
   119  	}
   120  	clouds, err := c.azureCLI.ListClouds()
   121  	if err != nil {
   122  		logger.Debugf("error getting clouds from az: %s", err)
   123  		return nil, errors.NotFoundf("credentials")
   124  	}
   125  	cloudMap := make(map[string]azurecli.Cloud, len(clouds))
   126  	for _, cloud := range clouds {
   127  		cloudMap[cloud.Name] = cloud
   128  	}
   129  	var defaultCredential string
   130  	authCredentials := make(map[string]cloud.Credential)
   131  	for _, acc := range accounts {
   132  		cloudInfo, ok := cloudMap[acc.CloudName]
   133  		if !ok {
   134  			continue
   135  		}
   136  		cred, err := c.accountCredential(acc)
   137  		if err != nil {
   138  			logger.Debugf("cannot get credential for %s: %s", acc.Name, err)
   139  			continue
   140  		}
   141  		cred.Label = fmt.Sprintf("%s subscription %s", cloudInfo.Name, acc.Name)
   142  		authCredentials[acc.Name] = cred
   143  		if acc.IsDefault {
   144  			defaultCredential = acc.Name
   145  		}
   146  	}
   147  	if len(authCredentials) < 1 {
   148  		return nil, errors.NotFoundf("credentials")
   149  	}
   150  	return &cloud.CloudCredential{
   151  		DefaultCredential: defaultCredential,
   152  		AuthCredentials:   authCredentials,
   153  	}, nil
   154  }
   155  
   156  // FinalizeCredential is part of the environs.ProviderCredentials interface.
   157  func (c environProviderCredentials) FinalizeCredential(
   158  	ctx environs.FinalizeCredentialContext,
   159  	args environs.FinalizeCredentialParams,
   160  ) (*cloud.Credential, error) {
   161  	switch authType := args.Credential.AuthType(); authType {
   162  	case deviceCodeAuthType:
   163  		subscriptionId := args.Credential.Attributes()[credAttrSubscriptionId]
   164  
   165  		var azCloudName string
   166  		switch args.CloudName {
   167  		case "azure":
   168  			azCloudName = azureauth.AzureCloud
   169  		case "azure-china":
   170  			azCloudName = azureauth.AzureChinaCloud
   171  		case "azure-gov":
   172  			azCloudName = azureauth.AzureUSGovernment
   173  		default:
   174  			return nil, errors.Errorf("unknown Azure cloud name %q", args.CloudName)
   175  		}
   176  
   177  		if subscriptionId != "" {
   178  			opts := azcore.ClientOptions{
   179  				Cloud:     azureCloud(args.CloudName, args.CloudEndpoint, args.CloudIdentityEndpoint),
   180  				Transport: c.transporter,
   181  			}
   182  			clientOpts := arm.ClientOptions{ClientOptions: opts}
   183  			sdkCtx := context.Background()
   184  			tenantID, err := azureauth.DiscoverTenantID(sdkCtx, subscriptionId, clientOpts)
   185  			if err != nil {
   186  				return nil, errors.Trace(err)
   187  			}
   188  			return c.deviceCodeCredential(ctx, args, azureauth.ServicePrincipalParams{
   189  				CloudName:      azCloudName,
   190  				SubscriptionId: subscriptionId,
   191  				TenantId:       tenantID,
   192  			})
   193  		}
   194  
   195  		params, err := c.getServicePrincipalParams(azCloudName)
   196  		if err != nil {
   197  			return nil, errors.Trace(err)
   198  		}
   199  		return c.azureCLICredential(ctx, args, params)
   200  	case clientCredentialsAuthType:
   201  		return &args.Credential, nil
   202  	default:
   203  		return nil, errors.NotSupportedf("%q auth-type", authType)
   204  	}
   205  }
   206  
   207  func (c environProviderCredentials) deviceCodeCredential(
   208  	ctx environs.FinalizeCredentialContext,
   209  	args environs.FinalizeCredentialParams,
   210  	params azureauth.ServicePrincipalParams,
   211  ) (*cloud.Credential, error) {
   212  	sdkCtx := context.Background()
   213  	applicationId, spObjectId, password, err := c.servicePrincipalCreator.InteractiveCreate(sdkCtx, ctx.GetStderr(), params)
   214  	if err != nil {
   215  		return nil, errors.Trace(err)
   216  	}
   217  	out := cloud.NewCredential(clientCredentialsAuthType, map[string]string{
   218  		credAttrSubscriptionId:      params.SubscriptionId,
   219  		credAttrAppId:               applicationId,
   220  		credAttrApplicationObjectId: spObjectId,
   221  		credAttrAppPassword:         password,
   222  	})
   223  	out.Label = args.Credential.Label
   224  	return &out, nil
   225  }
   226  
   227  func (c environProviderCredentials) azureCLICredential(
   228  	ctx environs.FinalizeCredentialContext,
   229  	args environs.FinalizeCredentialParams,
   230  	params azureauth.ServicePrincipalParams,
   231  ) (*cloud.Credential, error) {
   232  	cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{
   233  		TenantID: params.TenantId,
   234  	})
   235  	if err != nil {
   236  		return nil, errors.Trace(err)
   237  	}
   238  	params.Credential = cred
   239  
   240  	if err != nil {
   241  		return nil, errors.Annotatef(err, "cannot get access token for %s", params.SubscriptionId)
   242  	}
   243  
   244  	sdkCtx := context.Background()
   245  	applicationId, spObjectId, password, err := c.servicePrincipalCreator.Create(sdkCtx, params)
   246  	if err != nil {
   247  		return nil, errors.Annotatef(err, "cannot create service principal")
   248  	}
   249  	out := cloud.NewCredential(clientCredentialsAuthType, map[string]string{
   250  		credAttrSubscriptionId:      params.SubscriptionId,
   251  		credAttrAppId:               applicationId,
   252  		credAttrApplicationObjectId: spObjectId,
   253  		credAttrAppPassword:         password,
   254  	})
   255  	out.Label = args.Credential.Label
   256  	return &out, nil
   257  }
   258  
   259  func (c environProviderCredentials) accountCredential(
   260  	acc azurecli.Account,
   261  ) (cloud.Credential, error) {
   262  	cred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{
   263  		TenantID:                   acc.AuthTenantId(),
   264  		AdditionallyAllowedTenants: []string{"*"},
   265  	})
   266  	if err != nil {
   267  		return cloud.Credential{}, errors.Annotate(err, "cannot get az cli credential")
   268  	}
   269  	sdkCtx := context.Background()
   270  	applicationId, spObjectId, password, err := c.servicePrincipalCreator.Create(sdkCtx, azureauth.ServicePrincipalParams{
   271  		Credential:     cred,
   272  		SubscriptionId: acc.ID,
   273  		TenantId:       acc.TenantId,
   274  	})
   275  	if err != nil {
   276  		return cloud.Credential{}, errors.Annotate(err, "cannot get service principal")
   277  	}
   278  
   279  	return cloud.NewCredential(clientCredentialsAuthType, map[string]string{
   280  		credAttrSubscriptionId:      acc.ID,
   281  		credAttrAppId:               applicationId,
   282  		credAttrApplicationObjectId: spObjectId,
   283  		credAttrAppPassword:         password,
   284  	}), nil
   285  }
   286  
   287  func (c environProviderCredentials) getServicePrincipalParams(cloudName string) (azureauth.ServicePrincipalParams, error) {
   288  	accounts, err := c.azureCLI.FindAccountsWithCloudName(cloudName)
   289  	if err != nil {
   290  		return azureauth.ServicePrincipalParams{}, errors.Annotatef(err, "cannot get accounts")
   291  	}
   292  	if len(accounts) < 1 {
   293  		return azureauth.ServicePrincipalParams{}, errors.Errorf("no %s accounts found", cloudName)
   294  	}
   295  	acc := accounts[0]
   296  	for _, a := range accounts[1:] {
   297  		if a.IsDefault {
   298  			acc = a
   299  		}
   300  	}
   301  	return azureauth.ServicePrincipalParams{
   302  		CloudName:      cloudName,
   303  		SubscriptionId: acc.ID,
   304  		TenantId:       acc.AuthTenantId(),
   305  	}, nil
   306  
   307  }