feat: implement fine-grained access tokens on /users/{username}/repos & /orgs/{org}/repos

**Breaking**: when using a public-only access tokens, private
repositories were not filtered out by the `/users/{username}/repos` or
`/orgs/{org}/repos` APIs.  This access has been removed in this change.
This commit is contained in:
Mathieu Fenniak 2026-02-24 19:11:45 -07:00
parent a309db27f2
commit cac675bc21
No known key found for this signature in database
2 changed files with 81 additions and 6 deletions

View file

@ -20,10 +20,11 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
repos, count, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
AuthorizationReducer: ctx.Reducer,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepositories", err)
@ -37,9 +38,9 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
apiRepos := make([]*api.Repository, 0, len(repos))
for i := range repos {
permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer)
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, repos[i], ctx.Doer, ctx.Reducer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
return
}
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAccess() {

View file

@ -55,6 +55,80 @@ func TestAPIUserReposWithWrongToken(t *testing.T) {
assert.Contains(t, resp.Body.String(), "access token does not exist")
}
func TestAPIUserReposAccessTokenResources(t *testing.T) {
defer tests.PrepareTestEnv(t)()
var repos []api.Repository
// Test cases repo1 (public), repo2 (private), repo16 (private).
session := loginUser(t, "user2")
find := func() (bool, bool, bool) {
foundRepo1 := false // public user2/repo1
foundRepo2 := false // private user2/repo2
foundRepo16 := false // second private repo user2/repo16 used in fine-grain testing, included as baseline
for _, repo := range repos {
switch repo.Name {
case "repo1":
foundRepo1 = true
case "repo2":
foundRepo2 = true
case "repo16":
foundRepo16 = true
}
}
return foundRepo1, foundRepo2, foundRepo16
}
t.Run("all access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
req := NewRequest(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth(allToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
foundRepo1, foundRepo2, foundRepo16 := find()
assert.True(t, foundRepo1) // public user2/repo1
assert.True(t, foundRepo2) // private user2/repo2
assert.True(t, foundRepo16) // private user2/repo16, used in fine-grain testing, included as baseline
})
t.Run("public-only access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
req := NewRequest(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth(publicOnlyToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
foundRepo1, foundRepo2, foundRepo16 := find()
assert.True(t, foundRepo1) // public user2/repo1
assert.False(t, foundRepo2) // private user2/repo2
assert.False(t, foundRepo16) // private user2/repo16
})
t.Run("specific repo access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository},
[]int64{2},
)
req := NewRequest(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth(repo2OnlyToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
foundRepo1, foundRepo2, foundRepo16 := find()
assert.True(t, foundRepo1) // public user2/repo1, allowed as it's public and read-access only
assert.True(t, foundRepo2) // private user2/repo2, allowed inside fine-grain
assert.False(t, foundRepo16) // private user2/repo16, denied outside fine-grain
})
}
func TestAPISearchRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const keyword = "test"