0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

fix(mgmt): skip bearer authn for mgmt route (#1267)

Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
peusebiu 2023-03-16 21:02:59 +02:00 committed by GitHub
parent 150ee88945
commit 4d0bbf1e00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 269 additions and 85 deletions

View file

@ -55,7 +55,18 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
}
vars := mux.Vars(request)
name := vars["name"]
// we want to bypass auth for mgmt route
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
header := request.Header.Get("Authorization")
if (header == "" || header == "Basic Og==") && isMgmtRequested {
next.ServeHTTP(response, request)
return
}
action := auth.PullAction
if m := request.Method; m != http.MethodGet && m != http.MethodHead {
action = auth.PushAction

View file

@ -19,15 +19,12 @@ import (
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/chartmuseum/auth"
"github.com/gorilla/mux"
"github.com/mitchellh/mapstructure"
vldap "github.com/nmcclain/ldap"
notreg "github.com/notaryproject/notation-go/registry"
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
@ -72,18 +69,6 @@ const (
AuthorizationAllRepos = "**"
)
type (
accessTokenResponse struct {
AccessToken string `json:"access_token"` //nolint:tagliatelle // token format
}
authHeader struct {
Realm string
Service string
Scope string
}
)
func getCredString(username, password string) string {
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
if err != nil {
@ -1892,7 +1877,7 @@ func TestLDAPFailures(t *testing.T) {
func TestBearerAuth(t *testing.T) {
Convey("Make a new controller", t, func() {
authTestServer := makeAuthTestServer()
authTestServer := test.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
defer authTestServer.Close()
port := test.GetFreePort()
@ -1925,7 +1910,7 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader := parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader := test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -1933,7 +1918,7 @@ func TestBearerAuth(t *testing.T) {
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var goodToken accessTokenResponse
var goodToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
@ -1955,7 +1940,7 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -1984,7 +1969,7 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2013,7 +1998,7 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2037,7 +2022,7 @@ func TestBearerAuth(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2045,7 +2030,7 @@ func TestBearerAuth(t *testing.T) {
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var badToken accessTokenResponse
var badToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
@ -2060,7 +2045,7 @@ func TestBearerAuth(t *testing.T) {
func TestBearerAuthWithAllowReadAccess(t *testing.T) {
Convey("Make a new controller", t, func() {
authTestServer := makeAuthTestServer()
authTestServer := test.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
defer authTestServer.Close()
port := test.GetFreePort()
@ -2101,7 +2086,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader := parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader := test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2109,7 +2094,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var goodToken accessTokenResponse
var goodToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
@ -2125,7 +2110,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2154,7 +2139,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2183,7 +2168,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2207,7 +2192,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
@ -2215,7 +2200,7 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var badToken accessTokenResponse
var badToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
@ -2228,60 +2213,6 @@ func TestBearerAuthWithAllowReadAccess(t *testing.T) {
})
}
func makeAuthTestServer() *httptest.Server {
cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{
PrivateKeyPath: ServerKey,
Audience: "Zot Registry",
Issuer: "Zot",
AddKIDHeader: true,
})
if err != nil {
panic(err)
}
authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
scope := request.URL.Query().Get("scope")
parts := strings.Split(scope, ":")
name := parts[1]
actions := strings.Split(parts[2], ",")
if name == UnauthorizedNamespace {
actions = []string{}
}
access := []auth.AccessEntry{
{
Name: name,
Type: "repository",
Actions: actions,
},
}
token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1)
if err != nil {
panic(err)
}
response.Header().Set("Content-Type", "application/json")
fmt.Fprintf(response, `{"access_token": "%s"}`, token)
}))
return authTestServer
}
func parseBearerAuthHeader(authHeaderRaw string) *authHeader {
re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`)
matches := re.FindAllStringSubmatch(authHeaderRaw, -1)
matchmap := make(map[string]string)
for i := 0; i < len(matches); i++ {
matchmap[matches[i][1]] = matches[i][2]
}
var h authHeader
if err := mapstructure.Decode(matchmap, &h); err != nil {
panic(err)
}
return &h
}
func TestAuthorizationWithBasicAuth(t *testing.T) {
Convey("Make a new controller", t, func() {
port := test.GetFreePort()

View file

@ -5,7 +5,9 @@ package extensions_test
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"testing"
@ -21,6 +23,11 @@ import (
"zotregistry.io/zot/pkg/test"
)
const (
ServerCert = "../../test/data/server.cert"
ServerKey = "../../test/data/server.key"
)
func TestEnableExtension(t *testing.T) {
Convey("Verify log if sync disabled in config", t, func() {
globalDir := t.TempDir()
@ -509,3 +516,153 @@ func TestMgmtExtension(t *testing.T) {
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
})
}
func TestMgmtWithBearer(t *testing.T) {
Convey("Make a new controller", t, func() {
authorizedNamespace := "allowedrepo"
unauthorizedNamespace := "notallowedrepo"
authTestServer := test.MakeAuthTestServer(ServerKey, 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: ServerCert,
Realm: authTestServer.URL + "/auth/token",
Service: aurl.Host,
},
}
defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Mgmt = &extconf.MgmtConfig{
BaseConfig: extconf.BaseConfig{
Enable: &defaultValue,
},
}
conf.Storage.RootDirectory = t.TempDir()
ctlr := api.NewController(conf)
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 := test.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)
var goodToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().SetHeader("Authorization",
fmt.Sprintf("Bearer %s", goodToken.AccessToken)).Options(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
resp, err = resty.R().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = test.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", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().
Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = test.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)
var badToken test.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
// test mgmt route
resp, err = resty.R().Get(baseURL + constants.FullMgmtPrefix)
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)
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmtPrefix)
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)
})
}

79
pkg/test/bearer.go Normal file
View file

@ -0,0 +1,79 @@
package test
import (
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"time"
"github.com/chartmuseum/auth"
"github.com/mitchellh/mapstructure"
)
type (
AccessTokenResponse struct {
AccessToken string `json:"access_token"` //nolint:tagliatelle // token format
}
AuthHeader struct {
Realm string
Service string
Scope string
}
)
func MakeAuthTestServer(serverKey string, unauthorizedNamespace string) *httptest.Server {
cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{
PrivateKeyPath: serverKey,
Audience: "Zot Registry",
Issuer: "Zot",
AddKIDHeader: true,
})
if err != nil {
panic(err)
}
authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
scope := request.URL.Query().Get("scope")
parts := strings.Split(scope, ":")
name := parts[1]
actions := strings.Split(parts[2], ",")
if name == unauthorizedNamespace {
actions = []string{}
}
access := []auth.AccessEntry{
{
Name: name,
Type: "repository",
Actions: actions,
},
}
token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1)
if err != nil {
panic(err)
}
response.Header().Set("Content-Type", "application/json")
fmt.Fprintf(response, `{"access_token": "%s"}`, token)
}))
return authTestServer
}
func ParseBearerAuthHeader(authHeaderRaw string) *AuthHeader {
re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`)
matches := re.FindAllStringSubmatch(authHeaderRaw, -1)
matchmap := make(map[string]string)
for i := 0; i < len(matches); i++ {
matchmap[matches[i][1]] = matches[i][2]
}
var h AuthHeader
if err := mapstructure.Decode(matchmap, &h); err != nil {
panic(err)
}
return &h
}

View file

@ -1336,3 +1336,9 @@ func TestWriteImageToFileSystem(t *testing.T) {
So(err, ShouldNotBeNil)
})
}
func TestBearerServer(t *testing.T) {
Convey("test MakeAuthTestServer() no serve key", t, func() {
So(func() { test.MakeAuthTestServer("", "") }, ShouldPanic)
})
}