From 76c768edca94549df8bd4c926aaa4d5cfd110a40 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Fri, 21 Aug 2026 02:44:25 -0700 Subject: [PATCH] fix(api): enforce repository creation token authorization (#39007) (#39014) Backport #39007 by @bircni Reject public-only tokens for repository migrations and require repository scope for canonical organization repository creation. This aligns both routes with the existing token authorization boundaries. _Assisted-by: Codex:GPT-5_ Co-authored-by: bircni --- routers/api/v1/api.go | 4 +- ...pi_repository_creation_token_scope_test.go | 55 +++++++++++++++++++ tests/integration/org_count_test.go | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 tests/integration/api_repository_creation_token_scope_test.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index fa8d29c28b3..1961c0062a3 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1243,7 +1243,7 @@ func Routes() *web.Router { m.Get("/search", repo.Search) // (repo scope) - m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate) + m.Post("/migrate", reqToken(), rejectPublicOnly(), bind(api.MigrateRepoOptions{}), repo.Migrate) m.Group("/{username}/{reponame}", func() { m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff) @@ -1719,7 +1719,7 @@ func Routes() *web.Router { Delete(reqToken(), reqOrgOwnership(), org.Delete) m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename) m.Combo("/repos").Get(user.ListOrgRepos). - Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). + Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo). Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos) m.Group("/members", func() { m.Get("", reqToken(), org.ListMembers) diff --git a/tests/integration/api_repository_creation_token_scope_test.go b/tests/integration/api_repository_creation_token_scope_test.go new file mode 100644 index 00000000000..7091fd1ef7a --- /dev/null +++ b/tests/integration/api_repository_creation_token_scope_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + auth_model "gitea.dev/models/auth" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + api "gitea.dev/modules/structs" + "gitea.dev/tests" + + "github.com/stretchr/testify/require" +) + +func TestAPIRepositoryCreationTokenScopes(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + + t.Run("migration rejects public-only tokens", func(t *testing.T) { + publicOnlyToken := getUserToken(t, user.Name, + auth_model.AccessTokenScopeWriteRepository, + auth_model.AccessTokenScopePublicOnly, + ) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{Private: true}). + AddTokenAuth(publicOnlyToken) + MakeRequest(t, req, http.StatusForbidden) + + writeRepoToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository) + req = NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{Private: true}). + AddTokenAuth(writeRepoToken) + MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("organization creation requires repository scope", func(t *testing.T) { + orgOnlyToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteOrganization) + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/repos", &api.CreateRepoOption{Name: "missing-repository-scope"}). + AddTokenAuth(orgOnlyToken) + MakeRequest(t, req, http.StatusForbidden) + + orgRepoToken := getUserToken(t, user.Name, + auth_model.AccessTokenScopeWriteOrganization, + auth_model.AccessTokenScopeWriteRepository, + ) + req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/repos", &api.CreateRepoOption{Name: "repository-scope-allowed"}). + AddTokenAuth(orgRepoToken) + resp := MakeRequest(t, req, http.StatusCreated) + createdRepo := DecodeJSON(t, resp, &api.Repository{}) + require.Equal(t, "org3/repository-scope-allowed", createdRepo.FullName) + }) +} diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go index 687ec65463e..e01f0b55e25 100644 --- a/tests/integration/org_count_test.go +++ b/tests/integration/org_count_test.go @@ -27,7 +27,7 @@ func testOrgCounts(t *testing.T) { orgOwner := "user2" orgName := "testOrg" orgCollaborator := "user4" - ctx := NewAPITestContext(t, orgOwner, "repo1", auth_model.AccessTokenScopeWriteOrganization) + ctx := NewAPITestContext(t, orgOwner, "repo1", auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) var ownerCountRepos map[string]int var collabCountRepos map[string]int