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

+2 -2
View File
@@ -313,7 +313,7 @@ func (r *Router) Combo(pattern string, h ...any) *Combo {
// It is only designed to resolve some special cases which chi router can't handle.
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
g := &RouterPathGroup{r: r, pathParam: "*"}
g := &RouterPathGroup{r: r, pathParam: "*", middlewares: h} // run h only on a match, unmatched methods and paths also fall back here
fn(g)
r.Any(pattern, append(h, g.ServeHTTP)...)
r.Any(pattern, g.ServeHTTP)
}
+8 -7
View File
@@ -17,10 +17,11 @@ import (
)
type RouterPathGroup struct {
r *Router
pathParam string
matchers []*routerPathMatcher
unescape bool
r *Router
pathParam string
matchers []*routerPathMatcher
unescape bool
middlewares []any
}
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
@@ -55,7 +56,7 @@ func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
}
func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) {
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, g.middlewares, h...))
}
func (g *RouterPathGroup) UseUnescapedPath() {
@@ -127,8 +128,8 @@ func isValidMethod(name string) bool {
return false
}
func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher {
middlewares, handlerFunc, hasPreMiddlewares := wrapMiddlewareAndHandler(nil, patternRegexp.middlewares, h)
func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, uses []any, h ...any) *routerPathMatcher {
middlewares, handlerFunc, hasPreMiddlewares := wrapMiddlewareAndHandler(uses, patternRegexp.middlewares, h)
if hasPreMiddlewares {
panic("pre-middlewares are not supported in router path matcher")
}
+3 -3
View File
@@ -102,7 +102,7 @@ func TestPathProcessor(t *testing.T) {
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
chiCtx := chi.NewRouteContext()
chiCtx.RouteMethod = "GET"
p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound)
p := newRouterPathMatcher("GET", patternRegexp(pattern), nil, http.NotFound)
shouldProcess := expectedPathParams != nil
if unescape {
uri, _ = url.PathUnescape(uri)
@@ -236,12 +236,12 @@ func TestRouter(t *testing.T) {
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
method: "GET",
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
handlerMarks: []string{"s1", "not-found:/api/v1"},
handlerMarks: []string{"not-found:/api/v1"},
})
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
method: "GET",
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
pathParams: map[string]string{"*": "d1/d2/fn", "dir": "d1/d2", "file": "fn", "reponame": "the-repo", "username": "the-user"},
handlerMarks: []string{"s1"},
})