feat: implement fine-grained access tokens in /repos/{owner}/{repo}/pulls & /repos/{owner}/{repo}/compare/{basehead} APIs

As these APIs only work on forks, and it's not possible to change the
visibility of a fork from its parent, only testing the API access
pattern against the head is sufficient.  Also it is not a breaking
change due to checkTokenPublicOnly middleware already enforcing this for
public-only scopes, and the lack of ability to change a fork's
visibility.
This commit is contained in:
Mathieu Fenniak 2026-02-24 19:08:29 -07:00 committed by Mathieu Fenniak
parent 94dd94c2c0
commit f9a2167105
2 changed files with 51 additions and 4 deletions

View file

@ -1150,10 +1150,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
}
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
permBase, err := access_model.GetUserRepoPermissionWithReducer(ctx, baseRepo, ctx.Doer, ctx.Reducer)
if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
return nil, nil, nil, "", ""
}
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
@ -1169,10 +1169,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
}
// user should have permission to read headrepo's codes
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
permHead, err := access_model.GetUserRepoPermissionWithReducer(ctx, headRepo, ctx.Doer, ctx.Reducer)
if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
return nil, nil, nil, "", ""
}
if !permHead.CanRead(unit.TypeCode) {

View file

@ -18,6 +18,7 @@ import (
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
api "forgejo.org/modules/structs"
"forgejo.org/tests"
"github.com/stretchr/testify/assert"
)
@ -269,3 +270,49 @@ func testAPICompareCommits(t *testing.T, objectFormat git.ObjectFormat) {
}
})
}
func TestAPICompareCommitsAccessTokenResources(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Using the compare API, will be testing that the base repo's security checks implement fine-grained access
// controls (and baselines with all and public-only).
testCase := func(t *testing.T, repo, token string, expectedStatus int) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/compare/master...master", repo)).AddTokenAuth(token)
MakeRequest(t, req, expectedStatus)
}
t.Run("all access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
testCase(t, "user2/repo1", allToken, http.StatusOK) // public user2/repo1
testCase(t, "org3/repo3", allToken, http.StatusOK) // private org3/repo3
testCase(t, "user2/repo20", allToken, http.StatusOK) // private user2/repo20
})
t.Run("public-only access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadRepository)
testCase(t, "user2/repo1", publicOnlyToken, http.StatusOK) // public user2/repo1
testCase(t, "org3/repo3", publicOnlyToken, http.StatusNotFound) // private org3/repo3
testCase(t, "user2/repo20", publicOnlyToken, http.StatusNotFound) // private user2/repo20
})
t.Run("specific repo access token", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repo2OnlyToken := createFineGrainedRepoAccessToken(t, "user2",
[]auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadRepository},
[]int64{3},
)
testCase(t, "user2/repo1", repo2OnlyToken, http.StatusOK) // public user2/repo1
testCase(t, "org3/repo3", repo2OnlyToken, http.StatusOK) // private org3/repo3
testCase(t, "user2/repo20", repo2OnlyToken, http.StatusNotFound) // private user2/repo20, outside of fine-grain
})
}