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 }