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