github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/applicationoffers/base.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package applicationoffers
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sort"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names/v5"
    13  
    14  	"github.com/juju/juju/apiserver/authentication"
    15  	"github.com/juju/juju/apiserver/common/crossmodel"
    16  	"github.com/juju/juju/apiserver/facade"
    17  	jujucrossmodel "github.com/juju/juju/core/crossmodel"
    18  	"github.com/juju/juju/core/permission"
    19  	"github.com/juju/juju/rpc/params"
    20  	"github.com/juju/juju/state"
    21  )
    22  
    23  // BaseAPI provides various boilerplate methods used by the facade business logic.
    24  type BaseAPI struct {
    25  	Authorizer           facade.Authorizer
    26  	GetApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers
    27  	ControllerModel      Backend
    28  	StatePool            StatePool
    29  	getEnviron           environFromModelFunc
    30  	getControllerInfo    func() (apiAddrs []string, caCert string, _ error)
    31  	ctx                  context.Context
    32  }
    33  
    34  // checkAdmin ensures that the specified in user is a model or controller admin.
    35  func (api *BaseAPI) checkAdmin(user names.UserTag, backend Backend) error {
    36  	err := api.Authorizer.EntityHasPermission(user, permission.SuperuserAccess, backend.ControllerTag())
    37  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
    38  		return errors.Trace(err)
    39  	} else if err == nil {
    40  		return nil
    41  	}
    42  
    43  	return api.Authorizer.EntityHasPermission(user, permission.AdminAccess, backend.ModelTag())
    44  }
    45  
    46  // checkControllerAdmin ensures that the logged in user is a controller admin.
    47  func (api *BaseAPI) checkControllerAdmin() error {
    48  	return api.Authorizer.HasPermission(permission.SuperuserAccess, api.ControllerModel.ControllerTag())
    49  }
    50  
    51  // modelForName looks up the model details for the named model and returns
    52  // the model (if found), the absolute model model path which was used in the lookup,
    53  // and a bool indicating if the model was found,
    54  func (api *BaseAPI) modelForName(modelName, ownerName string) (Model, string, bool, error) {
    55  	modelPath := fmt.Sprintf("%s/%s", ownerName, modelName)
    56  	var model Model
    57  	uuids, err := api.ControllerModel.AllModelUUIDs()
    58  	if err != nil {
    59  		return nil, modelPath, false, errors.Trace(err)
    60  	}
    61  	for _, uuid := range uuids {
    62  		m, release, err := api.StatePool.GetModel(uuid)
    63  		if err != nil {
    64  			return nil, modelPath, false, errors.Trace(err)
    65  		}
    66  		defer release()
    67  		if m.Name() == modelName && m.Owner().Id() == ownerName {
    68  			model = m
    69  			break
    70  		}
    71  	}
    72  	return model, modelPath, model != nil, nil
    73  }
    74  
    75  func (api *BaseAPI) userDisplayName(backend Backend, userTag names.UserTag) (string, error) {
    76  	var displayName string
    77  	user, err := backend.User(userTag)
    78  	if err != nil && !errors.IsNotFound(err) {
    79  		return "", errors.Trace(err)
    80  	} else if err == nil {
    81  		displayName = user.DisplayName()
    82  	}
    83  	return displayName, nil
    84  }
    85  
    86  // applicationOffersFromModel gets details about remote applications that match given filters.
    87  func (api *BaseAPI) applicationOffersFromModel(
    88  	modelUUID string,
    89  	user names.UserTag,
    90  	requiredAccess permission.Access,
    91  	filters ...jujucrossmodel.ApplicationOfferFilter,
    92  ) ([]params.ApplicationOfferAdminDetailsV5, error) {
    93  	// Get the relevant backend for the specified model.
    94  	backend, releaser, err := api.StatePool.Get(modelUUID)
    95  	if err != nil {
    96  		return nil, errors.Trace(err)
    97  	}
    98  	defer releaser()
    99  
   100  	// If requireAdmin is true, the user must be a controller superuser
   101  	// or model admin to proceed.
   102  	var isAdmin bool
   103  	err = api.checkAdmin(user, backend)
   104  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   105  		return nil, err
   106  	}
   107  	isAdmin = err == nil
   108  	if requiredAccess == permission.AdminAccess && !isAdmin {
   109  		return nil, err
   110  	}
   111  
   112  	offers, err := api.GetApplicationOffers(backend).ListOffers(filters...)
   113  	if err != nil {
   114  		return nil, errors.Trace(err)
   115  	}
   116  
   117  	apiUserDisplayName, err := api.userDisplayName(backend, user)
   118  	if err != nil {
   119  		return nil, errors.Trace(err)
   120  	}
   121  
   122  	var results []params.ApplicationOfferAdminDetailsV5
   123  	for _, appOffer := range offers {
   124  		userAccess := permission.AdminAccess
   125  		// If the user is not a model admin, they need at least read
   126  		// access on an offer to see it.
   127  		if !isAdmin {
   128  			if userAccess, err = api.checkOfferAccess(user, backend, appOffer.OfferUUID); err != nil {
   129  				return nil, errors.Trace(err)
   130  			}
   131  			if userAccess == permission.NoAccess {
   132  				continue
   133  			}
   134  			isAdmin = userAccess == permission.AdminAccess
   135  		}
   136  		offerParams, app, err := api.makeOfferParams(backend, &appOffer)
   137  		// Just because we can't compose the result for one offer, log
   138  		// that and move on to the next one.
   139  		if err != nil {
   140  			logger.Warningf("cannot get application offer: %v", err)
   141  			continue
   142  		}
   143  		offerParams.Users = []params.OfferUserDetails{{
   144  			UserName:    user.Id(),
   145  			DisplayName: apiUserDisplayName,
   146  			Access:      string(userAccess),
   147  		}}
   148  		offer := params.ApplicationOfferAdminDetailsV5{
   149  			ApplicationOfferDetailsV5: *offerParams,
   150  		}
   151  		// Only admins can see some sensitive details of the offer.
   152  		if isAdmin {
   153  			if err := api.getOfferAdminDetails(user, backend, app, &offer); err != nil {
   154  				logger.Warningf("cannot get offer admin details: %v", err)
   155  			}
   156  		}
   157  		results = append(results, offer)
   158  	}
   159  	return results, nil
   160  }
   161  
   162  func (api *BaseAPI) getOfferAdminDetails(user names.UserTag, backend Backend, app crossmodel.Application, offer *params.ApplicationOfferAdminDetailsV5) error {
   163  	curl, _ := app.CharmURL()
   164  	conns, err := backend.OfferConnections(offer.OfferUUID)
   165  	if err != nil {
   166  		return errors.Trace(err)
   167  	}
   168  	offer.ApplicationName = app.Name()
   169  	offer.CharmURL = *curl
   170  	for _, oc := range conns {
   171  		connDetails := params.OfferConnection{
   172  			SourceModelTag: names.NewModelTag(oc.SourceModelUUID()).String(),
   173  			Username:       oc.UserName(),
   174  			RelationId:     oc.RelationId(),
   175  		}
   176  		rel, err := backend.KeyRelation(oc.RelationKey())
   177  		if err != nil {
   178  			return errors.Trace(err)
   179  		}
   180  		ep, err := rel.Endpoint(app.Name())
   181  		if err != nil {
   182  			return errors.Trace(err)
   183  		}
   184  		relStatus, err := rel.Status()
   185  		if err != nil {
   186  			return errors.Trace(err)
   187  		}
   188  		connDetails.Endpoint = ep.Name
   189  		connDetails.Status = params.EntityStatus{
   190  			Status: relStatus.Status,
   191  			Info:   relStatus.Message,
   192  			Data:   relStatus.Data,
   193  			Since:  relStatus.Since,
   194  		}
   195  		relIngress, err := backend.IngressNetworks(oc.RelationKey())
   196  		if err != nil && !errors.IsNotFound(err) {
   197  			return errors.Trace(err)
   198  		}
   199  		if err == nil {
   200  			connDetails.IngressSubnets = relIngress.CIDRS()
   201  		}
   202  		offer.Connections = append(offer.Connections, connDetails)
   203  	}
   204  
   205  	offerUsers, err := backend.GetOfferUsers(offer.OfferUUID)
   206  	if err != nil {
   207  		return errors.Trace(err)
   208  	}
   209  
   210  	for userName, access := range offerUsers {
   211  		if userName == user.Id() {
   212  			continue
   213  		}
   214  		displayName, err := api.userDisplayName(backend, names.NewUserTag(userName))
   215  		if err != nil {
   216  			return errors.Trace(err)
   217  		}
   218  		offer.Users = append(offer.Users, params.OfferUserDetails{
   219  			UserName:    userName,
   220  			DisplayName: displayName,
   221  			Access:      string(access),
   222  		})
   223  	}
   224  	return nil
   225  }
   226  
   227  // checkOfferAccess returns the level of access the authenticated user has to the offer,
   228  // so long as it is greater than the requested perm.
   229  func (api *BaseAPI) checkOfferAccess(user names.UserTag, backend Backend, offerUUID string) (permission.Access, error) {
   230  	access, err := backend.GetOfferAccess(offerUUID, user)
   231  	if err != nil && !errors.IsNotFound(err) {
   232  		return permission.NoAccess, errors.Trace(err)
   233  	}
   234  	if !access.EqualOrGreaterOfferAccessThan(permission.ReadAccess) {
   235  		return permission.NoAccess, nil
   236  	}
   237  	return access, nil
   238  }
   239  
   240  type offerModel struct {
   241  	model Model
   242  	err   error
   243  }
   244  
   245  // getModelsFromOffers returns a slice of models corresponding to the
   246  // specified offer URLs. Each result item has either a model or an error.
   247  func (api *BaseAPI) getModelsFromOffers(user names.UserTag, offerURLs ...string) ([]offerModel, error) {
   248  	// Cache the models found so far so we don't look them up more than once.
   249  	modelsCache := make(map[string]Model)
   250  	oneModel := func(offerURL string) (Model, error) {
   251  		url, err := jujucrossmodel.ParseOfferURL(offerURL)
   252  		if err != nil {
   253  			return nil, errors.Trace(err)
   254  		}
   255  		modelPath := fmt.Sprintf("%s/%s", url.User, url.ModelName)
   256  		if model, ok := modelsCache[modelPath]; ok {
   257  			return model, nil
   258  		}
   259  
   260  		ownerName := url.User
   261  		if ownerName == "" {
   262  			ownerName = user.Id()
   263  		}
   264  		model, absModelPath, ok, err := api.modelForName(url.ModelName, ownerName)
   265  		if err != nil {
   266  			return nil, errors.Trace(err)
   267  		}
   268  		if !ok {
   269  			return nil, errors.NotFoundf("model %q", absModelPath)
   270  		}
   271  		return model, nil
   272  	}
   273  
   274  	result := make([]offerModel, len(offerURLs))
   275  	for i, offerURL := range offerURLs {
   276  		var om offerModel
   277  		om.model, om.err = oneModel(offerURL)
   278  		result[i] = om
   279  	}
   280  	return result, nil
   281  }
   282  
   283  // getModelFilters splits the specified filters per model and returns
   284  // the model and filter details for each.
   285  func (api *BaseAPI) getModelFilters(user names.UserTag, filters params.OfferFilters) (
   286  	models map[string]Model,
   287  	filtersPerModel map[string][]jujucrossmodel.ApplicationOfferFilter,
   288  	_ error,
   289  ) {
   290  	models = make(map[string]Model)
   291  	filtersPerModel = make(map[string][]jujucrossmodel.ApplicationOfferFilter)
   292  
   293  	// Group the filters per model and then query each model with the relevant filters
   294  	// for that model.
   295  	modelUUIDs := make(map[string]string)
   296  	for _, f := range filters.Filters {
   297  		if f.ModelName == "" {
   298  			return nil, nil, errors.New("application offer filter must specify a model name")
   299  		}
   300  		ownerName := f.OwnerName
   301  		if ownerName == "" {
   302  			ownerName = user.Id()
   303  		}
   304  		var (
   305  			modelUUID string
   306  			ok        bool
   307  		)
   308  		if modelUUID, ok = modelUUIDs[f.ModelName]; !ok {
   309  			var err error
   310  			model, absModelPath, ok, err := api.modelForName(f.ModelName, ownerName)
   311  			if err != nil {
   312  				return nil, nil, errors.Trace(err)
   313  			}
   314  			if !ok {
   315  				err := errors.NotFoundf("model %q", absModelPath)
   316  				return nil, nil, errors.Trace(err)
   317  			}
   318  			// Record the UUID and model for next time.
   319  			modelUUID = model.UUID()
   320  			modelUUIDs[f.ModelName] = modelUUID
   321  			models[modelUUID] = model
   322  		}
   323  
   324  		// Record the filter and model details against the model UUID.
   325  		filters := filtersPerModel[modelUUID]
   326  		filter, err := makeOfferFilterFromParams(f)
   327  		if err != nil {
   328  			return nil, nil, errors.Trace(err)
   329  		}
   330  		filters = append(filters, filter)
   331  		filtersPerModel[modelUUID] = filters
   332  	}
   333  	return models, filtersPerModel, nil
   334  }
   335  
   336  // getApplicationOffersDetails gets details about remote applications that match given filter.
   337  func (api *BaseAPI) getApplicationOffersDetails(
   338  	user names.UserTag,
   339  	filters params.OfferFilters,
   340  	requiredPermission permission.Access,
   341  ) ([]params.ApplicationOfferAdminDetailsV5, error) {
   342  
   343  	// If there are no filters specified, that's an error since the
   344  	// caller is expected to specify at the least one or more models
   345  	// to avoid an unbounded query across all models.
   346  	if len(filters.Filters) == 0 {
   347  		return nil, errors.New("at least one offer filter is required")
   348  	}
   349  
   350  	// Gather all the filter details for doing a query for each model.
   351  	models, filtersPerModel, err := api.getModelFilters(user, filters)
   352  	if err != nil {
   353  		return nil, errors.Trace(err)
   354  	}
   355  
   356  	// Ensure the result is deterministic.
   357  	var allUUIDs []string
   358  	for modelUUID := range filtersPerModel {
   359  		allUUIDs = append(allUUIDs, modelUUID)
   360  	}
   361  	sort.Strings(allUUIDs)
   362  
   363  	// Do the per model queries.
   364  	var result []params.ApplicationOfferAdminDetailsV5
   365  	for _, modelUUID := range allUUIDs {
   366  		filters := filtersPerModel[modelUUID]
   367  		offers, err := api.applicationOffersFromModel(modelUUID, user, requiredPermission, filters...)
   368  		if err != nil {
   369  			return nil, errors.Trace(err)
   370  		}
   371  		model := models[modelUUID]
   372  
   373  		for _, offerDetails := range offers {
   374  			offerDetails.OfferURL = jujucrossmodel.MakeURL(model.Owner().Id(), model.Name(), offerDetails.OfferName, "")
   375  			result = append(result, offerDetails)
   376  		}
   377  	}
   378  	return result, nil
   379  }
   380  
   381  func makeOfferFilterFromParams(filter params.OfferFilter) (jujucrossmodel.ApplicationOfferFilter, error) {
   382  	offerFilter := jujucrossmodel.ApplicationOfferFilter{
   383  		OfferName:              filter.OfferName,
   384  		ApplicationName:        filter.ApplicationName,
   385  		ApplicationDescription: filter.ApplicationDescription,
   386  		Endpoints:              make([]jujucrossmodel.EndpointFilterTerm, len(filter.Endpoints)),
   387  		AllowedConsumers:       make([]string, len(filter.AllowedConsumerTags)),
   388  		ConnectedUsers:         make([]string, len(filter.ConnectedUserTags)),
   389  	}
   390  	for i, ep := range filter.Endpoints {
   391  		offerFilter.Endpoints[i] = jujucrossmodel.EndpointFilterTerm{
   392  			Name:      ep.Name,
   393  			Interface: ep.Interface,
   394  			Role:      ep.Role,
   395  		}
   396  	}
   397  	for i, tag := range filter.AllowedConsumerTags {
   398  		u, err := names.ParseUserTag(tag)
   399  		if err != nil {
   400  			return jujucrossmodel.ApplicationOfferFilter{}, errors.Trace(err)
   401  		}
   402  		offerFilter.AllowedConsumers[i] = u.Id()
   403  	}
   404  	for i, tag := range filter.ConnectedUserTags {
   405  		u, err := names.ParseUserTag(tag)
   406  		if err != nil {
   407  			return jujucrossmodel.ApplicationOfferFilter{}, errors.Trace(err)
   408  		}
   409  		offerFilter.ConnectedUsers[i] = u.Id()
   410  	}
   411  	return offerFilter, nil
   412  }
   413  
   414  func (api *BaseAPI) makeOfferParams(backend Backend,
   415  	offer *jujucrossmodel.ApplicationOffer,
   416  ) (*params.ApplicationOfferDetailsV5, crossmodel.Application, error) {
   417  	app, err := backend.Application(offer.ApplicationName)
   418  	if err != nil {
   419  		return nil, nil, errors.Trace(err)
   420  	}
   421  
   422  	result := params.ApplicationOfferDetailsV5{
   423  		SourceModelTag:         backend.ModelTag().String(),
   424  		OfferName:              offer.OfferName,
   425  		OfferUUID:              offer.OfferUUID,
   426  		ApplicationDescription: offer.ApplicationDescription,
   427  	}
   428  
   429  	// Create result.Endpoints both IAAS and CAAS can use.
   430  	for alias, ep := range offer.Endpoints {
   431  		result.Endpoints = append(result.Endpoints, params.RemoteEndpoint{
   432  			Name:      alias,
   433  			Interface: ep.Interface,
   434  			Role:      ep.Role,
   435  		})
   436  
   437  	}
   438  
   439  	// CAAS models don't have spaces.
   440  	model, err := backend.Model()
   441  	if err != nil {
   442  		return nil, nil, errors.Trace(err)
   443  	}
   444  	if model.Type() == state.ModelTypeCAAS {
   445  		return &result, app, nil
   446  	}
   447  
   448  	return &result, app, nil
   449  }