From 78be4cbe3cc7c5f5e3efd4efb44dd410e99e3df7 Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Fri, 10 Jul 2020 14:32:58 -0700 Subject: [PATCH] auth: support a read-only mode This is useful if we want to roll out experimental versions of zot pointing to some storage shared with another zot instance. Also, when under storage full conditions, will be useful to turn on this flag to prevent further writes. --- pkg/api/auth.go | 12 ++++++ pkg/api/config.go | 1 + pkg/api/controller_test.go | 80 +++++++++++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/pkg/api/auth.go b/pkg/api/auth.go index fb488e46..355f0379 100644 --- a/pkg/api/auth.go +++ b/pkg/api/auth.go @@ -91,6 +91,12 @@ func basicAuthHandler(c *Controller) mux.MiddlewareFunc { authFail(w, realm, 5) return } + + if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly { + // Reject modification requests in read-only mode + w.WriteHeader(http.StatusMethodNotAllowed) + return + } // Process request next.ServeHTTP(w, r) }) @@ -175,6 +181,12 @@ func basicAuthHandler(c *Controller) mux.MiddlewareFunc { return } + if (r.Method != http.MethodGet && r.Method != http.MethodHead) && c.Config.HTTP.ReadOnly { + // Reject modification requests in read-only mode + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + basicAuth := r.Header.Get("Authorization") if basicAuth == "" { authFail(w, realm, delay) diff --git a/pkg/api/config.go b/pkg/api/config.go index b07d6fed..e79e8c3b 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -46,6 +46,7 @@ type HTTPConfig struct { Auth *AuthConfig Realm string AllowReadAccess bool `mapstructure:",omitempty"` + ReadOnly bool `mapstructure:",omitempty"` } type LDAPConfig struct { diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 7cc6b544..93896374 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -33,10 +33,12 @@ const ( BaseURL1 = "http://127.0.0.1:8081" BaseURL2 = "http://127.0.0.1:8082" BaseURL3 = "http://127.0.0.1:8083" + BaseURL4 = "http://127.0.0.1:8084" BaseSecureURL2 = "https://127.0.0.1:8082" SecurePort1 = "8081" SecurePort2 = "8082" SecurePort3 = "8083" + SecurePort4 = "8084" username = "test" passphrase = "test" ServerCert = "../../test/data/server.cert" @@ -44,6 +46,7 @@ const ( CACert = "../../test/data/ca.crt" AuthorizedNamespace = "everyone/isallowed" UnauthorizedNamespace = "fortknox/notallowed" + ALICE = "alice" ) type ( @@ -109,8 +112,8 @@ func TestNew(t *testing.T) { func TestHtpasswdSingleCred(t *testing.T) { Convey("Single cred", t, func() { singleCredtests := []string{} - user := "alice" - password := "alice" + user := ALICE + password := ALICE singleCredtests = append(singleCredtests, getCredString(user, password)) singleCredtests = append(singleCredtests, getCredString(user, password)+"\n") @@ -1439,3 +1442,76 @@ func parseBearerAuthHeader(authHeaderRaw string) *authHeader { return &h } + +func TestHTTPReadOnly(t *testing.T) { + Convey("Single cred", t, func() { + singleCredtests := []string{} + user := ALICE + password := ALICE + singleCredtests = append(singleCredtests, getCredString(user, password)) + singleCredtests = append(singleCredtests, getCredString(user, password)+"\n") + + for _, testString := range singleCredtests { + func() { + config := api.NewConfig() + config.HTTP.Port = SecurePort4 + // enable read-only mode + config.HTTP.ReadOnly = true + + htpasswdPath := makeHtpasswdFileFromString(testString) + defer os.Remove(htpasswdPath) + config.HTTP.Auth = &api.AuthConfig{ + HTPasswd: api.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + c := api.NewController(config) + dir, err := ioutil.TempDir("", "oci-repo-test") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + c.Config.Storage.RootDirectory = dir + go func(controller *api.Controller) { + // this blocks + if err := controller.Run(); err != nil { + return + } + }(c) + // wait till ready + for { + _, err := resty.R().Get(BaseURL4) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + defer func(controller *api.Controller) { + ctx := context.Background() + _ = controller.Server.Shutdown(ctx) + }(c) + // with creds, should get expected status code + resp, _ := resty.R().SetBasicAuth(user, password).Get(BaseURL4 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with creds, should get expected status code + resp, _ = resty.R().SetBasicAuth(user, password).Get(BaseURL4 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with creds, any modifications should still fail on read-only mode + resp, err = resty.R().SetBasicAuth(user, password). + Post(BaseURL4 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 405) + + //with invalid creds, it should fail + resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(BaseURL4 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + }() + } + }) +}