From 268b4088fd2d0f8292e9cc757e6b6c96d0d78d6f Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Fri, 24 Jan 2020 15:32:38 -0600 Subject: [PATCH] Add support for bearer/token auth New options added to configuration file to reference a public key used to validate authorization tokens signed by an auth server with corresponding private key. Resolves #24 Signed-off-by: Peter Engelbert --- .gitignore | 4 + Makefile | 4 + WORKSPACE | 21 +++ examples/config-bearer-auth.json | 20 +++ go.mod | 1 + go.sum | 10 +- pkg/api/BUILD.bazel | 3 + pkg/api/auth.go | 69 ++++++++- pkg/api/config.go | 7 + pkg/api/controller_test.go | 254 +++++++++++++++++++++++++++++-- pkg/api/routes.go | 2 +- 11 files changed, 374 insertions(+), 21 deletions(-) create mode 100644 examples/config-bearer-auth.json diff --git a/.gitignore b/.gitignore index 14a36c38..ccb6b823 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ bazel-* coverage.txt test/data/ *.orig +.idea/ +coverage.html +tags +vendor/ diff --git a/Makefile b/Makefile index 242b1291..c0a5ea9d 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ test: $(shell mkdir -p test/data; cd test/data; ../scripts/gen_certs.sh; cd ${TOP_LEVEL}) go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic ./... +.PHONY: covhtml +covhtml: + go tool cover -html=coverage.txt -o coverage.html + .PHONY: check check: .bazel/golangcilint.yaml golangci-lint --version || curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.21.0 diff --git a/WORKSPACE b/WORKSPACE index d254e94f..1107ae9e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -134,6 +134,13 @@ go_repository( version = "v1.1.0", ) +go_repository( + name = "com_github_chartmuseum_auth", + importpath = "github.com/chartmuseum/auth", + sum = "h1:76rqyKtBdQAnC/YuT9ftL7OpLTDwfrfk8Ee8rD9OVOw=", + version = "v0.3.1", +) + go_repository( name = "com_github_client9_misspell", importpath = "github.com/client9/misspell", @@ -1051,6 +1058,13 @@ go_repository( version = "v0.0.0-20170223121919-b73f66626b33", ) +go_repository( + name = "com_github_mitchellh_mapstructure", + importpath = "github.com/mitchellh/mapstructure", + sum = "h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=", + version = "v1.1.2", +) + go_repository( name = "com_github_nmcclain_asn1_ber", importpath = "github.com/nmcclain/asn1-ber", @@ -1072,6 +1086,13 @@ go_repository( version = "v0.0.0-20160315200505-970db520ece7", ) +go_repository( + name = "com_github_opencontainers_go_digest", + importpath = "github.com/opencontainers/go-digest", + sum = "h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=", + version = "v1.0.0-rc1", +) + go_repository( name = "com_github_phayes_freeport", importpath = "github.com/phayes/freeport", diff --git a/examples/config-bearer-auth.json b/examples/config-bearer-auth.json new file mode 100644 index 00000000..e4af52c0 --- /dev/null +++ b/examples/config-bearer-auth.json @@ -0,0 +1,20 @@ +{ + "version":"0.1.0-dev", + "storage":{ + "rootDirectory":"/tmp/zot" + }, + "http": { + "address":"127.0.0.1", + "port":"8080", + "auth": { + "bearer": { + "realm": "https://auth.myreg.io/auth/token", + "service": "myauth", + "cert": "/etc/zot/auth.crt" + } + } + }, + "log":{ + "level":"debug" + } +} diff --git a/go.mod b/go.mod index 2fe7b2d0..a8a55d4e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/chartmuseum/auth v0.3.1 github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a github.com/go-chi/chi v4.0.2+incompatible // indirect github.com/go-ldap/ldap/v3 v3.1.3 diff --git a/go.sum b/go.sum index 77b354a1..cfdbbbde 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chartmuseum/auth v0.3.1 h1:76rqyKtBdQAnC/YuT9ftL7OpLTDwfrfk8Ee8rD9OVOw= +github.com/chartmuseum/auth v0.3.1/go.mod h1:hk7ENYpPKy5sEMkooBAuxBBtrsQjQtv9BNTLj7xZW2E= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -27,6 +29,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -88,8 +91,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -139,7 +140,10 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -181,6 +185,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= diff --git a/pkg/api/BUILD.bazel b/pkg/api/BUILD.bazel index 2433409d..92d09ea6 100644 --- a/pkg/api/BUILD.bazel +++ b/pkg/api/BUILD.bazel @@ -18,12 +18,15 @@ go_library( "//errors:go_default_library", "//pkg/log:go_default_library", "//pkg/storage:go_default_library", + "@com_github_chartmuseum_auth//:go_default_library", "@com_github_getlantern_deepcopy//:go_default_library", "@com_github_go_ldap_ldap_v3//:go_default_library", "@com_github_gorilla_handlers//:go_default_library", "@com_github_gorilla_mux//:go_default_library", "@com_github_json_iterator_go//:go_default_library", + "@com_github_mitchellh_mapstructure//:go_default_library", "@com_github_opencontainers_distribution_spec//:go_default_library", + "@com_github_opencontainers_go_digest//:go_default_library", "@com_github_opencontainers_image_spec//specs-go/v1:go_default_library", "@com_github_swaggo_http_swagger//:go_default_library", "@org_golang_x_crypto//bcrypt:go_default_library", diff --git a/pkg/api/auth.go b/pkg/api/auth.go index 729ea2c7..d265e39d 100644 --- a/pkg/api/auth.go +++ b/pkg/api/auth.go @@ -13,19 +13,65 @@ import ( "time" "github.com/anuvu/zot/errors" + "github.com/chartmuseum/auth" "github.com/gorilla/mux" "golang.org/x/crypto/bcrypt" ) -func authFail(w http.ResponseWriter, realm string, delay int) { - time.Sleep(time.Duration(delay) * time.Second) - w.Header().Set("WWW-Authenticate", realm) - w.Header().Set("Content-Type", "application/json") - WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED)) +const ( + bearerAuthDefaultAccessEntryType = "repository" +) + +func AuthHandler(c *Controller) mux.MiddlewareFunc { + if c.Config.HTTP.Auth != nil && + c.Config.HTTP.Auth.Bearer != nil && + c.Config.HTTP.Auth.Bearer.Cert != "" && + c.Config.HTTP.Auth.Bearer.Realm != "" && + c.Config.HTTP.Auth.Bearer.Service != "" { + return bearerAuthHandler(c) + } + + return basicAuthHandler(c) +} + +func bearerAuthHandler(c *Controller) mux.MiddlewareFunc { + authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{ + Realm: c.Config.HTTP.Auth.Bearer.Realm, + Service: c.Config.HTTP.Auth.Bearer.Service, + PublicKeyPath: c.Config.HTTP.Auth.Bearer.Cert, + AccessEntryType: bearerAuthDefaultAccessEntryType, + }) + if err != nil { + c.Log.Panic().Err(err).Msg("error creating bearer authorizer") + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + header := r.Header.Get("Authorization") + action := auth.PullAction + if m := r.Method; m != http.MethodGet && m != http.MethodHead { + action = auth.PushAction + } + permissions, err := authorizer.Authorize(header, action, name) + if err != nil { + c.Log.Error().Err(err).Msg("issue parsing Authorization header") + w.Header().Set("Content-Type", "application/json") + WriteJSON(w, http.StatusInternalServerError, NewError(UNSUPPORTED)) + return + } + if !permissions.Allowed { + authFail(w, permissions.WWWAuthenticateHeader, 0) + return + } + next.ServeHTTP(w, r) + }) + } } // nolint (gocyclo) - we use closure making this a complex subroutine -func BasicAuthHandler(c *Controller) mux.MiddlewareFunc { +func basicAuthHandler(c *Controller) mux.MiddlewareFunc { realm := c.Config.HTTP.Realm if realm == "" { realm = "Authorization Required" @@ -39,7 +85,7 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc { if c.Config.HTTP.AllowReadAccess && c.Config.HTTP.TLS.CACert != "" && r.TLS.VerifiedChains == nil && - r.Method != "GET" && r.Method != "HEAD" { + r.Method != http.MethodGet && r.Method != http.MethodHead { authFail(w, realm, 5) return } @@ -109,7 +155,7 @@ 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 { + if (r.Method == http.MethodGet || r.Method == http.MethodHead) && c.Config.HTTP.AllowReadAccess { // Process request next.ServeHTTP(w, r) return @@ -167,3 +213,10 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc { }) } } + +func authFail(w http.ResponseWriter, realm string, delay int) { + time.Sleep(time.Duration(delay) * time.Second) + w.Header().Set("WWW-Authenticate", realm) + w.Header().Set("Content-Type", "application/json") + WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED)) +} diff --git a/pkg/api/config.go b/pkg/api/config.go index 6ba81228..55505882 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -28,6 +28,13 @@ type AuthConfig struct { FailDelay int HTPasswd AuthHTPasswd LDAP *LDAPConfig + Bearer *BearerConfig +} + +type BearerConfig struct { + Realm string + Service string + Cert string } type HTTPConfig struct { diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 8cb9f0e5..bda87dab 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -9,27 +9,51 @@ import ( "fmt" "io/ioutil" "net" + "net/http" + "net/http/httptest" + "net/url" "os" + "regexp" + "strings" "testing" "time" "github.com/anuvu/zot/pkg/api" + "github.com/chartmuseum/auth" + "github.com/mitchellh/mapstructure" vldap "github.com/nmcclain/ldap" + godigest "github.com/opencontainers/go-digest" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" ) const ( - BaseURL1 = "http://127.0.0.1:8081" - BaseURL2 = "http://127.0.0.1:8082" - BaseSecureURL2 = "https://127.0.0.1:8082" - SecurePort1 = "8081" - SecurePort2 = "8082" - username = "test" - passphrase = "test" - ServerCert = "../../test/data/server.cert" - ServerKey = "../../test/data/server.key" - CACert = "../../test/data/ca.crt" + BaseURL1 = "http://127.0.0.1:8081" + BaseURL2 = "http://127.0.0.1:8082" + BaseURL3 = "http://127.0.0.1:8083" + BaseSecureURL2 = "https://127.0.0.1:8082" + SecurePort1 = "8081" + SecurePort2 = "8082" + SecurePort3 = "8083" + username = "test" + passphrase = "test" + ServerCert = "../../test/data/server.cert" + ServerKey = "../../test/data/server.key" + CACert = "../../test/data/ca.crt" + AuthorizedNamespace = "everyone/isallowed" + UnauthorizedNamespace = "fortknox/notallowed" +) + +type ( + accessTokenResponse struct { + AccessToken string `json:"access_token"` + } + + authHeader struct { + Realm string + Service string + Scope string + } ) func makeHtpasswdFile() string { @@ -782,3 +806,213 @@ func TestBasicAuthWithLDAP(t *testing.T) { So(resp.StatusCode(), ShouldEqual, 200) }) } + +func TestBearerAuth(t *testing.T) { + Convey("Make a new controller", t, func() { + authTestServer := makeAuthTestServer() + defer authTestServer.Close() + + config := api.NewConfig() + config.HTTP.Port = SecurePort3 + + u, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) + + config.HTTP.Auth = &api.AuthConfig{ + Bearer: &api.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: u.Host, + }, + } + c := api.NewController(config) + dir, err := ioutil.TempDir("", "oci-repo-test") + So(err, ShouldBeNil) + 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(BaseURL3) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() + + resp, err := resty.R().Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader := parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + var goodToken accessTokenResponse + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 202) + loc := resp.Header().Get("Location") + + resp, err = resty.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(BaseURL3 + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + SetQueryParam("digest", digest). + SetBody(blob). + Put(BaseURL3 + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 201) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)). + Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + resp, err = resty.R(). + Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + SetQueryParam("scope", authorizationHeader.Scope). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + var badToken accessTokenResponse + err = json.Unmarshal(resp.Body(), &badToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)). + Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + }) +} + +func makeAuthTestServer() *httptest.Server { + cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{ + PrivateKeyPath: ServerKey, + Audience: "Zot Registry", + Issuer: "Zot", + AddKIDHeader: true, + }) + if err != nil { + panic(err) + } + + authTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + scope := r.URL.Query().Get("scope") + parts := strings.Split(scope, ":") + name := parts[1] + actions := strings.Split(parts[2], ",") + if name == UnauthorizedNamespace { + actions = []string{} + } + access := []auth.AccessEntry{ + { + Name: name, + Type: "repository", + Actions: actions, + }, + } + token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1) + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"access_token": "%s"}`, token) + })) + + return authTestServer +} + +func parseBearerAuthHeader(authHeaderRaw string) *authHeader { + re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`) + matches := re.FindAllStringSubmatch(authHeaderRaw, -1) + m := make(map[string]string) + + for i := 0; i < len(matches); i++ { + m[matches[i][1]] = matches[i][2] + } + + var h authHeader + if err := mapstructure.Decode(m, &h); err != nil { + panic(err) + } + + return &h +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 95386b40..397c6357 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -51,7 +51,7 @@ func NewRouteHandler(c *Controller) *RouteHandler { } func (rh *RouteHandler) SetupRoutes() { - rh.c.Router.Use(BasicAuthHandler(rh.c)) + rh.c.Router.Use(AuthHandler(rh.c)) g := rh.c.Router.PathPrefix(RoutePrefix).Subrouter() { g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),