github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/provider/azure/internal/azureauth/interactive.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  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"path"
    11  	"time"
    12  
    13  	"github.com/Azure/azure-sdk-for-go/arm/authorization"
    14  	"github.com/Azure/azure-sdk-for-go/arm/resources/subscriptions"
    15  	"github.com/Azure/go-autorest/autorest"
    16  	"github.com/Azure/go-autorest/autorest/azure"
    17  	"github.com/Azure/go-autorest/autorest/to"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/utils"
    21  	"github.com/juju/utils/clock"
    22  
    23  	"github.com/juju/juju/provider/azure/internal/ad"
    24  	"github.com/juju/juju/provider/azure/internal/errorutils"
    25  	"github.com/juju/juju/provider/azure/internal/tracing"
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.provider.azure.internal.azureauth")
    29  
    30  const (
    31  	// jujuApplicationId is the ID of the Azure application that we use
    32  	// for interactive authentication. When the user logs in, a service
    33  	// principal will be created in their Active Directory tenant for
    34  	// the application.
    35  	jujuApplicationId = "cbb548f1-5039-4836-af0b-727e8571f6a9"
    36  
    37  	// passwordExpiryDuration is how long the application password we
    38  	// set will remain valid.
    39  	passwordExpiryDuration = 365 * 24 * time.Hour
    40  )
    41  
    42  // InteractiveCreateServicePrincipalFunc is a function type for
    43  // interactively creating service principals for a subscription.
    44  type InteractiveCreateServicePrincipalFunc func(
    45  	stderr io.Writer,
    46  	sender autorest.Sender,
    47  	requestInspector autorest.PrepareDecorator,
    48  	resourceManagerEndpoint string,
    49  	graphEndpoint string,
    50  	subscriptionId string,
    51  	clock clock.Clock,
    52  	newUUID func() (utils.UUID, error),
    53  ) (appId, password string, _ error)
    54  
    55  // InteractiveCreateServicePrincipal interactively creates service
    56  // principals for a subscription.
    57  func InteractiveCreateServicePrincipal(
    58  	stderr io.Writer,
    59  	sender autorest.Sender,
    60  	requestInspector autorest.PrepareDecorator,
    61  	resourceManagerEndpoint string,
    62  	graphEndpoint string,
    63  	subscriptionId string,
    64  	clock clock.Clock,
    65  	newUUID func() (utils.UUID, error),
    66  ) (appId, password string, _ error) {
    67  
    68  	subscriptionsClient := subscriptions.Client{
    69  		subscriptions.NewWithBaseURI(resourceManagerEndpoint),
    70  	}
    71  	subscriptionsClient.Sender = sender
    72  	setClientInspectors(&subscriptionsClient.Client, requestInspector, "azure.subscriptions")
    73  
    74  	oauthConfig, tenantId, err := OAuthConfig(
    75  		subscriptionsClient,
    76  		resourceManagerEndpoint,
    77  		subscriptionId,
    78  	)
    79  	if err != nil {
    80  		return "", "", errors.Trace(err)
    81  	}
    82  
    83  	client := autorest.NewClientWithUserAgent("juju")
    84  	client.Sender = sender
    85  	setClientInspectors(&client, requestInspector, "azure.autorest")
    86  
    87  	// Perform the interactive authentication. The user will be prompted to
    88  	// open a URL and input a device code, after which they will have to
    89  	// enter their username and password if they are not already
    90  	// authenticated with Azure.
    91  	fmt.Fprintln(stderr, "Initiating interactive authentication.")
    92  	fmt.Fprintln(stderr)
    93  	armResource := TokenResource(resourceManagerEndpoint)
    94  	clientId := jujuApplicationId
    95  	deviceCode, err := azure.InitiateDeviceAuth(&client, *oauthConfig, clientId, armResource)
    96  	if err != nil {
    97  		return "", "", errors.Annotate(err, "initiating interactive authentication")
    98  	}
    99  	fmt.Fprintln(stderr, to.String(deviceCode.Message)+"\n")
   100  	token, err := azure.WaitForUserCompletion(&client, deviceCode)
   101  	if err != nil {
   102  		return "", "", errors.Annotate(err, "waiting for interactive authentication to completed")
   103  	}
   104  
   105  	// Create service principal tokens that we can use to authorize API
   106  	// requests to Active Directory and Resource Manager. These tokens
   107  	// are only valid for a short amount of time, so we must create a
   108  	// service principal password that can be used to obtain new tokens.
   109  	armSpt, err := azure.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, armResource, *token)
   110  	if err != nil {
   111  		return "", "", errors.Annotate(err, "creating temporary ARM service principal token")
   112  	}
   113  	if client.Sender != nil {
   114  		armSpt.SetSender(client.Sender)
   115  	}
   116  	if err := armSpt.Refresh(); err != nil {
   117  		return "", "", errors.Trace(err)
   118  	}
   119  
   120  	// The application requires permissions for both ARM and AD, so we
   121  	// can use the token for both APIs.
   122  	graphResource := TokenResource(graphEndpoint)
   123  	graphToken := armSpt.Token
   124  	graphToken.Resource = graphResource
   125  	graphSpt, err := azure.NewServicePrincipalTokenFromManualToken(*oauthConfig, clientId, graphResource, graphToken)
   126  	if err != nil {
   127  		return "", "", errors.Annotate(err, "creating temporary Graph service principal token")
   128  	}
   129  	if client.Sender != nil {
   130  		graphSpt.SetSender(client.Sender)
   131  	}
   132  	if err := graphSpt.Refresh(); err != nil {
   133  		return "", "", errors.Trace(err)
   134  	}
   135  
   136  	directoryURL, err := url.Parse(graphEndpoint)
   137  	if err != nil {
   138  		return "", "", errors.Annotate(err, "parsing identity endpoint")
   139  	}
   140  	directoryURL.Path = path.Join(directoryURL.Path, tenantId)
   141  	directoryClient := ad.NewManagementClient(directoryURL.String())
   142  	authorizationClient := authorization.NewWithBaseURI(resourceManagerEndpoint, subscriptionId)
   143  	directoryClient.Authorizer = graphSpt
   144  	authorizationClient.Authorizer = armSpt
   145  	authorizationClient.Sender = client.Sender
   146  	directoryClient.Sender = client.Sender
   147  	setClientInspectors(&directoryClient.Client, requestInspector, "azure.directory")
   148  	setClientInspectors(&authorizationClient.Client, requestInspector, "azure.authorization")
   149  
   150  	userObject, err := ad.UsersClient{directoryClient}.GetCurrentUser()
   151  	if err != nil {
   152  		return "", "", errors.Trace(err)
   153  	}
   154  	fmt.Fprintf(stderr, "Authenticated as %q.\n", userObject.DisplayName)
   155  
   156  	fmt.Fprintln(stderr, "Creating/updating service principal.")
   157  	servicePrincipalObjectId, password, err := createOrUpdateServicePrincipal(
   158  		ad.ServicePrincipalsClient{directoryClient},
   159  		subscriptionId,
   160  		clock,
   161  		newUUID,
   162  	)
   163  	if err != nil {
   164  		return "", "", errors.Trace(err)
   165  	}
   166  
   167  	fmt.Fprintln(stderr, "Assigning Owner role to service principal.")
   168  	if err := createRoleAssignment(
   169  		authorizationClient,
   170  		subscriptionId,
   171  		servicePrincipalObjectId,
   172  		newUUID,
   173  	); err != nil {
   174  		return "", "", errors.Trace(err)
   175  	}
   176  	return jujuApplicationId, password, nil
   177  }
   178  
   179  func setClientInspectors(
   180  	client *autorest.Client,
   181  	requestInspector autorest.PrepareDecorator,
   182  	loggingModule string,
   183  ) {
   184  	logger := loggo.GetLogger(loggingModule)
   185  	client.ResponseInspector = tracing.RespondDecorator(logger)
   186  	client.RequestInspector = tracing.PrepareDecorator(logger)
   187  	if requestInspector != nil {
   188  		tracer := client.RequestInspector
   189  		client.RequestInspector = func(p autorest.Preparer) autorest.Preparer {
   190  			p = tracer(p)
   191  			p = requestInspector(p)
   192  			return p
   193  		}
   194  	}
   195  }
   196  
   197  func createOrUpdateServicePrincipal(
   198  	client ad.ServicePrincipalsClient,
   199  	subscriptionId string,
   200  	clock clock.Clock,
   201  	newUUID func() (utils.UUID, error),
   202  ) (servicePrincipalObjectId, password string, _ error) {
   203  	passwordCredential, err := preparePasswordCredential(clock, newUUID)
   204  	if err != nil {
   205  		return "", "", errors.Annotate(err, "preparing password credential")
   206  	}
   207  
   208  	servicePrincipal, err := client.Create(
   209  		ad.ServicePrincipalCreateParameters{
   210  			ApplicationID:       jujuApplicationId,
   211  			AccountEnabled:      true,
   212  			PasswordCredentials: []ad.PasswordCredential{passwordCredential},
   213  		},
   214  		nil, // abort
   215  	)
   216  	if err != nil {
   217  		if !isMultipleObjectsWithSameKeyValueErr(err) {
   218  			return "", "", errors.Trace(err)
   219  		}
   220  		// The service principal already exists, so we'll fall out
   221  		// and update the service principal's password credentials.
   222  	} else {
   223  		// The service principal was created successfully, with the
   224  		// requested password credential.
   225  		return servicePrincipal.ObjectID, passwordCredential.Value, nil
   226  	}
   227  
   228  	// The service principal already exists, so we need to query
   229  	// its object ID, and fetch the existing password credentials
   230  	// to update.
   231  	servicePrincipal, err = getServicePrincipal(client)
   232  	if err != nil {
   233  		return "", "", errors.Trace(err)
   234  	}
   235  	if err := addServicePrincipalPasswordCredential(
   236  		client, servicePrincipal.ObjectID,
   237  		passwordCredential,
   238  	); err != nil {
   239  		return "", "", errors.Annotate(err, "updating password credentials")
   240  	}
   241  	return servicePrincipal.ObjectID, passwordCredential.Value, nil
   242  }
   243  
   244  func isMultipleObjectsWithSameKeyValueErr(err error) bool {
   245  	if err, ok := errorutils.ServiceError(err); ok {
   246  		return err.Code == "Request_MultipleObjectsWithSameKeyValue"
   247  	}
   248  	return false
   249  }
   250  
   251  func preparePasswordCredential(
   252  	clock clock.Clock,
   253  	newUUID func() (utils.UUID, error),
   254  ) (ad.PasswordCredential, error) {
   255  	password, err := newUUID()
   256  	if err != nil {
   257  		return ad.PasswordCredential{}, errors.Annotate(err, "generating password")
   258  	}
   259  	passwordKeyUUID, err := newUUID()
   260  	if err != nil {
   261  		return ad.PasswordCredential{}, errors.Annotate(err, "generating password key ID")
   262  	}
   263  	startDate := clock.Now().UTC()
   264  	endDate := startDate.Add(passwordExpiryDuration)
   265  	return ad.PasswordCredential{
   266  		CustomKeyIdentifier: []byte("juju-" + startDate.Format("20060102")),
   267  		KeyId:               passwordKeyUUID.String(),
   268  		Value:               password.String(),
   269  		StartDate:           startDate,
   270  		EndDate:             endDate,
   271  	}, nil
   272  }
   273  
   274  func addServicePrincipalPasswordCredential(
   275  	client ad.ServicePrincipalsClient,
   276  	servicePrincipalObjectId string,
   277  	passwordCredential ad.PasswordCredential,
   278  ) error {
   279  	existing, err := client.ListPasswordCredentials(servicePrincipalObjectId)
   280  	if err != nil {
   281  		return errors.Trace(err)
   282  	}
   283  	passwordCredentials := append(existing.Value, passwordCredential)
   284  	_, err = client.UpdatePasswordCredentials(
   285  		servicePrincipalObjectId,
   286  		ad.PasswordCredentialsUpdateParameters{passwordCredentials},
   287  	)
   288  	return errors.Trace(err)
   289  }
   290  
   291  func getServicePrincipal(client ad.ServicePrincipalsClient) (ad.ServicePrincipal, error) {
   292  	// TODO(axw) filter by Service Principal Name (SPN).
   293  	// It works without that, but the response is noisy.
   294  	result, err := client.List("")
   295  	if err != nil {
   296  		return ad.ServicePrincipal{}, errors.Annotate(err, "listing service principals")
   297  	}
   298  	for _, sp := range result.Value {
   299  		if sp.ApplicationID == jujuApplicationId {
   300  			return sp, nil
   301  		}
   302  	}
   303  	return ad.ServicePrincipal{}, errors.NotFoundf("service principal")
   304  }
   305  
   306  func createRoleAssignment(
   307  	authorizationClient authorization.ManagementClient,
   308  	subscriptionId string,
   309  	servicePrincipalObjectId string,
   310  	newUUID func() (utils.UUID, error),
   311  ) error {
   312  	// Find the role definition with the name "Owner".
   313  	roleScope := path.Join("subscriptions", subscriptionId)
   314  	roleDefinitionsClient := authorization.RoleDefinitionsClient{authorizationClient}
   315  	result, err := roleDefinitionsClient.List(roleScope, "roleName eq 'Owner'")
   316  	if err != nil {
   317  		return errors.Annotate(err, "listing role definitions")
   318  	}
   319  	if result.Value == nil || len(*result.Value) == 0 {
   320  		return errors.NotFoundf("Owner role definition")
   321  	}
   322  	roleDefinitionId := (*result.Value)[0].ID
   323  
   324  	// The UUID value for the role assignment name is unimportant. Azure
   325  	// will prevent multiple role assignments for the same role definition
   326  	// and principal pair.
   327  	roleAssignmentUUID, err := newUUID()
   328  	if err != nil {
   329  		return errors.Annotate(err, "generating role assignment ID")
   330  	}
   331  	roleAssignmentsClient := authorization.RoleAssignmentsClient{authorizationClient}
   332  	roleAssignmentName := roleAssignmentUUID.String()
   333  	if _, err := roleAssignmentsClient.Create(roleScope, roleAssignmentName, authorization.RoleAssignmentCreateParameters{
   334  		Properties: &authorization.RoleAssignmentProperties{
   335  			RoleDefinitionID: roleDefinitionId,
   336  			PrincipalID:      to.StringPtr(servicePrincipalObjectId),
   337  		},
   338  	}); err != nil {
   339  		if err, ok := errorutils.ServiceError(err); ok {
   340  			const serviceErrorCodeRoleAssignmentExists = "RoleAssignmentExists"
   341  			if err.Code == serviceErrorCodeRoleAssignmentExists {
   342  				return nil
   343  			}
   344  		}
   345  		return errors.Annotate(err, "creating role assignment")
   346  	}
   347  	return nil
   348  }