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