From 4d0bbf1e0095bbf282b5e2cec2c5d0f024aa98d5 Mon Sep 17 00:00:00 2001 From: peusebiu Date: Thu, 16 Mar 2023 21:02:59 +0200 Subject: [PATCH] fix(mgmt): skip bearer authn for mgmt route (#1267) Signed-off-by: Petu Eusebiu --- pkg/api/authn.go | 11 +++ pkg/api/controller_test.go | 101 +++---------------- pkg/extensions/extensions_test.go | 157 ++++++++++++++++++++++++++++++ pkg/test/bearer.go | 79 +++++++++++++++ pkg/test/common_test.go | 6 ++ 5 files changed, 269 insertions(+), 85 deletions(-) create mode 100644 pkg/test/bearer.go diff --git a/pkg/api/authn.go b/pkg/api/authn.go index aa94ad71..46ec5de1 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -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 diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 06df2739..a3a0a9ce 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -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() diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index 9288752d..a838376e 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -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) + }) +} diff --git a/pkg/test/bearer.go b/pkg/test/bearer.go new file mode 100644 index 00000000..662e7015 --- /dev/null +++ b/pkg/test/bearer.go @@ -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 +} diff --git a/pkg/test/common_test.go b/pkg/test/common_test.go index 88a0cbb8..99d5c9f8 100644 --- a/pkg/test/common_test.go +++ b/pkg/test/common_test.go @@ -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) + }) +}