From 10199457b4f4736d9e41fdf058df2bf91b9df43d Mon Sep 17 00:00:00 2001 From: Ramkumar Chinchani Date: Wed, 28 Aug 2019 14:05:16 -0700 Subject: [PATCH] auth: allow for world-readable deployment mode --- WORKSPACE | 33 +++-- examples/config-example.json | 3 +- examples/config-example.yaml | 1 + go.mod | 6 +- go.sum | 13 ++ pkg/api/auth.go | 34 +++-- pkg/api/config.go | 11 +- pkg/api/controller.go | 10 +- pkg/api/controller_test.go | 265 ++++++++++++++++++++++++++++++++++- 9 files changed, 340 insertions(+), 36 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index df6db250..6ca31d90 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,29 +1,39 @@ workspace(name = "com_github_anuvu_zot") +go_version = "1.12.9" + +go_os = "linux" + +go_arch = "amd64" + + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") -go_rules_version = "0.18.6" +go_rules_version = "0.19.3" http_archive( name = "io_bazel_rules_go", - sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d", - urls = ["https://github.com/bazelbuild/rules_go/releases/download/{}/rules_go-{}.tar.gz".format(go_rules_version, go_rules_version)], + urls = [ + "https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/{}/rules_go-{}.tar.gz".format(go_rules_version, go_rules_version), + "https://github.com/bazelbuild/rules_go/releases/download/{}/rules_go-{}.tar.gz".format(go_rules_version, go_rules_version), + ], + sha256 = "313f2c7a23fecc33023563f082f381a32b9b7254f727a7dd2d6380ccc6dfe09b", ) -gazelle_version = "0.17.0" +gazelle_version = "0.18.1" http_archive( name = "bazel_gazelle", - sha256 = "3c681998538231a2d24d0c07ed5a7658cb72bfb5fd4bf9911157c0e9ac6a2687", + sha256 = "be9296bfd64882e3c08e3283c58fcb461fa6dd3c171764fcc4cf322f60615a9b", urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/{}/bazel-gazelle-{}.tar.gz".format(gazelle_version, gazelle_version)], ) -buildtools_version = "0.26.0" +buildtools_version = "0.25.1" http_archive( name = "com_github_bazelbuild_buildtools", - sha256 = "86592d703ecbe0c5cbb5139333a63268cf58d7efd2c459c8be8e69e77d135e29", + sha256 = "0a0920151acf18c51866331944d12db9023707a6861e78225366f5711efc845b", strip_prefix = "buildtools-{}".format(buildtools_version), urls = ["https://github.com/bazelbuild/buildtools/archive/{}.tar.gz".format(buildtools_version)], ) @@ -47,7 +57,14 @@ load("@bazel_skylib//lib:versions.bzl", "versions") versions.check(minimum_bazel_version = "0.26.1") -load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") +load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_register_toolchains", "go_rules_dependencies") + +go_download_sdk( + name = "go_sdk", + goos = go_os, + goarch = go_arch, + version = go_version, +) go_rules_dependencies() diff --git a/examples/config-example.json b/examples/config-example.json index 5c137912..dd111839 100644 --- a/examples/config-example.json +++ b/examples/config-example.json @@ -16,7 +16,8 @@ "path": "test/data/htpasswd" }, "failDelay": 5 - } + }, + "allowReadAccess": false }, "log":{ "level":"debug", diff --git a/examples/config-example.yaml b/examples/config-example.yaml index 95d03143..1a0fab66 100644 --- a/examples/config-example.yaml +++ b/examples/config-example.yaml @@ -14,6 +14,7 @@ http: htpasswd: path: test/data/htpasswd failDelay: 5 + allowReadAccess: false log: level: debug output: /tmp/zot.log diff --git a/go.mod b/go.mod index 67ff5a6f..2057fe00 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,10 @@ require ( github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 // indirect github.com/swaggo/http-swagger v0.0.0-20190614090009-c2865af9083e github.com/swaggo/swag v1.6.2 - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect + golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 // indirect + golang.org/x/text v0.3.2 // indirect + golang.org/x/tools v0.0.0-20190827205025-b29f5f60c37a // indirect gopkg.in/resty.v1 v1.12.0 ) diff --git a/go.sum b/go.sum index 0dc5c108..f266f6e8 100644 --- a/go.sum +++ b/go.sum @@ -162,6 +162,8 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -172,6 +174,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -185,16 +190,24 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468 h1:fTfk6GjmihJbK0mSUFgPPgYpsdmApQ86Mcd4GuKax9U= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190827205025-b29f5f60c37a h1:0JEq5ZQ3TgsRlFmz4BcD+E6U6cOk4pOImCQSyIG59ZM= +golang.org/x/tools v0.0.0-20190827205025-b29f5f60c37a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/pkg/api/auth.go b/pkg/api/auth.go index 28a7ff97..35571dca 100644 --- a/pkg/api/auth.go +++ b/pkg/api/auth.go @@ -21,22 +21,30 @@ func authFail(w http.ResponseWriter, realm string, delay int) { } func BasicAuthHandler(c *Controller) mux.MiddlewareFunc { - if c.Config.HTTP.Auth.HTPasswd.Path == "" { - // no authentication - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Process request - next.ServeHTTP(w, r) - }) - } - } - realm := c.Config.HTTP.Realm if realm == "" { realm = "Authorization Required" } realm = "Basic realm=" + strconv.Quote(realm) delay := c.Config.HTTP.Auth.FailDelay + + if c.Config.HTTP.Auth.HTPasswd.Path == "" { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if c.Config.HTTP.AllowReadAccess && + c.Config.HTTP.TLS.CACert != "" && + r.TLS.VerifiedChains == nil && + r.Method != "GET" && r.Method != "HEAD" { + authFail(w, realm, delay) + return + } + + // Process request + next.ServeHTTP(w, r) + }) + } + } + credMap := make(map[string]string) f, err := os.Open(c.Config.HTTP.Auth.HTPasswd.Path) @@ -56,6 +64,12 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if (r.Method == "GET" || r.Method == "HEAD") && c.Config.HTTP.AllowReadAccess { + // Process request + next.ServeHTTP(w, r) + 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 03af1e3e..cbb6ffd3 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -24,11 +24,12 @@ type AuthConfig struct { } type HTTPConfig struct { - Address string - Port string - TLS TLSConfig `mapstructure:",omitempty"` - Auth AuthConfig `mapstructure:",omitempty"` - Realm string + Address string + Port string + TLS TLSConfig `mapstructure:",omitempty"` + Auth AuthConfig `mapstructure:",omitempty"` + Realm string + AllowReadAccess bool `mapstructure:",omitempty"` } type LogConfig struct { diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 67e3364b..1b95c250 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -45,13 +45,13 @@ func (c *Controller) Run() error { return err } - clientAuth := tls.VerifyClientCertIfGiven - if c.Config.HTTP.Auth.HTPasswd.Path == "" { - clientAuth = tls.RequireAndVerifyClientCert - } - if c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { if c.Config.HTTP.TLS.CACert != "" { + clientAuth := tls.VerifyClientCertIfGiven + if c.Config.HTTP.Auth.HTPasswd.Path == "" && !c.Config.HTTP.AllowReadAccess { + clientAuth = tls.RequireAndVerifyClientCert + } + caCert, err := ioutil.ReadFile(c.Config.HTTP.TLS.CACert) if err != nil { panic(err) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 0e671635..5bdbfeaa 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -26,6 +26,7 @@ const ( htpasswdPath = "../../test/data/htpasswd" // nolint (gosec) - this is just test data ServerCert = "../../test/data/server.cert" ServerKey = "../../test/data/server.key" + CACert = "../../test/data/ca.crt" ) func TestNew(t *testing.T) { @@ -91,7 +92,7 @@ func TestBasicAuth(t *testing.T) { func TestTLSWithBasicAuth(t *testing.T) { Convey("Make a new controller", t, func() { - caCert, err := ioutil.ReadFile("../../test/data/ca.crt") + caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) @@ -158,9 +159,80 @@ func TestTLSWithBasicAuth(t *testing.T) { }) } +func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) { + Convey("Make a new controller", t, func() { + caCert, err := ioutil.ReadFile(CACert) + So(err, ShouldBeNil) + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) + defer func() { resty.SetTLSClientConfig(nil) }() + config := api.NewConfig() + config.HTTP.Port = SecurePort2 + config.HTTP.Auth.HTPasswd.Path = htpasswdPath + config.HTTP.TLS.Cert = ServerCert + config.HTTP.TLS.Key = ServerKey + config.HTTP.AllowReadAccess = true + + 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() { + // this blocks + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(BaseURL2) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + // accessing insecure HTTP site should fail + resp, err := resty.R().Get(BaseURL2) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + + // without creds, should still be allowed to access + resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with creds, should get expected status code + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // without creds, writes should fail + resp, err = resty.R().Post(BaseSecureURL2 + "/v2/repo/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + }) +} + func TestTLSMutualAuth(t *testing.T) { Convey("Make a new controller", t, func() { - caCert, err := ioutil.ReadFile("../../test/data/ca.crt") + caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) @@ -171,7 +243,7 @@ func TestTLSMutualAuth(t *testing.T) { config.HTTP.Port = SecurePort2 config.HTTP.TLS.Cert = ServerCert config.HTTP.TLS.Key = ServerKey - config.HTTP.TLS.CACert = "../../test/data/ca.crt" + config.HTTP.TLS.CACert = CACert c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") @@ -240,9 +312,9 @@ func TestTLSMutualAuth(t *testing.T) { }) } -func TestTLSMutualAndBasicAuth(t *testing.T) { +func TestTLSMutualAuthAllowReadAccess(t *testing.T) { Convey("Make a new controller", t, func() { - caCert, err := ioutil.ReadFile("../../test/data/ca.crt") + caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) @@ -253,7 +325,97 @@ func TestTLSMutualAndBasicAuth(t *testing.T) { config.HTTP.Port = SecurePort2 config.HTTP.TLS.Cert = ServerCert config.HTTP.TLS.Key = ServerKey - config.HTTP.TLS.CACert = "../../test/data/ca.crt" + config.HTTP.TLS.CACert = CACert + config.HTTP.AllowReadAccess = true + + 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() { + // this blocks + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(BaseURL2) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + // accessing insecure HTTP site should fail + resp, err := resty.R().Get(BaseURL2) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + + // without client certs and creds, reads are allowed + resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with creds but without certs, reads are allowed + resp, err = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // without creds, writes should fail + resp, err = resty.R().Post(BaseSecureURL2 + "/v2/repo/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + // setup TLS mutual auth + cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key") + So(err, ShouldBeNil) + + resty.SetCertificates(cert) + defer func() { resty.SetCertificates(tls.Certificate{}) }() + + // with client certs but without creds, should succeed + resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with client certs and creds, should get expected status code + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + + // with client certs, creds shouldn't matter + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) +} + +func TestTLSMutualAndBasicAuth(t *testing.T) { + Convey("Make a new controller", t, func() { + caCert, err := ioutil.ReadFile(CACert) + So(err, ShouldBeNil) + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) + defer func() { resty.SetTLSClientConfig(nil) }() + config := api.NewConfig() + config.HTTP.Port = SecurePort2 + config.HTTP.TLS.Cert = ServerCert + config.HTTP.TLS.Key = ServerKey + config.HTTP.TLS.CACert = CACert config.HTTP.Auth.HTPasswd.Path = htpasswdPath c := api.NewController(config) @@ -325,3 +487,94 @@ func TestTLSMutualAndBasicAuth(t *testing.T) { So(resp.StatusCode(), ShouldEqual, 200) }) } + +func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) { + Convey("Make a new controller", t, func() { + caCert, err := ioutil.ReadFile(CACert) + So(err, ShouldBeNil) + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) + defer func() { resty.SetTLSClientConfig(nil) }() + config := api.NewConfig() + config.HTTP.Port = SecurePort2 + config.HTTP.TLS.Cert = ServerCert + config.HTTP.TLS.Key = ServerKey + config.HTTP.TLS.CACert = CACert + config.HTTP.Auth.HTPasswd.Path = htpasswdPath + config.HTTP.AllowReadAccess = true + + 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() { + // this blocks + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(BaseURL2) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + // accessing insecure HTTP site should fail + resp, err := resty.R().Get(BaseURL2) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + + // without client certs and creds, should fail + _, err = resty.R().Get(BaseSecureURL2) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + + // with creds but without certs, should succeed + _, err = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 400) + + // setup TLS mutual auth + cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key") + So(err, ShouldBeNil) + + resty.SetCertificates(cert) + defer func() { resty.SetCertificates(tls.Certificate{}) }() + + // with client certs but without creds, reads should succeed + resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // with only client certs, writes should fail + resp, err = resty.R().Post(BaseSecureURL2 + "/v2/repo/blobs/uploads/") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + // with client certs and creds, should get expected status code + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 404) + + resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + }) +}