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 &paramsCloud, 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 := &params.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 = &params.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  }