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 }