88
94
reqHandlerCacheSize = 50
91
func New(pool *charmstore.Pool, config charmstore.ServerParams) *Handler {
97
func New(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) *Handler {
95
102
searchCache: cache.New(config.SearchCacheMaxAge),
96
103
locator: config.PublicKeyLocator,
97
identityClient: identity.NewClient(&identity.Params{
98
URL: config.IdentityAPIURL,
99
Client: agent.NewClient(config.AgentUsername, config.AgentKey),
104
identityClient: idmclient.New(idmclient.NewParams{
105
BaseURL: config.IdentityAPIURL,
106
Client: agent.NewClient(config.AgentUsername, config.AgentKey),
106
113
func (h *Handler) Close() {
117
RequiredEntityFields = charmstore.FieldSelector(
123
"promulgated-revision",
128
RequiredBaseEntityFields = charmstore.FieldSelector(
137
// StoreWithChannel associates a Store with a channel that will be used
138
// to resolve any channel-ambiguous requests.
139
type StoreWithChannel struct {
141
Channel params.Channel
144
func (s *StoreWithChannel) FindBestEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
145
return s.Store.FindBestEntity(url, s.Channel, fields)
148
func (s *StoreWithChannel) FindBaseEntity(url *charm.URL, fields map[string]int) (*mongodoc.BaseEntity, error) {
149
return s.Store.FindBaseEntity(url, fields)
152
// ValidChannels holds the set of all allowed channels
153
// that can be passed as a "?channel=" parameter.
154
var ValidChannels = map[params.Channel]bool{
155
params.UnpublishedChannel: true,
156
params.DevelopmentChannel: true,
157
params.StableChannel: true,
109
160
// NewReqHandler returns an instance of a *ReqHandler
110
// suitable for handling an HTTP request. After use, the ReqHandler.Close
161
// suitable for handling the given HTTP request. After use, the ReqHandler.Close
111
162
// method should be called to close it.
113
164
// If no handlers are available, it returns an error with
114
165
// a charmstore.ErrTooManySessions cause.
115
func (h *Handler) NewReqHandler() (*ReqHandler, error) {
166
func (h *Handler) NewReqHandler(req *http.Request) (*ReqHandler, error) {
168
// Validate all the values for channel, even though
169
// most endpoints will only ever use the first one.
170
// PUT to an archive is the notable exception.
171
for _, ch := range req.Form["channel"] {
172
if !ValidChannels[params.Channel(ch)] {
173
return nil, badRequestf(nil, "invalid channel %q specified in request", ch)
116
176
store, err := h.Pool.RequestStore()
118
178
if errgo.Cause(err) == charmstore.ErrTooManySessions {
154
221
Id: map[string]router.IdHandler{
155
222
"archive": h.serveArchive,
156
"archive/": resolveId(authId(h.serveArchiveFile)),
157
"diagram.svg": resolveId(authId(h.serveDiagram)),
223
"archive/": resolveId(authId(h.serveArchiveFile), "blobname", "blobhash"),
224
"diagram.svg": resolveId(authId(h.serveDiagram), "bundledata"),
158
225
"expand-id": resolveId(authId(h.serveExpandId)),
159
"icon.svg": resolveId(authId(h.serveIcon)),
226
"icon.svg": resolveId(authId(h.serveIcon), "contents", "blobname"),
227
"publish": resolveId(h.servePublish),
160
228
"promulgate": resolveId(h.serveAdminPromulgate),
161
"publish": h.servePublish,
162
"readme": resolveId(authId(h.serveReadMe)),
229
"readme": resolveId(authId(h.serveReadMe), "contents", "blobname"),
163
230
"resources": resolveId(authId(h.serveResources)),
165
232
Meta: map[string]router.BulkIncludeHandler{
169
236
"bundle-metadata": h.EntityHandler(h.metaBundleMetadata, "bundledata"),
170
237
"bundles-containing": h.EntityHandler(h.metaBundlesContaining),
171
238
"bundle-unit-count": h.EntityHandler(h.metaBundleUnitCount, "bundleunitcount"),
239
"published": h.EntityHandler(h.metaPublished, "development", "stable"),
172
240
"charm-actions": h.EntityHandler(h.metaCharmActions, "charmactions"),
173
241
"charm-config": h.EntityHandler(h.metaCharmConfig, "charmconfig"),
174
242
"charm-metadata": h.EntityHandler(h.metaCharmMetadata, "charmmeta"),
175
243
"charm-related": h.EntityHandler(h.metaCharmRelated, "charmprovidedinterfaces", "charmrequiredinterfaces"),
244
"common-info": h.puttableBaseEntityHandler(
249
"common-info/": h.puttableBaseEntityHandler(
250
h.metaCommonInfoWithKey,
251
h.putMetaCommonInfoWithKey,
176
254
"extra-info": h.puttableEntityHandler(
178
256
h.putMetaExtraInfo,
201
269
"id-revision": h.EntityHandler(h.metaIdRevision, "_id"),
202
270
"id-series": h.EntityHandler(h.metaIdSeries, "_id"),
203
271
"manifest": h.EntityHandler(h.metaManifest, "blobname"),
204
"perm": h.puttableBaseEntityHandler(h.metaPerm, h.putMetaPerm, "acls", "developmentacls"),
205
"perm/": h.puttableBaseEntityHandler(h.metaPermWithKey, h.putMetaPermWithKey, "acls", "developmentacls"),
272
"perm": h.puttableBaseEntityHandler(h.metaPerm, h.putMetaPerm, "channelacls"),
273
"perm/": h.puttableBaseEntityHandler(h.metaPermWithKey, h.putMetaPermWithKey, "channelacls"),
206
274
"promulgated": h.baseEntityHandler(h.metaPromulgated, "promulgated"),
275
"resources": h.EntityHandler(h.metaResources, "charmmeta"),
207
276
"revision-info": router.SingleIncludeHandler(h.metaRevisionInfo),
208
277
"stats": h.EntityHandler(h.metaStats),
209
278
"supported-series": h.EntityHandler(h.metaSupportedSeries, "supportedseries"),
210
279
"tags": h.EntityHandler(h.metaTags, "charmmeta", "bundledata"),
280
"terms": h.EntityHandler(h.metaTerms, "charmmeta"),
212
282
// endpoints not yet implemented:
213
283
// "color": router.SingleIncludeHandler(h.metaColor),
227
297
// request-specific instance of ReqHandler and
228
298
// calling ServeHTTP on that.
229
299
func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
230
// When requests in this handler use router.RelativeURL, we want
231
// the "absolute path" there to be interpreted relative to the
232
// root of this handler, not the absolute root of the web server,
233
// which may be abitrarily many levels up.
234
req.RequestURI = req.URL.Path
236
rh, err := h.NewReqHandler()
300
rh, err := h.NewReqHandler(req)
238
302
router.WriteError(w, err)
250
314
// NewAPIHandler returns a new Handler as an http Handler.
251
315
// It is defined for the convenience of callers that require a
252
316
// charmstore.NewAPIHandlerFunc.
253
func NewAPIHandler(pool *charmstore.Pool, config charmstore.ServerParams) charmstore.HTTPCloseHandler {
254
return New(pool, config)
317
func NewAPIHandler(pool *charmstore.Pool, config charmstore.ServerParams, rootPath string) charmstore.HTTPCloseHandler {
318
return New(pool, config, rootPath)
257
321
// Close closes the ReqHandler. This should always be called when the
258
322
// ReqHandler is done with.
259
323
func (h *ReqHandler) Close() {
262
327
reqHandlerPool.Put(h)
267
332
func (h *ReqHandler) Reset() {
270
336
h.auth = authorization{}
273
339
// ResolveURL implements router.Context.ResolveURL.
274
func (h ReqHandler) ResolveURL(url *charm.URL) (*router.ResolvedURL, error) {
275
return resolveURL(h.Store, url)
340
func (h *ReqHandler) ResolveURL(url *charm.URL) (*router.ResolvedURL, error) {
341
return resolveURL(h.Cache, url)
344
// ResolveURL implements router.Context.ResolveURLs.
345
func (h *ReqHandler) ResolveURLs(urls []*charm.URL) ([]*router.ResolvedURL, error) {
346
h.Cache.StartFetch(urls)
347
rurls := make([]*router.ResolvedURL, len(urls))
348
for i, url := range urls {
350
rurls[i], err = resolveURL(h.Cache, url)
351
if err != nil && errgo.Cause(err) != params.ErrNotFound {
358
// WillIncludeMetadata implements router.Context.WillIncludeMetadata.
359
func (h *ReqHandler) WillIncludeMetadata(includes []string) {
360
for _, inc := range includes {
361
// Find what handler will be used for the include
362
// and prime the cache so that it will preemptively fetch
363
// any fields involved.
364
fi, ok := h.Router.MetaHandler(inc).(*router.FieldIncludeHandler)
365
if !ok || len(fi.P.Fields) == 0 {
368
fields := make(map[string]int)
369
for _, f := range fi.P.Fields {
372
switch fi.P.Key.(type) {
373
case entityHandlerKey:
374
h.Cache.AddEntityFields(fields)
375
case baseEntityHandlerKey:
376
h.Cache.AddBaseEntityFields(fields)
278
381
// resolveURL implements URL resolving for the ReqHandler.
279
382
// It's defined as a separate function so it can be more
280
383
// easily unit-tested.
281
func resolveURL(store *charmstore.Store, url *charm.URL) (*router.ResolvedURL, error) {
282
entity, err := store.FindBestEntity(url, "_id", "promulgated-revision")
283
if err != nil && errgo.Cause(err) != params.ErrNotFound {
284
return nil, errgo.Mask(err)
286
if errgo.Cause(err) == params.ErrNotFound {
287
return nil, noMatchingURLError(url)
384
func resolveURL(cache *entitycache.Cache, url *charm.URL) (*router.ResolvedURL, error) {
385
// We've added promulgated-url as a required field, so
386
// we'll always get it from the Entity result.
387
entity, err := cache.Entity(url, nil)
389
return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
289
391
rurl := &router.ResolvedURL{
290
392
URL: *entity.URL,
291
393
PromulgatedRevision: -1,
292
Development: url.Channel == charm.DevelopmentChannel,
294
395
if url.User == "" {
295
396
rurl.PromulgatedRevision = entity.PromulgatedRevision
398
// Ensure the base URL is in the cache too, so that
399
// its canonical URL is in the cache, so that when
400
// we come to look up the base URL from the resolved
401
// URL, it will hit the cached base entity.
402
// We don't actually care if it succeeds or fails, so we ignore
404
cache.BaseEntity(entity.BaseURL, nil)
300
func noMatchingURLError(url *charm.URL) error {
301
return errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %q", url)
304
408
type EntityHandlerFunc func(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error)
306
410
type baseEntityHandlerFunc func(entity *mongodoc.BaseEntity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error)
311
415
return h.puttableEntityHandler(f, nil, fields...)
418
type entityHandlerKey struct{}
314
420
func (h *ReqHandler) puttableEntityHandler(get EntityHandlerFunc, handlePut router.FieldPutFunc, fields ...string) router.BulkIncludeHandler {
315
421
handleGet := func(doc interface{}, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
316
422
edoc := doc.(*mongodoc.Entity)
317
423
val, err := get(edoc, id, path, flags, req)
318
424
return val, errgo.Mask(err, errgo.Any)
320
type entityHandlerKey struct{}
321
return router.FieldIncludeHandler(router.FieldIncludeHandlerParams{
426
return router.NewFieldIncludeHandler(router.FieldIncludeHandlerParams{
322
427
Key: entityHandlerKey{},
323
428
Query: h.entityQuery,
335
440
return h.puttableBaseEntityHandler(f, nil, fields...)
443
type baseEntityHandlerKey struct{}
338
445
func (h *ReqHandler) puttableBaseEntityHandler(get baseEntityHandlerFunc, handlePut router.FieldPutFunc, fields ...string) router.BulkIncludeHandler {
339
446
handleGet := func(doc interface{}, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
340
447
edoc := doc.(*mongodoc.BaseEntity)
341
448
val, err := get(edoc, id, path, flags, req)
342
449
return val, errgo.Mask(err, errgo.Any)
344
type baseEntityHandlerKey struct{}
345
return router.FieldIncludeHandler(router.FieldIncludeHandlerParams{
451
return router.NewFieldIncludeHandler(router.FieldIncludeHandlerParams{
346
452
Key: baseEntityHandlerKey{},
347
453
Query: h.baseEntityQuery,
421
func (h *ReqHandler) baseEntityQuery(id *router.ResolvedURL, selector map[string]int, req *http.Request) (interface{}, error) {
422
fields := make([]string, 0, len(selector))
423
for k, v := range selector {
427
fields = append(fields, k)
429
val, err := h.Store.FindBaseEntity(&id.URL, fields...)
527
func (h *ReqHandler) baseEntityQuery(id *router.ResolvedURL, fields map[string]int, req *http.Request) (interface{}, error) {
528
val, err := h.Cache.BaseEntity(&id.URL, fields)
430
529
if errgo.Cause(err) == params.ErrNotFound {
431
530
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", id)
439
538
func (h *ReqHandler) entityQuery(id *router.ResolvedURL, selector map[string]int, req *http.Request) (interface{}, error) {
440
val, err := h.Store.FindEntity(id, fieldsFromSelector(selector)...)
539
val, err := h.Cache.Entity(&id.URL, selector)
441
540
if errgo.Cause(err) == params.ErrNotFound {
541
logger.Infof("entity %#v not found: %#v", id, err)
442
542
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", id)
466
555
router.WriteError(w, errNotImplemented)
469
// POST id/resources/name.stream
470
// https://github.com/juju/charmstore/blob/v4/docs/API.md#post-idresourcesnamestream
472
// GET id/resources/name.stream[-revision]/arch/filename
473
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idresourcesnamestream-revisionarchfilename
475
// PUT id/resources/[~user/]series/name.stream-revision/arch?sha256=hash
476
// https://github.com/juju/charmstore/blob/v4/docs/API.md#put-idresourcesuserseriesnamestream-revisionarchsha256hash
477
func (h *ReqHandler) serveResources(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
478
return errNotImplemented
481
558
// GET id/expand-id
482
559
// https://docs.google.com/a/canonical.com/document/d/1TgRA7jW_mmXoKH3JiwBbtPvQu7WiM6XMrz1wSrhTMXw/edit#bookmark=id.4xdnvxphb2si
483
560
func (h *ReqHandler) serveExpandId(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
491
568
// to return entities that match appropriately.
493
570
// Retrieve all the entities with the same base URL.
494
// Note that we don't do any permission checking of the returned URLs.
495
// This is because we know that the user is allowed to read at
496
// least the resolved URL passed into serveExpandId.
497
// If this does not specify "development", then no development
498
// revisions will be chosen, so the single ACL already checked
499
// is sufficient. If it *does* specify "development", then we assume
500
// that the development ACLs are more restrictive than the
501
// non-development ACLs, and given that, we can allow all
503
q := h.Store.EntitiesQuery(baseURL).Select(bson.D{{"_id", 1}, {"promulgated-url", 1}, {"development", 1}})
571
q := h.Store.EntitiesQuery(baseURL).Select(bson.D{{"_id", 1}, {"promulgated-url", 1}})
504
572
if id.PromulgatedRevision != -1 {
505
573
q = q.Sort("-series", "-promulgated-revision")
601
672
return entity.CharmConfig, nil
676
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetaterms
677
func (h *ReqHandler) metaTerms(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
678
if entity.URL.Series == "bundle" {
681
if entity.CharmMeta == nil || len(entity.CharmMeta.Terms) == 0 {
682
return []string{}, nil
684
return entity.CharmMeta.Terms, nil
604
687
// GET id/meta/color
605
688
func (h *ReqHandler) metaColor(id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
606
689
return nil, errNotImplemented
625
708
// GET id/meta/hash256
626
709
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetahash256
627
710
func (h *ReqHandler) metaHash256(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
628
// TODO frankban: remove this lazy calculation after the cshash256
629
// command is run in the production db. At that point, entities
630
// always have their blobhash256 field populated, and there is no
631
// need for this lazy evaluation anymore.
632
if entity.BlobHash256 == "" {
634
if entity.BlobHash256, err = h.Store.UpdateEntitySHA256(id); err != nil {
635
return nil, errgo.Notef(err, "cannot retrieve the SHA256 hash for entity %s", entity.URL)
638
711
return ¶ms.HashResponse{
639
712
Sum: entity.BlobHash256,
702
775
q = q.Sort("-revision")
704
var docs []*mongodoc.Entity
705
if err := q.Select(bson.D{{"_id", 1}, {"promulgated-url", 1}}).All(&docs); err != nil {
706
return "", errgo.Notef(err, "cannot get ids")
710
return "", errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", id)
712
777
var response params.RevisionInfoResponse
713
for _, doc := range docs {
778
iter := h.Cache.Iter(q, nil)
781
rurl := charmstore.EntityResolvedURL(e)
782
if err := h.AuthorizeEntity(rurl, req); err != nil {
783
// We're not authorized to see the entity, so leave it out.
784
// Note that the only time this will happen is when
785
// the original URL is promulgated and has a development channel,
786
// the charm has changed owners, and the old owner and
787
// the new one have different dev ACLs. It's easiest
788
// and most reliable just to check everything though.
714
791
if id.PromulgatedRevision != -1 {
715
response.Revisions = append(response.Revisions, doc.PromulgatedURL)
792
response.Revisions = append(response.Revisions, rurl.PromulgatedURL())
717
response.Revisions = append(response.Revisions, doc.URL)
794
response.Revisions = append(response.Revisions, &rurl.URL)
721
// Write the response in JSON format.
797
if err := iter.Err(); err != nil {
798
return nil, errgo.Notef(err, "iteration failed")
722
800
return &response, nil
937
1016
if err := json.Unmarshal(*val, &perms); err != nil {
938
1017
return errgo.Mask(err)
942
field = "developmentacls"
945
for _, p := range perms.Read {
946
if p == params.Everyone {
951
updater.UpdateField("public", isPublic, nil)
1019
ch, err := h.entityChannel(id)
1021
return errgo.Mask(err)
953
updater.UpdateField(field+".read", perms.Read, &audit.Entry{
1023
// TODO use only one UpdateField operation?
1024
updater.UpdateField(string("channelacls."+ch+".read"), perms.Read, &audit.Entry{
954
1025
Op: audit.OpSetPerm,
955
1026
Entity: &id.URL,
956
1027
ACL: &audit.ACL{
957
1028
Read: perms.Read,
960
updater.UpdateField(field+".write", perms.Write, &audit.Entry{
1031
updater.UpdateField(string("channelacls."+ch+".write"), perms.Write, &audit.Entry{
961
1032
Op: audit.OpSetPerm,
962
1033
Entity: &id.URL,
963
1034
ACL: &audit.ACL{
995
1067
// PUT id/meta/perm/key
996
1068
// https://github.com/juju/charmstore/blob/v4/docs/API.md#put-idmetapermkey
997
1069
func (h *ReqHandler) putMetaPermWithKey(id *router.ResolvedURL, path string, val *json.RawMessage, updater *router.FieldUpdater, req *http.Request) error {
1070
ch, err := h.entityChannel(id)
1072
return errgo.Mask(err)
998
1074
var perms []string
999
1075
if err := json.Unmarshal(*val, &perms); err != nil {
1000
1076
return errgo.Mask(err)
1004
field = "developmentacls"
1008
updater.UpdateField(field+".read", perms, &audit.Entry{
1080
updater.UpdateField(string("channelacls."+ch+".read"), perms, &audit.Entry{
1009
1081
Op: audit.OpSetPerm,
1010
1082
Entity: &id.URL,
1011
1083
ACL: &audit.ACL{
1015
if !id.Development {
1017
for _, p := range perms {
1018
if p == params.Everyone {
1023
updater.UpdateField("public", isPublic, nil)
1025
1087
updater.UpdateSearch()
1028
updater.UpdateField(field+".write", perms, &audit.Entry{
1090
updater.UpdateField(string("channelacls."+ch+".write"), perms, &audit.Entry{
1029
1091
Op: audit.OpSetPerm,
1030
1092
Entity: &id.URL,
1031
1093
ACL: &audit.ACL{
1037
1099
return errgo.WithCausef(nil, params.ErrNotFound, "unknown permission")
1102
// GET id/meta/published
1103
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetapublished
1104
func (h *ReqHandler) metaPublished(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
1105
baseEntity, err := h.Cache.BaseEntity(entity.URL, charmstore.FieldSelector("channelentities"))
1107
return nil, errgo.Mask(err)
1109
info := make([]params.PublishedInfo, 0, 2)
1110
if entity.Development {
1111
info = append(info, params.PublishedInfo{
1112
Channel: params.DevelopmentChannel,
1116
info = append(info, params.PublishedInfo{
1117
Channel: params.StableChannel,
1120
for i, pinfo := range info {
1121
// The entity is current for a channel if any series within
1122
// a channel refers to the entity.
1123
for _, url := range baseEntity.ChannelEntities[pinfo.Channel] {
1124
if *url == *entity.URL {
1125
info[i].Current = true
1129
return ¶ms.PublishedResponse{
1040
1134
// GET id/meta/archive-upload-time
1041
1135
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idmetaarchive-upload-time
1042
1136
func (h *ReqHandler) metaArchiveUploadTime(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
1080
1174
query := h.Store.DB.Entities().
1081
1175
Find(findQuery).
1082
Sort("-uploadtime").
1083
Select(bson.D{{"_id", 1}, {"uploadtime", 1}})
1177
iter := h.Cache.Iter(query, charmstore.FieldSelector("uploadtime"))
1085
1179
results := []params.Published{}
1087
var entity mongodoc.Entity
1088
iter := query.Iter()
1089
for iter.Next(&entity) {
1182
entity := iter.Entity()
1090
1183
// Ignore entities that aren't readable by the current user.
1091
if err := h.AuthorizeEntity(charmstore.EntityResolvedURL(&entity), r); err != nil {
1184
if err := h.AuthorizeEntity(charmstore.EntityResolvedURL(entity), r); err != nil {
1094
1187
results = append(results, params.Published{
1227
1323
if promulgate.Promulgated {
1228
// Set write permissions for the non-development entity to promulgators
1229
// only, so that the user cannot just publish newer promulgated
1230
// versions of the charm or bundle. Promulgators are responsible of
1231
// reviewing and publishing subsequent revisions of this entity.
1324
// Set write permissions to promulgators only, so that
1325
// the user cannot just publish newer promulgated
1326
// versions of the charm or bundle. Promulgators are
1327
// responsible of reviewing and publishing subsequent
1328
// revisions of this entity.
1232
1329
if err := h.updateBaseEntity(id, map[string]interface{}{
1233
"acls.write": []string{PromulgatorsGroup},
1330
"channelacls.stable.write": []string{PromulgatorsGroup},
1234
1331
}, nil); err != nil {
1235
1332
return errgo.Notef(err, "cannot set permissions for %q", id)
1350
// validPublishChannels holds the set of channels that can
1351
// be the target of a publish request.
1352
var validPublishChannels = map[params.Channel]bool{
1353
params.DevelopmentChannel: true,
1354
params.StableChannel: true,
1253
1357
// PUT id/publish
1254
1358
// See https://github.com/juju/charmstore/blob/v4/docs/API.md#put-idpublish
1255
func (h *ReqHandler) servePublish(id *charm.URL, w http.ResponseWriter, req *http.Request) error {
1359
func (h *ReqHandler) servePublish(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
1256
1360
// Perform basic validation of the request.
1257
1361
if req.Method != "PUT" {
1258
1362
return errgo.WithCausef(nil, params.ErrMethodNotAllowed, "%s not allowed", req.Method)
1260
if id.Channel != "" {
1261
return errgo.WithCausef(nil, params.ErrForbidden, "can only set publish on published URL, %q provided", id)
1264
1365
// Retrieve the requested action from the request body.
1265
1366
var publish struct {
1266
1367
params.PublishRequest `httprequest:",body"`
1268
1369
if err := httprequest.Unmarshal(httprequest.Params{Request: req}, &publish); err != nil {
1269
return errgo.WithCausef(err, params.ErrBadRequest, "cannot unmarshal publish request body")
1370
return badRequestf(err, "cannot unmarshal publish request body")
1372
chans := publish.Channels
1373
if len(chans) == 0 {
1374
return badRequestf(nil, "no channels provided")
1376
for _, c := range chans {
1377
if !validPublishChannels[c] {
1378
return badRequestf(nil, "cannot publish to %q", c)
1272
// Retrieve the resolved URL for the entity to update. It will be referring
1273
// to the entity under development is the action is to publish a charm or
1274
// bundle, or the published one otherwise.
1276
if publish.Published {
1277
url = *id.WithChannel(charm.DevelopmentChannel)
1279
rurl, err := h.Router.Context.ResolveURL(&url)
1382
// Retrieve the base entity so that we can check permissions.
1383
baseEntity, err := h.Cache.BaseEntity(&id.URL, charmstore.FieldSelector("channelacls"))
1280
1384
if err != nil {
1281
1385
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
1284
// Authorize the operation: users must have write permissions on the
1285
// published charm or bundle.
1287
prurl.Development = false
1288
if err := h.AuthorizeEntity(&prurl, req); err != nil {
1289
return errgo.Mask(err, errgo.Any)
1292
// Update the entity.
1293
if err := h.Store.SetDevelopment(rurl, !publish.Published); err != nil {
1294
return errgo.NoteMask(err, "cannot publish or unpublish charm or bundle", errgo.Is(params.ErrNotFound))
1297
// Return information on the updated charm or bundle.
1298
rurl.Development = !publish.Published
1299
return httprequest.WriteJSON(w, http.StatusOK, ¶ms.PublishResponse{
1300
Id: rurl.UserOwnedURL(),
1301
PromulgatedId: rurl.PromulgatedURL(),
1388
// Authorize the operation. Users must have write permissions on the ACLs
1389
// on the channel being published to.
1390
for _, c := range chans {
1391
if _, err := h.authorize(req, baseEntity.ChannelACLs[c].Write, true, id); err != nil {
1392
return errgo.Mask(err, errgo.Any)
1396
// TODO(ericsnow) Actually handle the resources.
1397
if len(publish.Resources) > 0 {
1398
return errNotImplemented
1401
if err := h.Store.Publish(id, chans...); err != nil {
1402
return errgo.NoteMask(err, "cannot publish charm or bundle", errgo.Is(params.ErrNotFound))
1404
// TODO add publish audit
1305
1408
// serveSetAuthCookie sets the provided macaroon slice as a cookie on the
1351
1454
// ResolvedIdHandler returns an id handler that uses h.Router.Context.ResolveURL
1352
1455
// to resolves any entity ids before calling f with the resolved id.
1457
// Any specified fields will be added to the fields required by the cache, so
1458
// they will be pre-fetched by ResolveURL.
1354
1460
// Note that it only accesses h.Router.Context when the returned
1355
1461
// handler is called.
1356
func (h *ReqHandler) ResolvedIdHandler(f ResolvedIdHandler) router.IdHandler {
1462
func (h *ReqHandler) ResolvedIdHandler(f ResolvedIdHandler, cacheFields ...string) router.IdHandler {
1463
fields := charmstore.FieldSelector(cacheFields...)
1357
1464
return func(id *charm.URL, w http.ResponseWriter, req *http.Request) error {
1465
h.Cache.AddEntityFields(fields)
1358
1466
rid, err := h.Router.Context.ResolveURL(id)
1359
1467
if err != nil {
1360
1468
return errgo.Mask(err, errgo.Is(params.ErrNotFound))