Skip to content

Commit

Permalink
feat(userpreferences): update allowed methods header for user prefere…
Browse files Browse the repository at this point in the history
…nces routes

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
  • Loading branch information
laurentiuNiculae committed May 9, 2023
1 parent 449f0d0 commit 680d722
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 16 deletions.
3 changes: 3 additions & 0 deletions pkg/api/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)

return
Expand Down Expand Up @@ -95,6 +96,7 @@ func noPasswdAuth(realm string, config *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)

return
Expand Down Expand Up @@ -193,6 +195,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)
response.WriteHeader(http.StatusNoContent)

return
Expand Down
6 changes: 6 additions & 0 deletions pkg/api/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
resource := vars["name"]
reference, ok := vars["reference"]

if request.Method == http.MethodOptions {
next.ServeHTTP(response, request)

return
}

// bypass authz for /v2/ route
if request.RequestURI == "/v2/" {
next.ServeHTTP(response, request)
Expand Down
3 changes: 0 additions & 3 deletions pkg/api/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ func (c *Controller) CORSHandler(response http.ResponseWriter, request *http.Req
} else {
response.Header().Set("Access-Control-Allow-Origin", c.Config.HTTP.AllowOrigin)
}

response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")
}

func DumpRuntimeParams(log log.Logger) {
Expand Down
84 changes: 84 additions & 0 deletions pkg/api/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ func TestHtpasswdSingleCred(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
So(len(resp.Header()), ShouldEqual, 4)
So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

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

func TestAllowMethodsHeader(t *testing.T) {
Convey("Options request", t, func() {
dir := t.TempDir()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = dir

simpleUser := "simpleUser"
simpleUserPassword := "simpleUserPass"
credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))

htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
defer os.Remove(htpasswdPath)

conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}

conf.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{simpleUser},
Actions: []string{"read", "create"},
},
},
},
},
}

defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
}

ctlr := api.NewController(conf)

ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()

simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)

digest := godigest.FromString("digest")

// /v2
resp, err := simpleUserClient.Options(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

// /v2/{name}/tags/list
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/tags/list")
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

// /v2/{name}/manifests/{reference}
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

// /v2/{name}/referrers/{digest}
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

// /v2/_catalog
resp, err = simpleUserClient.Options(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

// /v2/_oci/ext/discover
resp, err = simpleUserClient.Options(baseURL + "/v2/_oci/ext/discover")
So(err, ShouldBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,OPTIONS")

})
}

func TestHtpasswdTwoCreds(t *testing.T) {
Convey("Two creds", t, func() {
twoCredTests := []string{}
Expand Down
68 changes: 57 additions & 11 deletions pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
gqlPlayground "zotregistry.io/zot/pkg/debug/gqlplayground"
debug "zotregistry.io/zot/pkg/debug/swagger"
ext "zotregistry.io/zot/pkg/extensions"
Expand All @@ -53,10 +54,6 @@ func NewRouteHandler(c *Controller) *RouteHandler {
return rh
}

func allowedMethods(method string) []string {
return []string{http.MethodOptions, method}
}

func (rh *RouteHandler) SetupRoutes() {
prefixedRouter := rh.c.Router.PathPrefix(constants.RoutePrefix).Subrouter()
prefixedRouter.Use(AuthHandler(rh.c))
Expand All @@ -75,11 +72,11 @@ func (rh *RouteHandler) SetupRoutes() {
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
{
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
rh.ListTags).Methods(allowedMethods("GET")...)
rh.ListTags).Methods(zcommon.AllowedMethods("GET")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
rh.CheckManifest).Methods(allowedMethods("HEAD")...)
rh.CheckManifest).Methods(zcommon.AllowedMethods("HEAD")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
rh.GetManifest).Methods(allowedMethods("GET")...)
rh.GetManifest).Methods(zcommon.AllowedMethods("GET")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
rh.UpdateManifest).Methods("PUT")
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
Expand All @@ -102,13 +99,13 @@ func (rh *RouteHandler) SetupRoutes() {
rh.DeleteBlobUpload).Methods("DELETE")
// support for OCI artifact references
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", zreg.NameRegexp.String()),
rh.GetReferrers).Methods(allowedMethods("GET")...)
rh.GetReferrers).Methods(zcommon.AllowedMethods("GET")...)
prefixedRouter.HandleFunc(constants.ExtCatalogPrefix,
rh.ListRepositories).Methods(allowedMethods("GET")...)
rh.ListRepositories).Methods(zcommon.AllowedMethods("GET")...)
prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix,
rh.ListExtensions).Methods(allowedMethods("GET")...)
rh.ListExtensions).Methods(zcommon.AllowedMethods("GET")...)
prefixedRouter.HandleFunc("/",
rh.CheckVersionSupport).Methods(allowedMethods("GET")...)
rh.CheckVersionSupport).Methods(zcommon.AllowedMethods("GET")...)
}

// support for ORAS artifact reference types (alpha 1) - image signature use case
Expand Down Expand Up @@ -146,6 +143,13 @@ func (rh *RouteHandler) SetupRoutes() {
// @Produce json
// @Success 200 {string} string "ok".
func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

response.Header().Set(constants.DistAPIVersion, "registry/2.0")
// NOTE: compatibility workaround - return this header in "allowed-read" mode to allow for clients to
// work correctly
Expand Down Expand Up @@ -178,6 +182,13 @@ type ImageTags struct {
// @Failure 404 {string} string "not found"
// @Failure 400 {string} string "bad request".
func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

vars := mux.Vars(request)

name, ok := vars["name"]
Expand Down Expand Up @@ -301,6 +312,13 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req
// @Failure 404 {string} string "not found"
// @Failure 500 {string} string "internal server error".
func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

vars := mux.Vars(request)
name, ok := vars["name"]

Expand Down Expand Up @@ -367,6 +385,13 @@ type ExtensionList struct {
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/manifests/{reference} [get].
func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

vars := mux.Vars(request)
name, ok := vars["name"]

Expand Down Expand Up @@ -468,6 +493,13 @@ func getReferrers(ctx context.Context, routeHandler *RouteHandler,
// @Failure 500 {string} string "internal server error"
// @Router /v2/{name}/references/{digest} [get].
func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

vars := mux.Vars(request)

name, ok := vars["name"]
Expand Down Expand Up @@ -1501,6 +1533,13 @@ type RepositoryList struct {
// @Failure 500 {string} string "internal server error"
// @Router /v2/_catalog [get].
func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
response.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if request.Method == http.MethodOptions {
return
}

combineRepoList := make([]string, 0)

subStore := rh.c.StoreController.SubStore
Expand Down Expand Up @@ -1560,6 +1599,13 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
// @Success 200 {object} api.ExtensionList
// @Router /v2/_oci/ext/discover [get].
func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if r.Method == http.MethodOptions {
return
}

extensionList := ext.GetExtensions(rh.c.Config)

WriteJSON(w, http.StatusOK, extensionList)
Expand Down
4 changes: 4 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const (
caCertFilename = "ca.crt"
)

func AllowedMethods(method string) []string {
return []string{http.MethodOptions, method}
}

func Contains(slice []string, item string) bool {
for _, v := range slice {
if item == v {
Expand Down
10 changes: 9 additions & 1 deletion pkg/extensions/extension_userprefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/storage"
Expand All @@ -31,12 +32,19 @@ func SetupUserPreferencesRoutes(config *config.Config, router *mux.Router, store

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

userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(http.MethodPut)
userprefsRouter.HandleFunc("", HandleUserPrefs(repoDB, log)).Methods(zcommon.AllowedMethods(http.MethodPut)...)
}
}

func HandleUserPrefs(repoDB repodb.RepoDB, log log.Logger) func(w http.ResponseWriter, r *http.Request) {
return func(rsp http.ResponseWriter, req *http.Request) {
rsp.Header().Set("Access-Control-Allow-Methods", "HEAD,GET,POST,PUT,OPTIONS")
rsp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type")

if req.Method == http.MethodOptions {
return
}

if !queryHasParams(req.URL.Query(), []string{"action"}) {
rsp.WriteHeader(http.StatusBadRequest)

Expand Down
37 changes: 36 additions & 1 deletion pkg/extensions/extension_userprefs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,54 @@ import (

"github.com/gorilla/mux"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"

zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
"zotregistry.io/zot/pkg/extensions"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)

var ErrTestError = errors.New("TestError")

const UserprefsBaseURL = "http://127.0.0.1:8080/v2/_zot/ext/userprefs"
func TestAllowedMethodsHeader(t *testing.T) {
defaultVal := true

Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
baseURL := test.GetBaseURL(port)

ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()

ctrlManager := test.NewControllerManager(ctlr)

ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()

resp, _ := resty.R().Options(baseURL + constants.FullUserPreferencesPrefix)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,POST,PUT,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}

func TestHandlers(t *testing.T) {
const UserprefsBaseURL = "http://127.0.0.1:8080/v2/_zot/ext/userprefs"

log := log.NewLogger("debug", "")
mockrepoDB := mocks.RepoDBMock{}

Expand Down

0 comments on commit 680d722

Please sign in to comment.