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 }