From cdadd0c78855a8f07a71d9a8fc982c19b133a192 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 2 Nov 2026 19:29:49 +0000 Subject: [PATCH] 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 --- routers/api/v1/api.go | 63 +++++++++----------- tests/integration/api_actions_runner_test.go | 13 ++++ 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0a364b27c4f..0d72a390e28 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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) diff --git a/tests/integration/api_actions_runner_test.go b/tests/integration/api_actions_runner_test.go index 216511795b2..c710f758588 100644 --- a/tests/integration/api_actions_runner_test.go +++ b/tests/integration/api_actions_runner_test.go @@ -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)