From d4656906309dc9b91f11158bbc26d495f54733c5 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 6 Mar 2025 23:32:13 +0100 Subject: [PATCH] [feat]: add support for EC/ED25519 public keys for token authentication (#2998) * feat: rework token auth to allow ED25519/EC public keys Signed-off-by: evanebb * fix: shadow err variable to hopefully avoid data race Signed-off-by: evanebb * fix: apply golangci-lint feedback Signed-off-by: evanebb * fix: simplify public key loading by only supporting certificates, fixes ED25519 certificate handling Signed-off-by: evanebb * test: add golang-jwt based test auth server and test RSA/EC/ED25519 keys Signed-off-by: evanebb * fix: restrict allowed signing algorithms as recommended by library Signed-off-by: evanebb * test: add more bearer authorizer tests Signed-off-by: evanebb * fix: apply more golangci-lint feedback Signed-off-by: evanebb * test: ensure chmod calls run on test failure for authn errors test Signed-off-by: evanebb * fix: verify issued-at in given token if present Pulls the validation in-line with the old library Signed-off-by: evanebb --------- Signed-off-by: evanebb --- errors/errors.go | 4 + go.mod | 2 +- pkg/api/authn.go | 124 +++-- pkg/api/bearer.go | 136 +++++ pkg/api/bearer_test.go | 121 +++++ pkg/api/controller_test.go | 808 ++++++++++++++++++------------ pkg/extensions/extensions_test.go | 274 +++++----- pkg/extensions/sync/sync_test.go | 535 ++++++++++---------- pkg/test/auth/bearer.go | 84 +++- pkg/test/auth/bearer_test.go | 8 +- test/scripts/gen_certs.sh | 76 +++ 11 files changed, 1413 insertions(+), 759 deletions(-) create mode 100644 pkg/api/bearer.go create mode 100644 pkg/api/bearer_test.go diff --git a/errors/errors.go b/errors/errors.go index b49c0f87..d64dcc9b 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -175,4 +175,8 @@ var ( ErrImageNotFound = errors.New("image not found") ErrAmbiguousInput = errors.New("input is not specific enough") ErrReceivedUnexpectedAuthHeader = errors.New("received unexpected www-authenticate header") + ErrNoBearerToken = errors.New("no bearer token given") + ErrInvalidBearerToken = errors.New("invalid bearer token given") + ErrInsufficientScope = errors.New("bearer token does not have sufficient scope") + ErrCouldNotLoadCertificate = errors.New("failed to load certificate") ) diff --git a/go.mod b/go.mod index 1ecaad11..e7aae618 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/go-redis/redismock/v9 v9.2.0 github.com/go-redsync/redsync/v4 v4.13.0 github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-containerregistry v0.20.3 github.com/google/go-github/v62 v62.0.0 github.com/google/uuid v1.6.0 @@ -273,7 +274,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/pkg/api/authn.go b/pkg/api/authn.go index 947f8e79..7435c88e 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -6,17 +6,16 @@ import ( "crypto/x509" "encoding/base64" "encoding/hex" + "encoding/pem" "errors" "fmt" "net" "net/http" "os" - "regexp" "strconv" "strings" "time" - "github.com/chartmuseum/auth" guuid "github.com/gofrs/uuid" "github.com/google/go-github/v62/github" "github.com/google/uuid" @@ -39,9 +38,8 @@ import ( ) const ( - bearerAuthDefaultAccessEntryType = "repository" - issuedAtOffset = 5 * time.Second - relyingPartyCookieMaxAge = 120 + issuedAtOffset = 5 * time.Second + relyingPartyCookieMaxAge = 120 ) type AuthnMiddleware struct { @@ -404,17 +402,17 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun } func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { - authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{ - Realm: ctlr.Config.HTTP.Auth.Bearer.Realm, - Service: ctlr.Config.HTTP.Auth.Bearer.Service, - PublicKeyPath: ctlr.Config.HTTP.Auth.Bearer.Cert, - AccessEntryType: bearerAuthDefaultAccessEntryType, - EmptyDefaultNamespace: true, - }) + certificate, err := loadCertificateFromFile(ctlr.Config.HTTP.Auth.Bearer.Cert) if err != nil { - ctlr.Log.Panic().Err(err).Msg("failed to create bearer authorizer") + ctlr.Log.Panic().Err(err).Msg("failed to load certificate for bearer authentication") } + authorizer := NewBearerAuthorizer( + ctlr.Config.HTTP.Auth.Bearer.Realm, + ctlr.Config.HTTP.Auth.Bearer.Service, + certificate.PublicKey, + ) + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodOptions { @@ -425,8 +423,6 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { } acCtrlr := NewAccessController(ctlr.Config) - vars := mux.Vars(request) - name := vars["name"] // we want to bypass auth for mgmt route isMgmtRequested := request.RequestURI == constants.FullMgmt @@ -439,67 +435,40 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { return } - action := auth.PullAction - if m := request.Method; m != http.MethodGet && m != http.MethodHead { - action = auth.PushAction + var requestedAccess *ResourceAction + + if request.RequestURI != "/v2/" { + // if this is not the base route, the requested repository/action must be authorized + vars := mux.Vars(request) + name := vars["name"] + + action := "pull" + if m := request.Method; m != http.MethodGet && m != http.MethodHead { + action = "push" + } + + requestedAccess = &ResourceAction{ + Type: "repository", + Name: name, + Action: action, + } } - var permissions *auth.Permission - - // Empty scope should be allowed according to the distribution auth spec - // This is only necessary for the bearer auth type - if request.RequestURI == "/v2/" && authorizer.Type == auth.BearerAuthAuthorizerType { - if header == "" { - // first request that clients make (without any header) - WWWAuthenticateHeader := fmt.Sprintf("Bearer realm=\"%s\",service=\"%s\",scope=\"\"", - authorizer.Realm, authorizer.Service) - - permissions = &auth.Permission{ - // challenge for the client to use to authenticate to /v2/ - WWWAuthenticateHeader: WWWAuthenticateHeader, - Allowed: false, - } - } else { - // subsequent requests with token on /v2/ - bearerTokenMatch := regexp.MustCompile("(?i)bearer (.*)") - - signedString := bearerTokenMatch.ReplaceAllString(header, "$1") - - // If the token is valid, our job is done - // Since this is the /v2 base path and we didn't pass a scope to the auth header in the previous step - // there is no access check to enforce - _, err := authorizer.TokenDecoder.DecodeToken(signedString) - if err != nil { - ctlr.Log.Error().Err(err).Msg("failed to parse Authorization header") - response.Header().Set("Content-Type", "application/json") - zcommon.WriteJSON(response, http.StatusUnauthorized, apiErr.NewError(apiErr.UNSUPPORTED)) - - return - } - - permissions = &auth.Permission{ - Allowed: true, - } - } - } else { - var err error - - // subsequent requests with token on /v2// - permissions, err = authorizer.Authorize(header, action, name) - if err != nil { - ctlr.Log.Error().Err(err).Msg("failed to parse Authorization header") + err := authorizer.Authorize(header, requestedAccess) + if err != nil { + var challenge *AuthChallengeError + if errors.As(err, &challenge) { + ctlr.Log.Debug().Err(challenge).Msg("bearer token authorization failed") response.Header().Set("Content-Type", "application/json") - zcommon.WriteJSON(response, http.StatusInternalServerError, apiErr.NewError(apiErr.UNSUPPORTED)) + response.Header().Set("WWW-Authenticate", challenge.Header()) + zcommon.WriteJSON(response, http.StatusUnauthorized, apiErr.NewError(apiErr.UNAUTHORIZED)) return } - } - if !permissions.Allowed { + ctlr.Log.Error().Err(err).Msg("failed to parse Authorization header") response.Header().Set("Content-Type", "application/json") - response.Header().Set("WWW-Authenticate", permissions.WWWAuthenticateHeader) - - zcommon.WriteJSON(response, http.StatusUnauthorized, apiErr.NewError(apiErr.UNAUTHORIZED)) + zcommon.WriteJSON(response, http.StatusUnauthorized, apiErr.NewError(apiErr.UNSUPPORTED)) return } @@ -932,3 +901,22 @@ func GenerateAPIKey(uuidGenerator guuid.Generator, log log.Logger, return apiKey, apiKeyID.String(), err } + +func loadCertificateFromFile(path string) (*x509.Certificate, error) { + rawCert, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("%w: %w, path %s", zerr.ErrCouldNotLoadCertificate, err, path) + } + + block, _ := pem.Decode(rawCert) + if block == nil { + return nil, fmt.Errorf("%w: no valid PEM data found", zerr.ErrCouldNotLoadCertificate) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("%w: %w", zerr.ErrCouldNotLoadCertificate, err) + } + + return cert, nil +} diff --git a/pkg/api/bearer.go b/pkg/api/bearer.go new file mode 100644 index 00000000..861654c0 --- /dev/null +++ b/pkg/api/bearer.go @@ -0,0 +1,136 @@ +package api + +import ( + "crypto" + "fmt" + "regexp" + "slices" + + "github.com/golang-jwt/jwt/v5" + + zerr "zotregistry.dev/zot/errors" +) + +var bearerTokenMatch = regexp.MustCompile("(?i)bearer (.*)") + +// ResourceAccess is a single entry in the private 'access' claim specified by the distribution token authentication +// specification. +type ResourceAccess struct { + Type string `json:"type"` + Name string `json:"name"` + Actions []string `json:"actions"` +} + +type ResourceAction struct { + Type string `json:"type"` + Name string `json:"name"` + Action string `json:"action"` +} + +// ClaimsWithAccess is a claim set containing the private 'access' claim specified by the distribution token +// authentication specification, in addition to the standard registered claims. +// https://distribution.github.io/distribution/spec/auth/jwt/ +type ClaimsWithAccess struct { + Access []ResourceAccess `json:"access"` + jwt.RegisteredClaims +} + +type AuthChallengeError struct { + err error + realm string + service string + resourceAction *ResourceAction +} + +func (c AuthChallengeError) Error() string { + return c.err.Error() +} + +// Header constructs an appropriate value for the WWW-Authenticate header to be returned to the client. +func (c AuthChallengeError) Header() string { + if c.resourceAction == nil { + // no access was requested, so return an empty scope + return fmt.Sprintf("Bearer realm=\"%s\",service=\"%s\",scope=\"\"", + c.realm, c.service) + } + + return fmt.Sprintf("Bearer realm=\"%s\",service=\"%s\",scope=\"%s:%s:%s\"", + c.realm, c.service, c.resourceAction.Type, c.resourceAction.Name, c.resourceAction.Action) +} + +type BearerAuthorizer struct { + realm string + service string + key crypto.PublicKey +} + +func NewBearerAuthorizer(realm string, service string, key crypto.PublicKey) BearerAuthorizer { + return BearerAuthorizer{ + realm: realm, + service: service, + key: key, + } +} + +// Authorize verifies whether the bearer token in the given Authorization header is valid, and whether it has sufficient +// scope for the requested resource action. If an authorization error occurs (e.g. no token is given or the token has +// insufficient scope), an AuthChallengeError is returned as the error. +func (a *BearerAuthorizer) Authorize(header string, requested *ResourceAction) error { + challenge := &AuthChallengeError{ + realm: a.realm, + service: a.service, + resourceAction: requested, + } + + if header == "" { + // if no bearer token is set in the authorization header, return the authentication challenge + challenge.err = zerr.ErrNoBearerToken + + return challenge + } + + signedString := bearerTokenMatch.ReplaceAllString(header, "$1") + + token, err := jwt.ParseWithClaims(signedString, &ClaimsWithAccess{}, func(token *jwt.Token) (interface{}, error) { + return a.key, nil + }, jwt.WithValidMethods(a.allowedSigningAlgorithms()), jwt.WithIssuedAt()) + if err != nil { + return fmt.Errorf("%w: %w", zerr.ErrInvalidBearerToken, err) + } + + if requested == nil { + // the token is valid and no access is requested, so we do not have to validate the access claim + return nil + } + + claims, ok := token.Claims.(*ClaimsWithAccess) + if !ok { + return fmt.Errorf("%w: invalid claims type", zerr.ErrInvalidBearerToken) + } + + // check whether the requested access is allowed by the scope of the token + for _, allowed := range claims.Access { + if allowed.Type != requested.Type { + continue + } + + if allowed.Name != requested.Name { + continue + } + + if !slices.Contains(allowed.Actions, requested.Action) { + continue + } + + // requested action is allowed, so don't return an error + return nil + } + + challenge.err = zerr.ErrInsufficientScope + + return challenge +} + +func (a *BearerAuthorizer) allowedSigningAlgorithms() []string { + return []string{"EdDSA", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"} +} diff --git a/pkg/api/bearer_test.go b/pkg/api/bearer_test.go new file mode 100644 index 00000000..69d62d9e --- /dev/null +++ b/pkg/api/bearer_test.go @@ -0,0 +1,121 @@ +package api_test + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + . "github.com/smartystreets/goconvey/convey" + + zerr "zotregistry.dev/zot/errors" + "zotregistry.dev/zot/pkg/api" +) + +func TestBearerAuthorizer(t *testing.T) { + Convey("Test bearer token authorization", t, func() { + signingMethod := jwt.SigningMethodRS256 + + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + pubKey := privKey.Public() + + authorizer := api.NewBearerAuthorizer("realm", "service", pubKey) + + Convey("Empty authorization header given", func() { + err := authorizer.Authorize("", nil) + So(err, ShouldBeError, zerr.ErrNoBearerToken) + }) + + Convey("Valid token", func() { + access := []api.ResourceAccess{ + { + Name: "authorized-repository", + Type: "repository", + Actions: []string{"pull"}, + }, + } + + now := time.Now() + claims := api.ClaimsWithAccess{ + Access: access, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)), + IssuedAt: jwt.NewNumericDate(now), + Issuer: "Zot", + Audience: []string{"Zot Registry"}, + }, + } + + token, err := jwt.NewWithClaims(signingMethod, claims).SignedString(privKey) + if err != nil { + panic(err) + } + + authHeader := "Bearer " + token + + Convey("Unauthorized type", func() { + requested := &api.ResourceAction{ + Type: "registry", + Name: "catalog", + Action: "*", + } + + err := authorizer.Authorize(authHeader, requested) + So(err, ShouldHaveSameTypeAs, &api.AuthChallengeError{}) + So(err, ShouldBeError, zerr.ErrInsufficientScope) + }) + + Convey("Unauthorized name", func() { + requested := &api.ResourceAction{ + Type: "repository", + Name: "unauthorized-repository", + Action: "pull", + } + + err := authorizer.Authorize(authHeader, requested) + So(err, ShouldHaveSameTypeAs, &api.AuthChallengeError{}) + So(err, ShouldBeError, zerr.ErrInsufficientScope) + }) + + Convey("Unauthorized action", func() { + requested := &api.ResourceAction{ + Type: "repository", + Name: "authorized-repository", + Action: "push", + } + + err := authorizer.Authorize(authHeader, requested) + So(err, ShouldHaveSameTypeAs, &api.AuthChallengeError{}) + So(err, ShouldBeError, zerr.ErrInsufficientScope) + }) + + Convey("Successful authorization with requested access", func() { + requested := &api.ResourceAction{ + Type: "repository", + Name: "authorized-repository", + Action: "pull", + } + + err := authorizer.Authorize(authHeader, requested) + So(err, ShouldBeNil) + }) + + Convey("Successful authorization without requested access", func() { + err := authorizer.Authorize(authHeader, nil) + So(err, ShouldBeNil) + }) + }) + + Convey("Invalid token", func() { + authHeader := "invalid" + + err := authorizer.Authorize(authHeader, nil) + So(err, ShouldWrap, zerr.ErrInvalidBearerToken) + }) + }) +} diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 81063ef2..a6305359 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -78,6 +78,10 @@ const ( ServerCert = "../../test/data/server.cert" ServerKey = "../../test/data/server.key" CACert = "../../test/data/ca.crt" + ServerCertECDSA = "../../test/data/server-ecdsa.cert" + ServerKeyECDSA = "../../test/data/server-ecdsa.key" + ServerCertED25519 = "../../test/data/server-ed25519.cert" + ServerKeyED25519 = "../../test/data/server-ed25519.key" UnauthorizedNamespace = "fortknox/notallowed" AuthorizationNamespace = "authz/image" LDAPAddress = "127.0.0.1" @@ -2310,6 +2314,11 @@ func TestAuthnErrors(t *testing.T) { err := os.WriteFile(tmpFile, []byte("test"), 0o000) So(err, ShouldBeNil) + defer func() { + err := os.Chmod(tmpFile, 0o644) + So(err, ShouldBeNil) + }() + conf.HTTP.Auth.LDAP = (&config.LDAPConfig{ Insecure: true, Address: LDAPAddress, @@ -2324,9 +2333,6 @@ func TestAuthnErrors(t *testing.T) { So(func() { api.AuthHandler(ctlr) }, ShouldPanic) - - err = os.Chmod(tmpFile, 0o644) - So(err, ShouldBeNil) }) Convey("ldap CA certs is empty", t, func() { @@ -2387,6 +2393,11 @@ func TestAuthnErrors(t *testing.T) { err := os.WriteFile(tmpFile, []byte("test"), 0o000) So(err, ShouldBeNil) + defer func() { + err := os.Chmod(tmpFile, 0o644) + So(err, ShouldBeNil) + }() + conf.HTTP.Auth.HTPasswd = config.AuthHTPasswd{ Path: tmpFile, } @@ -2396,9 +2407,63 @@ func TestAuthnErrors(t *testing.T) { So(func() { api.AuthHandler(ctlr) }, ShouldPanic) + }) - err = os.Chmod(tmpFile, 0o644) + Convey("Bearer auth invalid PEM data", t, func() { + port := test.GetFreePort() + conf := config.New() + conf.HTTP.Port = port + tmpDir := t.TempDir() + tmpFile := path.Join(tmpDir, "invalid-server.cert") + + err := os.WriteFile(tmpFile, []byte("invalid"), 0o000) So(err, ShouldBeNil) + + defer func() { + err := os.Chmod(tmpFile, 0o644) + So(err, ShouldBeNil) + }() + + conf.HTTP.Auth.Bearer = &config.BearerConfig{ + Realm: "realm", + Service: "service", + Cert: tmpFile, + } + + ctlr := makeController(conf, t.TempDir()) + + So(func() { + api.AuthHandler(ctlr) + }, ShouldPanic) + }) + + Convey("Bearer auth invalid certificate", t, func() { + port := test.GetFreePort() + conf := config.New() + conf.HTTP.Port = port + tmpDir := t.TempDir() + tmpFile := path.Join(tmpDir, "invalid-server.cert") + + // 'invalid' encoded as PEM + err := os.WriteFile(tmpFile, []byte("-----BEGIN CERTIFICATE-----\naW52YWxpZA==\n-----END CERTIFICATE-----"), 0o000) + So(err, ShouldBeNil) + + defer func() { + err := os.Chmod(tmpFile, 0o644) + So(err, ShouldBeNil) + }() + + conf.HTTP.Auth.Bearer = &config.BearerConfig{ + Realm: "realm", + Service: "service", + Cert: tmpFile, + } + + ctlr := makeController(conf, t.TempDir()) + + So(func() { + api.AuthHandler(ctlr) + }, ShouldPanic) }) Convey("NewRelyingPartyGithub fail", t, func() { @@ -2411,6 +2476,11 @@ func TestAuthnErrors(t *testing.T) { err := os.WriteFile(tmpFile, []byte("test"), 0o000) So(err, ShouldBeNil) + defer func() { + err := os.Chmod(tmpFile, 0o644) + So(err, ShouldBeNil) + }() + conf.HTTP.Auth.HTPasswd = config.AuthHTPasswd{ Path: tmpFile, } @@ -2418,9 +2488,6 @@ func TestAuthnErrors(t *testing.T) { So(func() { api.NewRelyingPartyGithub(conf, "prov", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic) - - err = os.Chmod(tmpFile, 0o644) - So(err, ShouldBeNil) }) } @@ -3846,190 +3913,294 @@ func TestLDAPClient(t *testing.T) { }) } +func TestBearerAuthMultipleAlgorithms(t *testing.T) { + testCases := []struct { + name string + key string + cert string + alg string + }{ + { + "RSA signing key", + ServerKey, + ServerCert, + "RS256", + }, + { + "ECDSA signing key", + ServerKeyECDSA, + ServerCertECDSA, + "ES256", + }, + { + "ED25519 signing key", + ServerKeyED25519, + ServerCertED25519, + "EdDSA", + }, + } + + for _, testCase := range testCases { + Convey("Make a new controller with "+testCase.name, t, func() { + authTestServer := authutils.MakeAuthTestServer(testCase.key, testCase.alg, UnauthorizedNamespace) + defer authTestServer.Close() + + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) + + conf.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: testCase.cert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, + }, + } + ctlr := makeController(conf, t.TempDir()) + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + + defer cm.StopServer() + + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + var goodToken authutils.AccessTokenResponse + + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + } +} + func TestBearerAuth(t *testing.T) { - Convey("Make a new controller", t, func() { - authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace) - defer authTestServer.Close() + testCases := []struct { + name string + useLegacyAuthTestServer bool + }{ + { + name: "new authentication test server", + useLegacyAuthTestServer: false, + }, + { + name: "legacy authentication test server", + useLegacyAuthTestServer: true, + }, + } - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) + for _, testCase := range testCases { + Convey("Make a new controller with "+testCase.name, t, func() { + var authTestServer *httptest.Server + if testCase.useLegacyAuthTestServer { + authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, UnauthorizedNamespace) + } else { + authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", UnauthorizedNamespace) + } + defer authTestServer.Close() - conf := config.New() - conf.HTTP.Port = port + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) - aurl, err := url.Parse(authTestServer.URL) - So(err, ShouldBeNil) + conf := config.New() + conf.HTTP.Port = port - conf.HTTP.Auth = &config.AuthConfig{ - Bearer: &config.BearerConfig{ - Cert: ServerCert, - Realm: authTestServer.URL + "/auth/token", - Service: aurl.Host, - }, - } - ctlr := makeController(conf, t.TempDir()) + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) + conf.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, + }, + } + ctlr := makeController(conf, t.TempDir()) - defer cm.StopServer() + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) - blob := []byte("hello, blob!") - digest := godigest.FromBytes(blob).String() + defer cm.StopServer() - resp, err := resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() - authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) - resp, err = resty.R(). - SetQueryParam("service", authorizationHeader.Service). - Get(authorizationHeader.Realm) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var goodToken authutils.AccessTokenResponse + authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + var goodToken authutils.AccessTokenResponse - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - // trigger decode error - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+"invalidToken"). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - resp, err = resty.R().SetHeader("Authorization", - "Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) + // trigger decode error + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+"invalidToken"). + Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - s1, seed1 := test.GenerateRandomName() - s2, seed2 := test.GenerateRandomName() - repoName := s1 + "/" + s2 + resp, err = resty.R().SetHeader("Authorization", + "Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) - ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName") + s1, seed1 := test.GenerateRandomName() + s2, seed2 := test.GenerateRandomName() + repoName := s1 + "/" + s2 - resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName") - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc := resp.Header().Get("Location") + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - SetHeader("Content-Length", strconv.Itoa(len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc := resp.Header().Get("Location") - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R(). + SetHeader("Content-Length", strconv.Itoa(len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Content-Length", strconv.Itoa(len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/" + repoName + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Content-Length", strconv.Itoa(len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/" + repoName + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/" + repoName + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/" + repoName + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - authorizationHeader = authutils.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, http.StatusOK) + resp, err = resty.R(). + Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var badToken authutils.AccessTokenResponse + authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &badToken) - So(err, ShouldBeNil) + var badToken authutils.AccessTokenResponse - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+badToken.AccessToken). - Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - }) + err = json.Unmarshal(resp.Body(), &badToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+badToken.AccessToken). + Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }) + } } func TestBearerAuthWrongAuthorizer(t *testing.T) { @@ -4054,183 +4225,204 @@ func TestBearerAuthWrongAuthorizer(t *testing.T) { } func TestBearerAuthWithAllowReadAccess(t *testing.T) { - Convey("Make a new controller", t, func() { - authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace) - defer authTestServer.Close() + testCases := []struct { + name string + useLegacyAuthTestServer bool + }{ + { + name: "new authentication test server", + useLegacyAuthTestServer: false, + }, + { + name: "legacy authentication test server", + useLegacyAuthTestServer: true, + }, + } - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) + for _, testCase := range testCases { + Convey("Make a new controller with"+testCase.name, t, func() { + var authTestServer *httptest.Server + if testCase.useLegacyAuthTestServer { + authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, UnauthorizedNamespace) + } else { + authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", UnauthorizedNamespace) + } + defer authTestServer.Close() - conf := config.New() - conf.HTTP.Port = port + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) - aurl, err := url.Parse(authTestServer.URL) - So(err, ShouldBeNil) + conf := config.New() + conf.HTTP.Port = port - conf.HTTP.Auth = &config.AuthConfig{ - Bearer: &config.BearerConfig{ - Cert: ServerCert, - Realm: authTestServer.URL + "/auth/token", - Service: aurl.Host, - }, - } - ctlr := makeController(conf, t.TempDir()) + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) - conf.HTTP.AccessControl = &config.AccessControlConfig{ - Repositories: config.Repositories{ - test.AuthorizationAllRepos: config.PolicyGroup{ - AnonymousPolicy: []string{"read"}, + conf.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, }, - }, - } + } + ctlr := makeController(conf, t.TempDir()) - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Repositories: config.Repositories{ + test.AuthorizationAllRepos: config.PolicyGroup{ + AnonymousPolicy: []string{"read"}, + }, + }, + } - defer cm.StopServer() + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) - blob := []byte("hello, blob!") - digest := godigest.FromBytes(blob).String() + defer cm.StopServer() - resp, err := resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() - authorizationHeader := authutils.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, http.StatusOK) + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var goodToken authutils.AccessTokenResponse + authorizationHeader := authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + var goodToken authutils.AccessTokenResponse - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - s1, seed1 := test.GenerateRandomName() - s2, seed2 := test.GenerateRandomName() - repoName := s1 + "/" + s2 + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName") + s1, seed1 := test.GenerateRandomName() + s2, seed2 := test.GenerateRandomName() + repoName := s1 + "/" + s2 - resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName") - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc := resp.Header().Get("Location") + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - SetHeader("Content-Length", strconv.Itoa(len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Post(baseURL + "/v2/" + repoName + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc := resp.Header().Get("Location") - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R(). + SetHeader("Content-Length", strconv.Itoa(len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Content-Length", strconv.Itoa(len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/" + repoName + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Content-Length", strconv.Itoa(len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/" + repoName + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/" + repoName + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/" + repoName + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - authorizationHeader = authutils.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, http.StatusOK) + resp, err = resty.R(). + Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var badToken authutils.AccessTokenResponse - err = json.Unmarshal(resp.Body(), &badToken) - So(err, ShouldBeNil) + authorizationHeader = authutils.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, http.StatusOK) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+badToken.AccessToken). - Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - }) + var badToken authutils.AccessTokenResponse + err = json.Unmarshal(resp.Body(), &badToken) + So(err, ShouldBeNil) + + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+badToken.AccessToken). + Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }) + } } func TestNewRelyingPartyOIDC(t *testing.T) { diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index 66086523..d1fed0c7 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -6,6 +6,7 @@ package extensions_test import ( "encoding/json" "net/http" + "net/http/httptest" "net/url" "os" "testing" @@ -868,160 +869,181 @@ func TestMgmtExtension(t *testing.T) { } func TestMgmtWithBearer(t *testing.T) { - Convey("Make a new controller", t, func() { - authorizedNamespace := "allowedrepo" - unauthorizedNamespace := "notallowedrepo" + testCases := []struct { + name string + useLegacyAuthTestServer bool + }{ + { + name: "new authentication test server", + useLegacyAuthTestServer: false, + }, + { + name: "legacy authentication test server", + useLegacyAuthTestServer: true, + }, + } - authTestServer := authutils.MakeAuthTestServer(ServerKey, unauthorizedNamespace) - defer authTestServer.Close() + for _, testCase := range testCases { + Convey("Make a new controller with "+testCase.name, t, func() { + authorizedNamespace := "allowedrepo" + unauthorizedNamespace := "notallowedrepo" - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) + var authTestServer *httptest.Server + if testCase.useLegacyAuthTestServer { + authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, unauthorizedNamespace) + } else { + authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", unauthorizedNamespace) + } + defer authTestServer.Close() - conf := config.New() - conf.HTTP.Port = port + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) - aurl, err := url.Parse(authTestServer.URL) - So(err, ShouldBeNil) + conf := config.New() + conf.HTTP.Port = port - conf.HTTP.Auth = &config.AuthConfig{ - Bearer: &config.BearerConfig{ - Cert: ServerCert, - Realm: authTestServer.URL + "/auth/token", - Service: aurl.Host, - }, - } + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) - defaultValue := true + conf.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, + }, + } - conf.Extensions = &extconf.ExtensionConfig{} - conf.Extensions.Search = &extconf.SearchConfig{} - conf.Extensions.Search.Enable = &defaultValue - conf.Extensions.Search.CVE = nil - conf.Extensions.UI = &extconf.UIConfig{} - conf.Extensions.UI.Enable = &defaultValue + defaultValue := true - conf.Storage.RootDirectory = t.TempDir() + conf.Extensions = &extconf.ExtensionConfig{} + conf.Extensions.Search = &extconf.SearchConfig{} + conf.Extensions.Search.Enable = &defaultValue + conf.Extensions.Search.CVE = nil + conf.Extensions.UI = &extconf.UIConfig{} + conf.Extensions.UI.Enable = &defaultValue - ctlr := api.NewController(conf) + conf.Storage.RootDirectory = t.TempDir() - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) - defer cm.StopServer() + ctlr := api.NewController(conf) - resp, err := resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() - authorizationHeader := authutils.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, http.StatusOK) + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var goodToken authutils.AccessTokenResponse + authorizationHeader := authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + var goodToken authutils.AccessTokenResponse - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R().SetHeader("Authorization", - "Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - resp, err = resty.R().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R().SetHeader("Authorization", + "Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) - authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + resp, err = resty.R().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + authorizationHeader = authutils.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, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - resp, err = resty.R(). - Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - authorizationHeader = authutils.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, http.StatusOK) + resp, err = resty.R(). + Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var badToken authutils.AccessTokenResponse + authorizationHeader = authutils.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, http.StatusOK) - err = json.Unmarshal(resp.Body(), &badToken) - So(err, ShouldBeNil) + var badToken authutils.AccessTokenResponse - resp, err = resty.R(). - SetHeader("Authorization", "Bearer "+badToken.AccessToken). - Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + err = json.Unmarshal(resp.Body(), &badToken) + So(err, ShouldBeNil) - // test mgmt route - resp, err = resty.R().Get(baseURL + constants.FullMgmt) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + resp, err = resty.R(). + SetHeader("Authorization", "Bearer "+badToken.AccessToken). + Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - mgmtResp := extensions.StrippedConfig{} - err = json.Unmarshal(resp.Body(), &mgmtResp) - So(err, ShouldBeNil) - So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion) - So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil) - So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm) - So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service) - So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil) - So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) - So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse) + // test mgmt route + resp, err = resty.R().Get(baseURL + constants.FullMgmt) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmt) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + mgmtResp := extensions.StrippedConfig{} + err = json.Unmarshal(resp.Body(), &mgmtResp) + So(err, ShouldBeNil) + So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion) + So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil) + So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm) + So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service) + So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil) + So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) + So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse) - mgmtResp = extensions.StrippedConfig{} - err = json.Unmarshal(resp.Body(), &mgmtResp) - So(err, ShouldBeNil) - So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion) - So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil) - So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm) - So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service) - So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil) - So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) - So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse) - }) + resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmt) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + mgmtResp = extensions.StrippedConfig{} + err = json.Unmarshal(resp.Body(), &mgmtResp) + So(err, ShouldBeNil) + So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion) + So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil) + So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm) + So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service) + So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil) + So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil) + So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse) + }) + } } func TestAllowedMethodsHeaderMgmt(t *testing.T) { diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index 53abcae1..c7f2b423 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net/http" + "net/http/httptest" "net/url" "os" "path" @@ -2553,124 +2554,301 @@ func TestTLS(t *testing.T) { } func TestBearerAuth(t *testing.T) { - Convey("Verify periodically sync bearer auth", t, func() { - updateDuration, _ := time.ParseDuration("1h") - // a repo for which clients do not have access, sync shouldn't be able to sync it - unauthorizedNamespace := testCveImage + testCases := []struct { + name string + useLegacyAuthTestServer bool + }{ + { + name: "new authentication test server", + useLegacyAuthTestServer: false, + }, + { + name: "legacy authentication test server", + useLegacyAuthTestServer: true, + }, + } - authTestServer := authutils.MakeAuthTestServer(ServerKey, unauthorizedNamespace) - defer authTestServer.Close() + for _, testCase := range testCases { + Convey("Verify periodically sync bearer auth with "+testCase.name, t, func() { + updateDuration, _ := time.ParseDuration("1h") + // a repo for which clients do not have access, sync shouldn't be able to sync it + unauthorizedNamespace := testCveImage - sctlr, srcBaseURL, _, _, srcClient := makeUpstreamServer(t, false, false) + var authTestServer *httptest.Server + if testCase.useLegacyAuthTestServer { + authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, unauthorizedNamespace) + } else { + authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", unauthorizedNamespace) + } + defer authTestServer.Close() - aurl, err := url.Parse(authTestServer.URL) - So(err, ShouldBeNil) + sctlr, srcBaseURL, _, _, srcClient := makeUpstreamServer(t, false, false) - sctlr.Config.HTTP.Auth = &config.AuthConfig{ - Bearer: &config.BearerConfig{ - Cert: ServerCert, - Realm: authTestServer.URL + "/auth/token", - Service: aurl.Host, - }, - } + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) - scm := test.NewControllerManager(sctlr) - scm.StartAndWait(sctlr.Config.HTTP.Port) - - defer scm.StopServer() - - registryName := sync.StripRegistryTransport(srcBaseURL) - credentialsFile := makeCredentialsFile(fmt.Sprintf(`{"%s":{"username": "%s", "password": "%s"}}`, - registryName, username, password)) - - var tlsVerify bool - - syncRegistryConfig := syncconf.RegistryConfig{ - Content: []syncconf.Content{ - { - Prefix: "**", // sync everything + sctlr.Config.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, }, - }, - URLs: []string{srcBaseURL}, - PollInterval: updateDuration, - TLSVerify: &tlsVerify, - CertDir: "", - } + } - defaultVal := true - syncConfig := &syncconf.Config{ - Enable: &defaultVal, - CredentialsFile: credentialsFile, - Registries: []syncconf.RegistryConfig{syncRegistryConfig}, - } + scm := test.NewControllerManager(sctlr) + scm.StartAndWait(sctlr.Config.HTTP.Port) - dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig) + defer scm.StopServer() - dcm := test.NewControllerManager(dctlr) - dcm.StartAndWait(dctlr.Config.HTTP.Port) + registryName := sync.StripRegistryTransport(srcBaseURL) + credentialsFile := makeCredentialsFile(fmt.Sprintf(`{"%s":{"username": "%s", "password": "%s"}}`, + registryName, username, password)) - defer dcm.StopServer() + var tlsVerify bool - var ( - srcTagsList TagsList - destTagsList TagsList - ) + syncRegistryConfig := syncconf.RegistryConfig{ + Content: []syncconf.Content{ + { + Prefix: "**", // sync everything + }, + }, + URLs: []string{srcBaseURL}, + PollInterval: updateDuration, + TLSVerify: &tlsVerify, + CertDir: "", + } - resp, err := srcClient.R().Get(srcBaseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + defaultVal := true + syncConfig := &syncconf.Config{ + Enable: &defaultVal, + CredentialsFile: credentialsFile, + Registries: []syncconf.RegistryConfig{syncRegistryConfig}, + } - authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) - resp, err = resty.R(). - SetQueryParam("service", authorizationHeader.Service). - Get(authorizationHeader.Realm) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig) - var goodToken authutils.AccessTokenResponse + dcm := test.NewControllerManager(dctlr) + dcm.StartAndWait(dctlr.Config.HTTP.Port) - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + defer dcm.StopServer() - resp, err = srcClient.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(srcBaseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + var ( + srcTagsList TagsList + destTagsList TagsList + ) - resp, err = srcClient.R().Get(srcBaseURL + "/v2/" + testImage + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + resp, err := srcClient.R().Get(srcBaseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - authorizationHeader = authutils.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, http.StatusOK) + authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - goodToken = authutils.AccessTokenResponse{} - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) + var goodToken authutils.AccessTokenResponse - resp, err = srcClient.R().SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(srcBaseURL + "/v2/" + testImage + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) - err = json.Unmarshal(resp.Body(), &srcTagsList) - if err != nil { - panic(err) - } + resp, err = srcClient.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(srcBaseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = srcClient.R().Get(srcBaseURL + "/v2/" + testImage + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + authorizationHeader = authutils.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, http.StatusOK) + + goodToken = authutils.AccessTokenResponse{} + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = srcClient.R().SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(srcBaseURL + "/v2/" + testImage + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &srcTagsList) + if err != nil { + panic(err) + } + + for { + resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list") + if err != nil { + panic(err) + } + + err = json.Unmarshal(resp.Body(), &destTagsList) + if err != nil { + panic(err) + } + + if len(destTagsList.Tags) > 0 { + break + } + + time.Sleep(500 * time.Millisecond) + } + + So(destTagsList, ShouldResemble, srcTagsList) + + waitSyncFinish(dctlr.Config.Log.Output) + + resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // unauthorized namespace + resp, err = destClient.R().Get(destBaseURL + "/v2/" + testCveImage + "/manifests/" + testImageTag) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) + + Convey("Verify ondemand sync bearer auth", t, func() { + // a repo for which clients do not have access, sync shouldn't be able to sync it + unauthorizedNamespace := testCveImage + + var authTestServer *httptest.Server + if testCase.useLegacyAuthTestServer { + authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, unauthorizedNamespace) + } else { + authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", unauthorizedNamespace) + } + defer authTestServer.Close() + + sctlr, srcBaseURL, _, _, srcClient := makeUpstreamServer(t, false, false) + + aurl, err := url.Parse(authTestServer.URL) + So(err, ShouldBeNil) + + sctlr.Config.HTTP.Auth = &config.AuthConfig{ + Bearer: &config.BearerConfig{ + Cert: ServerCert, + Realm: authTestServer.URL + "/auth/token", + Service: aurl.Host, + }, + } + + scm := test.NewControllerManager(sctlr) + scm.StartAndWait(sctlr.Config.HTTP.Port) + + defer scm.StopServer() + + registryName := sync.StripRegistryTransport(srcBaseURL) + credentialsFile := makeCredentialsFile(fmt.Sprintf(`{"%s":{"username": "%s", "password": "%s"}}`, + registryName, username, password)) + + var tlsVerify bool + + syncRegistryConfig := syncconf.RegistryConfig{ + Content: []syncconf.Content{ + { + Prefix: "**", // sync everything + }, + }, + URLs: []string{srcBaseURL}, + TLSVerify: &tlsVerify, + OnDemand: true, + CertDir: "", + } + + defaultVal := true + syncConfig := &syncconf.Config{ + Enable: &defaultVal, + CredentialsFile: credentialsFile, + Registries: []syncconf.RegistryConfig{syncRegistryConfig}, + } + + dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig) + + dcm := test.NewControllerManager(dctlr) + dcm.StartAndWait(dctlr.Config.HTTP.Port) + + defer dcm.StopServer() + + var ( + srcTagsList TagsList + destTagsList TagsList + ) + + resp, err := srcClient.R().Get(srcBaseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) + resp, err = resty.R(). + SetQueryParam("service", authorizationHeader.Service). + Get(authorizationHeader.Realm) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + var goodToken authutils.AccessTokenResponse + + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = srcClient.R(). + SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(srcBaseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = srcClient.R().Get(srcBaseURL + "/v2/" + testImage + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + authorizationHeader = authutils.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, http.StatusOK) + + goodToken = authutils.AccessTokenResponse{} + err = json.Unmarshal(resp.Body(), &goodToken) + So(err, ShouldBeNil) + + resp, err = srcClient.R().SetHeader("Authorization", "Bearer "+goodToken.AccessToken). + Get(srcBaseURL + "/v2/" + testImage + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &srcTagsList) + if err != nil { + panic(err) + } + + // sync on demand + resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) - for { resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list") if err != nil { panic(err) @@ -2681,165 +2859,14 @@ func TestBearerAuth(t *testing.T) { panic(err) } - if len(destTagsList.Tags) > 0 { - break - } + So(destTagsList, ShouldResemble, srcTagsList) - time.Sleep(500 * time.Millisecond) - } - - So(destTagsList, ShouldResemble, srcTagsList) - - waitSyncFinish(dctlr.Config.Log.Output) - - resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // unauthorized namespace - resp, err = destClient.R().Get(destBaseURL + "/v2/" + testCveImage + "/manifests/" + testImageTag) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) - }) - - Convey("Verify ondemand sync bearer auth", t, func() { - // a repo for which clients do not have access, sync shouldn't be able to sync it - unauthorizedNamespace := testCveImage - - authTestServer := authutils.MakeAuthTestServer(ServerKey, unauthorizedNamespace) - defer authTestServer.Close() - - sctlr, srcBaseURL, _, _, srcClient := makeUpstreamServer(t, false, false) - - aurl, err := url.Parse(authTestServer.URL) - So(err, ShouldBeNil) - - sctlr.Config.HTTP.Auth = &config.AuthConfig{ - Bearer: &config.BearerConfig{ - Cert: ServerCert, - Realm: authTestServer.URL + "/auth/token", - Service: aurl.Host, - }, - } - - scm := test.NewControllerManager(sctlr) - scm.StartAndWait(sctlr.Config.HTTP.Port) - - defer scm.StopServer() - - registryName := sync.StripRegistryTransport(srcBaseURL) - credentialsFile := makeCredentialsFile(fmt.Sprintf(`{"%s":{"username": "%s", "password": "%s"}}`, - registryName, username, password)) - - var tlsVerify bool - - syncRegistryConfig := syncconf.RegistryConfig{ - Content: []syncconf.Content{ - { - Prefix: "**", // sync everything - }, - }, - URLs: []string{srcBaseURL}, - TLSVerify: &tlsVerify, - OnDemand: true, - CertDir: "", - } - - defaultVal := true - syncConfig := &syncconf.Config{ - Enable: &defaultVal, - CredentialsFile: credentialsFile, - Registries: []syncconf.RegistryConfig{syncRegistryConfig}, - } - - dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig) - - dcm := test.NewControllerManager(dctlr) - dcm.StartAndWait(dctlr.Config.HTTP.Port) - - defer dcm.StopServer() - - var ( - srcTagsList TagsList - destTagsList TagsList - ) - - resp, err := srcClient.R().Get(srcBaseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - - authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate")) - resp, err = resty.R(). - SetQueryParam("service", authorizationHeader.Service). - Get(authorizationHeader.Realm) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - var goodToken authutils.AccessTokenResponse - - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) - - resp, err = srcClient.R(). - SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(srcBaseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - resp, err = srcClient.R().Get(srcBaseURL + "/v2/" + testImage + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - - authorizationHeader = authutils.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, http.StatusOK) - - goodToken = authutils.AccessTokenResponse{} - err = json.Unmarshal(resp.Body(), &goodToken) - So(err, ShouldBeNil) - - resp, err = srcClient.R().SetHeader("Authorization", "Bearer "+goodToken.AccessToken). - Get(srcBaseURL + "/v2/" + testImage + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - err = json.Unmarshal(resp.Body(), &srcTagsList) - if err != nil { - panic(err) - } - - // sync on demand - resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list") - if err != nil { - panic(err) - } - - err = json.Unmarshal(resp.Body(), &destTagsList) - if err != nil { - panic(err) - } - - So(destTagsList, ShouldResemble, srcTagsList) - - // unauthorized namespace - resp, err = destClient.R().Get(destBaseURL + "/v2/" + testCveImage + "/manifests/" + testImageTag) - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) - }) + // unauthorized namespace + resp, err = destClient.R().Get(destBaseURL + "/v2/" + testCveImage + "/manifests/" + testImageTag) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) + } } func TestBasicAuth(t *testing.T) { diff --git a/pkg/test/auth/bearer.go b/pkg/test/auth/bearer.go index f305d5b0..eada8d5c 100644 --- a/pkg/test/auth/bearer.go +++ b/pkg/test/auth/bearer.go @@ -1,15 +1,20 @@ package auth import ( + "crypto" "fmt" "net/http" "net/http/httptest" + "os" "regexp" "strings" "time" "github.com/chartmuseum/auth" + "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" + + "zotregistry.dev/zot/pkg/api" ) type ( @@ -24,7 +29,60 @@ type ( } ) -func MakeAuthTestServer(serverKey string, unauthorizedNamespace string) *httptest.Server { +func MakeAuthTestServer(serverKey, signAlg string, unauthorizedNamespace string) *httptest.Server { + signingKey := loadPrivateKeyFromFile(serverKey) + signingMethod := jwt.GetSigningMethod(signAlg) + + authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + var access []api.ResourceAccess + + scope := request.URL.Query().Get("scope") + if scope != "" { + parts := strings.Split(scope, ":") + name := parts[1] + actions := strings.Split(parts[2], ",") + + if name == unauthorizedNamespace { + actions = []string{} + } + + access = []api.ResourceAccess{ + { + Name: name, + Type: "repository", + Actions: actions, + }, + } + } + + now := time.Now() + claims := api.ClaimsWithAccess{ + Access: access, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)), + IssuedAt: jwt.NewNumericDate(now), + Issuer: "Zot", + Audience: []string{"Zot Registry"}, + }, + } + + token := jwt.NewWithClaims(signingMethod, claims) + + signedString, err := token.SignedString(signingKey) + if err != nil { + panic(err) + } + + response.Header().Set("Content-Type", "application/json") + fmt.Fprintf(response, `{"access_token": "%s"}`, signedString) + })) + + return authTestServer +} + +// MakeAuthTestServerLegacy makes a test HTTP server to generate bearer tokens using the github.com/chartmuseum/auth +// package, to verify backward compatibility of the token authentication process with older versions of zot. +func MakeAuthTestServerLegacy(serverKey string, unauthorizedNamespace string) *httptest.Server { cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{ PrivateKeyPath: serverKey, Audience: "Zot Registry", @@ -85,3 +143,27 @@ func ParseBearerAuthHeader(authHeaderRaw string) *AuthHeader { return &h } + +func loadPrivateKeyFromFile(path string) crypto.PrivateKey { + privateKeyBytes, err := os.ReadFile(path) + if err != nil { + panic(err) + } + + rsaKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes) + if err == nil { + return rsaKey + } + + ecKey, err := jwt.ParseECPrivateKeyFromPEM(privateKeyBytes) + if err == nil { + return ecKey + } + + edKey, err := jwt.ParseEdPrivateKeyFromPEM(privateKeyBytes) + if err == nil { + return edKey + } + + panic("no valid private key found in file " + path) +} diff --git a/pkg/test/auth/bearer_test.go b/pkg/test/auth/bearer_test.go index 148e2f73..9fcea60c 100644 --- a/pkg/test/auth/bearer_test.go +++ b/pkg/test/auth/bearer_test.go @@ -10,6 +10,12 @@ import ( func TestBearerServer(t *testing.T) { Convey("test MakeAuthTestServer() no serve key", t, func() { - So(func() { auth.MakeAuthTestServer("", "") }, ShouldPanic) + So(func() { auth.MakeAuthTestServer("", "", "") }, ShouldPanic) + }) +} + +func TestBearerServerLegacy(t *testing.T) { + Convey("test MakeAuthTestServerLegacy() no serve key", t, func() { + So(func() { auth.MakeAuthTestServerLegacy("", "") }, ShouldPanic) }) } diff --git a/test/scripts/gen_certs.sh b/test/scripts/gen_certs.sh index c52f6247..2351dd90 100755 --- a/test/scripts/gen_certs.sh +++ b/test/scripts/gen_certs.sh @@ -2,6 +2,7 @@ set -xe +# RSA openssl req \ -newkey rsa:2048 \ -nodes \ @@ -45,3 +46,78 @@ openssl x509 \ -CAkey ca.key \ -CAcreateserial \ -out client.cert + +# ECDSA +openssl ecparam \ + -name prime256v1 \ + -genkey \ + -noout \ + -out ca-ecdsa.key + +openssl req \ + -new \ + -key ca-ecdsa.key \ + -nodes \ + -days 3650 \ + -x509 \ + -out ca-ecdsa.crt \ + -subj "/CN=*" + +openssl ecparam \ + -name prime256v1 \ + -genkey \ + -noout \ + -out server-ecdsa.key + +openssl req \ + -new \ + -key server-ecdsa.key \ + -nodes \ + -out server-ecdsa.csr \ + -subj "/OU=TestServer/CN=*" + +openssl x509 \ + -req \ + -days 3650 \ + -sha256 \ + -in server-ecdsa.csr \ + -CA ca-ecdsa.crt \ + -CAkey ca-ecdsa.key \ + -CAcreateserial \ + -out server-ecdsa.cert \ + -extfile <(echo subjectAltName = IP:127.0.0.1) + +# ED25519 +openssl genpkey \ + -algorithm ed25519 \ + -out ca-ed25519.key + +openssl req \ + -new \ + -key ca-ed25519.key \ + -nodes \ + -days 3650 \ + -x509 \ + -out ca-ed25519.crt \ + -subj "/CN=*" + +openssl genpkey \ + -algorithm ed25519 \ + -out server-ed25519.key + +openssl req \ + -new \ + -key server-ed25519.key \ + -nodes \ + -out server-ed25519.csr \ + -subj "/OU=TestServer/CN=*" + +openssl x509 \ + -req \ + -days 3650 \ + -in server-ed25519.csr \ + -CA ca-ed25519.crt \ + -CAkey ca-ed25519.key \ + -CAcreateserial \ + -out server-ed25519.cert \ + -extfile <(echo subjectAltName = IP:127.0.0.1)