github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"fmt"
     8  	"sort"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	"gopkg.in/juju/names.v2"
    13  
    14  	"github.com/juju/juju/apiserver/common"
    15  	"github.com/juju/juju/apiserver/common/crossmodel"
    16  	"github.com/juju/juju/apiserver/facade"
    17  	"github.com/juju/juju/apiserver/params"
    18  	jujucrossmodel "github.com/juju/juju/core/crossmodel"
    19  	"github.com/juju/juju/environs"
    20  	"github.com/juju/juju/environs/context"
    21  	"github.com/juju/juju/permission"
    22  	"github.com/juju/juju/state"
    23  )
    24  
    25  // BaseAPI provides various boilerplate methods used by the facade business logic.
    26  type BaseAPI struct {
    27  	Authorizer           facade.Authorizer
    28  	GetApplicationOffers func(interface{}) jujucrossmodel.ApplicationOffers
    29  	ControllerModel      Backend
    30  	StatePool            StatePool
    31  	getEnviron           environFromModelFunc
    32  	getControllerInfo    func() (apiAddrs []string, caCert string, _ error)
    33  	callContext          context.ProviderCallContext
    34  }
    35  
    36  // checkPermission ensures that the logged in user holds the given permission on an entity.
    37  func (api *BaseAPI) checkPermission(tag names.Tag, perm permission.Access) error {
    38  	allowed, err := api.Authorizer.HasPermission(perm, tag)
    39  	if err != nil {
    40  		return errors.Trace(err)
    41  	}
    42  	if !allowed {
    43  		return common.ErrPerm
    44  	}
    45  	return nil
    46  }
    47  
    48  // checkAdmin ensures that the logged in user is a model or controller admin.
    49  func (api *BaseAPI) checkAdmin(backend Backend) error {
    50  	allowed, err := api.Authorizer.HasPermission(permission.AdminAccess, backend.ModelTag())
    51  	if err != nil {
    52  		return errors.Trace(err)
    53  	}
    54  	if !allowed {
    55  		allowed, err = api.Authorizer.HasPermission(permission.SuperuserAccess, backend.ControllerTag())
    56  	}
    57  	if err != nil {
    58  		return errors.Trace(err)
    59  	}
    60  	if !allowed {
    61  		return common.ErrPerm
    62  	}
    63  	return nil
    64  }
    65  
    66  // modelForName looks up the model details for the named model and returns
    67  // the model (if found), the absolute model model path which was used in the lookup,
    68  // and a bool indicating if the model was found,
    69  func (api *BaseAPI) modelForName(modelName, ownerName string) (Model, string, bool, error) {
    70  	user := api.Authorizer.GetAuthTag()
    71  	if ownerName == "" {
    72  		ownerName = user.Id()
    73  	}
    74  	modelPath := fmt.Sprintf("%s/%s", ownerName, modelName)
    75  	var model Model
    76  	uuids, err := api.ControllerModel.AllModelUUIDs()
    77  	if err != nil {
    78  		return nil, modelPath, false, errors.Trace(err)
    79  	}
    80  	for _, uuid := range uuids {
    81  		m, release, err := api.StatePool.GetModel(uuid)
    82  		if err != nil {
    83  			return nil, modelPath, false, errors.Trace(err)
    84  		}
    85  		defer release()
    86  		if m.Name() == modelName && m.Owner().Id() == ownerName {
    87  			model = m
    88  			break
    89  		}
    90  	}
    91  	return model, modelPath, model != nil, nil
    92  }
    93  
    94  func (api *BaseAPI) userDisplayName(backend Backend, userTag names.UserTag) (string, error) {
    95  	var displayName string
    96  	user, err := backend.User(userTag)
    97  	if err != nil && !errors.IsNotFound(err) {
    98  		return "", errors.Trace(err)
    99  	} else if err == nil {
   100  		displayName = user.DisplayName()
   101  	}
   102  	return displayName, nil
   103  }
   104  
   105  // applicationOffersFromModel gets details about remote applications that match given filters.
   106  func (api *BaseAPI) applicationOffersFromModel(
   107  	modelUUID string,
   108  	requiredAccess permission.Access,
   109  	filters ...jujucrossmodel.ApplicationOfferFilter,
   110  ) ([]params.ApplicationOfferAdminDetails, error) {
   111  	// Get the relevant backend for the specified model.
   112  	backend, releaser, err := api.StatePool.Get(modelUUID)
   113  	if err != nil {
   114  		return nil, errors.Trace(err)
   115  	}
   116  	defer releaser()
   117  
   118  	// If requireAdmin is true, the user must be a controller superuser
   119  	// or model admin to proceed.
   120  	isAdmin := false
   121  	err = api.checkAdmin(backend)
   122  	if err != nil && err != common.ErrPerm {
   123  		return nil, errors.Trace(err)
   124  	}
   125  	isAdmin = err == nil
   126  	if requiredAccess == permission.AdminAccess && !isAdmin {
   127  		return nil, common.ErrPerm
   128  	}
   129  
   130  	offers, err := api.GetApplicationOffers(backend).ListOffers(filters...)
   131  	if err != nil {
   132  		return nil, errors.Trace(err)
   133  	}
   134  
   135  	apiUserTag := api.Authorizer.GetAuthTag().(names.UserTag)
   136  	apiUserDisplayName, err := api.userDisplayName(backend, apiUserTag)
   137  	if err != nil {
   138  		return nil, errors.Trace(err)
   139  	}
   140  
   141  	var results []params.ApplicationOfferAdminDetails
   142  	for _, appOffer := range offers {
   143  		userAccess := permission.AdminAccess
   144  		// If the user is not a model admin, they need at least read
   145  		// access on an offer to see it.
   146  		if !isAdmin {
   147  			if userAccess, err = api.checkOfferAccess(backend, appOffer.OfferUUID, requiredAccess); err != nil {
   148  				return nil, errors.Trace(err)
   149  			}
   150  			if userAccess == permission.NoAccess {
   151  				continue
   152  			}
   153  			isAdmin = userAccess == permission.AdminAccess
   154  		}
   155  		offerParams, app, err := api.makeOfferParams(backend, &appOffer)
   156  		// Just because we can't compose the result for one offer, log
   157  		// that and move on to the next one.
   158  		if err != nil {
   159  			logger.Warningf("cannot get application offer: %v", err)
   160  			continue
   161  		}
   162  		offerParams.Users = []params.OfferUserDetails{{
   163  			UserName:    apiUserTag.Id(),
   164  			DisplayName: apiUserDisplayName,
   165  			Access:      string(userAccess),
   166  		}}
   167  		offer := params.ApplicationOfferAdminDetails{
   168  			ApplicationOfferDetails: *offerParams,
   169  		}
   170  		// Only admins can see some sensitive details of the offer.
   171  		if isAdmin {
   172  			if err := api.getOfferAdminDetails(backend, app, &offer); err != nil {
   173  				logger.Warningf("cannot get offer admin details: %v", err)
   174  			}
   175  		}
   176  		results = append(results, offer)
   177  	}
   178  	return results, nil
   179  }
   180  
   181  func (api *BaseAPI) getOfferAdminDetails(backend Backend, app crossmodel.Application, offer *params.ApplicationOfferAdminDetails) error {
   182  	curl, _ := app.CharmURL()
   183  	conns, err := backend.OfferConnections(offer.OfferUUID)
   184  	if err != nil {
   185  		return errors.Trace(err)
   186  	}
   187  	offer.ApplicationName = app.Name()
   188  	offer.CharmURL = curl.String()
   189  	for _, oc := range conns {
   190  		connDetails := params.OfferConnection{
   191  			SourceModelTag: names.NewModelTag(oc.SourceModelUUID()).String(),
   192  			Username:       oc.UserName(),
   193  			RelationId:     oc.RelationId(),
   194  		}
   195  		rel, err := backend.KeyRelation(oc.RelationKey())
   196  		if err != nil {
   197  			return errors.Trace(err)
   198  		}
   199  		ep, err := rel.Endpoint(app.Name())
   200  		if err != nil {
   201  			return errors.Trace(err)
   202  		}
   203  		relStatus, err := rel.Status()
   204  		if err != nil {
   205  			return errors.Trace(err)
   206  		}
   207  		connDetails.Endpoint = ep.Name
   208  		connDetails.Status = params.EntityStatus{
   209  			Status: relStatus.Status,
   210  			Info:   relStatus.Message,
   211  			Data:   relStatus.Data,
   212  			Since:  relStatus.Since,
   213  		}
   214  		relIngress, err := backend.IngressNetworks(oc.RelationKey())
   215  		if err != nil && !errors.IsNotFound(err) {
   216  			return errors.Trace(err)
   217  		}
   218  		if err == nil {
   219  			connDetails.IngressSubnets = relIngress.CIDRS()
   220  		}
   221  		offer.Connections = append(offer.Connections, connDetails)
   222  	}
   223  
   224  	offerUsers, err := backend.GetOfferUsers(offer.OfferUUID)
   225  	if err != nil {
   226  		return errors.Trace(err)
   227  	}
   228  
   229  	apiUserTag := api.Authorizer.GetAuthTag().(names.UserTag)
   230  	for userName, access := range offerUsers {
   231  		if userName == apiUserTag.Id() {
   232  			continue
   233  		}
   234  		displayName, err := api.userDisplayName(backend, names.NewUserTag(userName))
   235  		if err != nil {
   236  			return errors.Trace(err)
   237  		}
   238  		offer.Users = append(offer.Users, params.OfferUserDetails{
   239  			UserName:    userName,
   240  			DisplayName: displayName,
   241  			Access:      string(access),
   242  		})
   243  	}
   244  	return nil
   245  }
   246  
   247  // checkOfferAccess returns the level of access the authenticated user has to the offer,
   248  // so long as it is greater than the requested perm.
   249  func (api *BaseAPI) checkOfferAccess(backend Backend, offerUUID string, perm permission.Access) (permission.Access, error) {
   250  	apiUser := api.Authorizer.GetAuthTag().(names.UserTag)
   251  	access, err := backend.GetOfferAccess(offerUUID, apiUser)
   252  	if err != nil && !errors.IsNotFound(err) {
   253  		return permission.NoAccess, errors.Trace(err)
   254  	}
   255  	if !access.EqualOrGreaterOfferAccessThan(permission.ReadAccess) {
   256  		return permission.NoAccess, nil
   257  	}
   258  	return access, nil
   259  }
   260  
   261  type offerModel struct {
   262  	model Model
   263  	err   error
   264  }
   265  
   266  // getModelsFromOffers returns a slice of models corresponding to the
   267  // specified offer URLs. Each result item has either a model or an error.
   268  func (api *BaseAPI) getModelsFromOffers(offerURLs ...string) ([]offerModel, error) {
   269  	// Cache the models found so far so we don't look them up more than once.
   270  	modelsCache := make(map[string]Model)
   271  	oneModel := func(offerURL string) (Model, error) {
   272  		url, err := jujucrossmodel.ParseOfferURL(offerURL)
   273  		if err != nil {
   274  			return nil, errors.Trace(err)
   275  		}
   276  		modelPath := fmt.Sprintf("%s/%s", url.User, url.ModelName)
   277  		if model, ok := modelsCache[modelPath]; ok {
   278  			return model, nil
   279  		}
   280  
   281  		model, absModelPath, ok, err := api.modelForName(url.ModelName, url.User)
   282  		if err != nil {
   283  			return nil, errors.Trace(err)
   284  		}
   285  		if !ok {
   286  			return nil, errors.NotFoundf("model %q", absModelPath)
   287  		}
   288  		return model, nil
   289  	}
   290  
   291  	result := make([]offerModel, len(offerURLs))
   292  	for i, offerURL := range offerURLs {
   293  		var om offerModel
   294  		om.model, om.err = oneModel(offerURL)
   295  		result[i] = om
   296  	}
   297  	return result, nil
   298  }
   299  
   300  // getModelFilters splits the specified filters per model and returns
   301  // the model and filter details for each.
   302  func (api *BaseAPI) getModelFilters(filters params.OfferFilters) (
   303  	models map[string]Model,
   304  	filtersPerModel map[string][]jujucrossmodel.ApplicationOfferFilter,
   305  	_ error,
   306  ) {
   307  	models = make(map[string]Model)
   308  	filtersPerModel = make(map[string][]jujucrossmodel.ApplicationOfferFilter)
   309  
   310  	// Group the filters per model and then query each model with the relevant filters
   311  	// for that model.
   312  	modelUUIDs := make(map[string]string)
   313  	for _, f := range filters.Filters {
   314  		if f.ModelName == "" {
   315  			return nil, nil, errors.New("application offer filter must specify a model name")
   316  		}
   317  		var (
   318  			modelUUID string
   319  			ok        bool
   320  		)
   321  		if modelUUID, ok = modelUUIDs[f.ModelName]; !ok {
   322  			var err error
   323  			model, absModelPath, ok, err := api.modelForName(f.ModelName, f.OwnerName)
   324  			if err != nil {
   325  				return nil, nil, errors.Trace(err)
   326  			}
   327  			if !ok {
   328  				err := errors.NotFoundf("model %q", absModelPath)
   329  				return nil, nil, errors.Trace(err)
   330  			}
   331  			// Record the UUID and model for next time.
   332  			modelUUID = model.UUID()
   333  			modelUUIDs[f.ModelName] = modelUUID
   334  			models[modelUUID] = model
   335  		}
   336  
   337  		// Record the filter and model details against the model UUID.
   338  		filters := filtersPerModel[modelUUID]
   339  		filter, err := makeOfferFilterFromParams(f)
   340  		if err != nil {
   341  			return nil, nil, errors.Trace(err)
   342  		}
   343  		filters = append(filters, filter)
   344  		filtersPerModel[modelUUID] = filters
   345  	}
   346  	return models, filtersPerModel, nil
   347  }
   348  
   349  // getApplicationOffersDetails gets details about remote applications that match given filter.
   350  func (api *BaseAPI) getApplicationOffersDetails(
   351  	filters params.OfferFilters,
   352  	requiredPermission permission.Access,
   353  ) ([]params.ApplicationOfferAdminDetails, error) {
   354  
   355  	// If there are no filters specified, that's an error since the
   356  	// caller is expected to specify at the least one or more models
   357  	// to avoid an unbounded query across all models.
   358  	if len(filters.Filters) == 0 {
   359  		return nil, errors.New("at least one offer filter is required")
   360  	}
   361  
   362  	// Gather all the filter details for doing a query for each model.
   363  	models, filtersPerModel, err := api.getModelFilters(filters)
   364  	if err != nil {
   365  		return nil, errors.Trace(err)
   366  	}
   367  
   368  	// Ensure the result is deterministic.
   369  	var allUUIDs []string
   370  	for modelUUID := range filtersPerModel {
   371  		allUUIDs = append(allUUIDs, modelUUID)
   372  	}
   373  	sort.Strings(allUUIDs)
   374  
   375  	// Do the per model queries.
   376  	var result []params.ApplicationOfferAdminDetails
   377  	for _, modelUUID := range allUUIDs {
   378  		filters := filtersPerModel[modelUUID]
   379  		offers, err := api.applicationOffersFromModel(modelUUID, requiredPermission, filters...)
   380  		if err != nil {
   381  			return nil, errors.Trace(err)
   382  		}
   383  		model := models[modelUUID]
   384  
   385  		for _, offerDetails := range offers {
   386  			offerDetails.OfferURL = jujucrossmodel.MakeURL(model.Owner().Name(), model.Name(), offerDetails.OfferName, "")
   387  			result = append(result, offerDetails)
   388  		}
   389  	}
   390  	return result, nil
   391  }
   392  
   393  func makeOfferFilterFromParams(filter params.OfferFilter) (jujucrossmodel.ApplicationOfferFilter, error) {
   394  	offerFilter := jujucrossmodel.ApplicationOfferFilter{
   395  		OfferName:              filter.OfferName,
   396  		ApplicationName:        filter.ApplicationName,
   397  		ApplicationDescription: filter.ApplicationDescription,
   398  		Endpoints:              make([]jujucrossmodel.EndpointFilterTerm, len(filter.Endpoints)),
   399  		AllowedConsumers:       make([]string, len(filter.AllowedConsumerTags)),
   400  		ConnectedUsers:         make([]string, len(filter.ConnectedUserTags)),
   401  	}
   402  	for i, ep := range filter.Endpoints {
   403  		offerFilter.Endpoints[i] = jujucrossmodel.EndpointFilterTerm{
   404  			Name:      ep.Name,
   405  			Interface: ep.Interface,
   406  			Role:      ep.Role,
   407  		}
   408  	}
   409  	for i, tag := range filter.AllowedConsumerTags {
   410  		u, err := names.ParseUserTag(tag)
   411  		if err != nil {
   412  			return jujucrossmodel.ApplicationOfferFilter{}, errors.Trace(err)
   413  		}
   414  		offerFilter.AllowedConsumers[i] = u.Id()
   415  	}
   416  	for i, tag := range filter.ConnectedUserTags {
   417  		u, err := names.ParseUserTag(tag)
   418  		if err != nil {
   419  			return jujucrossmodel.ApplicationOfferFilter{}, errors.Trace(err)
   420  		}
   421  		offerFilter.ConnectedUsers[i] = u.Id()
   422  	}
   423  	return offerFilter, nil
   424  }
   425  
   426  func (api *BaseAPI) makeOfferParams(backend Backend, offer *jujucrossmodel.ApplicationOffer) (
   427  	*params.ApplicationOfferDetails, crossmodel.Application, error,
   428  ) {
   429  	app, err := backend.Application(offer.ApplicationName)
   430  	if err != nil {
   431  		return nil, nil, errors.Trace(err)
   432  	}
   433  	appBindings, err := app.EndpointBindings()
   434  	if err != nil {
   435  		return nil, nil, errors.Trace(err)
   436  	}
   437  	result := params.ApplicationOfferDetails{
   438  		SourceModelTag:         backend.ModelTag().String(),
   439  		OfferName:              offer.OfferName,
   440  		OfferUUID:              offer.OfferUUID,
   441  		ApplicationDescription: offer.ApplicationDescription,
   442  	}
   443  
   444  	// CAAS models don't have spaces.
   445  	model, err := backend.Model()
   446  	if err != nil {
   447  		return nil, nil, errors.Trace(err)
   448  	}
   449  
   450  	spaceNames := set.NewStrings()
   451  	for alias, ep := range offer.Endpoints {
   452  		result.Endpoints = append(result.Endpoints, params.RemoteEndpoint{
   453  			Name:      alias,
   454  			Interface: ep.Interface,
   455  			Role:      ep.Role,
   456  		})
   457  		if model.Type() == state.ModelTypeCAAS {
   458  			continue
   459  		}
   460  
   461  		spaceName, ok := appBindings[ep.Name]
   462  		if !ok {
   463  			// There should always be some binding (even if it's to the default space).
   464  			// This isn't currently the case so add the default binding here.
   465  			logger.Warningf("no binding for %q endpoint on application %q", ep.Name, offer.ApplicationName)
   466  			if result.Bindings == nil {
   467  				result.Bindings = make(map[string]string)
   468  			}
   469  			result.Bindings[ep.Name] = environs.DefaultSpaceName
   470  		}
   471  		spaceNames.Add(spaceName)
   472  	}
   473  
   474  	if model.Type() == state.ModelTypeCAAS {
   475  		return &result, app, nil
   476  	}
   477  
   478  	spaces, err := api.collectRemoteSpaces(backend, spaceNames.SortedValues())
   479  	if errors.IsNotSupported(err) {
   480  		// Provider doesn't support ProviderSpaceInfo; continue
   481  		// without any space information, we shouldn't short-circuit
   482  		// cross-model connections.
   483  		return &result, app, nil
   484  	}
   485  	if err != nil {
   486  		return nil, nil, errors.Trace(err)
   487  	}
   488  
   489  	// Ensure bindings only contains entries for which we have spaces.
   490  	for epName, spaceName := range appBindings {
   491  		space, ok := spaces[spaceName]
   492  		if !ok {
   493  			continue
   494  		}
   495  		if result.Bindings == nil {
   496  			result.Bindings = make(map[string]string)
   497  		}
   498  		result.Bindings[epName] = spaceName
   499  		result.Spaces = append(result.Spaces, space)
   500  	}
   501  	return &result, app, nil
   502  }
   503  
   504  // collectRemoteSpaces gets provider information about the spaces from
   505  // the state passed in. (This state will be for a different model than
   506  // this API instance, which is why the results are *remote* spaces.)
   507  // These can be used by the provider later on to decide whether a
   508  // connection can be made via cloud-local addresses. If the provider
   509  // doesn't support getting ProviderSpaceInfo the NotSupported error
   510  // will be returned.
   511  func (api *BaseAPI) collectRemoteSpaces(backend Backend, spaceNames []string) (map[string]params.RemoteSpace, error) {
   512  	env, err := api.getEnviron(backend.ModelUUID())
   513  	if err != nil {
   514  		return nil, errors.Trace(err)
   515  	}
   516  
   517  	netEnv, ok := environs.SupportsNetworking(env)
   518  	if !ok {
   519  		logger.Debugf("cloud provider doesn't support networking, not getting space info")
   520  		return nil, nil
   521  	}
   522  
   523  	results := make(map[string]params.RemoteSpace)
   524  	for _, name := range spaceNames {
   525  		space := environs.DefaultSpaceInfo
   526  		if name != environs.DefaultSpaceName {
   527  			dbSpace, err := backend.Space(name)
   528  			if err != nil {
   529  				return nil, errors.Trace(err)
   530  			}
   531  			space, err = spaceInfoFromState(dbSpace)
   532  			if err != nil {
   533  				return nil, errors.Trace(err)
   534  			}
   535  		}
   536  		providerSpace, err := netEnv.ProviderSpaceInfo(api.callContext, space)
   537  		if err != nil && !errors.IsNotFound(err) {
   538  			return nil, errors.Trace(err)
   539  		}
   540  		if providerSpace == nil {
   541  			logger.Warningf("no provider space info for %q", name)
   542  			continue
   543  		}
   544  		remoteSpace := paramsFromProviderSpaceInfo(providerSpace)
   545  		// Use the name from state in case provider and state disagree.
   546  		remoteSpace.Name = name
   547  		results[name] = remoteSpace
   548  	}
   549  	return results, nil
   550  }