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 }