feat: implement fine-grained access tokens in /user/repos

**Breaking**: a user's own public-only access tokens were previously
visible in the `/users/repos` API.  This access has been removed in this
change.
This commit is contained in:
Mathieu Fenniak 2026-02-24 19:12:14 -07:00
parent 4bd81d3363
commit a309db27f2
No known key found for this signature in database
2 changed files with 87 additions and 7 deletions

View file

@ -115,11 +115,12 @@ func ListMyRepos(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
opts := &repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
OwnerID: ctx.Doer.ID,
Private: ctx.IsSigned,
IncludeDescription: true,
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
OwnerID: ctx.Doer.ID,
Private: ctx.IsSigned,
IncludeDescription: true,
AuthorizationReducer: ctx.Reducer,
}
orderBy := ctx.FormTrim("order_by")
switch orderBy {
@ -148,9 +149,14 @@ func ListMyRepos(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadOwner", err)
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, repo, ctx.Doer, ctx.Reducer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
} else if !permission.HasAccess() {
// It shouldn't happen that a repo is returned from SearchRepository which we have no access to at all. Due
// to the pagination of the API it doesn't make sense to skip it, as we wouldn't be giving the right number
// of results back to the API consumer.
ctx.Error(http.StatusInternalServerError, "InvalidAuthorizationReducer", "Repository was available from SearchRepository, but not readable.")
}
results[i] = convert.ToRepo(ctx, repo, permission)
}

View file

@ -966,3 +966,77 @@ func TestAPIListOwnRepoSorting(t *testing.T) {
assert.Equal(t, "test_workflows", repos[1].Name)
})
}
func TestAPIListOwnRepoAccessTokenResources(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/user/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/user/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/user/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
})
}