mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
fix: CVE-2025-30204 - golang-jwt DoS vulnerability via excessive memory allocation (#3687)
* fix: CVE-2025-30204 - golang-jwt DoS vulnerability via excessive memory allocation Signed-off-by: Asgeir Nilsen <asgeir@twingine.no> * fix: linting Signed-off-by: Asgeir Nilsen <asgeir@twingine.no> * chore: update project-zot/mockoidc to remove golang-jwt v3 Signed-off-by: Asgeir Nilsen <asgeir@twingine.no> * test: Add more tests for bearer tokens Signed-off-by: Asgeir Nilsen <asgeir@twingine.no> * fix: Rewrite tests to remove MakeAuthTestServerLegacy Signed-off-by: Asgeir Nilsen <asgeir@twingine.no> --------- Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>
This commit is contained in:
committed by
GitHub
parent
e2ba7c8e20
commit
708adf63d4
+305
-347
@@ -3524,213 +3524,192 @@ func TestBearerAuthMultipleAlgorithms(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBearerAuth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
useLegacyAuthTestServer bool
|
||||
}{
|
||||
{
|
||||
name: "new authentication test server",
|
||||
useLegacyAuthTestServer: false,
|
||||
},
|
||||
{
|
||||
name: "legacy authentication test server",
|
||||
useLegacyAuthTestServer: true,
|
||||
},
|
||||
}
|
||||
Convey("Make a new controller", t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath, _, _ := setupBearerAuthServerCerts(t, tlsutils.KeyTypeRSA)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
Convey("Make a new controller with "+testCase.name, t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath, _, _ := setupBearerAuthServerCerts(t, tlsutils.KeyTypeRSA)
|
||||
authTestServer := authutils.MakeAuthTestServer(serverKeyPath, "RS256", UnauthorizedNamespace)
|
||||
defer authTestServer.Close()
|
||||
|
||||
var authTestServer *httptest.Server
|
||||
if testCase.useLegacyAuthTestServer {
|
||||
authTestServer = authutils.MakeAuthTestServerLegacy(serverKeyPath, UnauthorizedNamespace)
|
||||
} else {
|
||||
authTestServer = authutils.MakeAuthTestServer(serverKeyPath, "RS256", UnauthorizedNamespace)
|
||||
}
|
||||
defer authTestServer.Close()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
ctlr := makeController(conf, t.TempDir())
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
ctlr := makeController(conf, t.TempDir())
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
defer cm.StopServer()
|
||||
|
||||
defer cm.StopServer()
|
||||
blob := []byte("hello, blob!")
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
|
||||
blob := []byte("hello, blob!")
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
resp, err := resty.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Authorization", "Bearer "+goodToken.AccessToken).
|
||||
Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// 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)
|
||||
|
||||
// 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).Options(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
|
||||
resp, err = resty.R().SetHeader("Authorization",
|
||||
"Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
||||
s1, seed1 := test.GenerateRandomName()
|
||||
s2, seed2 := test.GenerateRandomName()
|
||||
repoName := s1 + "/" + s2
|
||||
|
||||
s1, seed1 := test.GenerateRandomName()
|
||||
s2, seed2 := test.GenerateRandomName()
|
||||
repoName := s1 + "/" + s2
|
||||
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
|
||||
|
||||
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
|
||||
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().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/")
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
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")
|
||||
|
||||
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")
|
||||
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").
|
||||
SetQueryParam("digest", digest).
|
||||
SetBody(blob).
|
||||
Put(baseURL + loc)
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
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").
|
||||
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)
|
||||
|
||||
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)
|
||||
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.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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
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.StatusOK)
|
||||
|
||||
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)
|
||||
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().
|
||||
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
var badToken authutils.AccessTokenResponse
|
||||
|
||||
var badToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &badToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
@@ -3755,207 +3734,186 @@ func TestBearerAuthWrongAuthorizer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBearerAuthWithAllowReadAccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
useLegacyAuthTestServer bool
|
||||
}{
|
||||
{
|
||||
name: "new authentication test server",
|
||||
useLegacyAuthTestServer: false,
|
||||
},
|
||||
{
|
||||
name: "legacy authentication test server",
|
||||
useLegacyAuthTestServer: true,
|
||||
},
|
||||
}
|
||||
Convey("Make a new controller", t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath, _, _ := setupBearerAuthServerCerts(t, tlsutils.KeyTypeRSA)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
Convey("Make a new controller with"+testCase.name, t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath, _, _ := setupBearerAuthServerCerts(t, tlsutils.KeyTypeRSA)
|
||||
authTestServer := authutils.MakeAuthTestServer(serverKeyPath, "RS256", UnauthorizedNamespace)
|
||||
defer authTestServer.Close()
|
||||
|
||||
var authTestServer *httptest.Server
|
||||
if testCase.useLegacyAuthTestServer {
|
||||
authTestServer = authutils.MakeAuthTestServerLegacy(serverKeyPath, UnauthorizedNamespace)
|
||||
} else {
|
||||
authTestServer = authutils.MakeAuthTestServer(serverKeyPath, "RS256", UnauthorizedNamespace)
|
||||
}
|
||||
defer authTestServer.Close()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
ctlr := makeController(conf, t.TempDir())
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
||||
Repositories: config.Repositories{
|
||||
test.AuthorizationAllRepos: config.PolicyGroup{
|
||||
AnonymousPolicy: []string{"read"},
|
||||
},
|
||||
}
|
||||
ctlr := makeController(conf, t.TempDir())
|
||||
},
|
||||
}
|
||||
|
||||
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
||||
Repositories: config.Repositories{
|
||||
test.AuthorizationAllRepos: config.PolicyGroup{
|
||||
AnonymousPolicy: []string{"read"},
|
||||
},
|
||||
},
|
||||
}
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
defer cm.StopServer()
|
||||
|
||||
defer cm.StopServer()
|
||||
blob := []byte("hello, blob!")
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
|
||||
blob := []byte("hello, blob!")
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
resp, err := resty.R().Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
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).
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Authorization", "Bearer "+goodToken.AccessToken).
|
||||
Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
s1, seed1 := test.GenerateRandomName()
|
||||
s2, seed2 := test.GenerateRandomName()
|
||||
repoName := s1 + "/" + s2
|
||||
|
||||
s1, seed1 := test.GenerateRandomName()
|
||||
s2, seed2 := test.GenerateRandomName()
|
||||
repoName := s1 + "/" + s2
|
||||
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
|
||||
|
||||
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
|
||||
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().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/")
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
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")
|
||||
|
||||
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")
|
||||
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").
|
||||
SetQueryParam("digest", digest).
|
||||
SetBody(blob).
|
||||
Put(baseURL + loc)
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
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").
|
||||
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)
|
||||
|
||||
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)
|
||||
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.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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
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.StatusOK)
|
||||
|
||||
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)
|
||||
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().
|
||||
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
var badToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &badToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
|
||||
+128
-150
@@ -5,7 +5,6 @@ package extensions_test
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -875,184 +874,163 @@ func TestMgmtExtension(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMgmtWithBearer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
useLegacyAuthTestServer bool
|
||||
}{
|
||||
{
|
||||
name: "new authentication test server",
|
||||
useLegacyAuthTestServer: false,
|
||||
},
|
||||
{
|
||||
name: "legacy authentication test server",
|
||||
useLegacyAuthTestServer: true,
|
||||
},
|
||||
}
|
||||
Convey("Make a new controller", t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath := setupTestServerCerts(t)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
Convey("Make a new controller with "+testCase.name, t, func() {
|
||||
// Generate certificates dynamically for the test
|
||||
serverCertPath, serverKeyPath := setupTestServerCerts(t)
|
||||
authorizedNamespace := "allowedrepo"
|
||||
unauthorizedNamespace := "notallowedrepo"
|
||||
|
||||
authorizedNamespace := "allowedrepo"
|
||||
unauthorizedNamespace := "notallowedrepo"
|
||||
authTestServer := authutils.MakeAuthTestServer(serverKeyPath, "RS256", unauthorizedNamespace)
|
||||
defer authTestServer.Close()
|
||||
|
||||
var authTestServer *httptest.Server
|
||||
if testCase.useLegacyAuthTestServer {
|
||||
authTestServer = authutils.MakeAuthTestServerLegacy(serverKeyPath, unauthorizedNamespace)
|
||||
} else {
|
||||
authTestServer = authutils.MakeAuthTestServer(serverKeyPath, "RS256", unauthorizedNamespace)
|
||||
}
|
||||
defer authTestServer.Close()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
defaultValue := true
|
||||
|
||||
defaultValue := true
|
||||
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
|
||||
|
||||
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
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
cm := test.NewControllerManager(ctlr)
|
||||
cm.StartAndWait(port)
|
||||
defer cm.StopServer()
|
||||
|
||||
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)
|
||||
|
||||
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).
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
resp, err = resty.R().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/")
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
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)
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
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)
|
||||
|
||||
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)
|
||||
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().
|
||||
Post(baseURL + "/v2/" + unauthorizedNamespace + "/blobs/uploads/")
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
var badToken authutils.AccessTokenResponse
|
||||
|
||||
var badToken authutils.AccessTokenResponse
|
||||
err = json.Unmarshal(resp.Body(), &badToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
// test mgmt route
|
||||
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// test mgmt route
|
||||
resp, err = resty.R().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)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowedMethodsHeaderMgmt(t *testing.T) {
|
||||
|
||||
+263
-290
@@ -10,7 +10,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -2725,311 +2724,129 @@ func TestTLS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBearerAuth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
useLegacyAuthTestServer bool
|
||||
}{
|
||||
{
|
||||
name: "new authentication test server",
|
||||
useLegacyAuthTestServer: false,
|
||||
},
|
||||
{
|
||||
name: "legacy authentication test server",
|
||||
useLegacyAuthTestServer: true,
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
// Generate certificates for bearer auth
|
||||
tempDir := t.TempDir()
|
||||
_, serverCertPath, serverKeyPath, _, _, _ := setupTestCertsForSync(t, tempDir)
|
||||
|
||||
// Generate certificates for bearer auth
|
||||
tempDir := t.TempDir()
|
||||
_, serverCertPath, serverKeyPath, _, _, _ := setupTestCertsForSync(t, tempDir)
|
||||
authTestServer := authutils.MakeAuthTestServer(serverKeyPath, "RS256", unauthorizedNamespace)
|
||||
defer authTestServer.Close()
|
||||
|
||||
var authTestServer *httptest.Server
|
||||
if testCase.useLegacyAuthTestServer {
|
||||
authTestServer = authutils.MakeAuthTestServerLegacy(serverKeyPath, unauthorizedNamespace)
|
||||
} else {
|
||||
authTestServer = authutils.MakeAuthTestServer(serverKeyPath, "RS256", unauthorizedNamespace)
|
||||
}
|
||||
defer authTestServer.Close()
|
||||
sctlr, srcBaseURL, _, srcClient := makeUpstreamServer(t, false, false)
|
||||
|
||||
sctlr, srcBaseURL, _, srcClient := makeUpstreamServer(t, false, false)
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
aurl, err := url.Parse(authTestServer.URL)
|
||||
So(err, ShouldBeNil)
|
||||
sctlr.Config.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
Realm: authTestServer.URL + "/auth/token",
|
||||
Service: aurl.Host,
|
||||
},
|
||||
}
|
||||
|
||||
sctlr.Config.HTTP.Auth = &config.AuthConfig{
|
||||
Bearer: &config.BearerConfig{
|
||||
Cert: serverCertPath,
|
||||
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(t.TempDir(), 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},
|
||||
PollInterval: updateDuration,
|
||||
TLSVerify: &tlsVerify,
|
||||
CertDir: "",
|
||||
MaxRetries: &maxRetries,
|
||||
}
|
||||
|
||||
scm := test.NewControllerManager(sctlr)
|
||||
scm.StartAndWait(sctlr.Config.HTTP.Port)
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
CredentialsFile: credentialsFile,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
|
||||
defer scm.StopServer()
|
||||
dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
|
||||
registryName := sync.StripRegistryTransport(srcBaseURL)
|
||||
credentialsFile := makeCredentialsFile(t.TempDir(), fmt.Sprintf(`{"%s":{"username": "%s", "password": "%s"}}`,
|
||||
registryName, username, password))
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
|
||||
var tlsVerify bool
|
||||
defer dcm.StopServer()
|
||||
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{
|
||||
{
|
||||
Prefix: "**", // sync everything
|
||||
},
|
||||
},
|
||||
URLs: []string{srcBaseURL},
|
||||
PollInterval: updateDuration,
|
||||
TLSVerify: &tlsVerify,
|
||||
CertDir: "",
|
||||
MaxRetries: &maxRetries,
|
||||
}
|
||||
var (
|
||||
srcTagsList TagsList
|
||||
destTagsList TagsList
|
||||
)
|
||||
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
CredentialsFile: credentialsFile,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
resp, err := srcClient.R().Get(srcBaseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
dctlr, destBaseURL, _, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
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)
|
||||
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
|
||||
defer dcm.StopServer()
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var (
|
||||
srcTagsList TagsList
|
||||
destTagsList TagsList
|
||||
)
|
||||
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/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
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).
|
||||
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).
|
||||
SetQueryParam("scope", authorizationHeader.Scope).
|
||||
Get(authorizationHeader.Realm)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var goodToken authutils.AccessTokenResponse
|
||||
goodToken = authutils.AccessTokenResponse{}
|
||||
err = json.Unmarshal(resp.Body(), &goodToken)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
// Generate certificates for bearer auth
|
||||
tempDir := t.TempDir()
|
||||
_, serverCertPath, serverKeyPath, _, _, _ := setupTestCertsForSync(t, tempDir)
|
||||
|
||||
var authTestServer *httptest.Server
|
||||
if testCase.useLegacyAuthTestServer {
|
||||
authTestServer = authutils.MakeAuthTestServerLegacy(serverKeyPath, unauthorizedNamespace)
|
||||
} else {
|
||||
authTestServer = authutils.MakeAuthTestServer(serverKeyPath, "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: serverCertPath,
|
||||
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(t.TempDir(), 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: "",
|
||||
MaxRetries: &maxRetries,
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -3040,14 +2857,170 @@ func TestBearerAuth(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
So(destTagsList, ShouldResemble, srcTagsList)
|
||||
if len(destTagsList.Tags) > 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// unauthorized namespace
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testCveImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
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
|
||||
|
||||
// Generate certificates for bearer auth
|
||||
tempDir := t.TempDir()
|
||||
_, serverCertPath, serverKeyPath, _, _, _ := setupTestCertsForSync(t, tempDir)
|
||||
|
||||
authTestServer := authutils.MakeAuthTestServer(serverKeyPath, "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: serverCertPath,
|
||||
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(t.TempDir(), 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: "",
|
||||
MaxRetries: &maxRetries,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chartmuseum/auth"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
@@ -86,62 +85,6 @@ func MakeAuthTestServer(serverKey, signAlg string, unauthorizedNamespace string)
|
||||
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",
|
||||
Issuer: "Zot",
|
||||
AddKIDHeader: true,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet {
|
||||
response.WriteHeader(http.StatusMethodNotAllowed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var access []auth.AccessEntry
|
||||
|
||||
scopes := request.URL.Query()["scope"]
|
||||
|
||||
for _, scope := range scopes {
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(scope, ":")
|
||||
name := parts[1]
|
||||
actions := strings.Split(parts[2], ",")
|
||||
|
||||
if name == unauthorizedNamespace {
|
||||
actions = []string{}
|
||||
}
|
||||
|
||||
access = append(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)
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
auth "zotregistry.dev/zot/v2/pkg/test/auth"
|
||||
@@ -14,8 +27,124 @@ func TestBearerServer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBearerServerLegacy(t *testing.T) {
|
||||
Convey("test MakeAuthTestServerLegacy() no serve key", t, func() {
|
||||
So(func() { auth.MakeAuthTestServerLegacy("", "") }, ShouldPanic)
|
||||
// doGet performs an HTTP GET request with context.
|
||||
func doGet(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
// doPost performs an HTTP POST request with context.
|
||||
func doPost(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
func TestNewTokenGeneration(t *testing.T) {
|
||||
Convey("test new token generation", t, func() {
|
||||
tempDir := t.TempDir()
|
||||
keyPath := filepath.Join(tempDir, "server.key")
|
||||
certPath := filepath.Join(tempDir, "server.crt")
|
||||
|
||||
// Generate an RSA key pair
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Write the private key to a file
|
||||
keyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: keyBytes,
|
||||
})
|
||||
err = os.WriteFile(keyPath, keyPEM, 0o600)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Create a self-signed certificate
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "test",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(
|
||||
rand.Reader, template, template, &privateKey.PublicKey, privateKey,
|
||||
)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
err = os.WriteFile(certPath, certPEM, 0o600)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("new server should generate valid tokens", func() {
|
||||
server := auth.MakeAuthTestServer(keyPath, "RS256", "unauthorized-repo")
|
||||
defer server.Close()
|
||||
|
||||
resp, err := doGet(server.URL + "?scope=repository:test-repo:pull,push")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var tokenResp auth.AccessTokenResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
|
||||
So(err, ShouldBeNil)
|
||||
So(tokenResp.AccessToken, ShouldNotBeEmpty)
|
||||
|
||||
// Parse and verify the token
|
||||
token, err := jwt.Parse(tokenResp.AccessToken, func(token *jwt.Token) (any, error) {
|
||||
return &privateKey.PublicKey, nil
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(token.Valid, ShouldBeTrue)
|
||||
So(token.Method.Alg(), ShouldEqual, "RS256")
|
||||
})
|
||||
|
||||
Convey("new server should reject non-GET requests", func() {
|
||||
server := auth.MakeAuthTestServer(keyPath, "RS256", "unauthorized-repo")
|
||||
defer server.Close()
|
||||
|
||||
resp, err := doPost(server.URL)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusMethodNotAllowed)
|
||||
resp.Body.Close()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseBearerAuthHeader(t *testing.T) {
|
||||
Convey("test ParseBearerAuthHeader", t, func() {
|
||||
Convey("should parse valid bearer auth header", func() {
|
||||
header := `Bearer realm="https://auth.example.com/token",` +
|
||||
`service="registry.example.com",scope="repository:myrepo:pull"`
|
||||
parsed := auth.ParseBearerAuthHeader(header)
|
||||
|
||||
So(parsed.Realm, ShouldEqual, "https://auth.example.com/token")
|
||||
So(parsed.Service, ShouldEqual, "registry.example.com")
|
||||
So(parsed.Scope, ShouldEqual, "repository:myrepo:pull")
|
||||
})
|
||||
|
||||
Convey("should handle empty scope", func() {
|
||||
header := `Bearer realm="https://auth.example.com/token",` +
|
||||
`service="registry.example.com",scope=""`
|
||||
parsed := auth.ParseBearerAuthHeader(header)
|
||||
|
||||
So(parsed.Realm, ShouldEqual, "https://auth.example.com/token")
|
||||
So(parsed.Service, ShouldEqual, "registry.example.com")
|
||||
So(parsed.Scope, ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user