feat: implement fine-grained access tokens in /teams/{id}/repos/{org}/{repo}

**Breaking*: /teams/{id}/repos/{org}/{repo} previously allowed read
access to private repositories even if a "public-only" access token was
in-use.  This has been restricted to only return public repositories in
this case.
This commit is contained in:
Mathieu Fenniak 2026-02-24 19:01:18 -07:00 committed by Mathieu Fenniak
parent c89504d573
commit 0c2ece0ae7
5 changed files with 108 additions and 2 deletions

View file

@ -632,9 +632,13 @@ func GetTeamRepo(ctx *context.APIContext) {
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, "GetTeamRepos", err)
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
return
}
if !permission.HasAccess() {
ctx.NotFound()
return
}

View file

@ -319,3 +319,81 @@ func TestAPIGetTeamRepo(t *testing.T) {
AddTokenAuth(token5)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetTeamRepoAccessTokenResources(t *testing.T) {
defer unittest.OverrideFixtures("tests/integration/fixtures/TestAPIGetTeamRepoAccessTokenResources")()
defer tests.PrepareTestEnv(t)()
// Test cases org3/repo21 (public), org3/repo3 (private), org3/repo5 (private) --
// TestAPIGetTeamReposAccessTokenResources fixtures create a team w/ ID=26 that contains all three repos.
session := loginUser(t, "user2")
var repo api.Repository
t.Run("all access token", func(t *testing.T) {
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
t.Run("allowed public repo21", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo21").AddTokenAuth(allToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.False(t, repo.Private)
})
t.Run("allowed private repo3", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo3").AddTokenAuth(allToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.True(t, repo.Private)
})
// org3/repo5 is a second repo used in fine-grain testing below, so we include it in other tests as a baseline
t.Run("allowed private repo5", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo5").AddTokenAuth(allToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.True(t, repo.Private)
})
})
t.Run("public-only access token", func(t *testing.T) {
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadOrganization)
t.Run("allowed public repo21", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo21").AddTokenAuth(publicOnlyToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.False(t, repo.Private)
})
t.Run("denied private repo3", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo3").AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("denied private repo5", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo5").AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusNotFound)
})
})
t.Run("specific repo access token", func(t *testing.T) {
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadOrganization},
[]int64{3},
)
t.Run("allowed public repo21", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo21").AddTokenAuth(repo2OnlyToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.False(t, repo.Private)
})
t.Run("allowed inside fine-grain repo3", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo3").AddTokenAuth(repo2OnlyToken)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.True(t, repo.Private)
})
t.Run("denied private outside fine-grain repo5", func(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/teams/26/repos/org3/repo5").AddTokenAuth(repo2OnlyToken)
MakeRequest(t, req, http.StatusNotFound)
})
})
}

View file

@ -0,0 +1,4 @@
-
id: 26
org_id: 3
includes_all_repositories: false

View file

@ -0,0 +1,15 @@
-
id: 20
org_id: 3
team_id: 26
repo_id: 32 # org3/repo21 - public
-
id: 21
org_id: 3
team_id: 26
repo_id: 3 # org3/repo3 - private
-
id: 22
org_id: 3
team_id: 26
repo_id: 5 # org3/repo5 - private

View file

@ -0,0 +1,5 @@
-
id: 30
org_id: 3
team_id: 26
uid: 2