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  }