diff options
author | Gusted <postmaster@gusted.xyz> | 2024-08-28 08:23:48 +0200 |
---|---|---|
committer | Gusted <postmaster@gusted.xyz> | 2024-08-28 10:33:32 +0200 |
commit | 5a871f6095a5aee88336a5128bd0e0f1477474a6 (patch) | |
tree | 52f5ba268462160f78fd0afda957c79e01d330c4 /tests | |
parent | Merge pull request 'Remove 15 unused strings' (#5139) from 0ko/forgejo:i18n-c... (diff) | |
download | forgejo-5a871f6095a5aee88336a5128bd0e0f1477474a6.tar.xz forgejo-5a871f6095a5aee88336a5128bd0e0f1477474a6.zip |
[SEC] Ensure propagation of API scopes for Conan and Container authentication
- The Conan and Container packages use a different type of
authentication. It first authenticates via the regular way (api tokens
or user:password, handled via `auth.Basic`) and then generates a JWT
token that is used by the package software (such as Docker) to do the
action they wanted to do. This JWT token didn't properly propagate the
API scopes that the token was generated for, and thus could lead to a
'scope escalation' within the Conan and Container packages, read
access to write access.
- Store the API scope in the JWT token, so it can be propagated on
subsequent calls that uses that JWT token.
- Integration test added.
- Resolves #5128
Diffstat (limited to 'tests')
-rw-r--r-- | tests/integration/api_packages_conan_test.go | 79 | ||||
-rw-r--r-- | tests/integration/api_packages_container_test.go | 38 |
2 files changed, 116 insertions, 1 deletions
diff --git a/tests/integration/api_packages_conan_test.go b/tests/integration/api_packages_conan_test.go index 003867441b..9d8f435068 100644 --- a/tests/integration/api_packages_conan_test.go +++ b/tests/integration/api_packages_conan_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" conan_model "code.gitea.io/gitea/models/packages/conan" @@ -224,6 +225,45 @@ func TestPackageConan(t *testing.T) { assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities")) }) + t.Run("Token Scope Authentication", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, user.Name) + + testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedStatusCode int) { + t.Helper() + + token := getTokenForLoggedInUser(t, session, scope) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + body := resp.Body.String() + assert.NotEmpty(t, body) + + recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, "TestScope", version1, "testing", channel1) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL), map[string]int64{ + conanfileName: 64, + "removed.txt": 0, + }).AddTokenAuth(token) + MakeRequest(t, req, expectedStatusCode) + } + + t.Run("Read permission", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusUnauthorized) + }) + + t.Run("Write permission", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusOK) + }) + }) + token := "" t.Run("Authenticate", func(t *testing.T) { @@ -481,6 +521,43 @@ func TestPackageConan(t *testing.T) { token := "" + t.Run("Token Scope Authentication", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, user.Name) + + testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedStatusCode int) { + t.Helper() + + token := getTokenForLoggedInUser(t, session, scope) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + body := resp.Body.String() + assert.NotEmpty(t, body) + + recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, "TestScope", version1, "testing", channel1, revision1) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", recipeURL, conanfileName), strings.NewReader("Doesn't need to be valid")). + AddTokenAuth("Bearer " + body) + MakeRequest(t, req, expectedStatusCode) + } + + t.Run("Read permission", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusUnauthorized) + }) + + t.Run("Write permission", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusCreated) + }) + }) + t.Run("Authenticate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -512,7 +589,7 @@ func TestPackageConan(t *testing.T) { pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan) require.NoError(t, err) - assert.Len(t, pvs, 2) + assert.Len(t, pvs, 3) }) }) diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 1197408830..3c28f45660 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -78,6 +78,7 @@ func TestPackageContainer(t *testing.T) { indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}` anonymousToken := "" + readUserToken := "" userToken := "" t.Run("Authenticate", func(t *testing.T) { @@ -140,6 +141,30 @@ func TestPackageContainer(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). AddTokenAuth(userToken) MakeRequest(t, req, http.StatusOK) + + // Token that should enforce the read scope. + t.Run("Read scope", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage) + + req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) + req.SetBasicAuth(user.Name, token) + + resp := MakeRequest(t, req, http.StatusOK) + + tokenResponse := &TokenResponse{} + DecodeJSON(t, resp, &tokenResponse) + + assert.NotEmpty(t, tokenResponse.Token) + + readUserToken = fmt.Sprintf("Bearer %s", tokenResponse.Token) + + req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). + AddTokenAuth(readUserToken) + MakeRequest(t, req, http.StatusOK) + }) }) }) @@ -163,6 +188,10 @@ func TestPackageContainer(t *testing.T) { AddTokenAuth(anonymousToken) MakeRequest(t, req, http.StatusUnauthorized) + req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). + AddTokenAuth(readUserToken) + MakeRequest(t, req, http.StatusUnauthorized) + req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent)). AddTokenAuth(userToken) MakeRequest(t, req, http.StatusBadRequest) @@ -319,6 +348,11 @@ func TestPackageContainer(t *testing.T) { MakeRequest(t, req, http.StatusUnauthorized) req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). + AddTokenAuth(readUserToken). + SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). AddTokenAuth(userToken). SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") resp := MakeRequest(t, req, http.StatusCreated) @@ -521,6 +555,10 @@ func TestPackageContainer(t *testing.T) { req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). AddTokenAuth(anonymousToken) MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). + AddTokenAuth(readUserToken) + MakeRequest(t, req, http.StatusOK) }) t.Run("GetBlob", func(t *testing.T) { |