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) + }() + } + }) +}