feat: add commitstatus access token scope (#40144)

Add `read:commitstatus` and `write:commitstatus` token scopes, so CI and
other integrations can report commit statuses without a token that
grants full repository access.

- The commit status endpoints (`/statuses/{sha}`,
`/commits/{ref}/status` and `/commits/{ref}/statuses`) accept the new
scopes, and the repository scopes keep working as they include them
- Creating a status still requires code write permission on the
repository, the scope only narrows what the token may do
- `public-only` tokens still cannot reach private repositories
- `PathGroup` middlewares now run only for a matched path, so unrelated
methods and paths that fall back to the group are not rejected by its
scope check

Fixes: https://github.com/go-gitea/gitea/issues/32625
Ref: https://github.com/go-gitea/gitea/pull/35383
Docs: https://gitea.com/gitea/docs/pulls/622
Signed-off-by: Norbert Szulc <norbert@not7cd.net>
Co-authored-by: Aiden Scandella <git@sca.ndella.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-11-01 06:27:12 +00:00
1 parent b46b820cec
commit da7b81d407
11 files changed
+87 -28

No files matched your search

+33
View File
@@ -204,6 +204,39 @@ func TestRepoCommitsWithStatus(t *testing.T) {
assert.NotEmpty(t, statuses)
assert.NotEmpty(t, status.Statuses)
})
t.Run("TokenScopes", func(t *testing.T) {
commitIDs := map[string]string{"repo1": "65f1bf27bc3bf70f64657658635e66094edbcb4d", "repo2": "1032bbf17fbc0d9c95bb5418dabe8f8c99278700"}
for _, test := range []struct {
name string
userID int64
reponame string
scope auth_model.AccessTokenScope
getCode, postCode int
}{
{"WriteCommitStatus", 2, "repo1", auth_model.AccessTokenScopeWriteCommitStatus, http.StatusOK, http.StatusCreated},
{"ReadCommitStatus", 2, "repo1", auth_model.AccessTokenScopeReadCommitStatus, http.StatusOK, http.StatusForbidden},
{"WriteRepository", 2, "repo1", auth_model.AccessTokenScopeWriteRepository, http.StatusOK, http.StatusCreated},
{"ReadRepository", 2, "repo1", auth_model.AccessTokenScopeReadRepository, http.StatusOK, http.StatusForbidden},
{"UnrelatedScope", 2, "repo1", auth_model.AccessTokenScopeWriteIssue, http.StatusForbidden, http.StatusForbidden},
{"NonWriter", 4, "repo1", auth_model.AccessTokenScopeWriteCommitStatus, http.StatusOK, http.StatusForbidden},
{"PublicOnlyPrivateRepo", 2, "repo2", "public-only,write:commitstatus", http.StatusNotFound, http.StatusNotFound},
} {
t.Run(test.name, func(t *testing.T) {
token := &auth_model.AccessToken{UID: test.userID, Name: test.name, Scope: test.scope}
require.NoError(t, auth_model.NewAccessToken(t.Context(), token))
repoLink, commitID := "/api/v1/repos/user2/"+test.reponame, commitIDs[test.reponame]
for _, link := range []string{"/statuses/" + commitID, "/commits/master/status", "/commits/master/statuses"} {
MakeRequest(t, NewRequest(t, "GET", repoLink+link).AddTokenAuth(token.Token), test.getCode)
}
MakeRequest(t, NewRequestWithJSON(t, "POST", repoLink+"/statuses/"+commitID, api.CreateStatusOption{State: commitstatus.CommitStatusSuccess}).AddTokenAuth(token.Token), test.postCode)
})
}
t.Run("UnmatchedCommitPathSkipsCommitStatusScope", func(t *testing.T) {
token := &auth_model.AccessToken{UID: 2, Name: "UnmatchedCommitPath", Scope: auth_model.AccessTokenScopeWriteIssue}
require.NoError(t, auth_model.NewAccessToken(t.Context(), token))
MakeRequest(t, NewRequest(t, "POST", "/api/v1/repos/user2/repo1/commits/"+commitIDs["repo1"]+"/pull").AddTokenAuth(token.Token), http.StatusNotFound)
})
})
}
func TestRepoCommitsStatus(t *testing.T) {