From e4ee1a27565e355f9a4538c056b6771e6ddcfa9d Mon Sep 17 00:00:00 2001 From: Mathieu Fenniak Date: Tue, 24 Feb 2026 19:05:33 -0700 Subject: [PATCH] feat: implement fine-grained access tokens in /repos/{owner}/{repo}/issues/{index}/dependencies **Breaking**: Public-only tokens previously had the capability to view private repositories through this API, which has been revoked by this change to support fine-grained access tokens. --- routers/api/v1/repo/issue_dependency.go | 4 +- tests/integration/api_issue_test.go | 107 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 400575d37c..6f2d7dbb4c 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -115,9 +115,9 @@ func GetIssueDependencies(ctx *context.APIContext) { perm = existPerm } else { var err error - perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) + perm, err = access_model.GetUserRepoPermissionWithReducer(ctx, &blocker.Repository, ctx.Doer, ctx.Reducer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.ServerError("GetUserRepoPermissionWithReducer", err) return } repoPerms[blocker.RepoID] = perm diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 554155e5f7..9ebb919681 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -886,3 +886,110 @@ func TestAPIIssueDependencyPermissions(t *testing.T) { }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) // as otherUserRepo is a private repo we can't link a dependency to it } + +func TestAPIIssueDependencyAccessTokenResources(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + + // Create an issue on a repo, repo1 -- call it issue1. repo256 is used because it's configured with + // EnableDependencies:true in its issue unit. + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo256/issues", &api.CreateIssueOption{ + Body: "issue body", + Title: "issue title", + }).AddTokenAuth(writeToken) + resp := MakeRequest(t, req, http.StatusCreated) + var issue1 api.Issue + DecodeJSON(t, resp, &issue1) + + // On three other issues, on a public repo (repo1), on two private repos (repo2, org3/repo3), create new issues. Add + // each issue as a dependency of issue1. (typically repo16 is used in similar tests for a second private repo, but + // can't be used here because it doesn't have the issue unit enabled) + for _, repo := range []string{"user2/repo1", "user2/repo2", "org3/repo3"} { + var dependency api.Issue + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/issues", repo), &api.CreateIssueOption{ + Body: "repo1 issue dependency", + Title: "important dependency", + }).AddTokenAuth(writeToken) + resp = MakeRequest(t, req, http.StatusCreated) + DecodeJSON(t, resp, &dependency) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/user2/repo256/issues/%d/dependencies", issue1.Index), api.IssueMeta{ + Owner: dependency.Repo.Owner, + Name: dependency.Repo.Name, + Index: dependency.Index, + }).AddTokenAuth(writeToken) + MakeRequest(t, req, http.StatusCreated) + } + + // The remainder of this test reads the dependencies on issue1 with different access token resources and see if the + // dependencies are visible or hidden. + var issues []*api.Issue + find := func() (bool, bool, bool) { + foundRepo1 := false // public repo1 + foundRepo2 := false // private repo2 + foundRepo3 := false // second public repo used in fine-grain testing, included as baseline + for _, issue := range issues { + if issue.Repo != nil { + switch issue.Repo.Name { + case "repo1": + foundRepo1 = true + case "repo2": + foundRepo2 = true + case "repo3": + foundRepo3 = true + } + } + } + return foundRepo1, foundRepo2, foundRepo3 + } + + t.Run("all access token", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + allToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo256/issues/%d/dependencies", issue1.Index)).AddTokenAuth(allToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + foundRepo1, foundRepo2, foundRepo3 := find() + + assert.True(t, foundRepo1) // public repo1 + assert.True(t, foundRepo2) // private repo2 + assert.True(t, foundRepo3) // private org3/repo3, 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", fmt.Sprintf("/api/v1/repos/user2/repo256/issues/%d/dependencies", issue1.Index)).AddTokenAuth(publicOnlyToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + foundRepo1, foundRepo2, foundRepo3 := find() + + assert.True(t, foundRepo1) // public repo1 + assert.False(t, foundRepo2) // private repo2 + assert.False(t, foundRepo3) // private org3/repo3 + }) + + 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", fmt.Sprintf("/api/v1/repos/user2/repo256/issues/%d/dependencies", issue1.Index)).AddTokenAuth(repo2OnlyToken) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &issues) + foundRepo1, foundRepo2, foundRepo3 := find() + + assert.True(t, foundRepo1) // public repo1, allowed as it's public and read-access only + assert.True(t, foundRepo2) // private repo2, allowed inside fine-grain + assert.False(t, foundRepo3) // private org3/repo3, denied outside fine-grain + }) +}