0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-27 23:01:43 -05:00

feat(userpreferences): update allowed methods header for user preferences routes (#1430)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-05-10 20:09:53 +03:00 committed by GitHub
parent 6269dbea0c
commit 3be690c2ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 198 additions and 16 deletions

View file

@ -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
@ -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
@ -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

View file

@ -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)

View file

@ -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) {

View file

@ -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/")
@ -493,6 +494,88 @@ 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{}

View file

@ -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"
@ -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))
@ -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()),
@ -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
@ -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
@ -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"]
@ -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"]
@ -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"]
@ -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"]
@ -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
@ -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)

View file

@ -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 {

View file

@ -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"
@ -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)

View file

@ -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{}