0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-06 22:40:28 -05:00

routes: changes required to do browser authentication

whenever we make a request that contains header apart from CORS allowed header, browser sends a preflight request
and in response accept *Access-Control-Allow-Headers*.

preflight request is in form of OPTIONS method, added new http handler func to set headers
and returns HTTP status ok in case of OPTIONS method.

in case of authorization, request contains authorization header
added authorization header in Access-Control-Allow-Headers list

added AllowOrigin field in HTTPConfig this field value is set to Access-Control-Allow-Origin header and will give zot adminstrator to limit incoming request.

Signed-off-by: Shivam Mishra <shimish2@cisco.com>
This commit is contained in:
Shivam Mishra 2022-02-16 01:15:13 +00:00 committed by Ramkumar Chinchani
parent aee94218aa
commit b8010e1ee4
7 changed files with 147 additions and 34 deletions

View file

@ -45,6 +45,11 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
response.WriteHeader(http.StatusNoContent)
return
}
vars := mux.Vars(request) vars := mux.Vars(request)
name := vars["name"] name := vars["name"]
header := request.Header.Get("Authorization") header := request.Header.Get("Authorization")
@ -72,6 +77,37 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
} }
} }
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 {
response.WriteHeader(http.StatusNoContent)
return
}
if config.HTTP.AllowReadAccess &&
config.HTTP.TLS.CACert != "" &&
request.TLS.VerifiedChains == nil &&
request.Method != http.MethodGet && request.Method != http.MethodHead {
authFail(response, realm, 5) //nolint:gomnd
return
}
if (request.Method != http.MethodGet && request.Method != http.MethodHead) && config.HTTP.ReadOnly {
// Reject modification requests in read-only mode
response.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Process request
next.ServeHTTP(response, request)
})
}
}
// nolint:gocyclo // we use closure making this a complex subroutine // nolint:gocyclo // we use closure making this a complex subroutine
func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
realm := ctlr.Config.HTTP.Realm realm := ctlr.Config.HTTP.Realm
@ -84,28 +120,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
// no password based authN, if neither LDAP nor HTTP BASIC is enabled // no password based authN, if neither LDAP nor HTTP BASIC is enabled
if ctlr.Config.HTTP.Auth == nil || if ctlr.Config.HTTP.Auth == nil ||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil) { (ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil) {
return func(next http.Handler) http.Handler { return noPasswdAuth(realm, ctlr.Config)
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if ctlr.Config.HTTP.AllowReadAccess &&
ctlr.Config.HTTP.TLS.CACert != "" &&
request.TLS.VerifiedChains == nil &&
request.Method != http.MethodGet && request.Method != http.MethodHead {
authFail(response, realm, 5) //nolint:gomnd
return
}
if (request.Method != http.MethodGet && request.Method != http.MethodHead) && ctlr.Config.HTTP.ReadOnly {
// Reject modification requests in read-only mode
response.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Process request
next.ServeHTTP(response, request)
})
}
} }
credMap := make(map[string]string) credMap := make(map[string]string)
@ -177,6 +192,11 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
response.WriteHeader(http.StatusNoContent)
return
}
if (request.Method == http.MethodGet || request.Method == http.MethodHead) && ctlr.Config.HTTP.AllowReadAccess { if (request.Method == http.MethodGet || request.Method == http.MethodHead) && ctlr.Config.HTTP.AllowReadAccess {
// Process request // Process request
next.ServeHTTP(response, request) next.ServeHTTP(response, request)
@ -185,7 +205,6 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
} }
if (request.Method != http.MethodGet && request.Method != http.MethodHead) && ctlr.Config.HTTP.ReadOnly { if (request.Method != http.MethodGet && request.Method != http.MethodHead) && ctlr.Config.HTTP.ReadOnly {
// Reject modification requests in read-only mode
response.WriteHeader(http.StatusMethodNotAllowed) response.WriteHeader(http.StatusMethodNotAllowed)
return return

View file

@ -64,6 +64,7 @@ type RatelimitConfig struct {
type HTTPConfig struct { type HTTPConfig struct {
Address string Address string
Port string Port string
AllowOrigin string // comma separated
TLS *TLSConfig TLS *TLSConfig
Auth *AuthConfig Auth *AuthConfig
RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"` RawAccessControl map[string]interface{} `mapstructure:"accessControl,omitempty"`

View file

@ -57,19 +57,29 @@ func NewController(config *config.Config) *Controller {
return &controller return &controller
} }
func DefaultHeaders() mux.MiddlewareFunc { func (c *Controller) CORSHeaders() mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
// CORS // CORS
response.Header().Set("Access-Control-Allow-Origin", "*") c.CORSHandler(response, request)
response.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
// handle the request
next.ServeHTTP(response, request) next.ServeHTTP(response, request)
}) })
} }
} }
func (c *Controller) CORSHandler(response http.ResponseWriter, request *http.Request) {
// allow origin as specified in config if not accept request from anywhere.
if c.Config.HTTP.AllowOrigin == "" {
response.Header().Set("Access-Control-Allow-Origin", "*")
} 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")
}
func DumpRuntimeParams(log log.Logger) { func DumpRuntimeParams(log log.Logger) {
var rLimit syscall.Rlimit var rLimit syscall.Rlimit
@ -120,7 +130,7 @@ func (c *Controller) Run() error {
} }
engine.Use( engine.Use(
DefaultHeaders(), c.CORSHeaders(),
SessionLogger(c), SessionLogger(c),
handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log), handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log),
handlers.PrintRecoveryStack(false))) handlers.PrintRecoveryStack(false)))

View file

@ -248,6 +248,9 @@ func TestHtpasswdSingleCred(t *testing.T) {
Path: htpasswdPath, Path: htpasswdPath,
}, },
} }
conf.HTTP.AllowOrigin = conf.HTTP.Address
ctlr := api.NewController(conf) ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir() ctlr.Config.Storage.RootDirectory = t.TempDir()
@ -260,6 +263,14 @@ func TestHtpasswdSingleCred(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
header := []string{"Authorization"}
resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
So(len(resp.Header()), ShouldEqual, 4)
So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
// with invalid creds, it should fail // with invalid creds, it should fail
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
@ -1467,6 +1478,12 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().SetHeader("Authorization",
fmt.Sprintf("Bearer %s", goodToken.AccessToken)).Options(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(resp, ShouldNotBeNil) So(resp, ShouldNotBeNil)

View file

@ -57,6 +57,10 @@ func NewRouteHandler(c *Controller) *RouteHandler {
return rh return rh
} }
func allowedMethods(method string) []string {
return []string{http.MethodOptions, method}
}
func (rh *RouteHandler) SetupRoutes() { func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.Use(AuthHandler(rh.c)) rh.c.Router.Use(AuthHandler(rh.c))
// authz is being enabled because authn is found // authz is being enabled because authn is found
@ -68,11 +72,11 @@ func (rh *RouteHandler) SetupRoutes() {
prefixedRouter := rh.c.Router.PathPrefix(RoutePrefix).Subrouter() prefixedRouter := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{ {
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),
rh.ListTags).Methods("GET") rh.ListTags).Methods(allowedMethods("GET")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.CheckManifest).Methods("HEAD") rh.CheckManifest).Methods(allowedMethods("HEAD")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.GetManifest).Methods("GET") rh.GetManifest).Methods(allowedMethods("GET")...)
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
rh.UpdateManifest).Methods("PUT") rh.UpdateManifest).Methods("PUT")
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", NameRegexp.String()),
@ -94,9 +98,9 @@ func (rh *RouteHandler) SetupRoutes() {
prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()), prefixedRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", NameRegexp.String()),
rh.DeleteBlobUpload).Methods("DELETE") rh.DeleteBlobUpload).Methods("DELETE")
prefixedRouter.HandleFunc("/_catalog", prefixedRouter.HandleFunc("/_catalog",
rh.ListRepositories).Methods("GET") rh.ListRepositories).Methods(allowedMethods("GET")...)
prefixedRouter.HandleFunc("/", prefixedRouter.HandleFunc("/",
rh.CheckVersionSupport).Methods("GET") rh.CheckVersionSupport).Methods(allowedMethods("GET")...)
} }
// support for oras artifact reference types (alpha 1) - image signature use case // support for oras artifact reference types (alpha 1) - image signature use case

View file

@ -96,7 +96,7 @@ func SetupRoutes(config *config.Config, router *mux.Router, storeController stor
resConfig = search.GetResolverConfig(log, storeController, false) resConfig = search.GetResolverConfig(log, storeController, false)
} }
router.PathPrefix("/query").Methods("GET", "POST"). router.PathPrefix("/query").Methods("GET", "POST", "OPTIONS").
Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig))) Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig)))
} }

View file

@ -9,6 +9,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"path" "path"
"testing" "testing"
@ -669,3 +670,64 @@ func TestCVEConfig(t *testing.T) {
}() }()
}) })
} }
func TestHTTPOptionsResponse(t *testing.T) {
Convey("Test http options response", t, func() {
conf := config.New()
port := GetFreePort()
conf.HTTP.Port = port
baseURL := GetBaseURL(port)
ctlr := api.NewController(conf)
firstDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
secondDir, err := ioutil.TempDir("", "oci-repo-test")
if err != nil {
panic(err)
}
defer os.RemoveAll(firstDir)
defer os.RemoveAll(secondDir)
err = CopyFiles("../../../../test/data", path.Join(secondDir, "a"))
if err != nil {
panic(err)
}
ctlr.Config.Storage.RootDirectory = firstDir
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{
RootDirectory: secondDir,
}
ctlr.Config.Storage.SubPaths = subPaths
go func() {
// this blocks
if err := ctlr.Run(); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(baseURL)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
resp, _ := resty.R().Options(baseURL + "/v2/_catalog")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
defer func() {
ctx := context.Background()
_ = ctlr.Server.Shutdown(ctx)
}()
})
}