Skip to content

Commit 680d722

Browse files
feat(userpreferences): update allowed methods header for user preferences routes
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
1 parent 449f0d0 commit 680d722

8 files changed

Lines changed: 199 additions & 16 deletions

File tree

pkg/api/authn.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
4949
return func(next http.Handler) http.Handler {
5050
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
5151
if request.Method == http.MethodOptions {
52+
next.ServeHTTP(response, request)
5253
response.WriteHeader(http.StatusNoContent)
5354

5455
return
@@ -95,6 +96,7 @@ func noPasswdAuth(realm string, config *config.Config) mux.MiddlewareFunc {
9596
return func(next http.Handler) http.Handler {
9697
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
9798
if request.Method == http.MethodOptions {
99+
next.ServeHTTP(response, request)
98100
response.WriteHeader(http.StatusNoContent)
99101

100102
return
@@ -193,6 +195,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
193195
return func(next http.Handler) http.Handler {
194196
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
195197
if request.Method == http.MethodOptions {
198+
next.ServeHTTP(response, request)
196199
response.WriteHeader(http.StatusNoContent)
197200

198201
return

pkg/api/authz.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
228228
resource := vars["name"]
229229
reference, ok := vars["reference"]
230230

231+
if request.Method == http.MethodOptions {
232+
next.ServeHTTP(response, request)
233+
234+
return
235+
}
236+
231237
// bypass authz for /v2/ route
232238
if request.RequestURI == "/v2/" {
233239
next.ServeHTTP(response, request)

pkg/api/controller.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@ func (c *Controller) CORSHandler(response http.ResponseWriter, request *http.Req
8585
} else {
8686
response.Header().Set("Access-Control-Allow-Origin", c.Config.HTTP.AllowOrigin)
8787
}
88-
89-
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
90-
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
9188
}
9289

9390
func DumpRuntimeParams(log log.Logger) {

pkg/api/controller_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ func TestHtpasswdSingleCred(t *testing.T) {
483483
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
484484
So(len(resp.Header()), ShouldEqual, 4)
485485
So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
486+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
486487

487488
// with invalid creds, it should fail
488489
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
@@ -493,6 +494,89 @@ func TestHtpasswdSingleCred(t *testing.T) {
493494
})
494495
}
495496

497+
func TestAllowMethodsHeader(t *testing.T) {
498+
Convey("Options request", t, func() {
499+
dir := t.TempDir()
500+
port := test.GetFreePort()
501+
baseURL := test.GetBaseURL(port)
502+
conf := config.New()
503+
conf.HTTP.Port = port
504+
conf.Storage.RootDirectory = dir
505+
506+
simpleUser := "simpleUser"
507+
simpleUserPassword := "simpleUserPass"
508+
credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))
509+
510+
htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
511+
defer os.Remove(htpasswdPath)
512+
513+
conf.HTTP.Auth = &config.AuthConfig{
514+
HTPasswd: config.AuthHTPasswd{
515+
Path: htpasswdPath,
516+
},
517+
}
518+
519+
conf.HTTP.AccessControl = &config.AccessControlConfig{
520+
Repositories: config.Repositories{
521+
"**": config.PolicyGroup{
522+
Policies: []config.Policy{
523+
{
524+
Users: []string{simpleUser},
525+
Actions: []string{"read", "create"},
526+
},
527+
},
528+
},
529+
},
530+
}
531+
532+
defaultVal := true
533+
conf.Extensions = &extconf.ExtensionConfig{
534+
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
535+
}
536+
537+
ctlr := api.NewController(conf)
538+
539+
ctlrManager := test.NewControllerManager(ctlr)
540+
ctlrManager.StartAndWait(port)
541+
defer ctlrManager.StopServer()
542+
543+
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
544+
545+
digest := godigest.FromString("digest")
546+
547+
// /v2
548+
resp, err := simpleUserClient.Options(baseURL + "/v2/")
549+
So(err, ShouldBeNil)
550+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
551+
552+
// /v2/{name}/tags/list
553+
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/tags/list")
554+
So(err, ShouldBeNil)
555+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
556+
557+
// /v2/{name}/manifests/{reference}
558+
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
559+
So(err, ShouldBeNil)
560+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
561+
562+
// /v2/{name}/referrers/{digest}
563+
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())
564+
So(err, ShouldBeNil)
565+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
566+
567+
// /v2/_catalog
568+
resp, err = simpleUserClient.Options(baseURL + "/v2/_catalog")
569+
So(err, ShouldBeNil)
570+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
571+
572+
// /v2/_oci/ext/discover
573+
resp, err = simpleUserClient.Options(baseURL + "/v2/_oci/ext/discover")
574+
So(err, ShouldBeNil)
575+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")
576+
577+
})
578+
}
579+
496580
func TestHtpasswdTwoCreds(t *testing.T) {
497581
Convey("Two creds", t, func() {
498582
twoCredTests := []string{}

pkg/api/routes.go

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
zerr "zotregistry.io/zot/errors"
3232
"zotregistry.io/zot/pkg/api/constants"
33+
zcommon "zotregistry.io/zot/pkg/common"
3334
gqlPlayground "zotregistry.io/zot/pkg/debug/gqlplayground"
3435
debug "zotregistry.io/zot/pkg/debug/swagger"
3536
ext "zotregistry.io/zot/pkg/extensions"
@@ -53,10 +54,6 @@ func NewRouteHandler(c *Controller) *RouteHandler {
5354
return rh
5455
}
5556

56-
func allowedMethods(method string) []string {
57-
return []string{http.MethodOptions, method}
58-
}
59-
6057
func (rh *RouteHandler) SetupRoutes() {
6158
prefixedRouter := rh.c.Router.PathPrefix(constants.RoutePrefix).Subrouter()
6259
prefixedRouter.Use(AuthHandler(rh.c))
@@ -75,11 +72,11 @@ func (rh *RouteHandler) SetupRoutes() {
7572
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
7673
{
7774
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
78-
rh.ListTags).Methods(allowedMethods("GET")...)
75+
rh.ListTags).Methods(zcommon.AllowedMethods("GET")...)
7976
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
80-
rh.CheckManifest).Methods(allowedMethods("HEAD")...)
77+
rh.CheckManifest).Methods(zcommon.AllowedMethods("HEAD")...)
8178
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
82-
rh.GetManifest).Methods(allowedMethods("GET")...)
79+
rh.GetManifest).Methods(zcommon.AllowedMethods("GET")...)
8380
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
8481
rh.UpdateManifest).Methods("PUT")
8582
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
@@ -102,13 +99,13 @@ func (rh *RouteHandler) SetupRoutes() {
10299
rh.DeleteBlobUpload).Methods("DELETE")
103100
// support for OCI artifact references
104101
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", zreg.NameRegexp.String()),
105-
rh.GetReferrers).Methods(allowedMethods("GET")...)
102+
rh.GetReferrers).Methods(zcommon.AllowedMethods("GET")...)
106103
prefixedRouter.HandleFunc(constants.ExtCatalogPrefix,
107-
rh.ListRepositories).Methods(allowedMethods("GET")...)
104+
rh.ListRepositories).Methods(zcommon.AllowedMethods("GET")...)
108105
prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix,
109-
rh.ListExtensions).Methods(allowedMethods("GET")...)
106+
rh.ListExtensions).Methods(zcommon.AllowedMethods("GET")...)
110107
prefixedRouter.HandleFunc("/",
111-
rh.CheckVersionSupport).Methods(allowedMethods("GET")...)
108+
rh.CheckVersionSupport).Methods(zcommon.AllowedMethods("GET")...)
112109
}
113110

114111
// support for ORAS artifact reference types (alpha 1) - image signature use case
@@ -146,6 +143,13 @@ func (rh *RouteHandler) SetupRoutes() {
146143
// @Produce json
147144
// @Success 200 {string} string "ok".
148145
func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, request *http.Request) {
146+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
147+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
148+
149+
if request.Method == http.MethodOptions {
150+
return
151+
}
152+
149153
response.Header().Set(constants.DistAPIVersion, "registry/2.0")
150154
// NOTE: compatibility workaround - return this header in "allowed-read" mode to allow for clients to
151155
// work correctly
@@ -178,6 +182,13 @@ type ImageTags struct {
178182
// @Failure 404 {string} string "not found"
179183
// @Failure 400 {string} string "bad request".
180184
func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Request) {
185+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
186+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
187+
188+
if request.Method == http.MethodOptions {
189+
return
190+
}
191+
181192
vars := mux.Vars(request)
182193

183194
name, ok := vars["name"]
@@ -301,6 +312,13 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req
301312
// @Failure 404 {string} string "not found"
302313
// @Failure 500 {string} string "internal server error".
303314
func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *http.Request) {
315+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
316+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
317+
318+
if request.Method == http.MethodOptions {
319+
return
320+
}
321+
304322
vars := mux.Vars(request)
305323
name, ok := vars["name"]
306324

@@ -367,6 +385,13 @@ type ExtensionList struct {
367385
// @Failure 500 {string} string "internal server error"
368386
// @Router /v2/{name}/manifests/{reference} [get].
369387
func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.Request) {
388+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
389+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
390+
391+
if request.Method == http.MethodOptions {
392+
return
393+
}
394+
370395
vars := mux.Vars(request)
371396
name, ok := vars["name"]
372397

@@ -468,6 +493,13 @@ func getReferrers(ctx context.Context, routeHandler *RouteHandler,
468493
// @Failure 500 {string} string "internal server error"
469494
// @Router /v2/{name}/references/{digest} [get].
470495
func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) {
496+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
497+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
498+
499+
if request.Method == http.MethodOptions {
500+
return
501+
}
502+
471503
vars := mux.Vars(request)
472504

473505
name, ok := vars["name"]
@@ -1501,6 +1533,13 @@ type RepositoryList struct {
15011533
// @Failure 500 {string} string "internal server error"
15021534
// @Router /v2/_catalog [get].
15031535
func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *http.Request) {
1536+
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
1537+
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
1538+
1539+
if request.Method == http.MethodOptions {
1540+
return
1541+
}
1542+
15041543
combineRepoList := make([]string, 0)
15051544

15061545
subStore := rh.c.StoreController.SubStore
@@ -1560,6 +1599,13 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
15601599
// @Success 200 {object} api.ExtensionList
15611600
// @Router /v2/_oci/ext/discover [get].
15621601
func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) {
1602+
w.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
1603+
w.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
1604+
1605+
if r.Method == http.MethodOptions {
1606+
return
1607+
}
1608+
15631609
extensionList := ext.GetExtensions(rh.c.Config)
15641610

15651611
WriteJSON(w, http.StatusOK, extensionList)

pkg/common/common.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const (
3030
caCertFilename = "ca.crt"
3131
)
3232

33+
func AllowedMethods(method string) []string {
34+
return []string{http.MethodOptions, method}
35+
}
36+
3337
func Contains(slice []string, item string) bool {
3438
for _, v := range slice {
3539
if item == v {

pkg/extensions/extension_userprefs.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
zerr "zotregistry.io/zot/errors"
1414
"zotregistry.io/zot/pkg/api/config"
1515
"zotregistry.io/zot/pkg/api/constants"
16+
zcommon "zotregistry.io/zot/pkg/common"
1617
"zotregistry.io/zot/pkg/log"
1718
"zotregistry.io/zot/pkg/meta/repodb"
1819
"zotregistry.io/zot/pkg/storage"
@@ -31,12 +32,19 @@ func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, store
3132

3233
userprefsRouter := router.PathPrefix(constants.ExtUserPreferencesPrefix).Subrouter()
3334

34-
userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(http.MethodPut)
35+
userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(zcommon.AllowedMethods(http.MethodPut)...)
3536
}
3637
}
3738

3839
func HandleUserPrefs(repoDB repodb.RepoDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) {
3940
return func(rsp http.ResponseWriter, req *http.Request) {
41+
rsp.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,OPTIONS")
42+
rsp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
43+
44+
if req.Method == http.MethodOptions {
45+
return
46+
}
47+
4048
if !queryHasParams(req.URL.Query(), []string{"action"}) {
4149
rsp.WriteHeader(http.StatusBadRequest)
4250

pkg/extensions/extension_userprefs_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,54 @@ import (
1313

1414
"github.com/gorilla/mux"
1515
. "github.com/smartystreets/goconvey/convey"
16+
"gopkg.in/resty.v1"
1617

1718
zerr "zotregistry.io/zot/errors"
19+
"zotregistry.io/zot/pkg/api"
20+
"zotregistry.io/zot/pkg/api/config"
21+
"zotregistry.io/zot/pkg/api/constants"
1822
"zotregistry.io/zot/pkg/extensions"
23+
extconf "zotregistry.io/zot/pkg/extensions/config"
1924
"zotregistry.io/zot/pkg/log"
2025
"zotregistry.io/zot/pkg/meta/repodb"
26+
"zotregistry.io/zot/pkg/test"
2127
"zotregistry.io/zot/pkg/test/mocks"
2228
)
2329

2430
var ErrTestError = errors.New("TestError")
2531

26-
const UserprefsBaseURL = "http://127.0.0.1:8080/v2/_zot/ext/userprefs"
32+
func TestAllowedMethodsHeader(t *testing.T) {
33+
defaultVal := true
34+
35+
Convey("Test http options response", t, func() {
36+
conf := config.New()
37+
port := test.GetFreePort()
38+
conf.HTTP.Port = port
39+
conf.Extensions = &extconf.ExtensionConfig{
40+
Search: &extconf.SearchConfig{
41+
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
42+
},
43+
}
44+
baseURL := test.GetBaseURL(port)
45+
46+
ctlr := api.NewController(conf)
47+
ctlr.Config.Storage.RootDirectory = t.TempDir()
48+
49+
ctrlManager := test.NewControllerManager(ctlr)
50+
51+
ctrlManager.StartAndWait(port)
52+
defer ctrlManager.StopServer()
53+
54+
resp, _ := resty.R().Options(baseURL + constants.FullUserPreferencesPrefix)
55+
So(resp, ShouldNotBeNil)
56+
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,PUT,OPTIONS")
57+
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
58+
})
59+
}
2760

2861
func TestHandlers(t *testing.T) {
62+
const UserprefsBaseURL = "http://127.0.0.1:8080/v2/_zot/ext/userprefs"
63+
2964
log := log.NewLogger("debug", "")
3065
mockrepoDB := mocks.RepoDBMock{}
3166

0 commit comments

Comments
 (0)