loading up the forgejo repo on tangled to test page performance
at forgejo 582 lines 14 kB view raw
1// Copyright 2018 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package integration 5 6import ( 7 "fmt" 8 "net/http" 9 "testing" 10 11 auth_model "forgejo.org/models/auth" 12 "forgejo.org/models/unittest" 13 user_model "forgejo.org/models/user" 14 "forgejo.org/modules/log" 15 api "forgejo.org/modules/structs" 16 "forgejo.org/tests" 17 18 "github.com/stretchr/testify/assert" 19) 20 21// TestAPICreateAndDeleteToken tests that token that was just created can be deleted 22func TestAPICreateAndDeleteToken(t *testing.T) { 23 defer tests.PrepareTestEnv(t)() 24 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 25 26 newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 27 deleteAPIAccessToken(t, newAccessToken, user) 28 29 newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 30 deleteAPIAccessToken(t, newAccessToken, user) 31} 32 33func TestAPIGetTokens(t *testing.T) { 34 defer tests.PrepareTestEnv(t)() 35 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 36 37 // with basic auth... 38 req := NewRequest(t, "GET", "/api/v1/users/user2/tokens"). 39 AddBasicAuth(user.Name) 40 MakeRequest(t, req, http.StatusOK) 41 42 // ... or with a token. 43 newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 44 req = NewRequest(t, "GET", "/api/v1/users/user2/tokens"). 45 AddTokenAuth(newAccessToken.Token) 46 MakeRequest(t, req, http.StatusOK) 47 deleteAPIAccessToken(t, newAccessToken, user) 48} 49 50// TestAPIDeleteMissingToken ensures that error is thrown when token not found 51func TestAPIDeleteMissingToken(t *testing.T) { 52 defer tests.PrepareTestEnv(t)() 53 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 54 55 req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", unittest.NonexistentID). 56 AddBasicAuth(user.Name) 57 MakeRequest(t, req, http.StatusNotFound) 58} 59 60// TestAPIGetTokensPermission ensures that only the admin can get tokens from other users 61func TestAPIGetTokensPermission(t *testing.T) { 62 defer tests.PrepareTestEnv(t)() 63 64 // admin can get tokens for other users 65 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 66 req := NewRequest(t, "GET", "/api/v1/users/user2/tokens"). 67 AddBasicAuth(user.Name) 68 MakeRequest(t, req, http.StatusOK) 69 70 // non-admin can get tokens for himself 71 user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 72 req = NewRequest(t, "GET", "/api/v1/users/user2/tokens"). 73 AddBasicAuth(user.Name) 74 MakeRequest(t, req, http.StatusOK) 75 76 // non-admin can't get tokens for other users 77 user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) 78 req = NewRequest(t, "GET", "/api/v1/users/user2/tokens"). 79 AddBasicAuth(user.Name) 80 MakeRequest(t, req, http.StatusForbidden) 81} 82 83// TestAPIDeleteTokensPermission ensures that only the admin can delete tokens from other users 84func TestAPIDeleteTokensPermission(t *testing.T) { 85 defer tests.PrepareTestEnv(t)() 86 87 admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 88 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 89 user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) 90 91 // admin can delete tokens for other users 92 createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 93 req := NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-1"). 94 AddBasicAuth(admin.Name) 95 MakeRequest(t, req, http.StatusNoContent) 96 97 // non-admin can delete tokens for himself 98 createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 99 req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-2"). 100 AddBasicAuth(user2.Name) 101 MakeRequest(t, req, http.StatusNoContent) 102 103 // non-admin can't delete tokens for other users 104 createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) 105 req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-3"). 106 AddBasicAuth(user4.Name) 107 MakeRequest(t, req, http.StatusForbidden) 108} 109 110type permission struct { 111 category auth_model.AccessTokenScopeCategory 112 level auth_model.AccessTokenScopeLevel 113} 114 115type requiredScopeTestCase struct { 116 url string 117 method string 118 requiredPermissions []permission 119} 120 121func (c *requiredScopeTestCase) Name() string { 122 return fmt.Sprintf("%v %v", c.method, c.url) 123} 124 125// TestAPIDeniesPermissionBasedOnTokenScope tests that API routes forbid access 126// when the correct token scope is not included. 127func TestAPIDeniesPermissionBasedOnTokenScope(t *testing.T) { 128 defer tests.PrepareTestEnv(t)() 129 130 // We'll assert that each endpoint, when fetched with a token with all 131 // scopes *except* the ones specified, a forbidden status code is returned. 132 // 133 // This is to protect against endpoints having their access check copied 134 // from other endpoints and not updated. 135 // 136 // Test cases are in alphabetical order by URL. 137 testCases := []requiredScopeTestCase{ 138 { 139 "/api/v1/admin/emails", 140 "GET", 141 []permission{ 142 { 143 auth_model.AccessTokenScopeCategoryAdmin, 144 auth_model.Read, 145 }, 146 }, 147 }, 148 { 149 "/api/v1/admin/users", 150 "GET", 151 []permission{ 152 { 153 auth_model.AccessTokenScopeCategoryAdmin, 154 auth_model.Read, 155 }, 156 }, 157 }, 158 { 159 "/api/v1/admin/users", 160 "POST", 161 []permission{ 162 { 163 auth_model.AccessTokenScopeCategoryAdmin, 164 auth_model.Write, 165 }, 166 }, 167 }, 168 { 169 "/api/v1/admin/users/user2", 170 "PATCH", 171 []permission{ 172 { 173 auth_model.AccessTokenScopeCategoryAdmin, 174 auth_model.Write, 175 }, 176 }, 177 }, 178 { 179 "/api/v1/admin/users/user2/orgs", 180 "GET", 181 []permission{ 182 { 183 auth_model.AccessTokenScopeCategoryAdmin, 184 auth_model.Read, 185 }, 186 }, 187 }, 188 { 189 "/api/v1/admin/users/user2/orgs", 190 "POST", 191 []permission{ 192 { 193 auth_model.AccessTokenScopeCategoryAdmin, 194 auth_model.Write, 195 }, 196 }, 197 }, 198 { 199 "/api/v1/admin/orgs", 200 "GET", 201 []permission{ 202 { 203 auth_model.AccessTokenScopeCategoryAdmin, 204 auth_model.Read, 205 }, 206 }, 207 }, 208 { 209 "/api/v1/notifications", 210 "GET", 211 []permission{ 212 { 213 auth_model.AccessTokenScopeCategoryNotification, 214 auth_model.Read, 215 }, 216 }, 217 }, 218 { 219 "/api/v1/notifications", 220 "PUT", 221 []permission{ 222 { 223 auth_model.AccessTokenScopeCategoryNotification, 224 auth_model.Write, 225 }, 226 }, 227 }, 228 { 229 "/api/v1/org/org1/repos", 230 "POST", 231 []permission{ 232 { 233 auth_model.AccessTokenScopeCategoryOrganization, 234 auth_model.Write, 235 }, 236 { 237 auth_model.AccessTokenScopeCategoryRepository, 238 auth_model.Write, 239 }, 240 }, 241 }, 242 { 243 "/api/v1/packages/user1/type/name/1", 244 "GET", 245 []permission{ 246 { 247 auth_model.AccessTokenScopeCategoryPackage, 248 auth_model.Read, 249 }, 250 }, 251 }, 252 { 253 "/api/v1/packages/user1/type/name/1", 254 "DELETE", 255 []permission{ 256 { 257 auth_model.AccessTokenScopeCategoryPackage, 258 auth_model.Write, 259 }, 260 }, 261 }, 262 { 263 "/api/v1/repos/user1/repo1", 264 "GET", 265 []permission{ 266 { 267 auth_model.AccessTokenScopeCategoryRepository, 268 auth_model.Read, 269 }, 270 }, 271 }, 272 { 273 "/api/v1/repos/user1/repo1", 274 "PATCH", 275 []permission{ 276 { 277 auth_model.AccessTokenScopeCategoryRepository, 278 auth_model.Write, 279 }, 280 }, 281 }, 282 { 283 "/api/v1/repos/user1/repo1", 284 "DELETE", 285 []permission{ 286 { 287 auth_model.AccessTokenScopeCategoryRepository, 288 auth_model.Write, 289 }, 290 }, 291 }, 292 { 293 "/api/v1/repos/user1/repo1/branches", 294 "GET", 295 []permission{ 296 { 297 auth_model.AccessTokenScopeCategoryRepository, 298 auth_model.Read, 299 }, 300 }, 301 }, 302 { 303 "/api/v1/repos/user1/repo1/archive/foo", 304 "GET", 305 []permission{ 306 { 307 auth_model.AccessTokenScopeCategoryRepository, 308 auth_model.Read, 309 }, 310 }, 311 }, 312 { 313 "/api/v1/repos/user1/repo1/issues", 314 "GET", 315 []permission{ 316 { 317 auth_model.AccessTokenScopeCategoryIssue, 318 auth_model.Read, 319 }, 320 }, 321 }, 322 { 323 "/api/v1/repos/user1/repo1/media/foo", 324 "GET", 325 []permission{ 326 { 327 auth_model.AccessTokenScopeCategoryRepository, 328 auth_model.Read, 329 }, 330 }, 331 }, 332 { 333 "/api/v1/repos/user1/repo1/raw/foo", 334 "GET", 335 []permission{ 336 { 337 auth_model.AccessTokenScopeCategoryRepository, 338 auth_model.Read, 339 }, 340 }, 341 }, 342 { 343 "/api/v1/repos/user1/repo1/teams", 344 "GET", 345 []permission{ 346 { 347 auth_model.AccessTokenScopeCategoryRepository, 348 auth_model.Read, 349 }, 350 }, 351 }, 352 { 353 "/api/v1/repos/user1/repo1/teams/team1", 354 "PUT", 355 []permission{ 356 { 357 auth_model.AccessTokenScopeCategoryRepository, 358 auth_model.Write, 359 }, 360 }, 361 }, 362 { 363 "/api/v1/repos/user1/repo1/transfer", 364 "POST", 365 []permission{ 366 { 367 auth_model.AccessTokenScopeCategoryRepository, 368 auth_model.Write, 369 }, 370 }, 371 }, 372 // Private repo 373 { 374 "/api/v1/repos/user2/repo2", 375 "GET", 376 []permission{ 377 { 378 auth_model.AccessTokenScopeCategoryRepository, 379 auth_model.Read, 380 }, 381 }, 382 }, 383 // Private repo 384 { 385 "/api/v1/repos/user2/repo2", 386 "GET", 387 []permission{ 388 { 389 auth_model.AccessTokenScopeCategoryRepository, 390 auth_model.Read, 391 }, 392 }, 393 }, 394 { 395 "/api/v1/user", 396 "GET", 397 []permission{ 398 { 399 auth_model.AccessTokenScopeCategoryUser, 400 auth_model.Read, 401 }, 402 }, 403 }, 404 { 405 "/api/v1/user/emails", 406 "GET", 407 []permission{ 408 { 409 auth_model.AccessTokenScopeCategoryUser, 410 auth_model.Read, 411 }, 412 }, 413 }, 414 { 415 "/api/v1/user/emails", 416 "POST", 417 []permission{ 418 { 419 auth_model.AccessTokenScopeCategoryUser, 420 auth_model.Write, 421 }, 422 }, 423 }, 424 { 425 "/api/v1/user/emails", 426 "DELETE", 427 []permission{ 428 { 429 auth_model.AccessTokenScopeCategoryUser, 430 auth_model.Write, 431 }, 432 }, 433 }, 434 { 435 "/api/v1/user/applications/oauth2", 436 "GET", 437 []permission{ 438 { 439 auth_model.AccessTokenScopeCategoryUser, 440 auth_model.Read, 441 }, 442 }, 443 }, 444 { 445 "/api/v1/user/applications/oauth2", 446 "POST", 447 []permission{ 448 { 449 auth_model.AccessTokenScopeCategoryUser, 450 auth_model.Write, 451 }, 452 }, 453 }, 454 { 455 "/api/v1/users/search", 456 "GET", 457 []permission{ 458 { 459 auth_model.AccessTokenScopeCategoryUser, 460 auth_model.Read, 461 }, 462 }, 463 }, 464 // Private user 465 { 466 "/api/v1/users/user31", 467 "GET", 468 []permission{ 469 { 470 auth_model.AccessTokenScopeCategoryUser, 471 auth_model.Read, 472 }, 473 }, 474 }, 475 // Private user 476 { 477 "/api/v1/users/user31/gpg_keys", 478 "GET", 479 []permission{ 480 { 481 auth_model.AccessTokenScopeCategoryUser, 482 auth_model.Read, 483 }, 484 }, 485 }, 486 } 487 488 // User needs to be admin so that we can verify that tokens without admin 489 // scopes correctly deny access. 490 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 491 assert.True(t, user.IsAdmin, "User needs to be admin") 492 493 for _, testCase := range testCases { 494 runTestCase(t, &testCase, user) 495 } 496} 497 498// runTestCase Helper function to run a single test case. 499func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model.User) { 500 t.Run(testCase.Name(), func(t *testing.T) { 501 defer tests.PrintCurrentTest(t)() 502 503 // Create a token with all scopes NOT required by the endpoint. 504 var unauthorizedScopes []auth_model.AccessTokenScope 505 for _, category := range auth_model.AllAccessTokenScopeCategories { 506 // For permissions, Write > Read > NoAccess. So we need to 507 // find the minimum required, and only grant permission up to but 508 // not including the minimum required. 509 minRequiredLevel := auth_model.Write 510 categoryIsRequired := false 511 for _, requiredPermission := range testCase.requiredPermissions { 512 if requiredPermission.category != category { 513 continue 514 } 515 categoryIsRequired = true 516 if requiredPermission.level < minRequiredLevel { 517 minRequiredLevel = requiredPermission.level 518 } 519 } 520 unauthorizedLevel := auth_model.Write 521 if categoryIsRequired { 522 if minRequiredLevel == auth_model.Read { 523 unauthorizedLevel = auth_model.NoAccess 524 } else if minRequiredLevel == auth_model.Write { 525 unauthorizedLevel = auth_model.Read 526 } else { 527 assert.FailNow(t, "Invalid test case", "Unknown access token scope level: %v", minRequiredLevel) 528 } 529 } 530 531 if unauthorizedLevel == auth_model.NoAccess { 532 continue 533 } 534 cateogoryUnauthorizedScopes := auth_model.GetRequiredScopes( 535 unauthorizedLevel, 536 category) 537 unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...) 538 } 539 540 accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes) 541 defer deleteAPIAccessToken(t, accessToken, user) 542 543 // Request the endpoint. Verify that permission is denied. 544 req := NewRequest(t, testCase.method, testCase.url). 545 AddTokenAuth(accessToken.Token) 546 MakeRequest(t, req, http.StatusForbidden) 547 }) 548} 549 550// createAPIAccessTokenWithoutCleanUp Create an API access token and assert that 551// creation succeeded. The caller is responsible for deleting the token. 552func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes []auth_model.AccessTokenScope) api.AccessToken { 553 payload := map[string]any{ 554 "name": tokenName, 555 "scopes": scopes, 556 } 557 558 log.Debug("Requesting creation of token with scopes: %v", scopes) 559 req := NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload). 560 AddBasicAuth(user.Name) 561 resp := MakeRequest(t, req, http.StatusCreated) 562 563 var newAccessToken api.AccessToken 564 DecodeJSON(t, resp, &newAccessToken) 565 unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ 566 ID: newAccessToken.ID, 567 Name: newAccessToken.Name, 568 Token: newAccessToken.Token, 569 UID: user.ID, 570 }) 571 572 return newAccessToken 573} 574 575// deleteAPIAccessToken deletes an API access token and assert that deletion succeeded. 576func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) { 577 req := NewRequestf(t, "DELETE", "/api/v1/users/"+user.LoginName+"/tokens/%d", accessToken.ID). 578 AddBasicAuth(user.Name) 579 MakeRequest(t, req, http.StatusNoContent) 580 581 unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: accessToken.ID}) 582}