0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -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 http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodOptions {
response.WriteHeader(http.StatusNoContent)
return
}
vars := mux.Vars(request)
name := vars["name"]
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
func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
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
if ctlr.Config.HTTP.Auth == nil ||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil) {
return func(next http.Handler) http.Handler {
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)
})
}
return noPasswdAuth(realm, ctlr.Config)
}
credMap := make(map[string]string)
@ -177,6 +192,11 @@ 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 {
response.WriteHeader(http.StatusNoContent)
return
}
if (request.Method == http.MethodGet || request.Method == http.MethodHead) && ctlr.Config.HTTP.AllowReadAccess {
// Process 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 {
// Reject modification requests in read-only mode
response.WriteHeader(http.StatusMethodNotAllowed)
return

View file

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

View file

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

View file

@ -248,6 +248,9 @@ func TestHtpasswdSingleCred(t *testing.T) {
Path: htpasswdPath,
},
}
conf.HTTP.AllowOrigin = conf.HTTP.Address
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
@ -260,6 +263,14 @@ func TestHtpasswdSingleCred(t *testing.T) {
So(resp, ShouldNotBeNil)
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
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
@ -1467,6 +1478,12 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
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/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)

View file

@ -57,6 +57,10 @@ func NewRouteHandler(c *Controller) *RouteHandler {
return rh
}
func allowedMethods(method string) []string {
return []string{http.MethodOptions, method}
}
func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.Use(AuthHandler(rh.c))
// authz is being enabled because authn is found
@ -68,11 +72,11 @@ func (rh *RouteHandler) SetupRoutes() {
prefixedRouter := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{
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()),
rh.CheckManifest).Methods("HEAD")
rh.CheckManifest).Methods(allowedMethods("HEAD")...)
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()),
rh.UpdateManifest).Methods("PUT")
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()),
rh.DeleteBlobUpload).Methods("DELETE")
prefixedRouter.HandleFunc("/_catalog",
rh.ListRepositories).Methods("GET")
rh.ListRepositories).Methods(allowedMethods("GET")...)
prefixedRouter.HandleFunc("/",
rh.CheckVersionSupport).Methods("GET")
rh.CheckVersionSupport).Methods(allowedMethods("GET")...)
}
// 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)
}
router.PathPrefix("/query").Methods("GET", "POST").
router.PathPrefix("/query").Methods("GET", "POST", "OPTIONS").
Handler(gqlHandler.NewDefaultServer(search.NewExecutableSchema(resConfig)))
}

View file

@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"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)
}()
})
}