loading up the forgejo repo on tangled to test page performance
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}