From 0da4505a4963a5f401167fd719d928f80b741ad8 Mon Sep 17 00:00:00 2001 From: Mathieu Fenniak Date: Sun, 15 Feb 2026 14:17:38 -0700 Subject: [PATCH] feat: implement fine-grained access tokens in /repos/issues/search --- routers/api/v1/repo/issue.go | 2 + tests/integration/api_issue_test.go | 91 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index e3b41eed81..1ebfe8352c 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -166,6 +166,8 @@ func SearchIssues(ctx *context.APIContext) { // MySQL will return different results when sorting by null in some cases OrderBy: db.SearchOrderByAlphabetically, Actor: ctx.Doer, + + AuthorizationReducer: ctx.Reducer, } if ctx.IsSigned { opts.Private = !ctx.PublicOnly diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 282d418316..3f57e31757 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -660,6 +660,97 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { assert.Len(t, apiIssues, 2) } +func TestAPISearchIssuesAccessTokenResources(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + var issues []*api.Issue + + // Test repos: repo1 (public), repo2 (private), repo16 (private). + session := loginUser(t, "user2") + writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteIssue) + + // On those three test repos, create an issue with a specific title for search. + for _, repo := range []string{"repo1", "repo2", "repo16"} { + trueBool := true + // Enable issues on each target repo as well + req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/user2/%s", repo), &api.EditRepoOption{ + HasIssues: &trueBool, + }).AddTokenAuth(writeToken) + MakeRequest(t, req, http.StatusOK) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/user2/%s/issues", repo), &api.CreateIssueOption{ + Body: "body: abracadabra", + Title: "important issue", + }).AddTokenAuth(writeToken) + MakeRequest(t, req, http.StatusCreated) + } + + 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 _, issue := range issues { + switch issue.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.AccessTokenScopeReadIssue) + + req := NewRequest(t, "GET", "/api/v1/repos/issues/search").AddTokenAuth(allToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + 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.AccessTokenScopeReadIssue) + + req := NewRequest(t, "GET", "/api/v1/repos/issues/search").AddTokenAuth(publicOnlyToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + 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.AccessTokenScopeReadIssue}, + []int64{2}, + ) + + req := NewRequest(t, "GET", "/api/v1/repos/issues/search").AddTokenAuth(repo2OnlyToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + 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 TestAPIInternalAndExternalIssueTracker(t *testing.T) { defer tests.PrepareTestEnv(t)()