github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/applicationoffers/applicationoffers.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  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/names/v5"
    13  	jujutxn "github.com/juju/txn/v3"
    14  
    15  	"github.com/juju/juju/apiserver/authentication"
    16  	"github.com/juju/juju/apiserver/common"
    17  	commoncrossmodel "github.com/juju/juju/apiserver/common/crossmodel"
    18  	apiservererrors "github.com/juju/juju/apiserver/errors"
    19  	"github.com/juju/juju/apiserver/facade"
    20  	jujucrossmodel "github.com/juju/juju/core/crossmodel"
    21  	"github.com/juju/juju/core/permission"
    22  	"github.com/juju/juju/environs"
    23  	"github.com/juju/juju/rpc/params"
    24  )
    25  
    26  var logger = loggo.GetLogger("juju.apiserver.applicationoffers")
    27  
    28  type environFromModelFunc func(string) (environs.Environ, error)
    29  
    30  // OffersAPIv5 implements the cross model interface and is the concrete
    31  // implementation of the api end point.
    32  type OffersAPIv5 struct {
    33  	BaseAPI
    34  	dataDir     string
    35  	authContext *commoncrossmodel.AuthContext
    36  }
    37  
    38  // OffersAPIv4 implements the cross model interface and is the concrete
    39  // implementation of the api end point.
    40  type OffersAPIv4 struct {
    41  	OffersAPIv5
    42  }
    43  
    44  // createAPI returns a new application offers OffersAPI facade.
    45  func createOffersAPI(
    46  	getApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers,
    47  	getEnviron environFromModelFunc,
    48  	getControllerInfo func() ([]string, string, error),
    49  	backend Backend,
    50  	statePool StatePool,
    51  	authorizer facade.Authorizer,
    52  	resources facade.Resources,
    53  	authContext *commoncrossmodel.AuthContext,
    54  ) (*OffersAPIv5, error) {
    55  	if !authorizer.AuthClient() {
    56  		return nil, apiservererrors.ErrPerm
    57  	}
    58  
    59  	dataDir := resources.Get("dataDir").(common.StringResource)
    60  	api := &OffersAPIv5{
    61  		dataDir:     dataDir.String(),
    62  		authContext: authContext,
    63  		BaseAPI: BaseAPI{
    64  			ctx:                  context.Background(),
    65  			Authorizer:           authorizer,
    66  			GetApplicationOffers: getApplicationOffers,
    67  			ControllerModel:      backend,
    68  			StatePool:            statePool,
    69  			getEnviron:           getEnviron,
    70  			getControllerInfo:    getControllerInfo,
    71  		},
    72  	}
    73  	return api, nil
    74  }
    75  
    76  // Offer makes application endpoints available for consumption at a specified URL.
    77  func (api *OffersAPIv5) Offer(all params.AddApplicationOffers) (params.ErrorResults, error) {
    78  	result := make([]params.ErrorResult, len(all.Offers))
    79  
    80  	apiUser := api.Authorizer.GetAuthTag().(names.UserTag)
    81  	for i, one := range all.Offers {
    82  		modelTag, err := names.ParseModelTag(one.ModelTag)
    83  		if err != nil {
    84  			result[i].Error = apiservererrors.ServerError(err)
    85  			continue
    86  		}
    87  		backend, releaser, err := api.StatePool.Get(modelTag.Id())
    88  		if err != nil {
    89  			result[i].Error = apiservererrors.ServerError(err)
    90  			continue
    91  		}
    92  		defer releaser()
    93  
    94  		if err := api.checkAdmin(apiUser, backend); err != nil {
    95  			result[i].Error = apiservererrors.ServerError(err)
    96  			continue
    97  		}
    98  
    99  		owner := apiUser
   100  		// The V4 version of the api includes the offer owner in the params.
   101  		if one.OwnerTag != "" {
   102  			var err error
   103  			if owner, err = names.ParseUserTag(one.OwnerTag); err != nil {
   104  				result[i].Error = apiservererrors.ServerError(err)
   105  				continue
   106  			}
   107  		}
   108  		applicationOfferParams, err := api.makeAddOfferArgsFromParams(owner, backend, one)
   109  		if err != nil {
   110  			result[i].Error = apiservererrors.ServerError(err)
   111  			continue
   112  		}
   113  
   114  		offerBackend := api.GetApplicationOffers(backend)
   115  		if _, err = offerBackend.ApplicationOffer(applicationOfferParams.OfferName); err == nil {
   116  			_, err = offerBackend.UpdateOffer(applicationOfferParams)
   117  		} else {
   118  			_, err = offerBackend.AddOffer(applicationOfferParams)
   119  		}
   120  		result[i].Error = apiservererrors.ServerError(err)
   121  	}
   122  	return params.ErrorResults{Results: result}, nil
   123  }
   124  
   125  func (api *OffersAPIv5) makeAddOfferArgsFromParams(user names.UserTag, backend Backend, addOfferParams params.AddApplicationOffer) (jujucrossmodel.AddApplicationOfferArgs, error) {
   126  	result := jujucrossmodel.AddApplicationOfferArgs{
   127  		OfferName:              addOfferParams.OfferName,
   128  		ApplicationName:        addOfferParams.ApplicationName,
   129  		ApplicationDescription: addOfferParams.ApplicationDescription,
   130  		Endpoints:              addOfferParams.Endpoints,
   131  		Owner:                  user.Id(),
   132  		HasRead:                []string{common.EveryoneTagName},
   133  	}
   134  	if result.OfferName == "" {
   135  		result.OfferName = result.ApplicationName
   136  	}
   137  	application, err := backend.Application(addOfferParams.ApplicationName)
   138  	if err != nil {
   139  		return result, errors.Annotatef(err, "getting offered application %v", addOfferParams.ApplicationName)
   140  	}
   141  
   142  	if result.ApplicationDescription == "" {
   143  		ch, _, err := application.Charm()
   144  		if err != nil {
   145  			return result,
   146  				errors.Annotatef(err, "getting charm for application %v", addOfferParams.ApplicationName)
   147  		}
   148  		result.ApplicationDescription = ch.Meta().Description
   149  	}
   150  	return result, nil
   151  }
   152  
   153  // ListApplicationOffers gets deployed details about application offers that match given filter.
   154  // The results contain details about the deployed applications such as connection count.
   155  func (api *OffersAPIv5) ListApplicationOffers(filters params.OfferFilters) (params.QueryApplicationOffersResultsV5, error) {
   156  	var result params.QueryApplicationOffersResultsV5
   157  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   158  	offers, err := api.getApplicationOffersDetails(user, filters, permission.AdminAccess)
   159  	if err != nil {
   160  		return result, apiservererrors.ServerError(err)
   161  	}
   162  	result.Results = offers
   163  	return result, nil
   164  }
   165  
   166  // ListApplicationOffers gets deployed details about application offers that match given filter.
   167  // The results contain details about the deployed applications such as connection count.
   168  func (api *OffersAPIv4) ListApplicationOffers(filters params.OfferFilters) (params.QueryApplicationOffersResultsV4, error) {
   169  	res, err := api.OffersAPIv5.ListApplicationOffers(filters)
   170  	if err != nil {
   171  		return params.QueryApplicationOffersResultsV4{}, errors.Trace(err)
   172  	}
   173  	resultsV4 := make([]params.ApplicationOfferAdminDetailsV4, len(res.Results))
   174  	for i, result := range res.Results {
   175  		resultsV4[i] = params.ApplicationOfferAdminDetailsV4{
   176  			ApplicationOfferAdminDetailsV5: result,
   177  		}
   178  	}
   179  
   180  	return params.QueryApplicationOffersResultsV4{
   181  		Results: resultsV4,
   182  	}, nil
   183  }
   184  
   185  // ModifyOfferAccess changes the application offer access granted to users.
   186  func (api *OffersAPIv5) ModifyOfferAccess(args params.ModifyOfferAccessRequest) (result params.ErrorResults, _ error) {
   187  	result = params.ErrorResults{
   188  		Results: make([]params.ErrorResult, len(args.Changes)),
   189  	}
   190  	if len(args.Changes) == 0 {
   191  		return result, nil
   192  	}
   193  
   194  	err := api.Authorizer.HasPermission(permission.SuperuserAccess, api.ControllerModel.ControllerTag())
   195  	if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   196  		return result, errors.Trace(err)
   197  	}
   198  	isControllerAdmin := err == nil
   199  
   200  	offerURLs := make([]string, len(args.Changes))
   201  	for i, arg := range args.Changes {
   202  		offerURLs[i] = arg.OfferURL
   203  	}
   204  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   205  	models, err := api.getModelsFromOffers(user, offerURLs...)
   206  	if err != nil {
   207  		return result, errors.Trace(err)
   208  	}
   209  
   210  	for i, arg := range args.Changes {
   211  		if models[i].err != nil {
   212  			result.Results[i].Error = apiservererrors.ServerError(models[i].err)
   213  			continue
   214  		}
   215  		err = api.modifyOneOfferAccess(user, models[i].model.UUID(), isControllerAdmin, arg)
   216  		result.Results[i].Error = apiservererrors.ServerError(err)
   217  	}
   218  	return result, nil
   219  }
   220  
   221  func (api *OffersAPIv5) modifyOneOfferAccess(user names.UserTag, modelUUID string, isControllerAdmin bool, arg params.ModifyOfferAccess) error {
   222  	backend, releaser, err := api.StatePool.Get(modelUUID)
   223  	if err != nil {
   224  		return errors.Trace(err)
   225  	}
   226  	defer releaser()
   227  
   228  	offerAccess := permission.Access(arg.Access)
   229  	if err := permission.ValidateOfferAccess(offerAccess); err != nil {
   230  		return errors.Annotate(err, "could not modify offer access")
   231  	}
   232  
   233  	url, err := jujucrossmodel.ParseOfferURL(arg.OfferURL)
   234  	if err != nil {
   235  		return errors.Trace(err)
   236  	}
   237  
   238  	canModifyOffer := isControllerAdmin
   239  	if !canModifyOffer {
   240  		err = api.Authorizer.HasPermission(permission.AdminAccess, backend.ModelTag())
   241  		if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   242  			return errors.Trace(err)
   243  		}
   244  	}
   245  
   246  	if !canModifyOffer {
   247  		offer, err := backend.ApplicationOffer(url.ApplicationName)
   248  		if err != nil {
   249  			return apiservererrors.ErrPerm
   250  		}
   251  		access, err := backend.GetOfferAccess(offer.OfferUUID, user)
   252  		if err != nil && !errors.IsNotFound(err) {
   253  			return errors.Trace(err)
   254  		} else if err == nil {
   255  			canModifyOffer = access == permission.AdminAccess
   256  		}
   257  	}
   258  	if !canModifyOffer {
   259  		return apiservererrors.ErrPerm
   260  	}
   261  
   262  	targetUserTag, err := names.ParseUserTag(arg.UserTag)
   263  	if err != nil {
   264  		return errors.Annotate(err, "could not modify offer access")
   265  	}
   266  	return api.changeOfferAccess(backend, url.ApplicationName, targetUserTag, arg.Action, offerAccess)
   267  }
   268  
   269  // changeOfferAccess performs the requested access grant or revoke action for the
   270  // specified user on the specified application offer.
   271  func (api *OffersAPIv5) changeOfferAccess(
   272  	backend Backend,
   273  	offerName string,
   274  	targetUserTag names.UserTag,
   275  	action params.OfferAction,
   276  	access permission.Access,
   277  ) error {
   278  	offer, err := backend.ApplicationOffer(offerName)
   279  	if err != nil {
   280  		return errors.Trace(err)
   281  	}
   282  	offerTag := names.NewApplicationOfferTag(offer.OfferUUID)
   283  	switch action {
   284  	case params.GrantOfferAccess:
   285  		return api.grantOfferAccess(backend, offerTag, targetUserTag, access)
   286  	case params.RevokeOfferAccess:
   287  		return api.revokeOfferAccess(backend, offerTag, targetUserTag, access)
   288  	default:
   289  		return errors.Errorf("unknown action %q", action)
   290  	}
   291  }
   292  
   293  func (api *OffersAPIv5) grantOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error {
   294  	err := backend.CreateOfferAccess(offerTag, targetUserTag, access)
   295  	if errors.IsAlreadyExists(err) {
   296  		offerAccess, err := backend.GetOfferAccess(offerTag.Id(), targetUserTag)
   297  		if errors.IsNotFound(err) {
   298  			// Conflicts with prior check, must be inconsistent state.
   299  			err = jujutxn.ErrExcessiveContention
   300  		}
   301  		if err != nil {
   302  			return errors.Annotate(err, "could not look up offer access for user")
   303  		}
   304  
   305  		// Only set access if greater access is being granted.
   306  		if offerAccess.EqualOrGreaterOfferAccessThan(access) {
   307  			return errors.Errorf("user already has %q access or greater", access)
   308  		}
   309  		if err = backend.UpdateOfferAccess(offerTag, targetUserTag, access); err != nil {
   310  			return errors.Annotate(err, "could not set offer access for user")
   311  		}
   312  		return nil
   313  	}
   314  	return errors.Annotate(err, "could not grant offer access")
   315  }
   316  
   317  func (api *OffersAPIv5) revokeOfferAccess(backend Backend, offerTag names.ApplicationOfferTag, targetUserTag names.UserTag, access permission.Access) error {
   318  	switch access {
   319  	case permission.ReadAccess:
   320  		// Revoking read access removes all access.
   321  		err := backend.RemoveOfferAccess(offerTag, targetUserTag)
   322  		return errors.Annotate(err, "could not revoke offer access")
   323  	case permission.ConsumeAccess:
   324  		// Revoking consume access sets read-only.
   325  		err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ReadAccess)
   326  		return errors.Annotate(err, "could not set offer access to read-only")
   327  	case permission.AdminAccess:
   328  		// Revoking admin access sets read-consume.
   329  		err := backend.UpdateOfferAccess(offerTag, targetUserTag, permission.ConsumeAccess)
   330  		return errors.Annotate(err, "could not set offer access to read-consume")
   331  
   332  	default:
   333  		return errors.Errorf("don't know how to revoke %q access", access)
   334  	}
   335  }
   336  
   337  // ApplicationOffers gets details about remote applications that match given URLs.
   338  func (api *OffersAPIv5) ApplicationOffers(urls params.OfferURLs) (params.ApplicationOffersResults, error) {
   339  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   340  	return api.getApplicationOffers(user, urls)
   341  }
   342  
   343  func (api *OffersAPIv5) getApplicationOffers(user names.UserTag, urls params.OfferURLs) (params.ApplicationOffersResults, error) {
   344  	var results params.ApplicationOffersResults
   345  	results.Results = make([]params.ApplicationOfferResult, len(urls.OfferURLs))
   346  
   347  	var (
   348  		filters []params.OfferFilter
   349  		// fullURLs contains the URL strings from the url args,
   350  		// with any optional parts like model owner filled in.
   351  		// It is used to process the result offers.
   352  		fullURLs []string
   353  	)
   354  	for i, urlStr := range urls.OfferURLs {
   355  		url, err := jujucrossmodel.ParseOfferURL(urlStr)
   356  		if err != nil {
   357  			results.Results[i].Error = apiservererrors.ServerError(err)
   358  			continue
   359  		}
   360  		if url.User == "" {
   361  			url.User = user.Id()
   362  		}
   363  		if url.HasEndpoint() {
   364  			results.Results[i].Error = apiservererrors.ServerError(
   365  				errors.Errorf("saas application %q shouldn't include endpoint", url))
   366  			continue
   367  		}
   368  		if url.Source != "" {
   369  			results.Results[i].Error = apiservererrors.ServerError(
   370  				errors.NotSupportedf("query for non-local application offers"))
   371  			continue
   372  		}
   373  		fullURLs = append(fullURLs, url.String())
   374  		filters = append(filters, api.filterFromURL(url))
   375  	}
   376  	if len(filters) == 0 {
   377  		return results, nil
   378  	}
   379  	offers, err := api.getApplicationOffersDetails(user, params.OfferFilters{filters}, permission.ReadAccess)
   380  	if err != nil {
   381  		return results, apiservererrors.ServerError(err)
   382  	}
   383  	offersByURL := make(map[string]params.ApplicationOfferAdminDetailsV5)
   384  	for _, offer := range offers {
   385  		offersByURL[offer.OfferURL] = offer
   386  	}
   387  
   388  	for i, urlStr := range fullURLs {
   389  		offer, ok := offersByURL[urlStr]
   390  		if !ok {
   391  			err = errors.NotFoundf("application offer %q", urlStr)
   392  			results.Results[i].Error = apiservererrors.ServerError(err)
   393  			continue
   394  		}
   395  		results.Results[i].Result = &offer
   396  	}
   397  	return results, nil
   398  }
   399  
   400  // FindApplicationOffers gets details about remote applications that match given filter.
   401  func (api *OffersAPIv5) FindApplicationOffers(filters params.OfferFilters) (params.QueryApplicationOffersResultsV5, error) {
   402  	var result params.QueryApplicationOffersResultsV5
   403  	var filtersToUse params.OfferFilters
   404  
   405  	// If there is only one filter term, and no model is specified, add in
   406  	// any models the user can see and query across those.
   407  	// If there's more than one filter term, each must specify a model.
   408  	if len(filters.Filters) == 1 && filters.Filters[0].ModelName == "" {
   409  		uuids, err := api.ControllerModel.AllModelUUIDs()
   410  		if err != nil {
   411  			return result, errors.Trace(err)
   412  		}
   413  		for _, uuid := range uuids {
   414  			m, release, err := api.StatePool.GetModel(uuid)
   415  			if err != nil {
   416  				return result, errors.Trace(err)
   417  			}
   418  			defer release()
   419  			modelFilter := filters.Filters[0]
   420  			modelFilter.ModelName = m.Name()
   421  			modelFilter.OwnerName = m.Owner().Id()
   422  			filtersToUse.Filters = append(filtersToUse.Filters, modelFilter)
   423  		}
   424  	} else {
   425  		filtersToUse = filters
   426  	}
   427  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   428  	offers, err := api.getApplicationOffersDetails(user, filtersToUse, permission.ReadAccess)
   429  	if err != nil {
   430  		return result, apiservererrors.ServerError(err)
   431  	}
   432  	result.Results = offers
   433  	return result, nil
   434  }
   435  
   436  // GetConsumeDetails returns the details necessary to pass to another model
   437  // to allow the specified args user to consume the offers represented by the args URLs.
   438  func (api *OffersAPIv5) GetConsumeDetails(args params.ConsumeOfferDetailsArg) (params.ConsumeOfferDetailsResults, error) {
   439  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   440  	// Prefer args user if provided.
   441  	if args.UserTag != "" {
   442  		// Only controller admins can get consume details for another user.
   443  		err := api.checkControllerAdmin()
   444  		if err != nil {
   445  			return params.ConsumeOfferDetailsResults{}, errors.Trace(err)
   446  		}
   447  		user, err = names.ParseUserTag(args.UserTag)
   448  		if err != nil {
   449  			return params.ConsumeOfferDetailsResults{}, errors.Trace(err)
   450  		}
   451  	}
   452  	return api.getConsumeDetails(user, args.OfferURLs)
   453  }
   454  
   455  // getConsumeDetails returns the details necessary to pass to another model to
   456  // to allow the specified user to consume the specified offers represented by the urls.
   457  func (api *OffersAPIv5) getConsumeDetails(user names.UserTag, urls params.OfferURLs) (params.ConsumeOfferDetailsResults, error) {
   458  	var consumeResults params.ConsumeOfferDetailsResults
   459  	results := make([]params.ConsumeOfferDetailsResult, len(urls.OfferURLs))
   460  
   461  	offers, err := api.getApplicationOffers(user, urls)
   462  	if err != nil {
   463  		return consumeResults, apiservererrors.ServerError(err)
   464  	}
   465  
   466  	addrs, caCert, err := api.getControllerInfo()
   467  	if err != nil {
   468  		return consumeResults, apiservererrors.ServerError(err)
   469  	}
   470  
   471  	controllerInfo := &params.ExternalControllerInfo{
   472  		ControllerTag: api.ControllerModel.ControllerTag().String(),
   473  		Addrs:         addrs,
   474  		CACert:        caCert,
   475  	}
   476  
   477  	for i, result := range offers.Results {
   478  		results[i].Error = result.Error
   479  		if result.Error != nil {
   480  			continue
   481  		}
   482  		offer := result.Result
   483  		offerDetails := &offer.ApplicationOfferDetailsV5
   484  		results[i].Offer = offerDetails
   485  		results[i].ControllerInfo = controllerInfo
   486  
   487  		modelTag, err := names.ParseModelTag(offerDetails.SourceModelTag)
   488  		if err != nil {
   489  			results[i].Error = apiservererrors.ServerError(err)
   490  			continue
   491  		}
   492  		backend, releaser, err := api.StatePool.Get(modelTag.Id())
   493  		if err != nil {
   494  			results[i].Error = apiservererrors.ServerError(err)
   495  			continue
   496  		}
   497  		defer releaser()
   498  
   499  		err = api.checkAdmin(user, backend)
   500  		if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   501  			results[i].Error = apiservererrors.ServerError(err)
   502  			continue
   503  		}
   504  		isAdmin := err == nil
   505  		if !isAdmin {
   506  			appOffer := names.NewApplicationOfferTag(offer.OfferUUID)
   507  			err := api.Authorizer.EntityHasPermission(user, permission.ConsumeAccess, appOffer)
   508  			if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) {
   509  				results[i].Error = apiservererrors.ServerError(err)
   510  				continue
   511  			}
   512  			if err != nil {
   513  				// This logic is purely for JaaS.
   514  				// Jaas has already checked permissions of args.UserTag in their side, so we don't need to check it again.
   515  				// But as a TODO, we need to set the ConsumeOfferMacaroon's expiry time to 0 to force go to
   516  				// discharge flow once they got the macaroon.
   517  				err := api.checkControllerAdmin()
   518  				if err != nil {
   519  					results[i].Error = apiservererrors.ServerError(err)
   520  					continue
   521  				}
   522  			}
   523  		}
   524  		offerMacaroon, err := api.authContext.CreateConsumeOfferMacaroon(api.ctx, offerDetails, user.Id(), urls.BakeryVersion)
   525  		if err != nil {
   526  			results[i].Error = apiservererrors.ServerError(err)
   527  			continue
   528  		}
   529  		results[i].Macaroon = offerMacaroon.M()
   530  	}
   531  	consumeResults.Results = results
   532  	return consumeResults, nil
   533  }
   534  
   535  // RemoteApplicationInfo returns information about the requested remote application.
   536  // This call currently has no client side API, only there for the Dashboard at this stage.
   537  func (api *OffersAPIv5) RemoteApplicationInfo(args params.OfferURLs) (params.RemoteApplicationInfoResults, error) {
   538  	results := make([]params.RemoteApplicationInfoResult, len(args.OfferURLs))
   539  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   540  	for i, url := range args.OfferURLs {
   541  		info, err := api.oneRemoteApplicationInfo(user, url)
   542  		results[i].Result = info
   543  		results[i].Error = apiservererrors.ServerError(err)
   544  	}
   545  	return params.RemoteApplicationInfoResults{results}, nil
   546  }
   547  
   548  func (api *OffersAPIv5) filterFromURL(url *jujucrossmodel.OfferURL) params.OfferFilter {
   549  	f := params.OfferFilter{
   550  		OwnerName: url.User,
   551  		ModelName: url.ModelName,
   552  		OfferName: url.ApplicationName,
   553  	}
   554  	return f
   555  }
   556  
   557  func (api *OffersAPIv5) oneRemoteApplicationInfo(user names.UserTag, urlStr string) (*params.RemoteApplicationInfo, error) {
   558  	url, err := jujucrossmodel.ParseOfferURL(urlStr)
   559  	if err != nil {
   560  		return nil, errors.Trace(err)
   561  	}
   562  
   563  	// We need at least read access to the model to see the application details.
   564  	// 	offer, err := api.offeredApplicationDetails(url, permission.ReadAccess)
   565  	offers, err := api.getApplicationOffersDetails(
   566  		user,
   567  		params.OfferFilters{[]params.OfferFilter{api.filterFromURL(url)}}, permission.ConsumeAccess)
   568  	if err != nil {
   569  		return nil, errors.Trace(err)
   570  	}
   571  
   572  	// The offers query succeeded but there were no offers matching the required offer name.
   573  	if len(offers) == 0 {
   574  		return nil, errors.NotFoundf("application offer %q", url.ApplicationName)
   575  	}
   576  	// Sanity check - this should never happen.
   577  	if len(offers) > 1 {
   578  		return nil, errors.Errorf("unexpected: %d matching offers for %q", len(offers), url.ApplicationName)
   579  	}
   580  	offer := offers[0]
   581  
   582  	return &params.RemoteApplicationInfo{
   583  		ModelTag:         offer.SourceModelTag,
   584  		Name:             url.ApplicationName,
   585  		Description:      offer.ApplicationDescription,
   586  		OfferURL:         url.String(),
   587  		SourceModelLabel: url.ModelName,
   588  		Endpoints:        offer.Endpoints,
   589  		IconURLPath:      fmt.Sprintf("rest/1.0/remote-application/%s/icon", url.ApplicationName),
   590  	}, nil
   591  }
   592  
   593  // DestroyOffers removes the offers specified by the given URLs, forcing if necessary.
   594  func (api *OffersAPIv5) DestroyOffers(args params.DestroyApplicationOffers) (params.ErrorResults, error) {
   595  	result := make([]params.ErrorResult, len(args.OfferURLs))
   596  
   597  	user := api.Authorizer.GetAuthTag().(names.UserTag)
   598  	models, err := api.getModelsFromOffers(user, args.OfferURLs...)
   599  	if err != nil {
   600  		return params.ErrorResults{}, errors.Trace(err)
   601  	}
   602  
   603  	for i, one := range args.OfferURLs {
   604  		url, err := jujucrossmodel.ParseOfferURL(one)
   605  		if err != nil {
   606  			result[i].Error = apiservererrors.ServerError(err)
   607  			continue
   608  		}
   609  		if models[i].err != nil {
   610  			result[i].Error = apiservererrors.ServerError(models[i].err)
   611  			continue
   612  		}
   613  		backend, releaser, err := api.StatePool.Get(models[i].model.UUID())
   614  		if err != nil {
   615  			result[i].Error = apiservererrors.ServerError(err)
   616  			continue
   617  		}
   618  		defer releaser()
   619  
   620  		if err := api.checkAdmin(user, backend); err != nil {
   621  			result[i].Error = apiservererrors.ServerError(err)
   622  			continue
   623  		}
   624  		err = api.GetApplicationOffers(backend).Remove(url.ApplicationName, args.Force)
   625  		result[i].Error = apiservererrors.ServerError(err)
   626  	}
   627  	return params.ErrorResults{Results: result}, nil
   628  }