github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/internal/azureauth/serviceprincipal_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azureauth_test 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io/ioutil" 12 "net/http" 13 "time" 14 15 "github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization" 16 "github.com/Azure/go-autorest/autorest" 17 "github.com/Azure/go-autorest/autorest/adal" 18 "github.com/Azure/go-autorest/autorest/mocks" 19 "github.com/Azure/go-autorest/autorest/to" 20 "github.com/juju/clock/testclock" 21 "github.com/juju/testing" 22 jc "github.com/juju/testing/checkers" 23 "github.com/juju/utils" 24 gc "gopkg.in/check.v1" 25 26 "github.com/juju/juju/provider/azure/internal/ad" 27 "github.com/juju/juju/provider/azure/internal/azureauth" 28 "github.com/juju/juju/provider/azure/internal/azuretesting" 29 ) 30 31 func clockStartTime() time.Time { 32 t, _ := time.Parse("2006-Jan-02 3:04am", "2016-Sep-19 9:47am") 33 return t 34 } 35 36 type InteractiveSuite struct { 37 testing.IsolationSuite 38 clock *testclock.Clock 39 newUUID func() (utils.UUID, error) 40 } 41 42 var _ = gc.Suite(&InteractiveSuite{}) 43 44 func deviceCodeSender() autorest.Sender { 45 return azuretesting.NewSenderWithValue(adal.DeviceCode{ 46 DeviceCode: to.StringPtr("device-code"), 47 Interval: to.Int64Ptr(1), // 1 second between polls 48 Message: to.StringPtr("open your browser, etc."), 49 }) 50 } 51 52 func tokenSender() autorest.Sender { 53 return azuretesting.NewSenderWithValue(adal.Token{ 54 RefreshToken: "refresh-token", 55 ExpiresOn: json.Number(fmt.Sprint(time.Now().Add(time.Hour).Unix())), 56 }) 57 } 58 59 func passwordCredentialsListSender() autorest.Sender { 60 return azuretesting.NewSenderWithValue(ad.PasswordCredentialsListResult{ 61 Value: []ad.PasswordCredential{{ 62 KeyId: "password-credential-key-id", 63 }}, 64 }) 65 } 66 67 func updatePasswordCredentialsSender() autorest.Sender { 68 sender := mocks.NewSender() 69 sender.AppendResponse(mocks.NewResponseWithStatus("", http.StatusNoContent)) 70 return sender 71 } 72 73 func currentUserSender() autorest.Sender { 74 return azuretesting.NewSenderWithValue(ad.AADObject{ 75 DisplayName: "Foo Bar", 76 }) 77 } 78 79 func createServicePrincipalSender() autorest.Sender { 80 return azuretesting.NewSenderWithValue(ad.ServicePrincipal{ 81 ApplicationID: "cbb548f1-5039-4836-af0b-727e8571f6a9", 82 ObjectID: "sp-object-id", 83 }) 84 } 85 86 func createServicePrincipalAlreadyExistsSender(withUTF8BOM bool) autorest.Sender { 87 sender := mocks.NewSender() 88 bodyData := `{"odata.error":{"code":"Request_MultipleObjectsWithSameKeyValue"}}` 89 if withUTF8BOM { 90 bodyData = "\ufeff" + bodyData 91 } 92 body := mocks.NewBody(bodyData) 93 sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusConflict, "")) 94 return sender 95 } 96 97 func createServicePrincipalNotExistSender() autorest.Sender { 98 sender := mocks.NewSender() 99 bodyData := `{"odata.error":{"code":"Request_ResourceNotFound","message":{"lang":"en","value":"... does not exist in the directory ..."}}}` 100 body := mocks.NewBody(bodyData) 101 sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusNotFound, "")) 102 return sender 103 } 104 105 func createServicePrincipalNotReferenceSender() autorest.Sender { 106 sender := mocks.NewSender() 107 // Error message cribbed from https://github.com/kubernetes/kubernetes-anywhere/issues/251 108 bodyData := `{"odata.error":{"code":"Request_BadRequest","message":{"lang":"en","value":"The appId of the service principal does not reference a valid application object."}}}` 109 body := mocks.NewBody(bodyData) 110 sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusBadRequest, "")) 111 return sender 112 } 113 114 func servicePrincipalListSender() autorest.Sender { 115 return azuretesting.NewSenderWithValue(ad.ServicePrincipalListResult{ 116 Value: []ad.ServicePrincipal{{ 117 ApplicationID: "cbb548f1-5039-4836-af0b-727e8571f6a9", 118 ObjectID: "sp-object-id", 119 }}, 120 }) 121 } 122 123 func roleDefinitionListSender() autorest.Sender { 124 roleDefinitions := []authorization.RoleDefinition{{ 125 ID: to.StringPtr("owner-role-id"), 126 Name: to.StringPtr("Owner"), 127 }} 128 return azuretesting.NewSenderWithValue(authorization.RoleDefinitionListResult{ 129 Value: &roleDefinitions, 130 }) 131 } 132 133 func roleAssignmentSender() autorest.Sender { 134 return azuretesting.NewSenderWithValue(authorization.RoleAssignment{}) 135 } 136 137 func roleAssignmentAlreadyExistsSender() autorest.Sender { 138 sender := mocks.NewSender() 139 body := mocks.NewBody(`{"error":{"code":"RoleAssignmentExists", "message":"Odata v4 compliant message"}}`) 140 sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusConflict, "")) 141 return sender 142 } 143 144 func roleAssignmentPrincipalNotExistSender() autorest.Sender { 145 sender := mocks.NewSender() 146 // Based on https://github.com/Azure/azure-powershell/issues/655#issuecomment-186332230 147 body := mocks.NewBody(`{"error":{"code":"PrincipalNotFound","message":"Principal foo does not exist in the directory bar"}}`) 148 sender.AppendResponse(mocks.NewResponseWithBodyAndStatus(body, http.StatusNotFound, "")) 149 return sender 150 } 151 152 func (s *InteractiveSuite) SetUpTest(c *gc.C) { 153 s.IsolationSuite.SetUpTest(c) 154 uuids := []string{ 155 "33333333-3333-3333-3333-333333333333", // password 156 "44444444-4444-4444-4444-444444444444", // password key ID 157 "55555555-5555-5555-5555-555555555555", // role assignment ID 158 } 159 s.newUUID = func() (utils.UUID, error) { 160 uuid, err := utils.UUIDFromString(uuids[0]) 161 if err != nil { 162 return utils.UUID{}, err 163 } 164 uuids = uuids[1:] 165 return uuid, nil 166 } 167 s.clock = testclock.NewClock(clockStartTime()) 168 } 169 170 func (s *InteractiveSuite) TestInteractive(c *gc.C) { 171 var requests []*http.Request 172 spc := azureauth.ServicePrincipalCreator{ 173 Sender: &azuretesting.Senders{ 174 oauthConfigSender(), 175 deviceCodeSender(), 176 tokenSender(), // CheckForUserCompletion returns a token. 177 178 // Token.Refresh returns a token. We do this 179 // twice: once for ARM, and once for AAD. 180 tokenSender(), 181 tokenSender(), 182 183 currentUserSender(), 184 createServicePrincipalSender(), 185 roleDefinitionListSender(), 186 roleAssignmentSender(), 187 }, 188 RequestInspector: azuretesting.RequestRecorder(&requests), 189 Clock: s.clock, 190 NewUUID: s.newUUID, 191 } 192 193 var stderr bytes.Buffer 194 subscriptionId := "22222222-2222-2222-2222-222222222222" 195 sdkCtx := context.Background() 196 197 appId, password, err := spc.InteractiveCreate(sdkCtx, &stderr, azureauth.ServicePrincipalParams{ 198 GraphEndpoint: "https://graph.invalid", 199 GraphResourceId: "https://graph.invalid", 200 ResourceManagerEndpoint: "https://arm.invalid", 201 ResourceManagerResourceId: "https://management.core.invalid/", 202 SubscriptionId: subscriptionId, 203 }) 204 c.Assert(err, jc.ErrorIsNil) 205 c.Assert(appId, gc.Equals, "cbb548f1-5039-4836-af0b-727e8571f6a9") 206 c.Assert(password, gc.Equals, "33333333-3333-3333-3333-333333333333") 207 c.Assert(stderr.String(), gc.Equals, ` 208 Initiating interactive authentication. 209 210 open your browser, etc. 211 212 Authenticated as "Foo Bar". 213 `[1:]) 214 215 c.Assert(requests, gc.HasLen, 9) 216 c.Check(requests[0].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222") 217 c.Check(requests[1].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/devicecode") 218 c.Check(requests[2].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 219 c.Check(requests[3].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 220 c.Check(requests[4].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 221 c.Check(requests[5].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/me") 222 c.Check(requests[6].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") 223 c.Check(requests[7].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleDefinitions") 224 c.Check(requests[8].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleAssignments/55555555-5555-5555-5555-555555555555") 225 226 // The service principal creation includes the password. Check that the 227 // password returned from the function is the same as the one set in the 228 // request. 229 var params ad.ServicePrincipalCreateParameters 230 err = json.NewDecoder(requests[6].Body).Decode(¶ms) 231 c.Assert(err, jc.ErrorIsNil) 232 c.Assert(params.PasswordCredentials, gc.HasLen, 1) 233 assertPasswordCredential(c, params.PasswordCredentials[0]) 234 } 235 236 func assertPasswordCredential(c *gc.C, cred ad.PasswordCredential) { 237 startDate := cred.StartDate 238 endDate := cred.EndDate 239 c.Assert(startDate, gc.Equals, clockStartTime()) 240 c.Assert(endDate.Sub(startDate), gc.Equals, 365*24*time.Hour) 241 242 cred.StartDate = time.Time{} 243 cred.EndDate = time.Time{} 244 c.Assert(cred, jc.DeepEquals, ad.PasswordCredential{ 245 CustomKeyIdentifier: []byte("juju-20160919"), 246 KeyId: "44444444-4444-4444-4444-444444444444", 247 Value: "33333333-3333-3333-3333-333333333333", 248 }) 249 } 250 251 func (s *InteractiveSuite) TestInteractiveRoleAssignmentAlreadyExists(c *gc.C) { 252 var requests []*http.Request 253 spc := azureauth.ServicePrincipalCreator{ 254 Sender: &azuretesting.Senders{ 255 oauthConfigSender(), 256 deviceCodeSender(), 257 tokenSender(), 258 tokenSender(), 259 tokenSender(), 260 currentUserSender(), 261 createServicePrincipalSender(), 262 roleDefinitionListSender(), 263 roleAssignmentAlreadyExistsSender(), 264 }, 265 RequestInspector: azuretesting.RequestRecorder(&requests), 266 Clock: s.clock, 267 NewUUID: s.newUUID, 268 } 269 sdkCtx := context.Background() 270 _, _, err := spc.InteractiveCreate(sdkCtx, ioutil.Discard, azureauth.ServicePrincipalParams{ 271 GraphEndpoint: "https://graph.invalid", 272 GraphResourceId: "https://graph.invalid", 273 ResourceManagerEndpoint: "https://arm.invalid", 274 ResourceManagerResourceId: "https://management.core.invalid/", 275 SubscriptionId: "22222222-2222-2222-2222-222222222222", 276 }) 277 c.Assert(err, jc.ErrorIsNil) 278 } 279 280 func (s *InteractiveSuite) TestInteractiveServicePrincipalAlreadyExists(c *gc.C) { 281 s.testInteractiveServicePrincipalAlreadyExists(c, false) 282 } 283 284 func (s *InteractiveSuite) TestInteractiveServicePrincipalAlreadyExistsWithUTF8BOM(c *gc.C) { 285 // We have observed that Azure sometimes responds with UTF-8 BOMs in 286 // JSON-encoded responses. Go's JSON decoder does not like this, so 287 // we have to strip it off. See: 288 // https://bugs.launchpad.net/juju/+bug/1657448 289 s.testInteractiveServicePrincipalAlreadyExists(c, true) 290 } 291 292 func (s *InteractiveSuite) testInteractiveServicePrincipalAlreadyExists(c *gc.C, withUTF8BOM bool) { 293 var requests []*http.Request 294 spc := azureauth.ServicePrincipalCreator{ 295 Sender: &azuretesting.Senders{ 296 oauthConfigSender(), 297 deviceCodeSender(), 298 tokenSender(), 299 tokenSender(), 300 tokenSender(), 301 currentUserSender(), 302 createServicePrincipalAlreadyExistsSender(withUTF8BOM), 303 servicePrincipalListSender(), 304 passwordCredentialsListSender(), 305 updatePasswordCredentialsSender(), 306 roleDefinitionListSender(), 307 roleAssignmentAlreadyExistsSender(), 308 }, 309 RequestInspector: azuretesting.RequestRecorder(&requests), 310 Clock: s.clock, 311 NewUUID: s.newUUID, 312 } 313 sdkCtx := context.Background() 314 _, password, err := spc.InteractiveCreate(sdkCtx, ioutil.Discard, azureauth.ServicePrincipalParams{ 315 GraphEndpoint: "https://graph.invalid", 316 GraphResourceId: "https://graph.invalid", 317 ResourceManagerEndpoint: "https://arm.invalid", 318 ResourceManagerResourceId: "https://management.core.invalid/", 319 SubscriptionId: "22222222-2222-2222-2222-222222222222", 320 }) 321 c.Assert(err, jc.ErrorIsNil) 322 c.Assert(password, gc.Equals, "33333333-3333-3333-3333-333333333333") 323 324 c.Assert(requests, gc.HasLen, 12) 325 c.Check(requests[0].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222") 326 c.Check(requests[1].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/devicecode") 327 c.Check(requests[2].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 328 c.Check(requests[3].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 329 c.Check(requests[4].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 330 c.Check(requests[5].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/me") 331 c.Check(requests[6].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") // create 332 c.Check(requests[7].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") // list 333 c.Check(requests[8].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals/sp-object-id/passwordCredentials") // list 334 c.Check(requests[9].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals/sp-object-id/passwordCredentials") // update 335 c.Check(requests[10].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleDefinitions") 336 c.Check(requests[11].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleAssignments/55555555-5555-5555-5555-555555555555") 337 338 // Make sure that we don't wipe existing password credentials, and that 339 // the new password credential matches the one returned from the 340 // function. 341 var params ad.PasswordCredentialsUpdateParameters 342 err = json.NewDecoder(requests[9].Body).Decode(¶ms) 343 c.Assert(err, jc.ErrorIsNil) 344 c.Assert(params.Value, gc.HasLen, 2) 345 c.Assert(params.Value[0], jc.DeepEquals, ad.PasswordCredential{ 346 KeyId: "password-credential-key-id", 347 StartDate: time.Time{}.UTC(), 348 EndDate: time.Time{}.UTC(), 349 }) 350 assertPasswordCredential(c, params.Value[1]) 351 } 352 353 func (s *InteractiveSuite) TestInteractiveServicePrincipalApplicationNotExist(c *gc.C) { 354 s.testInteractiveRetriesCreateServicePrincipal(c, createServicePrincipalNotExistSender()) 355 } 356 357 func (s *InteractiveSuite) TestInteractiveServicePrincipalApplicationNotReference(c *gc.C) { 358 s.testInteractiveRetriesCreateServicePrincipal(c, createServicePrincipalNotReferenceSender()) 359 } 360 361 func (s *InteractiveSuite) testInteractiveRetriesCreateServicePrincipal(c *gc.C, errorSender autorest.Sender) { 362 var requests []*http.Request 363 spc := azureauth.ServicePrincipalCreator{ 364 Sender: &azuretesting.Senders{ 365 oauthConfigSender(), 366 deviceCodeSender(), 367 tokenSender(), 368 tokenSender(), 369 tokenSender(), 370 currentUserSender(), 371 errorSender, 372 createServicePrincipalSender(), 373 roleDefinitionListSender(), 374 roleAssignmentAlreadyExistsSender(), 375 }, 376 RequestInspector: azuretesting.RequestRecorder(&requests), 377 Clock: &testclock.AutoAdvancingClock{ 378 Clock: s.clock, 379 Advance: s.clock.Advance, 380 }, 381 NewUUID: s.newUUID, 382 } 383 sdkCtx := context.Background() 384 _, password, err := spc.InteractiveCreate(sdkCtx, ioutil.Discard, azureauth.ServicePrincipalParams{ 385 GraphEndpoint: "https://graph.invalid", 386 GraphResourceId: "https://graph.invalid", 387 ResourceManagerEndpoint: "https://arm.invalid", 388 ResourceManagerResourceId: "https://management.core.invalid/", 389 SubscriptionId: "22222222-2222-2222-2222-222222222222", 390 }) 391 c.Assert(err, jc.ErrorIsNil) 392 c.Assert(password, gc.Equals, "33333333-3333-3333-3333-333333333333") 393 394 c.Assert(requests, gc.HasLen, 10) 395 c.Check(requests[0].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222") 396 c.Check(requests[1].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/devicecode") 397 c.Check(requests[2].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 398 c.Check(requests[3].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 399 c.Check(requests[4].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 400 c.Check(requests[5].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/me") 401 c.Check(requests[6].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") // create 402 c.Check(requests[7].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") // create 403 c.Check(requests[8].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleDefinitions") 404 c.Check(requests[9].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleAssignments/55555555-5555-5555-5555-555555555555") 405 } 406 407 func (s *InteractiveSuite) TestInteractiveRetriesRoleAssignment(c *gc.C) { 408 var requests []*http.Request 409 spc := azureauth.ServicePrincipalCreator{ 410 Sender: &azuretesting.Senders{ 411 oauthConfigSender(), 412 deviceCodeSender(), 413 tokenSender(), 414 tokenSender(), 415 tokenSender(), 416 currentUserSender(), 417 createServicePrincipalSender(), 418 roleDefinitionListSender(), 419 roleAssignmentPrincipalNotExistSender(), 420 roleAssignmentSender(), 421 }, 422 RequestInspector: azuretesting.RequestRecorder(&requests), 423 Clock: &testclock.AutoAdvancingClock{ 424 Clock: s.clock, 425 Advance: s.clock.Advance, 426 }, 427 NewUUID: s.newUUID, 428 } 429 sdkCtx := context.Background() 430 _, password, err := spc.InteractiveCreate(sdkCtx, ioutil.Discard, azureauth.ServicePrincipalParams{ 431 GraphEndpoint: "https://graph.invalid", 432 GraphResourceId: "https://graph.invalid", 433 ResourceManagerEndpoint: "https://arm.invalid", 434 ResourceManagerResourceId: "https://management.core.invalid/", 435 SubscriptionId: "22222222-2222-2222-2222-222222222222", 436 }) 437 c.Assert(err, jc.ErrorIsNil) 438 c.Assert(password, gc.Equals, "33333333-3333-3333-3333-333333333333") 439 440 c.Assert(requests, gc.HasLen, 10) 441 c.Check(requests[0].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222") 442 c.Check(requests[1].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/devicecode") 443 c.Check(requests[2].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 444 c.Check(requests[3].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 445 c.Check(requests[4].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/oauth2/token") 446 c.Check(requests[5].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/me") 447 c.Check(requests[6].URL.Path, gc.Equals, "/11111111-1111-1111-1111-111111111111/servicePrincipals") // create 448 c.Check(requests[7].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleDefinitions") 449 c.Check(requests[8].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleAssignments/55555555-5555-5555-5555-555555555555") 450 c.Check(requests[9].URL.Path, gc.Equals, "/subscriptions/22222222-2222-2222-2222-222222222222/providers/Microsoft.Authorization/roleAssignments/55555555-5555-5555-5555-555555555555") 451 }