loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[SEC] Ensure propagation of API scopes for Conan and Container authentication' (#5149) from gusted/forgejo-api-scope into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5149
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

Gusted 06d42e6f 8d053065

+151 -12
+1
release-notes/5149.md
··· 1 + The scope of application tokens is not verified when writing containers or Conan packages. This is of no consequence when the user associated with the application token does not have write access to packages. If the user has write access to packages, such a token can be used to write containers and Conan packages.
+7 -1
routers/api/packages/conan/auth.go
··· 22 22 23 23 // Verify extracts the user from the Bearer token 24 24 func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { 25 - uid, err := packages.ParseAuthorizationToken(req) 25 + uid, scope, err := packages.ParseAuthorizationToken(req) 26 26 if err != nil { 27 27 log.Trace("ParseAuthorizationToken: %v", err) 28 28 return nil, err ··· 30 30 31 31 if uid == 0 { 32 32 return nil, nil 33 + } 34 + 35 + // Propagate scope of the authorization token. 36 + if scope != "" { 37 + store.GetData()["IsApiToken"] = true 38 + store.GetData()["ApiTokenScope"] = scope 33 39 } 34 40 35 41 u, err := user_model.GetUserByID(req.Context(), uid)
+5 -1
routers/api/packages/conan/conan.go
··· 11 11 "strings" 12 12 "time" 13 13 14 + auth_model "code.gitea.io/gitea/models/auth" 14 15 "code.gitea.io/gitea/models/db" 15 16 packages_model "code.gitea.io/gitea/models/packages" 16 17 conan_model "code.gitea.io/gitea/models/packages/conan" ··· 117 118 return 118 119 } 119 120 120 - token, err := packages_service.CreateAuthorizationToken(ctx.Doer) 121 + // If there's an API scope, ensure it propagates. 122 + scope, _ := ctx.Data.GetData()["ApiTokenScope"].(auth_model.AccessTokenScope) 123 + 124 + token, err := packages_service.CreateAuthorizationToken(ctx.Doer, scope) 121 125 if err != nil { 122 126 apiError(ctx, http.StatusInternalServerError, err) 123 127 return
+7 -1
routers/api/packages/container/auth.go
··· 23 23 // Verify extracts the user from the Bearer token 24 24 // If it's an anonymous session a ghost user is returned 25 25 func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { 26 - uid, err := packages.ParseAuthorizationToken(req) 26 + uid, scope, err := packages.ParseAuthorizationToken(req) 27 27 if err != nil { 28 28 log.Trace("ParseAuthorizationToken: %v", err) 29 29 return nil, err ··· 31 31 32 32 if uid == 0 { 33 33 return nil, nil 34 + } 35 + 36 + // Propagate scope of the authorization token. 37 + if scope != "" { 38 + store.GetData()["IsApiToken"] = true 39 + store.GetData()["ApiTokenScope"] = scope 34 40 } 35 41 36 42 u, err := user_model.GetPossibleUserByID(req.Context(), uid)
+5 -1
routers/api/packages/container/container.go
··· 14 14 "strconv" 15 15 "strings" 16 16 17 + auth_model "code.gitea.io/gitea/models/auth" 17 18 packages_model "code.gitea.io/gitea/models/packages" 18 19 container_model "code.gitea.io/gitea/models/packages/container" 19 20 user_model "code.gitea.io/gitea/models/user" ··· 154 155 u = user_model.NewGhostUser() 155 156 } 156 157 157 - token, err := packages_service.CreateAuthorizationToken(u) 158 + // If there's an API scope, ensure it propagates. 159 + scope, _ := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) 160 + 161 + token, err := packages_service.CreateAuthorizationToken(u, scope) 158 162 if err != nil { 159 163 apiError(ctx, http.StatusInternalServerError, err) 160 164 return
+10 -7
services/packages/auth.go
··· 9 9 "strings" 10 10 "time" 11 11 12 + auth_model "code.gitea.io/gitea/models/auth" 12 13 user_model "code.gitea.io/gitea/models/user" 13 14 "code.gitea.io/gitea/modules/log" 14 15 "code.gitea.io/gitea/modules/setting" ··· 19 20 type packageClaims struct { 20 21 jwt.RegisteredClaims 21 22 UserID int64 23 + Scope auth_model.AccessTokenScope 22 24 } 23 25 24 - func CreateAuthorizationToken(u *user_model.User) (string, error) { 26 + func CreateAuthorizationToken(u *user_model.User, scope auth_model.AccessTokenScope) (string, error) { 25 27 now := time.Now() 26 28 27 29 claims := packageClaims{ ··· 30 32 NotBefore: jwt.NewNumericDate(now), 31 33 }, 32 34 UserID: u.ID, 35 + Scope: scope, 33 36 } 34 37 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 35 38 ··· 41 44 return tokenString, nil 42 45 } 43 46 44 - func ParseAuthorizationToken(req *http.Request) (int64, error) { 47 + func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenScope, error) { 45 48 h := req.Header.Get("Authorization") 46 49 if h == "" { 47 - return 0, nil 50 + return 0, "", nil 48 51 } 49 52 50 53 parts := strings.SplitN(h, " ", 2) 51 54 if len(parts) != 2 { 52 55 log.Error("split token failed: %s", h) 53 - return 0, fmt.Errorf("split token failed") 56 + return 0, "", fmt.Errorf("split token failed") 54 57 } 55 58 56 59 token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) { ··· 60 63 return setting.GetGeneralTokenSigningSecret(), nil 61 64 }) 62 65 if err != nil { 63 - return 0, err 66 + return 0, "", err 64 67 } 65 68 66 69 c, ok := token.Claims.(*packageClaims) 67 70 if !token.Valid || !ok { 68 - return 0, fmt.Errorf("invalid token claim") 71 + return 0, "", fmt.Errorf("invalid token claim") 69 72 } 70 73 71 - return c.UserID, nil 74 + return c.UserID, c.Scope, nil 72 75 }
+78 -1
tests/integration/api_packages_conan_test.go
··· 11 11 "testing" 12 12 "time" 13 13 14 + auth_model "code.gitea.io/gitea/models/auth" 14 15 "code.gitea.io/gitea/models/db" 15 16 "code.gitea.io/gitea/models/packages" 16 17 conan_model "code.gitea.io/gitea/models/packages/conan" ··· 222 223 resp := MakeRequest(t, req, http.StatusOK) 223 224 224 225 assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities")) 226 + }) 227 + 228 + t.Run("Token Scope Authentication", func(t *testing.T) { 229 + defer tests.PrintCurrentTest(t)() 230 + 231 + session := loginUser(t, user.Name) 232 + 233 + testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedStatusCode int) { 234 + t.Helper() 235 + 236 + token := getTokenForLoggedInUser(t, session, scope) 237 + 238 + req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url)). 239 + AddTokenAuth(token) 240 + resp := MakeRequest(t, req, http.StatusOK) 241 + 242 + body := resp.Body.String() 243 + assert.NotEmpty(t, body) 244 + 245 + recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, "TestScope", version1, "testing", channel1) 246 + 247 + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL), map[string]int64{ 248 + conanfileName: 64, 249 + "removed.txt": 0, 250 + }).AddTokenAuth(token) 251 + MakeRequest(t, req, expectedStatusCode) 252 + } 253 + 254 + t.Run("Read permission", func(t *testing.T) { 255 + defer tests.PrintCurrentTest(t)() 256 + 257 + testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusUnauthorized) 258 + }) 259 + 260 + t.Run("Write permission", func(t *testing.T) { 261 + defer tests.PrintCurrentTest(t)() 262 + 263 + testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusOK) 264 + }) 225 265 }) 226 266 227 267 token := "" ··· 481 521 482 522 token := "" 483 523 524 + t.Run("Token Scope Authentication", func(t *testing.T) { 525 + defer tests.PrintCurrentTest(t)() 526 + 527 + session := loginUser(t, user.Name) 528 + 529 + testCase := func(t *testing.T, scope auth_model.AccessTokenScope, expectedStatusCode int) { 530 + t.Helper() 531 + 532 + token := getTokenForLoggedInUser(t, session, scope) 533 + 534 + req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url)). 535 + AddTokenAuth(token) 536 + resp := MakeRequest(t, req, http.StatusOK) 537 + 538 + body := resp.Body.String() 539 + assert.NotEmpty(t, body) 540 + 541 + recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, "TestScope", version1, "testing", channel1, revision1) 542 + 543 + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", recipeURL, conanfileName), strings.NewReader("Doesn't need to be valid")). 544 + AddTokenAuth("Bearer " + body) 545 + MakeRequest(t, req, expectedStatusCode) 546 + } 547 + 548 + t.Run("Read permission", func(t *testing.T) { 549 + defer tests.PrintCurrentTest(t)() 550 + 551 + testCase(t, auth_model.AccessTokenScopeReadPackage, http.StatusUnauthorized) 552 + }) 553 + 554 + t.Run("Write permission", func(t *testing.T) { 555 + defer tests.PrintCurrentTest(t)() 556 + 557 + testCase(t, auth_model.AccessTokenScopeWritePackage, http.StatusCreated) 558 + }) 559 + }) 560 + 484 561 t.Run("Authenticate", func(t *testing.T) { 485 562 defer tests.PrintCurrentTest(t)() 486 563 ··· 512 589 513 590 pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan) 514 591 require.NoError(t, err) 515 - assert.Len(t, pvs, 2) 592 + assert.Len(t, pvs, 3) 516 593 }) 517 594 }) 518 595
+38
tests/integration/api_packages_container_test.go
··· 78 78 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"}}]}` 79 79 80 80 anonymousToken := "" 81 + readUserToken := "" 81 82 userToken := "" 82 83 83 84 t.Run("Authenticate", func(t *testing.T) { ··· 140 141 req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 141 142 AddTokenAuth(userToken) 142 143 MakeRequest(t, req, http.StatusOK) 144 + 145 + // Token that should enforce the read scope. 146 + t.Run("Read scope", func(t *testing.T) { 147 + defer tests.PrintCurrentTest(t)() 148 + 149 + session := loginUser(t, user.Name) 150 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage) 151 + 152 + req := NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL)) 153 + req.SetBasicAuth(user.Name, token) 154 + 155 + resp := MakeRequest(t, req, http.StatusOK) 156 + 157 + tokenResponse := &TokenResponse{} 158 + DecodeJSON(t, resp, &tokenResponse) 159 + 160 + assert.NotEmpty(t, tokenResponse.Token) 161 + 162 + readUserToken = fmt.Sprintf("Bearer %s", tokenResponse.Token) 163 + 164 + req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL)). 165 + AddTokenAuth(readUserToken) 166 + MakeRequest(t, req, http.StatusOK) 167 + }) 143 168 }) 144 169 }) 145 170 ··· 161 186 162 187 req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 163 188 AddTokenAuth(anonymousToken) 189 + MakeRequest(t, req, http.StatusUnauthorized) 190 + 191 + req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url)). 192 + AddTokenAuth(readUserToken) 164 193 MakeRequest(t, req, http.StatusUnauthorized) 165 194 166 195 req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent)). ··· 315 344 316 345 req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). 317 346 AddTokenAuth(anonymousToken). 347 + SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") 348 + MakeRequest(t, req, http.StatusUnauthorized) 349 + 350 + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent)). 351 + AddTokenAuth(readUserToken). 318 352 SetHeader("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") 319 353 MakeRequest(t, req, http.StatusUnauthorized) 320 354 ··· 520 554 521 555 req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 522 556 AddTokenAuth(anonymousToken) 557 + MakeRequest(t, req, http.StatusOK) 558 + 559 + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). 560 + AddTokenAuth(readUserToken) 523 561 MakeRequest(t, req, http.StatusOK) 524 562 }) 525 563