github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/cloud/cloud.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // Package cloud defines an API end point for functions dealing with 5 // the controller's cloud definition, and cloud credentials. 6 package cloud 7 8 import ( 9 "fmt" 10 "sort" 11 "strings" 12 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 "github.com/juju/txn" 16 "gopkg.in/juju/names.v2" 17 18 "github.com/juju/juju/apiserver/common" 19 "github.com/juju/juju/apiserver/common/credentialcommon" 20 "github.com/juju/juju/apiserver/facade" 21 "github.com/juju/juju/apiserver/params" 22 "github.com/juju/juju/cloud" 23 "github.com/juju/juju/environs" 24 environscontext "github.com/juju/juju/environs/context" 25 "github.com/juju/juju/permission" 26 "github.com/juju/juju/state" 27 ) 28 29 var logger = loggo.GetLogger("juju.apiserver.cloud") 30 31 // CloudV3 defines the methods on the cloud API facade, version 3. 32 type CloudV3 interface { 33 AddCloud(cloudArgs params.AddCloudArgs) error 34 AddCredentials(args params.TaggedCredentials) (params.ErrorResults, error) 35 CheckCredentialsModels(args params.TaggedCredentials) (params.UpdateCredentialResults, error) 36 Cloud(args params.Entities) (params.CloudResults, error) 37 Clouds() (params.CloudsResult, error) 38 Credential(args params.Entities) (params.CloudCredentialResults, error) 39 CredentialContents(credentialArgs params.CloudCredentialArgs) (params.CredentialContentResults, error) 40 DefaultCloud() (params.StringResult, error) 41 ModifyCloudAccess(args params.ModifyCloudAccessRequest) (params.ErrorResults, error) 42 RevokeCredentialsCheckModels(args params.RevokeCredentialArgs) (params.ErrorResults, error) 43 UpdateCredentialsCheckModels(args params.UpdateCredentialArgs) (params.UpdateCredentialResults, error) 44 UserCredentials(args params.UserClouds) (params.StringsResults, error) 45 } 46 47 // CloudV2 defines the methods on the cloud API facade, version 2. 48 type CloudV2 interface { 49 AddCloud(cloudArgs params.AddCloudArgs) error 50 AddCredentials(args params.TaggedCredentials) (params.ErrorResults, error) 51 Cloud(args params.Entities) (params.CloudResults, error) 52 Clouds() (params.CloudsResult, error) 53 Credential(args params.Entities) (params.CloudCredentialResults, error) 54 CredentialContents(credentialArgs params.CloudCredentialArgs) (params.CredentialContentResults, error) 55 DefaultCloud() (params.StringResult, error) 56 RemoveClouds(args params.Entities) (params.ErrorResults, error) 57 RevokeCredentials(args params.Entities) (params.ErrorResults, error) 58 UpdateCredentials(args params.TaggedCredentials) (params.ErrorResults, error) 59 UserCredentials(args params.UserClouds) (params.StringsResults, error) 60 } 61 62 // CloudV1 defines the methods on the cloud API facade, version 1. 63 type CloudV1 interface { 64 Cloud(args params.Entities) (params.CloudResults, error) 65 Clouds() (params.CloudsResult, error) 66 Credential(args params.Entities) (params.CloudCredentialResults, error) 67 DefaultCloud() (params.StringResult, error) 68 RevokeCredentials(args params.Entities) (params.ErrorResults, error) 69 UpdateCredentials(args params.TaggedCredentials) (params.ErrorResults, error) 70 UserCredentials(args params.UserClouds) (params.StringsResults, error) 71 } 72 73 // CloudAPI implements the cloud interface and is the concrete implementation 74 // of the api end point. 75 type CloudAPI struct { 76 backend Backend 77 ctlrBackend Backend 78 authorizer facade.Authorizer 79 apiUser names.UserTag 80 getCredentialsAuthFunc common.GetAuthFunc 81 callContext environscontext.ProviderCallContext 82 pool ModelPoolBackend 83 } 84 85 // CloudAPIV2 provides a way to wrap the different calls 86 // between version 2 and version 3 of the cloud API. 87 type CloudAPIV2 struct { 88 *CloudAPI 89 } 90 91 // CloudAPIV1 provides a way to wrap the different calls 92 // between version 1 and version 2 of the cloud API. 93 type CloudAPIV1 struct { 94 *CloudAPIV2 95 } 96 97 var ( 98 _ CloudV3 = (*CloudAPI)(nil) 99 _ CloudV2 = (*CloudAPIV2)(nil) 100 _ CloudV1 = (*CloudAPIV1)(nil) 101 ) 102 103 // NewFacadeV3 is used for API registration. 104 func NewFacadeV3(context facade.Context) (*CloudAPI, error) { 105 st := NewStateBackend(context.State()) 106 pool := NewModelPoolBackend(context.StatePool()) 107 ctlrSt := NewStateBackend(pool.SystemState()) 108 return NewCloudAPI(st, ctlrSt, pool, context.Auth(), state.CallContext(context.State())) 109 } 110 111 // NewFacadeV2 is used for API registration. 112 func NewFacadeV2(context facade.Context) (*CloudAPIV2, error) { 113 v3, err := NewFacadeV3(context) 114 if err != nil { 115 return nil, err 116 } 117 return &CloudAPIV2{v3}, nil 118 } 119 120 // NewFacadeV1 is used for API registration. 121 func NewFacadeV1(context facade.Context) (*CloudAPIV1, error) { 122 v2, err := NewFacadeV2(context) 123 if err != nil { 124 return nil, err 125 } 126 return &CloudAPIV1{v2}, nil 127 } 128 129 // NewCloudAPI creates a new API server endpoint for managing the controller's 130 // cloud definition and cloud credentials. 131 func NewCloudAPI(backend, ctlrBackend Backend, pool ModelPoolBackend, authorizer facade.Authorizer, callCtx environscontext.ProviderCallContext) (*CloudAPI, error) { 132 if !authorizer.AuthClient() { 133 return nil, common.ErrPerm 134 } 135 136 authUser, _ := authorizer.GetAuthTag().(names.UserTag) 137 getUserAuthFunc := func() (common.AuthFunc, error) { 138 isAdmin, err := authorizer.HasPermission(permission.SuperuserAccess, backend.ControllerTag()) 139 if err != nil && !errors.IsNotFound(err) { 140 return nil, err 141 } 142 return func(tag names.Tag) bool { 143 userTag, ok := tag.(names.UserTag) 144 if !ok { 145 return false 146 } 147 return isAdmin || userTag == authUser 148 }, nil 149 } 150 return &CloudAPI{ 151 backend: backend, 152 ctlrBackend: ctlrBackend, 153 authorizer: authorizer, 154 getCredentialsAuthFunc: getUserAuthFunc, 155 apiUser: authUser, 156 callContext: callCtx, 157 pool: pool, 158 }, nil 159 } 160 161 func (api *CloudAPI) canAccessCloud(cloud string, user names.UserTag, access permission.Access) (bool, error) { 162 perm, err := api.ctlrBackend.GetCloudAccess(cloud, user) 163 if errors.IsNotFound(err) { 164 return false, nil 165 } 166 if err != nil { 167 return false, errors.Trace(err) 168 } 169 return perm.EqualOrGreaterCloudAccessThan(access), nil 170 } 171 172 // Clouds returns the definitions of all clouds supported by the controller 173 // that the logged in user can see. 174 func (api *CloudAPI) Clouds() (params.CloudsResult, error) { 175 var result params.CloudsResult 176 clouds, err := api.backend.Clouds() 177 if err != nil { 178 return result, err 179 } 180 isAdmin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.ctlrBackend.ControllerTag()) 181 if err != nil && !errors.IsNotFound(err) { 182 return result, errors.Trace(err) 183 } 184 result.Clouds = make(map[string]params.Cloud) 185 for tag, aCloud := range clouds { 186 // Ensure user has permission to see the cloud. 187 if !isAdmin { 188 canAccess, err := api.canAccessCloud(tag.Id(), api.apiUser, permission.AddModelAccess) 189 if err != nil { 190 return result, err 191 } 192 if !canAccess { 193 continue 194 } 195 } 196 paramsCloud := common.CloudToParams(aCloud) 197 result.Clouds[tag.String()] = paramsCloud 198 } 199 return result, nil 200 } 201 202 // Cloud returns the cloud definitions for the specified clouds. 203 func (api *CloudAPI) Cloud(args params.Entities) (params.CloudResults, error) { 204 results := params.CloudResults{ 205 Results: make([]params.CloudResult, len(args.Entities)), 206 } 207 isAdmin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.ctlrBackend.ControllerTag()) 208 if err != nil && !errors.IsNotFound(err) { 209 return results, errors.Trace(err) 210 } 211 one := func(arg params.Entity) (*params.Cloud, error) { 212 tag, err := names.ParseCloudTag(arg.Tag) 213 if err != nil { 214 return nil, err 215 } 216 // Ensure user has permission to see the cloud. 217 if !isAdmin { 218 canAccess, err := api.canAccessCloud(tag.Id(), api.apiUser, permission.AddModelAccess) 219 if err != nil { 220 return nil, err 221 } 222 if !canAccess { 223 return nil, errors.NotFoundf("cloud %q", tag.Id()) 224 } 225 } 226 aCloud, err := api.backend.Cloud(tag.Id()) 227 if err != nil { 228 return nil, err 229 } 230 paramsCloud := common.CloudToParams(aCloud) 231 return ¶msCloud, nil 232 } 233 for i, arg := range args.Entities { 234 aCloud, err := one(arg) 235 if err != nil { 236 results.Results[i].Error = common.ServerError(err) 237 } else { 238 results.Results[i].Cloud = aCloud 239 } 240 } 241 return results, nil 242 } 243 244 // CloudInfo returns information about the specified clouds. 245 func (api *CloudAPI) CloudInfo(args params.Entities) (params.CloudInfoResults, error) { 246 results := params.CloudInfoResults{ 247 Results: make([]params.CloudInfoResult, len(args.Entities)), 248 } 249 250 oneCloudInfo := func(arg params.Entity) (*params.CloudInfo, error) { 251 tag, err := names.ParseCloudTag(arg.Tag) 252 if err != nil { 253 return nil, errors.Trace(err) 254 } 255 return api.getCloudInfo(tag) 256 } 257 258 for i, arg := range args.Entities { 259 cloudInfo, err := oneCloudInfo(arg) 260 if err != nil { 261 results.Results[i].Error = common.ServerError(err) 262 continue 263 } 264 results.Results[i].Result = cloudInfo 265 } 266 return results, nil 267 } 268 269 func cloudToParams(cloud cloud.Cloud) params.CloudDetails { 270 authTypes := make([]string, len(cloud.AuthTypes)) 271 for i, authType := range cloud.AuthTypes { 272 authTypes[i] = string(authType) 273 } 274 regions := make([]params.CloudRegion, len(cloud.Regions)) 275 for i, region := range cloud.Regions { 276 regions[i] = params.CloudRegion{ 277 Name: region.Name, 278 Endpoint: region.Endpoint, 279 IdentityEndpoint: region.IdentityEndpoint, 280 StorageEndpoint: region.StorageEndpoint, 281 } 282 } 283 return params.CloudDetails{ 284 Type: cloud.Type, 285 AuthTypes: authTypes, 286 Endpoint: cloud.Endpoint, 287 IdentityEndpoint: cloud.IdentityEndpoint, 288 StorageEndpoint: cloud.StorageEndpoint, 289 Regions: regions, 290 } 291 } 292 293 func (api *CloudAPI) getCloudInfo(tag names.CloudTag) (*params.CloudInfo, error) { 294 isAdmin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.ctlrBackend.ControllerTag()) 295 if err != nil && !errors.IsNotFound(err) { 296 return nil, errors.Trace(err) 297 } 298 // If not a controller admin, check for cloud admin. 299 if !isAdmin { 300 perm, err := api.ctlrBackend.GetCloudAccess(tag.Id(), api.apiUser) 301 if err != nil && !errors.IsNotFound(err) { 302 return nil, errors.Trace(err) 303 } 304 isAdmin = perm == permission.AdminAccess 305 } 306 307 aCloud, err := api.backend.Cloud(tag.Id()) 308 if err != nil { 309 return nil, errors.Trace(err) 310 } 311 info := params.CloudInfo{ 312 CloudDetails: cloudToParams(aCloud), 313 } 314 315 cloudUsers, err := api.ctlrBackend.GetCloudUsers(tag.Id()) 316 if err != nil { 317 return nil, errors.Trace(err) 318 } 319 for userId, perm := range cloudUsers { 320 if !isAdmin && api.apiUser.Id() != userId { 321 // The authenticated user is neither the a controller 322 // superuser, a cloud administrator, nor a cloud user, so 323 // has no business knowing about the cloud user. 324 continue 325 } 326 userTag := names.NewUserTag(userId) 327 displayName := userId 328 if userTag.IsLocal() { 329 u, err := api.backend.User(userTag) 330 if err != nil { 331 if _, ok := err.(state.DeletedUserError); !ok { 332 // We ignore deleted users for now. So if it is not a 333 // DeletedUserError we return the error. 334 return nil, errors.Trace(err) 335 } 336 continue 337 } 338 displayName = u.DisplayName() 339 } 340 341 userInfo := params.CloudUserInfo{ 342 UserName: userId, 343 DisplayName: displayName, 344 Access: string(perm), 345 } 346 347 if err != nil { 348 return nil, errors.Trace(err) 349 } 350 info.Users = append(info.Users, userInfo) 351 } 352 353 if len(info.Users) == 0 { 354 // No users, which means the authenticated user doesn't 355 // have access to the cloud. 356 return nil, errors.Trace(common.ErrPerm) 357 } 358 return &info, nil 359 } 360 361 // ListCloudInfo returns clouds that the specified user has access to. 362 // Controller admins (superuser) can list clouds for any user. 363 // Other users can only ask about their own clouds. 364 func (api *CloudAPI) ListCloudInfo(req params.ListCloudsRequest) (params.ListCloudInfoResults, error) { 365 result := params.ListCloudInfoResults{} 366 367 userTag, err := names.ParseUserTag(req.UserTag) 368 if err != nil { 369 return result, errors.Trace(err) 370 } 371 372 cloudInfos, err := api.ctlrBackend.CloudsForUser(userTag, req.All) 373 if err != nil { 374 return result, errors.Trace(err) 375 } 376 377 for _, ci := range cloudInfos { 378 info := ¶ms.ListCloudInfo{ 379 CloudDetails: cloudToParams(ci.Cloud), 380 Access: string(ci.Access), 381 } 382 result.Results = append(result.Results, params.ListCloudInfoResult{Result: info}) 383 } 384 return result, nil 385 } 386 387 // DefaultCloud returns the tag of the cloud that models will be 388 // created in by default. 389 func (api *CloudAPI) DefaultCloud() (params.StringResult, error) { 390 controllerModel, err := api.ctlrBackend.Model() 391 if err != nil { 392 return params.StringResult{}, err 393 } 394 return params.StringResult{ 395 Result: names.NewCloudTag(controllerModel.Cloud()).String(), 396 }, nil 397 } 398 399 // UserCredentials returns the cloud credentials for a set of users. 400 func (api *CloudAPI) UserCredentials(args params.UserClouds) (params.StringsResults, error) { 401 results := params.StringsResults{ 402 Results: make([]params.StringsResult, len(args.UserClouds)), 403 } 404 authFunc, err := api.getCredentialsAuthFunc() 405 if err != nil { 406 return results, err 407 } 408 for i, arg := range args.UserClouds { 409 userTag, err := names.ParseUserTag(arg.UserTag) 410 if err != nil { 411 results.Results[i].Error = common.ServerError(err) 412 continue 413 } 414 if !authFunc(userTag) { 415 results.Results[i].Error = common.ServerError(common.ErrPerm) 416 continue 417 } 418 cloudTag, err := names.ParseCloudTag(arg.CloudTag) 419 if err != nil { 420 results.Results[i].Error = common.ServerError(err) 421 continue 422 } 423 cloudCredentials, err := api.backend.CloudCredentials(userTag, cloudTag.Id()) 424 if err != nil { 425 results.Results[i].Error = common.ServerError(err) 426 continue 427 } 428 out := make([]string, 0, len(cloudCredentials)) 429 for tagId := range cloudCredentials { 430 out = append(out, names.NewCloudCredentialTag(tagId).String()) 431 } 432 results.Results[i].Result = out 433 } 434 return results, nil 435 } 436 437 // AddCredentials adds new credentials. 438 // In contrast to UpdateCredentials() below, the new credentials can be 439 // for a cloud that the controller does not manage (this is required 440 // for CAAS models) 441 func (api *CloudAPI) AddCredentials(args params.TaggedCredentials) (params.ErrorResults, error) { 442 results := params.ErrorResults{ 443 Results: make([]params.ErrorResult, len(args.Credentials)), 444 } 445 446 authFunc, err := api.getCredentialsAuthFunc() 447 if err != nil { 448 return results, err 449 } 450 for i, arg := range args.Credentials { 451 tag, err := names.ParseCloudCredentialTag(arg.Tag) 452 if err != nil { 453 results.Results[i].Error = common.ServerError(err) 454 continue 455 } 456 // NOTE(axw) if we add ACLs for cloud credentials, we'll need 457 // to change this auth check. 458 if !authFunc(tag.Owner()) { 459 results.Results[i].Error = common.ServerError(common.ErrPerm) 460 continue 461 } 462 463 in := cloud.NewCredential( 464 cloud.AuthType(arg.Credential.AuthType), 465 arg.Credential.Attributes, 466 ) 467 if err := api.backend.UpdateCloudCredential(tag, in); err != nil { 468 results.Results[i].Error = common.ServerError(err) 469 continue 470 } 471 } 472 return results, nil 473 } 474 475 // CheckCredentialsModels validates supplied cloud credentials' content against 476 // models that currently use these credentials. 477 // If there are any models that are using a credential and these models or their 478 // cloud instances are not going to be accessible with corresponding credential, 479 // there will be detailed validation errors per model. 480 func (api *CloudAPI) CheckCredentialsModels(args params.TaggedCredentials) (params.UpdateCredentialResults, error) { 481 return api.commonUpdateCredentials(false, false, args) 482 } 483 484 // UpdateCredentialsCheckModels updates a set of cloud credentials' content. 485 // If there are any models that are using a credential and these models 486 // are not going to be visible with updated credential content, 487 // there will be detailed validation errors per model. 488 // Controller admins can 'force' an update of the credential 489 // regardless of whether it is deemed valid or not. 490 func (api *CloudAPI) UpdateCredentialsCheckModels(args params.UpdateCredentialArgs) (params.UpdateCredentialResults, error) { 491 return api.commonUpdateCredentials(true, args.Force, params.TaggedCredentials{args.Credentials}) 492 } 493 494 func (api *CloudAPI) commonUpdateCredentials(update bool, force bool, args params.TaggedCredentials) (params.UpdateCredentialResults, error) { 495 if force { 496 // Only controller admins can ask for an update to be forced. 497 isControllerAdmin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.ctlrBackend.ControllerTag()) 498 if err != nil && !errors.IsNotFound(err) { 499 return params.UpdateCredentialResults{}, errors.Trace(err) 500 } 501 if !isControllerAdmin { 502 return params.UpdateCredentialResults{}, errors.Annotatef(common.ErrBadRequest, "unexpected force specified") 503 } 504 } 505 506 authFunc, err := api.getCredentialsAuthFunc() 507 if err != nil { 508 return params.UpdateCredentialResults{}, err 509 } 510 511 results := make([]params.UpdateCredentialResult, len(args.Credentials)) 512 for i, arg := range args.Credentials { 513 results[i].CredentialTag = arg.Tag 514 tag, err := names.ParseCloudCredentialTag(arg.Tag) 515 if err != nil { 516 results[i].Error = common.ServerError(err) 517 continue 518 } 519 // NOTE(axw) if we add ACLs for cloud credentials, we'll need 520 // to change this auth check. 521 if !authFunc(tag.Owner()) { 522 results[i].Error = common.ServerError(common.ErrPerm) 523 continue 524 } 525 in := cloud.NewCredential( 526 cloud.AuthType(arg.Credential.AuthType), 527 arg.Credential.Attributes, 528 ) 529 530 models, err := api.credentialModels(tag) 531 if err != nil { 532 results[i].Error = common.ServerError(err) 533 if !force { 534 // Could not determine if credential has models - do not continue updating this credential... 535 continue 536 } 537 } 538 539 var modelsErred bool 540 if len(models) > 0 { 541 var modelsResult []params.UpdateCredentialModelResult 542 for uuid, name := range models { 543 model := params.UpdateCredentialModelResult{ 544 ModelUUID: uuid, 545 ModelName: name, 546 } 547 model.Errors = api.validateCredentialForModel(uuid, tag, &in) 548 modelsResult = append(modelsResult, model) 549 if len(model.Errors) > 0 { 550 modelsErred = true 551 } 552 } 553 // since we get a map above, for consistency ensure that models are added 554 // sorted by model uuid. 555 sort.Slice(modelsResult, func(i, j int) bool { 556 return modelsResult[i].ModelUUID < modelsResult[j].ModelUUID 557 }) 558 results[i].Models = modelsResult 559 } 560 561 if modelsErred { 562 results[i].Error = common.ServerError(errors.New("some models are no longer visible")) 563 if !force { 564 // Some models that use this credential do not like the new content, do not update the credential... 565 continue 566 } 567 } 568 569 if update { 570 if err := api.backend.UpdateCloudCredential(tag, in); err != nil { 571 if errors.IsNotFound(err) { 572 err = errors.Errorf( 573 "cannot update credential %q: controller does not manage cloud %q", 574 tag.Name(), tag.Cloud().Id()) 575 } 576 results[i].Error = common.ServerError(err) 577 } 578 } 579 } 580 return params.UpdateCredentialResults{results}, nil 581 } 582 583 func (api *CloudAPI) credentialModels(tag names.CloudCredentialTag) (map[string]string, error) { 584 models, err := api.backend.CredentialModels(tag) 585 if err != nil && !errors.IsNotFound(err) { 586 return nil, errors.Trace(err) 587 } 588 return models, nil 589 } 590 591 func (api *CloudAPI) validateCredentialForModel(modelUUID string, tag names.CloudCredentialTag, credential *cloud.Credential) []params.ErrorResult { 592 var result []params.ErrorResult 593 594 modelState, err := api.pool.Get(modelUUID) 595 if err != nil { 596 return append(result, params.ErrorResult{common.ServerError(err)}) 597 } 598 defer modelState.Release() 599 600 modelErrors, err := validateNewCredentialForModelFunc( 601 modelState.Model(), 602 api.callContext, 603 tag, 604 credential, 605 ) 606 if err != nil { 607 return append(result, params.ErrorResult{common.ServerError(err)}) 608 } 609 if len(modelErrors.Results) > 0 { 610 return append(result, modelErrors.Results...) 611 } 612 return result 613 } 614 615 var validateNewCredentialForModelFunc = credentialcommon.ValidateNewModelCredential 616 617 // Mask out old methods from the new API versions. The API reflection 618 // code in rpc/rpcreflect/type.go:newMethod skips 2-argument methods, 619 // so this removes the method as far as the RPC machinery is concerned. 620 // UpdateCredentials was dropped in V3, replaced with UpdateCredentialsCheckModels. 621 func (*CloudAPI) UpdateCredentials(_, _ struct{}) {} 622 623 // Mask out old methods from the new API versions. The API reflection 624 // code in rpc/rpcreflect/type.go:newMethod skips 2-argument methods, 625 // so this removes the method as far as the RPC machinery is concerned. 626 // 627 // CheckCredentialsModels did not exist before V3. 628 func (*CloudAPIV2) CheckCredentialsModels(_, _ struct{}) {} 629 630 // UpdateCredentials updates a set of cloud credentials' content. 631 func (api *CloudAPIV2) UpdateCredentials(args params.TaggedCredentials) (params.ErrorResults, error) { 632 results := params.ErrorResults{ 633 Results: make([]params.ErrorResult, len(args.Credentials)), 634 } 635 636 updateResults, err := api.commonUpdateCredentials(true, false, args) 637 if err != nil { 638 return results, err 639 } 640 641 // If there are any models that are using a credential and these models 642 // are not going to be visible with updated credential content, 643 // there will be detailed validation errors per model. 644 // However, old return parameter structure could not hold this much detail and, 645 // thus, per-model-per-credential errors are squashed into per-credential errors. 646 for i, result := range updateResults.Results { 647 var resultErrors []params.ErrorResult 648 if result.Error != nil { 649 resultErrors = append(resultErrors, params.ErrorResult{result.Error}) 650 } 651 for _, m := range result.Models { 652 if len(m.Errors) > 0 { 653 modelErors := params.ErrorResults{m.Errors} 654 combined := errors.Annotatef(modelErors.Combine(), "model %q (uuid %v)", m.ModelName, m.ModelUUID) 655 resultErrors = append(resultErrors, params.ErrorResult{common.ServerError(combined)}) 656 } 657 } 658 if len(resultErrors) == 1 { 659 results.Results[i].Error = resultErrors[0].Error 660 continue 661 } 662 if len(resultErrors) > 1 { 663 credentialError := params.ErrorResults{resultErrors} 664 results.Results[i].Error = common.ServerError(credentialError.Combine()) 665 } 666 } 667 return results, nil 668 } 669 670 // Mask out old methods from the new API versions. The API reflection 671 // code in rpc/rpcreflect/type.go:newMethod skips 2-argument methods, 672 // so this removes the method as far as the RPC machinery is concerned. 673 // 674 // RevokeCredentials was dropped in V3, replaced with RevokeCredentialsCheckModel. 675 func (*CloudAPI) RevokeCredentials(_, _ struct{}) {} 676 677 // UpdateCredentials updates a set of cloud credentials' content. 678 func (api *CloudAPIV2) RevokeCredentials(args params.Entities) (params.ErrorResults, error) { 679 results := params.ErrorResults{ 680 Results: make([]params.ErrorResult, len(args.Entities)), 681 } 682 authFunc, err := api.getCredentialsAuthFunc() 683 if err != nil { 684 return results, err 685 } 686 for i, arg := range args.Entities { 687 tag, err := names.ParseCloudCredentialTag(arg.Tag) 688 if err != nil { 689 results.Results[i].Error = common.ServerError(err) 690 continue 691 } 692 // NOTE(axw) if we add ACLs for cloud credentials, we'll need 693 // to change this auth check. 694 if !authFunc(tag.Owner()) { 695 results.Results[i].Error = common.ServerError(common.ErrPerm) 696 continue 697 } 698 699 models, err := api.credentialModels(tag) 700 if err != nil { 701 logger.Warningf("could not get models that use credential %v: %v", tag, err) 702 } 703 if len(models) > 0 { 704 // For backward compatibility, we must proceed here regardless of whether the credential is used by any models, 705 // but, at least, let's log it. 706 logger.Warningf("credential %v will be deleted but it is still used by model%v", tag, modelsPretty(models)) 707 } 708 709 if err := api.backend.RemoveCloudCredential(tag); err != nil { 710 results.Results[i].Error = common.ServerError(err) 711 } 712 } 713 return results, nil 714 } 715 716 // Mask out old methods from the new API versions. The API reflection 717 // code in rpc/rpcreflect/type.go:newMethod skips 2-argument methods, 718 // so this removes the method as far as the RPC machinery is concerned. 719 // 720 // RevokeCredentialsCheckModels did not exist before V3. 721 func (*CloudAPIV2) RevokeCredentialsCheckModels(_, _ struct{}) {} 722 723 func plural(length int) string { 724 if length == 1 { 725 return "" 726 } 727 return "s" 728 } 729 730 func modelsPretty(in map[string]string) string { 731 // map keys are notoriously randomly ordered 732 uuids := []string{} 733 for uuid := range in { 734 uuids = append(uuids, uuid) 735 } 736 sort.Strings(uuids) 737 738 firstLine := ":\n- " 739 if len(uuids) == 1 { 740 firstLine = " " 741 } 742 743 return fmt.Sprintf("%v%v%v", 744 plural(len(in)), 745 firstLine, 746 strings.Join(uuids, "\n- "), 747 ) 748 } 749 750 // RevokeCredentialsCheckModels revokes a set of cloud credentials. 751 // If the credentials are used by any of the models, the credential deletion will be aborted. 752 // If credential-in-use needs to be revoked nonetheless, this method allows the use of force. 753 func (api *CloudAPI) RevokeCredentialsCheckModels(args params.RevokeCredentialArgs) (params.ErrorResults, error) { 754 // TODO (anastasiamac 2018-11-13) the behavior here needs to be changed to performed promised models checks and authorise use of force. 755 results := params.ErrorResults{ 756 Results: make([]params.ErrorResult, len(args.Credentials)), 757 } 758 authFunc, err := api.getCredentialsAuthFunc() 759 if err != nil { 760 return results, err 761 } 762 763 opMessage := func(force bool) string { 764 if force { 765 return "will be deleted but" 766 } 767 return "cannot be deleted as" 768 } 769 770 for i, arg := range args.Credentials { 771 tag, err := names.ParseCloudCredentialTag(arg.Tag) 772 if err != nil { 773 results.Results[i].Error = common.ServerError(err) 774 continue 775 } 776 // NOTE(axw) if we add ACLs for cloud credentials, we'll need 777 // to change this auth check. 778 if !authFunc(tag.Owner()) { 779 results.Results[i].Error = common.ServerError(common.ErrPerm) 780 continue 781 } 782 783 models, err := api.credentialModels(tag) 784 if err != nil { 785 if !arg.Force { 786 // Could not determine if credential has models - do not continue updating this credential... 787 results.Results[i].Error = common.ServerError(err) 788 continue 789 } 790 logger.Warningf("could not get models that use credential %v: %v", tag, err) 791 } 792 if len(models) != 0 { 793 logger.Warningf("credential %v %v it is used by model%v", 794 tag, 795 opMessage(arg.Force), 796 modelsPretty(models), 797 ) 798 if !arg.Force { 799 // Some models still use this credential - do not delete this credential... 800 results.Results[i].Error = common.ServerError(errors.Errorf("cannot delete credential %v: it is still used by %d model%v", tag, len(models), plural(len(models)))) 801 continue 802 } 803 } 804 805 if err := api.backend.RemoveCloudCredential(tag); err != nil { 806 results.Results[i].Error = common.ServerError(err) 807 } 808 } 809 return results, nil 810 } 811 812 // Credential returns the specified cloud credential for each tag, minus secrets. 813 func (api *CloudAPI) Credential(args params.Entities) (params.CloudCredentialResults, error) { 814 results := params.CloudCredentialResults{ 815 Results: make([]params.CloudCredentialResult, len(args.Entities)), 816 } 817 authFunc, err := api.getCredentialsAuthFunc() 818 if err != nil { 819 return results, err 820 } 821 822 for i, arg := range args.Entities { 823 credentialTag, err := names.ParseCloudCredentialTag(arg.Tag) 824 if err != nil { 825 results.Results[i].Error = common.ServerError(err) 826 continue 827 } 828 if !authFunc(credentialTag.Owner()) { 829 results.Results[i].Error = common.ServerError(common.ErrPerm) 830 continue 831 } 832 833 // Helper to look up and cache credential schemas for clouds. 834 schemaCache := make(map[string]map[cloud.AuthType]cloud.CredentialSchema) 835 credentialSchemas := func() (map[cloud.AuthType]cloud.CredentialSchema, error) { 836 cloudName := credentialTag.Cloud().Id() 837 if s, ok := schemaCache[cloudName]; ok { 838 return s, nil 839 } 840 aCloud, err := api.backend.Cloud(cloudName) 841 if err != nil { 842 return nil, err 843 } 844 provider, err := environs.Provider(aCloud.Type) 845 if err != nil { 846 return nil, err 847 } 848 schema := provider.CredentialSchemas() 849 schemaCache[cloudName] = schema 850 return schema, nil 851 } 852 cloudCredentials, err := api.backend.CloudCredentials(credentialTag.Owner(), credentialTag.Cloud().Id()) 853 if err != nil { 854 results.Results[i].Error = common.ServerError(err) 855 continue 856 } 857 858 cred, ok := cloudCredentials[credentialTag.Id()] 859 if !ok { 860 results.Results[i].Error = common.ServerError(errors.NotFoundf("credential %q", credentialTag.Name())) 861 continue 862 } 863 864 schemas, err := credentialSchemas() 865 if err != nil { 866 results.Results[i].Error = common.ServerError(err) 867 continue 868 } 869 870 attrs := cred.Attributes 871 var redacted []string 872 // Mask out the secrets. 873 if s, ok := schemas[cloud.AuthType(cred.AuthType)]; ok { 874 for _, attr := range s { 875 if attr.Hidden { 876 delete(attrs, attr.Name) 877 redacted = append(redacted, attr.Name) 878 } 879 } 880 } 881 results.Results[i].Result = ¶ms.CloudCredential{ 882 AuthType: cred.AuthType, 883 Attributes: attrs, 884 Redacted: redacted, 885 } 886 } 887 return results, nil 888 } 889 890 // AddCloud adds a new cloud, different from the one managed by the controller. 891 func (api *CloudAPI) AddCloud(cloudArgs params.AddCloudArgs) error { 892 err := api.backend.AddCloud(common.CloudFromParams(cloudArgs.Name, cloudArgs.Cloud), api.apiUser.Name()) 893 if err != nil { 894 return err 895 } 896 return nil 897 } 898 899 // RemoveClouds removes the specified clouds from the controller. 900 // If a cloud is in use (has models deployed to it), the removal will fail. 901 func (api *CloudAPI) RemoveClouds(args params.Entities) (params.ErrorResults, error) { 902 result := params.ErrorResults{ 903 Results: make([]params.ErrorResult, len(args.Entities)), 904 } 905 isAdmin, err := api.authorizer.HasPermission(permission.SuperuserAccess, api.ctlrBackend.ControllerTag()) 906 if err != nil && !errors.IsNotFound(err) { 907 return result, errors.Trace(err) 908 } 909 for i, entity := range args.Entities { 910 tag, err := names.ParseCloudTag(entity.Tag) 911 if err != nil { 912 result.Results[i].Error = common.ServerError(err) 913 continue 914 } 915 // Ensure user has permission to remove the cloud. 916 if !isAdmin { 917 canAccess, err := api.canAccessCloud(tag.Id(), api.apiUser, permission.AdminAccess) 918 if err != nil { 919 result.Results[i].Error = common.ServerError(err) 920 continue 921 } 922 if !canAccess { 923 result.Results[i].Error = common.ServerError(common.ErrPerm) 924 continue 925 } 926 } 927 err = api.backend.RemoveCloud(tag.Id()) 928 result.Results[i].Error = common.ServerError(err) 929 } 930 return result, nil 931 } 932 933 // CredentialContents returns the specified cloud credentials, 934 // including the secrets if requested. 935 // If no specific credential name/cloud was passed in, all credentials for this user 936 // are returned. 937 // Only credential owner can see its contents as well as what models use it. 938 // Controller admin has no special superpowers here and is treated the same as all other users. 939 func (api *CloudAPI) CredentialContents(args params.CloudCredentialArgs) (params.CredentialContentResults, error) { 940 // Helper to look up and cache credential schemas for clouds. 941 schemaCache := make(map[string]map[cloud.AuthType]cloud.CredentialSchema) 942 credentialSchemas := func(cloudName string) (map[cloud.AuthType]cloud.CredentialSchema, error) { 943 if s, ok := schemaCache[cloudName]; ok { 944 return s, nil 945 } 946 aCloud, err := api.backend.Cloud(cloudName) 947 if err != nil { 948 return nil, err 949 } 950 provider, err := environs.Provider(aCloud.Type) 951 if err != nil { 952 return nil, err 953 } 954 schema := provider.CredentialSchemas() 955 schemaCache[cloudName] = schema 956 return schema, nil 957 } 958 959 // Helper to parse state.Credential into an expected result item. 960 stateIntoParam := func(credential state.Credential, includeSecrets bool) params.CredentialContentResult { 961 schemas, err := credentialSchemas(credential.Cloud) 962 if err != nil { 963 return params.CredentialContentResult{Error: common.ServerError(err)} 964 } 965 attrs := map[string]string{} 966 // Filter out the secrets. 967 if s, ok := schemas[cloud.AuthType(credential.AuthType)]; ok { 968 for _, attr := range s { 969 if value, exists := credential.Attributes[attr.Name]; exists { 970 if attr.Hidden && !includeSecrets { 971 continue 972 } 973 attrs[attr.Name] = value 974 } 975 } 976 } 977 info := params.ControllerCredentialInfo{ 978 Content: params.CredentialContent{ 979 Name: credential.Name, 980 AuthType: credential.AuthType, 981 Attributes: attrs, 982 Cloud: credential.Cloud, 983 }, 984 } 985 986 // get models 987 tag, err := credential.CloudCredentialTag() 988 if err != nil { 989 return params.CredentialContentResult{Error: common.ServerError(err)} 990 } 991 992 models, err := api.backend.CredentialModelsAndOwnerAccess(tag) 993 if err != nil && !errors.IsNotFound(err) { 994 return params.CredentialContentResult{Error: common.ServerError(err)} 995 } 996 info.Models = make([]params.ModelAccess, len(models)) 997 for i, m := range models { 998 info.Models[i] = params.ModelAccess{m.ModelName, string(m.OwnerAccess)} 999 } 1000 1001 return params.CredentialContentResult{Result: &info} 1002 } 1003 1004 var result []params.CredentialContentResult 1005 if len(args.Credentials) == 0 { 1006 credentials, err := api.backend.AllCloudCredentials(api.apiUser) 1007 if err != nil { 1008 return params.CredentialContentResults{}, errors.Trace(err) 1009 } 1010 result = make([]params.CredentialContentResult, len(credentials)) 1011 for i, credential := range credentials { 1012 result[i] = stateIntoParam(credential, args.IncludeSecrets) 1013 } 1014 } else { 1015 // Helper to construct credential tag from cloud and name. 1016 credId := func(cloudName, credentialName string) string { 1017 return fmt.Sprintf("%s/%s/%s", 1018 cloudName, api.apiUser.Id(), credentialName, 1019 ) 1020 } 1021 1022 result = make([]params.CredentialContentResult, len(args.Credentials)) 1023 for i, given := range args.Credentials { 1024 id := credId(given.CloudName, given.CredentialName) 1025 tag := names.NewCloudCredentialTag(id) 1026 credential, err := api.backend.CloudCredential(tag) 1027 if err != nil { 1028 result[i] = params.CredentialContentResult{ 1029 Error: common.ServerError(err), 1030 } 1031 continue 1032 } 1033 result[i] = stateIntoParam(credential, args.IncludeSecrets) 1034 } 1035 } 1036 return params.CredentialContentResults{result}, nil 1037 } 1038 1039 // ModifyCloudAccess changes the model access granted to users. 1040 func (c *CloudAPI) ModifyCloudAccess(args params.ModifyCloudAccessRequest) (params.ErrorResults, error) { 1041 result := params.ErrorResults{ 1042 Results: make([]params.ErrorResult, len(args.Changes)), 1043 } 1044 if len(args.Changes) == 0 { 1045 return result, nil 1046 } 1047 1048 for i, arg := range args.Changes { 1049 cloudTag, err := names.ParseCloudTag(arg.CloudTag) 1050 if err != nil { 1051 result.Results[i].Error = common.ServerError(err) 1052 continue 1053 } 1054 _, err = c.backend.Cloud(cloudTag.Id()) 1055 if err != nil { 1056 result.Results[i].Error = common.ServerError(err) 1057 continue 1058 } 1059 if c.apiUser.String() == arg.UserTag { 1060 result.Results[i].Error = common.ServerError(errors.New("cannot change your own cloud access")) 1061 continue 1062 } 1063 1064 isAdmin, err := c.authorizer.HasPermission(permission.SuperuserAccess, c.backend.ControllerTag()) 1065 if err != nil { 1066 result.Results[i].Error = common.ServerError(err) 1067 continue 1068 } 1069 if !isAdmin { 1070 callerAccess, err := c.backend.GetCloudAccess(cloudTag.Id(), c.apiUser) 1071 if err != nil { 1072 result.Results[i].Error = common.ServerError(err) 1073 continue 1074 } 1075 if callerAccess != permission.AdminAccess { 1076 result.Results[i].Error = common.ServerError(common.ErrPerm) 1077 continue 1078 } 1079 } 1080 1081 cloudAccess := permission.Access(arg.Access) 1082 if err := permission.ValidateCloudAccess(cloudAccess); err != nil { 1083 result.Results[i].Error = common.ServerError(err) 1084 continue 1085 } 1086 1087 targetUserTag, err := names.ParseUserTag(arg.UserTag) 1088 if err != nil { 1089 result.Results[i].Error = common.ServerError(errors.Annotate(err, "could not modify cloud access")) 1090 continue 1091 } 1092 1093 result.Results[i].Error = common.ServerError( 1094 ChangeCloudAccess(c.backend, cloudTag.Id(), targetUserTag, arg.Action, cloudAccess)) 1095 } 1096 return result, nil 1097 } 1098 1099 // ChangeCloudAccess performs the requested access grant or revoke action for the 1100 // specified user on the cloud. 1101 func ChangeCloudAccess(backend Backend, cloud string, targetUserTag names.UserTag, action params.CloudAction, access permission.Access) error { 1102 switch action { 1103 case params.GrantCloudAccess: 1104 err := grantCloudAccess(backend, cloud, targetUserTag, access) 1105 if err != nil { 1106 return errors.Annotate(err, "could not grant cloud access") 1107 } 1108 return nil 1109 case params.RevokeCloudAccess: 1110 return revokeCloudAccess(backend, cloud, targetUserTag, access) 1111 default: 1112 return errors.Errorf("unknown action %q", action) 1113 } 1114 } 1115 1116 func grantCloudAccess(backend Backend, cloud string, targetUserTag names.UserTag, access permission.Access) error { 1117 err := backend.CreateCloudAccess(cloud, targetUserTag, access) 1118 if errors.IsAlreadyExists(err) { 1119 cloudAccess, err := backend.GetCloudAccess(cloud, targetUserTag) 1120 if errors.IsNotFound(err) { 1121 // Conflicts with prior check, must be inconsistent state. 1122 err = txn.ErrExcessiveContention 1123 } 1124 if err != nil { 1125 return errors.Annotate(err, "could not look up cloud access for user") 1126 } 1127 1128 // Only set access if greater access is being granted. 1129 if cloudAccess.EqualOrGreaterCloudAccessThan(access) { 1130 return errors.Errorf("user already has %q access or greater", access) 1131 } 1132 if err = backend.UpdateCloudAccess(cloud, targetUserTag, access); err != nil { 1133 return errors.Annotate(err, "could not set cloud access for user") 1134 } 1135 return nil 1136 1137 } 1138 if err != nil { 1139 return errors.Trace(err) 1140 } 1141 return nil 1142 } 1143 1144 func revokeCloudAccess(backend Backend, cloud string, targetUserTag names.UserTag, access permission.Access) error { 1145 switch access { 1146 case permission.AddModelAccess: 1147 // Revoking add-model access removes all access. 1148 err := backend.RemoveCloudAccess(cloud, targetUserTag) 1149 return errors.Annotate(err, "could not revoke cloud access") 1150 case permission.AdminAccess: 1151 // Revoking admin sets add-model. 1152 err := backend.UpdateCloudAccess(cloud, targetUserTag, permission.AddModelAccess) 1153 return errors.Annotate(err, "could not set cloud access to add-model") 1154 1155 default: 1156 return errors.Errorf("don't know how to revoke %q access", access) 1157 } 1158 }