fix(actions): make Actions API work with repo admin permission (#40174)

The web route already uses "repo admin" for repo-level Actions
management.

* Fix #40173

---------

Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
wxiaoguangandsilverwind authored and GitHub committed 2026-11-02 19:29:49 +00:00
1 parent 2ed6a6f61b
commit cdadd0c788
2 files changed
+41 -35

No files matched your search

+28 -35
View File
@@ -356,16 +356,6 @@ func reqSiteAdmin() func(ctx *context.APIContext) {
}
}
// reqOwner user should be the owner of the repo or site admin.
func reqOwner() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Repo.Permission.IsOwner() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should be the owner of the repo")
return
}
}
}
func reqRepoDangerZone() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !access_model.CanDoerManageRepoDangerZone(ctx, ctx.Doer, ctx.Repo.Repository, &ctx.Repo.Permission) {
@@ -379,22 +369,24 @@ func reqRepoDangerZone() func(ctx *context.APIContext) {
func reqSelfOrAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer {
ctx.APIError(http.StatusForbidden, "doer should be the site admin or be same as the contextUser")
ctx.APIError(http.StatusForbidden, "doer should be the site admin or be same as the target user")
return
}
}
}
// reqAdmin user should be an owner or a collaborator with admin write of a repository, or site admin
func reqAdmin() func(ctx *context.APIContext) {
func reqRepoAdmin() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should be an owner or a collaborator with admin write of a repository")
ctx.APIError(http.StatusForbidden, "user should be an owner or have admin permission of the repository")
return
}
}
}
// TODO: refactor to use reqRepoAdmin instead of reqAdmin in the future
func reqAdmin() func(ctx *context.APIContext) { return reqRepoAdmin() }
// reqRepoWriter user should have a permission to write to a repo, or be a site admin
func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
@@ -974,36 +966,37 @@ func Routes() *web.Router {
addActionsRoutes := func(
m *web.Router,
reqReaderCheck func(ctx *context.APIContext),
reqOwnerCheck func(ctx *context.APIContext),
reqManagerCheck func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqOwnerCheck, act.ListActionsSecrets)
m.Get("", act.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqOwnerCheck, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqOwnerCheck, act.DeleteSecret)
})
Put(bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(act.DeleteSecret)
}, reqManagerCheck)
m.Group("/variables", func() {
m.Get("", reqToken(), reqOwnerCheck, act.ListVariables)
m.Get("", act.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOwnerCheck, act.GetVariable).
Delete(reqToken(), reqOwnerCheck, act.DeleteVariable).
Post(reqToken(), reqOwnerCheck, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqOwnerCheck, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
Get(act.GetVariable).
Delete(act.DeleteVariable).
Post(bind(api.CreateVariableOption{}), act.CreateVariable).
Put(bind(api.UpdateVariableOption{}), act.UpdateVariable)
}, reqManagerCheck)
m.Group("/runners", func() {
m.Get("", reqToken(), reqOwnerCheck, act.ListRunners)
m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), reqOwnerCheck, bind(api.EditActionRunnerOption{}), act.UpdateRunner)
})
m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs)
})
m.Get("", act.ListRunners)
m.Post("/registration-token", act.CreateRegistrationToken)
m.Get("/{runner_id}", act.GetRunner)
m.Delete("/{runner_id}", act.DeleteRunner)
m.Patch("/{runner_id}", bind(api.EditActionRunnerOption{}), act.UpdateRunner)
}, reqManagerCheck)
m.Get("/runs", reqReaderCheck, act.ListWorkflowRuns)
m.Get("/jobs", reqReaderCheck, act.ListWorkflowJobs)
}, reqToken())
}
m.Group("", func() {
@@ -1263,7 +1256,7 @@ func Routes() *web.Router {
}, reqToken(), rejectPublicOnly())
// Adds the routes for secrets/variables and runner management
addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction())
addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqRepoAdmin(), repo.NewAction())
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
@@ -10,7 +10,11 @@ import (
"testing"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
api "gitea.dev/modules/structs"
repo_service "gitea.dev/services/repository"
"gitea.dev/tests"
"github.com/stretchr/testify/assert"
@@ -371,6 +375,15 @@ func testActionsRunnerRepo(t *testing.T) {
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("OrgAdminTeamMember", func(t *testing.T) {
adminTeam := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 12})
orgRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
require.NoError(t, repo_service.TeamAddRepository(t.Context(), adminTeam, orgRepo))
token := getUserToken(t, "user28", auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", "/api/v1/repos/org3/repo3/actions/runners/registration-token").AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
})
t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)