0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-04-01 02:42:32 -05:00

[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 <git@evanus.nl>

* fix: shadow err variable to hopefully avoid data race

Signed-off-by: evanebb <git@evanus.nl>

* fix: apply golangci-lint feedback

Signed-off-by: evanebb <git@evanus.nl>

* fix: simplify public key loading by only supporting certificates, fixes ED25519 certificate handling

Signed-off-by: evanebb <git@evanus.nl>

* test: add golang-jwt based test auth server and test RSA/EC/ED25519 keys

Signed-off-by: evanebb <git@evanus.nl>

* fix: restrict allowed signing algorithms as recommended by library

Signed-off-by: evanebb <git@evanus.nl>

* test: add more bearer authorizer tests

Signed-off-by: evanebb <git@evanus.nl>

* fix: apply more golangci-lint feedback

Signed-off-by: evanebb <git@evanus.nl>

* test: ensure chmod calls run on test failure for authn errors test

Signed-off-by: evanebb <git@evanus.nl>

* fix: verify issued-at in given token if present
Pulls the validation in-line with the old library

Signed-off-by: evanebb <git@evanus.nl>

---------

Signed-off-by: evanebb <git@evanus.nl>
This commit is contained in:
Evan 2025-03-06 23:32:13 +01:00 committed by GitHub
parent e7fb9c5e60
commit d465690630
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1413 additions and 759 deletions

View file

@ -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")
)

2
go.mod
View file

@ -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

View file

@ -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/<resource>/
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
}

136
pkg/api/bearer.go Normal file
View file

@ -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"}
}

121
pkg/api/bearer_test.go Normal file
View file

@ -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)
})
})
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)
})
}

View file

@ -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)