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