github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/internal/azureauth/serviceprincipal.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azureauth
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"path"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization"
    15  	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
    16  	"github.com/Azure/go-autorest/autorest"
    17  	"github.com/Azure/go-autorest/autorest/adal"
    18  	"github.com/Azure/go-autorest/autorest/to"
    19  	"github.com/juju/clock"
    20  	"github.com/juju/errors"
    21  	"github.com/juju/loggo"
    22  	"github.com/juju/retry"
    23  	"github.com/juju/utils"
    24  
    25  	"github.com/juju/juju/provider/azure/internal/ad"
    26  	"github.com/juju/juju/provider/azure/internal/errorutils"
    27  	"github.com/juju/juju/provider/azure/internal/tracing"
    28  	"github.com/juju/juju/provider/azure/internal/useragent"
    29  )
    30  
    31  var logger = loggo.GetLogger("juju.provider.azure.internal.azureauth")
    32  
    33  const (
    34  	// jujuApplicationId is the ID of the Azure application that we use
    35  	// for interactive authentication. When the user logs in, a service
    36  	// principal will be created in their Active Directory tenant for
    37  	// the application.
    38  	jujuApplicationId = "cbb548f1-5039-4836-af0b-727e8571f6a9"
    39  
    40  	// passwordExpiryDuration is how long the application password we
    41  	// set will remain valid.
    42  	passwordExpiryDuration = 365 * 24 * time.Hour
    43  )
    44  
    45  type ServicePrincipalParams struct {
    46  	// GraphEndpoint of the Azure graph API.
    47  	GraphEndpoint string
    48  
    49  	// GraphResourceId is the resource ID of the graph API that is
    50  	// used when acquiring access tokens.
    51  	GraphResourceId string
    52  
    53  	// GraphAuthorizer is the authorization needed to contact the
    54  	// Azure graph API.
    55  	GraphAuthorizer autorest.Authorizer
    56  
    57  	// ResourceManagerEndpoint is the endpoint of the azure resource
    58  	// manager API.
    59  	ResourceManagerEndpoint string
    60  
    61  	// ResourceManagerResourceId is the resource ID of the resource manager  API that is
    62  	// used when acquiring access tokens.
    63  	ResourceManagerResourceId string
    64  
    65  	// ResourceManagerAuthorizer is the authorization needed to
    66  	// contact the Azure resource manager API.
    67  	ResourceManagerAuthorizer autorest.Authorizer
    68  
    69  	// SubscriptionId is the subscription ID of the account creating
    70  	// the service principal.
    71  	SubscriptionId string
    72  
    73  	// TenantId is the tenant that the account creating the service
    74  	// principal belongs to.
    75  	TenantId string
    76  }
    77  
    78  func (p ServicePrincipalParams) directoryClient(sender autorest.Sender, requestInspector autorest.PrepareDecorator) ad.ManagementClient {
    79  	baseURL := p.GraphEndpoint
    80  	if !strings.HasSuffix(baseURL, "/") {
    81  		baseURL += "/"
    82  	}
    83  	baseURL += p.TenantId
    84  	directoryClient := ad.NewManagementClient(baseURL)
    85  	directoryClient.Authorizer = p.GraphAuthorizer
    86  	directoryClient.Sender = sender
    87  	setClientInspectors(&directoryClient.Client, requestInspector, "azure.directory")
    88  	return directoryClient
    89  }
    90  
    91  func (p ServicePrincipalParams) authorizationClient(sender autorest.Sender, requestInspector autorest.PrepareDecorator) authorization.BaseClient {
    92  	authorizationClient := authorization.NewWithBaseURI(p.ResourceManagerEndpoint, p.SubscriptionId)
    93  	useragent.UpdateClient(&authorizationClient.Client)
    94  	authorizationClient.Authorizer = p.ResourceManagerAuthorizer
    95  	authorizationClient.Sender = sender
    96  	setClientInspectors(&authorizationClient.Client, requestInspector, "azure.authorization")
    97  	return authorizationClient
    98  }
    99  
   100  func setClientInspectors(
   101  	client *autorest.Client,
   102  	requestInspector autorest.PrepareDecorator,
   103  	loggingModule string,
   104  ) {
   105  	logger := loggo.GetLogger(loggingModule)
   106  	client.ResponseInspector = tracing.RespondDecorator(logger)
   107  	client.RequestInspector = tracing.PrepareDecorator(logger)
   108  	if requestInspector != nil {
   109  		tracer := client.RequestInspector
   110  		client.RequestInspector = func(p autorest.Preparer) autorest.Preparer {
   111  			p = tracer(p)
   112  			p = requestInspector(p)
   113  			return p
   114  		}
   115  	}
   116  }
   117  
   118  type ServicePrincipalCreator struct {
   119  	Sender           autorest.Sender
   120  	RequestInspector autorest.PrepareDecorator
   121  	Clock            clock.Clock
   122  	NewUUID          func() (utils.UUID, error)
   123  }
   124  
   125  // InteractiveCreate creates a new ServicePrincipal by performing device
   126  // code authentication with Azure AD and creating the service principal
   127  // using the credentials that are obtained. Only GraphEndpoint,
   128  // GraphResourceId, ResourceManagerEndpoint, ResourceManagerResourceId
   129  // and SubscriptionId need to be specified in params, the other values
   130  // will be derived.
   131  func (c *ServicePrincipalCreator) InteractiveCreate(sdkCtx context.Context, stderr io.Writer, params ServicePrincipalParams) (appid, password string, _ error) {
   132  	subscriptionsClient := subscriptions.Client{
   133  		subscriptions.NewWithBaseURI(params.ResourceManagerEndpoint),
   134  	}
   135  	useragent.UpdateClient(&subscriptionsClient.Client)
   136  	subscriptionsClient.Sender = c.Sender
   137  	setClientInspectors(&subscriptionsClient.Client, c.RequestInspector, "azure.subscriptions")
   138  
   139  	oauthConfig, tenantId, err := OAuthConfig(
   140  		sdkCtx,
   141  		subscriptionsClient,
   142  		params.ResourceManagerEndpoint,
   143  		params.SubscriptionId,
   144  	)
   145  	if err != nil {
   146  		return "", "", errors.Trace(err)
   147  	}
   148  
   149  	client := autorest.NewClientWithUserAgent("")
   150  	useragent.UpdateClient(&client)
   151  	client.Sender = c.Sender
   152  	setClientInspectors(&client, c.RequestInspector, "azure.autorest")
   153  
   154  	// Perform the interactive authentication. The user will be prompted to
   155  	// open a URL and input a device code, after which they will have to
   156  	// enter their username and password if they are not already
   157  	// authenticated with Azure.
   158  	fmt.Fprintln(stderr, "Initiating interactive authentication.")
   159  	fmt.Fprintln(stderr)
   160  	clientId := jujuApplicationId
   161  	deviceCode, err := adal.InitiateDeviceAuth(&client, *oauthConfig, clientId, params.ResourceManagerResourceId)
   162  	if err != nil {
   163  		return "", "", errors.Annotate(err, "initiating interactive authentication")
   164  	}
   165  	fmt.Fprintln(stderr, to.String(deviceCode.Message)+"\n")
   166  	token, err := adal.WaitForUserCompletion(&client, deviceCode)
   167  	if err != nil {
   168  		return "", "", errors.Annotate(err, "waiting for interactive authentication to completed")
   169  	}
   170  
   171  	// Create service principal tokens that we can use to authorize API
   172  	// requests to Active Directory and Resource Manager. These tokens
   173  	// are only valid for a short amount of time, so we must create a
   174  	// service principal password that can be used to obtain new tokens.
   175  	armSpt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, params.ResourceManagerResourceId, *token)
   176  	if err != nil {
   177  		return "", "", errors.Annotate(err, "creating temporary ARM service principal token")
   178  	}
   179  	armSpt.SetSender(&client)
   180  	if err := armSpt.Refresh(); err != nil {
   181  		return "", "", errors.Trace(err)
   182  	}
   183  
   184  	// The application requires permissions for both ARM and AD, so we
   185  	// can use the token for both APIs.
   186  	graphToken := armSpt.Token()
   187  	graphToken.Resource = params.GraphResourceId
   188  	graphSpt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, params.GraphResourceId, graphToken)
   189  	if err != nil {
   190  		return "", "", errors.Annotate(err, "creating temporary Graph service principal token")
   191  	}
   192  	graphSpt.SetSender(&client)
   193  	if err := graphSpt.Refresh(); err != nil {
   194  		return "", "", errors.Trace(err)
   195  	}
   196  	params.GraphAuthorizer = autorest.NewBearerAuthorizer(graphSpt)
   197  	params.ResourceManagerAuthorizer = autorest.NewBearerAuthorizer(armSpt)
   198  	params.TenantId = tenantId
   199  
   200  	userObject, err := ad.UsersClient{params.directoryClient(c.Sender, c.RequestInspector)}.GetCurrentUser()
   201  	if err != nil {
   202  		return "", "", errors.Trace(err)
   203  	}
   204  	fmt.Fprintf(stderr, "Authenticated as %q.\n", userObject.DisplayName)
   205  
   206  	return c.Create(sdkCtx, params)
   207  }
   208  
   209  // Create creates a new service principal using the values specified in params.
   210  func (c *ServicePrincipalCreator) Create(sdkCtx context.Context, params ServicePrincipalParams) (appid, password string, _ error) {
   211  	servicePrincipalObjectId, password, err := c.createOrUpdateServicePrincipal(params)
   212  	if err != nil {
   213  		return "", "", errors.Trace(err)
   214  	}
   215  	if err := c.createRoleAssignment(sdkCtx, params, servicePrincipalObjectId); err != nil {
   216  		return "", "", errors.Trace(err)
   217  	}
   218  	return jujuApplicationId, password, nil
   219  }
   220  
   221  func (c *ServicePrincipalCreator) createOrUpdateServicePrincipal(params ServicePrincipalParams) (servicePrincipalObjectId, password string, _ error) {
   222  	passwordCredential, err := c.preparePasswordCredential()
   223  	if err != nil {
   224  		return "", "", errors.Annotate(err, "preparing password credential")
   225  	}
   226  
   227  	// Attempt to create the service principal. When the user
   228  	// authenticates, Azure will replicate the application
   229  	// into the user's AAD. This happens asynchronously, so
   230  	// it may not exist by the time we try to create the
   231  	// service principal; thus, we retry until it exists. The
   232  	// error checking is based on the logic in azure-cli's
   233  	// create_service_principal_for_rbac.
   234  	client := ad.ServicePrincipalsClient{params.directoryClient(c.Sender, c.RequestInspector)}
   235  	var servicePrincipal ad.ServicePrincipal
   236  	createServicePrincipal := func() error {
   237  		var err error
   238  		servicePrincipal, err = client.Create(
   239  			ad.ServicePrincipalCreateParameters{
   240  				ApplicationID:       jujuApplicationId,
   241  				AccountEnabled:      true,
   242  				PasswordCredentials: []ad.PasswordCredential{passwordCredential},
   243  			},
   244  			nil, // abort
   245  		)
   246  		return err
   247  	}
   248  	retryArgs := retry.CallArgs{
   249  		Func: createServicePrincipal,
   250  		IsFatalError: func(err error) bool {
   251  			serviceErr, ok := errorutils.ServiceError(err)
   252  			if ok && (strings.Contains(serviceErr.Message, " does not reference ") ||
   253  				strings.Contains(serviceErr.Message, " does not exist ")) {
   254  				// The application doesn't exist yet, retry later.
   255  				return false
   256  			}
   257  			return true
   258  		},
   259  		Clock:       c.clock(),
   260  		Delay:       5 * time.Second,
   261  		MaxDuration: time.Minute,
   262  	}
   263  	if err := retry.Call(retryArgs); err != nil {
   264  		if !isMultipleObjectsWithSameKeyValueErr(err) {
   265  			return "", "", errors.Annotate(err, "creating service principal")
   266  		}
   267  		// The service principal already exists, so we'll fall out
   268  		// and update the service principal's password credentials.
   269  	} else {
   270  		// The service principal was created successfully, with the
   271  		// requested password credential.
   272  		return servicePrincipal.ObjectID, passwordCredential.Value, nil
   273  	}
   274  
   275  	// The service principal already exists, so we need to query
   276  	// its object ID, and fetch the existing password credentials
   277  	// to update.
   278  	servicePrincipal, err = getServicePrincipal(client)
   279  	if err != nil {
   280  		return "", "", errors.Trace(err)
   281  	}
   282  	if err := addServicePrincipalPasswordCredential(
   283  		client, servicePrincipal.ObjectID,
   284  		passwordCredential,
   285  	); err != nil {
   286  		return "", "", errors.Annotate(err, "updating password credentials")
   287  	}
   288  	return servicePrincipal.ObjectID, passwordCredential.Value, nil
   289  }
   290  
   291  func isMultipleObjectsWithSameKeyValueErr(err error) bool {
   292  	if err, ok := errorutils.ServiceError(err); ok {
   293  		return err.Code == "Request_MultipleObjectsWithSameKeyValue"
   294  	}
   295  	return false
   296  }
   297  
   298  func (c *ServicePrincipalCreator) preparePasswordCredential() (ad.PasswordCredential, error) {
   299  	password, err := c.newUUID()
   300  	if err != nil {
   301  		return ad.PasswordCredential{}, errors.Annotate(err, "generating password")
   302  	}
   303  	passwordKeyUUID, err := c.newUUID()
   304  	if err != nil {
   305  		return ad.PasswordCredential{}, errors.Annotate(err, "generating password key ID")
   306  	}
   307  	startDate := c.clock().Now().UTC()
   308  	endDate := startDate.Add(passwordExpiryDuration)
   309  	return ad.PasswordCredential{
   310  		CustomKeyIdentifier: []byte("juju-" + startDate.Format("20060102")),
   311  		KeyId:               passwordKeyUUID.String(),
   312  		Value:               password.String(),
   313  		StartDate:           startDate,
   314  		EndDate:             endDate,
   315  	}, nil
   316  }
   317  
   318  func addServicePrincipalPasswordCredential(
   319  	client ad.ServicePrincipalsClient,
   320  	servicePrincipalObjectId string,
   321  	passwordCredential ad.PasswordCredential,
   322  ) error {
   323  	existing, err := client.ListPasswordCredentials(servicePrincipalObjectId)
   324  	if err != nil {
   325  		return errors.Trace(err)
   326  	}
   327  	passwordCredentials := append(existing.Value, passwordCredential)
   328  	_, err = client.UpdatePasswordCredentials(
   329  		servicePrincipalObjectId,
   330  		ad.PasswordCredentialsUpdateParameters{passwordCredentials},
   331  	)
   332  	return errors.Trace(err)
   333  }
   334  
   335  func getServicePrincipal(client ad.ServicePrincipalsClient) (ad.ServicePrincipal, error) {
   336  	// TODO(axw) filter by Service Principal Name (SPN).
   337  	// It works without that, but the response is noisy.
   338  	result, err := client.List("")
   339  	if err != nil {
   340  		return ad.ServicePrincipal{}, errors.Annotate(err, "listing service principals")
   341  	}
   342  	for _, sp := range result.Value {
   343  		if sp.ApplicationID == jujuApplicationId {
   344  			return sp, nil
   345  		}
   346  	}
   347  	return ad.ServicePrincipal{}, errors.NotFoundf("service principal")
   348  }
   349  
   350  func (c *ServicePrincipalCreator) createRoleAssignment(sdkCtx context.Context, params ServicePrincipalParams, servicePrincipalObjectId string) error {
   351  	client := params.authorizationClient(c.Sender, c.RequestInspector)
   352  	// Find the role definition with the name "Owner".
   353  	roleScope := path.Join("subscriptions", params.SubscriptionId)
   354  	roleDefinitionsClient := authorization.RoleDefinitionsClient{client}
   355  	result, err := roleDefinitionsClient.List(sdkCtx, roleScope, "roleName eq 'Owner'")
   356  	if err != nil {
   357  		return errors.Annotate(err, "listing role definitions")
   358  	}
   359  	ownerRoles := result.Values()
   360  	if len(ownerRoles) == 0 {
   361  		return errors.NotFoundf("Owner role definition")
   362  	}
   363  	roleDefinitionId := ownerRoles[0].ID
   364  
   365  	// The UUID value for the role assignment name is unimportant. Azure
   366  	// will prevent multiple role assignments for the same role definition
   367  	// and principal pair.
   368  	roleAssignmentUUID, err := c.newUUID()
   369  	if err != nil {
   370  		return errors.Annotate(err, "generating role assignment ID")
   371  	}
   372  	roleAssignmentsClient := authorization.RoleAssignmentsClient{client}
   373  	roleAssignmentName := roleAssignmentUUID.String()
   374  	retryArgs := retry.CallArgs{
   375  		Func: func() error {
   376  			_, err := roleAssignmentsClient.Create(
   377  				sdkCtx,
   378  				roleScope, roleAssignmentName,
   379  				authorization.RoleAssignmentCreateParameters{
   380  					Properties: &authorization.RoleAssignmentProperties{
   381  						RoleDefinitionID: roleDefinitionId,
   382  						PrincipalID:      to.StringPtr(servicePrincipalObjectId),
   383  					},
   384  				},
   385  			)
   386  			return err
   387  		},
   388  		IsFatalError: func(err error) bool {
   389  			serviceErr, ok := errorutils.ServiceError(err)
   390  			if ok && strings.Contains(serviceErr.Message, " does not exist in the directory ") {
   391  				// The service principal doesn't exist yet, retry later.
   392  				return false
   393  			}
   394  			return true
   395  		},
   396  		Clock:       c.clock(),
   397  		Delay:       5 * time.Second,
   398  		MaxDuration: time.Minute,
   399  	}
   400  	if err := retry.Call(retryArgs); err != nil {
   401  		if err, ok := errorutils.ServiceError(err); ok {
   402  			const serviceErrorCodeRoleAssignmentExists = "RoleAssignmentExists"
   403  			if err.Code == serviceErrorCodeRoleAssignmentExists {
   404  				return nil
   405  			}
   406  		}
   407  		return errors.Annotate(err, "creating role assignment")
   408  	}
   409  	return nil
   410  }
   411  
   412  func (c *ServicePrincipalCreator) clock() clock.Clock {
   413  	if c.Clock == nil {
   414  		return clock.WallClock
   415  	}
   416  	return c.Clock
   417  }
   418  
   419  func (c *ServicePrincipalCreator) newUUID() (utils.UUID, error) {
   420  	if c.NewUUID == nil {
   421  		return utils.NewUUID()
   422  	}
   423  	return c.NewUUID()
   424  }