From ba2a6fbc41c43512d6482faa1530f6ce542df8e5 Mon Sep 17 00:00:00 2001 From: Andreas Ahlenstorf Date: Sat, 10 Jan 2026 16:40:00 +0100 Subject: [PATCH] fix: disable actions endpoints of repository if actions are disabled (#10726) Some HTTP API endpoints related to Actions, like `/api/v1/repos/{owner}/{repository}/actions/runners`, were not disabled if Actions had been disabled on a repository. With this change, all endpoints related to Actions will be disabled if Actions are disabled. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10726 Reviewed-by: Mathieu Fenniak Co-authored-by: Andreas Ahlenstorf Co-committed-by: Andreas Ahlenstorf --- routers/api/v1/api.go | 16 +++++++--- tests/integration/api_repo_actions_test.go | 23 +++++++++++++++ tests/integration/api_repo_secrets_test.go | 26 ++++++++++++++++ tests/integration/api_repo_variables_test.go | 31 ++++++++++++++++++++ 4 files changed, 92 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b7bb74e558..d428294036 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -436,9 +436,16 @@ func reqSiteAdmin() func(ctx *context.APIContext) { } } -// reqOwner user should be the owner of the repo or site admin. -func reqOwner() func(ctx *context.APIContext) { +// reqOwner requires that the current user is either the owner of the repository or an administrator. If one or more +// unitTypes are given, it also requires that at least one the respective unitTypes is enabled. +func reqOwner(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if len(unitTypes) > 0 && !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool { + return ctx.Repo.Repository.UnitEnabled(ctx, unitType) + }) { + ctx.NotFound() + return + } if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() { ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo") return @@ -466,7 +473,8 @@ func reqAdmin() func(ctx *context.APIContext) { } } -// reqRepoWriter user should have a permission to write to a repo, or be a site admin +// reqRepoWriter requires that the current user has permission to write to a repository or that it is an administrator. +// One or more unitTypes have to be specified, and at least one of them has to be enabled. func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool { @@ -1145,7 +1153,7 @@ func Routes() *web.Route { }, reqToken()) addActionsRoutes( m, - reqOwner(), + reqOwner(unit.TypeActions), repo.NewAction(), ) m.Group("/hooks/git", func() { diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go index d12691b117..44d0cb2873 100644 --- a/tests/integration/api_repo_actions_test.go +++ b/tests/integration/api_repo_actions_test.go @@ -13,6 +13,7 @@ import ( actions_model "forgejo.org/models/actions" auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" @@ -20,6 +21,7 @@ import ( api "forgejo.org/modules/structs" "forgejo.org/modules/webhook" "forgejo.org/routers/api/v1/shared" + repo_service "forgejo.org/services/repository" files_service "forgejo.org/services/repository/files" "forgejo.org/tests" @@ -545,4 +547,25 @@ func TestAPIRepoActionsRunnerOperations(t *testing.T) { assert.Equal(t, "token does not have at least one of required scope(s): [write:repository]", errorMessage.Message) }) + + t.Run("Endpoints disabled if Actions disabled", func(t *testing.T) { + repository, _, cleanUp := tests.CreateDeclarativeRepo(t, user2, "no-actions", + []unit_model.Type{unit_model.TypeCode, unit_model.TypeActions}, []unit_model.Type{}, nil) + defer cleanUp() + + requestURL := fmt.Sprintf("/api/v1/repos/%s/actions/runners", repository.FullName()) + + request := NewRequest(t, "GET", requestURL) + request.AddTokenAuth(readToken) + MakeRequest(t, request, http.StatusOK) + + enabledUnits := []repo_model.RepoUnit{{RepoID: repository.ID, Type: unit_model.TypeCode}} + disabledUnits := []unit_model.Type{unit_model.TypeActions} + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repository, enabledUnits, disabledUnits) + require.NoError(t, err) + + request = NewRequest(t, "GET", requestURL) + request.AddTokenAuth(readToken) + MakeRequest(t, request, http.StatusNotFound) + }) } diff --git a/tests/integration/api_repo_secrets_test.go b/tests/integration/api_repo_secrets_test.go index a2c9439f03..3da4412dde 100644 --- a/tests/integration/api_repo_secrets_test.go +++ b/tests/integration/api_repo_secrets_test.go @@ -9,11 +9,16 @@ import ( "testing" auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" repo_model "forgejo.org/models/repo" + unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" api "forgejo.org/modules/structs" + repo_service "forgejo.org/services/repository" "forgejo.org/tests" + + "github.com/stretchr/testify/require" ) func TestAPIRepoSecrets(t *testing.T) { @@ -109,4 +114,25 @@ func TestAPIRepoSecrets(t *testing.T) { AddTokenAuth(token) MakeRequest(t, req, http.StatusBadRequest) }) + + t.Run("Endpoints disabled if Actions disabled", func(t *testing.T) { + repository, _, cleanUp := tests.CreateDeclarativeRepo(t, user, "no-actions", + []unit_model.Type{unit_model.TypeCode, unit_model.TypeActions}, []unit_model.Type{}, nil) + defer cleanUp() + + getURL := fmt.Sprintf("/api/v1/repos/%s/actions/secrets", repository.FullName()) + + getRequest := NewRequest(t, "GET", getURL) + getRequest.AddTokenAuth(token) + MakeRequest(t, getRequest, http.StatusOK) + + enabledUnits := []repo_model.RepoUnit{{RepoID: repository.ID, Type: unit_model.TypeCode}} + disabledUnits := []unit_model.Type{unit_model.TypeActions} + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repository, enabledUnits, disabledUnits) + require.NoError(t, err) + + getRequest = NewRequest(t, "GET", getURL) + getRequest.AddTokenAuth(token) + MakeRequest(t, getRequest, http.StatusNotFound) + }) } diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go index 27401dcdd3..9330ca6d7e 100644 --- a/tests/integration/api_repo_variables_test.go +++ b/tests/integration/api_repo_variables_test.go @@ -9,13 +9,17 @@ import ( "testing" auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" repo_model "forgejo.org/models/repo" + unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" api "forgejo.org/modules/structs" + repo_service "forgejo.org/services/repository" "forgejo.org/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPIRepoVariablesTestCreateRepositoryVariable(t *testing.T) { @@ -234,3 +238,30 @@ func TestAPIRepoVariablesGetAllRepositoryVariables(t *testing.T) { assert.Equal(t, "SECOND", actionVariables[1].Name) assert.Equal(t, "Dolor sit amet", actionVariables[1].Data) } + +func TestAPIRepoVariablesEndpointsDisabledIfActionsDisabled(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + repository, _, cleanUp := tests.CreateDeclarativeRepo(t, user2, "no-actions", + []unit_model.Type{unit_model.TypeCode, unit_model.TypeActions}, []unit_model.Type{}, nil) + defer cleanUp() + + getURL := fmt.Sprintf("/api/v1/repos/%s/actions/variables", repository.FullName()) + + getRequest := NewRequest(t, "GET", getURL) + getRequest.AddTokenAuth(token) + MakeRequest(t, getRequest, http.StatusOK) + + enabledUnits := []repo_model.RepoUnit{{RepoID: repository.ID, Type: unit_model.TypeCode}} + disabledUnits := []unit_model.Type{unit_model.TypeActions} + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repository, enabledUnits, disabledUnits) + require.NoError(t, err) + + getRequest = NewRequest(t, "GET", getURL) + getRequest.AddTokenAuth(token) + MakeRequest(t, getRequest, http.StatusNotFound) +}