// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( "net/http" "strconv" "testing" auth_model "gitea.dev/models/auth" "gitea.dev/models/organization" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" "gitea.dev/tests" "github.com/stretchr/testify/assert" ) func TestAPIUserReposPublicOnly(t *testing.T) { defer tests.PrepareTestEnv(t)() token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly) var repos []api.Repository for index, url := range []string{"/api/v1/user/repos", "/api/v1/users/user2/repos"} { req := NewRequest(t, "GET", url).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &repos) assert.NotEmpty(t, repos) for _, repo := range repos { assert.False(t, repo.Private) } assert.NotContains(t, repoNames(repos), "user2/repo2") if index == 1 { assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count")) } } } func repoNames(repos []api.Repository) []string { names := make([]string, 0, len(repos)) for _, repo := range repos { names = append(names, repo.FullName) } return names } func TestAPIPublicOnlyReadEndpoints(t *testing.T) { defer tests.PrepareTestEnv(t)() t.Run("RepoByID", func(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly) req := NewRequest(t, "GET", "/api/v1/repositories/1"). AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) req = NewRequest(t, "GET", "/api/v1/repositories/2"). AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) }) t.Run("ActivityFeeds", func(t *testing.T) { var activities []api.Activity for _, testCase := range []struct { url string scope auth_model.AccessTokenScope tokenOwners []string }{ {"/api/v1/users/user2/activities/feeds", auth_model.AccessTokenScopeReadUser, []string{"user2", "user1"}}, {"/api/v1/orgs/org3/activities/feeds", auth_model.AccessTokenScopeReadOrganization, []string{"user2"}}, } { token := getUserToken(t, "user2", testCase.scope) req := NewRequest(t, "GET", testCase.url).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &activities) assert.NotEmpty(t, activities) for _, tokenOwner := range testCase.tokenOwners { publicToken := getUserToken(t, tokenOwner, testCase.scope, auth_model.AccessTokenScopePublicOnly) req = NewRequest(t, "GET", testCase.url).AddTokenAuth(publicToken) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &activities) assertPublicActivitiesOnly(t, activities) } } }) t.Run("OrgMembership", func(t *testing.T) { // user2 is a member of the private org private_org35 org := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"}) // a full org-scoped token can read the membership permissions token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization) req := NewRequestf(t, "GET", "/api/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) // a public-only token must not disclose permissions for a private org publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly) req = NewRequestf(t, "GET", "/api/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(publicToken) MakeRequest(t, req, http.StatusNotFound) var members, publicMembers []*api.User DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(token), http.StatusOK), &members) DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members").AddTokenAuth(publicToken), http.StatusOK), &publicMembers) assert.Len(t, members, 3) assert.Len(t, publicMembers, 2) MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(token), http.StatusNoContent) MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/members/user4").AddTokenAuth(publicToken), http.StatusSeeOther) }) } func TestAPITeamReposPublicOnly(t *testing.T) { defer tests.PrepareTestEnv(t)() // team 1 (Owners of org3) has access to the private repos org3/repo3 and org3/repo5 team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) privateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) privateRepo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5}) // a full org+repo scoped token sees the private repos token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository) req := NewRequestf(t, "GET", "/api/v1/teams/%d/repos", team.ID).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) repos := DecodeJSON(t, resp, []api.Repository{}) assert.Contains(t, repoNames(repos), privateRepo.FullName()) // a public-only token must not receive any private repo publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly) req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos", team.ID).AddTokenAuth(publicToken) resp = MakeRequest(t, req, http.StatusOK) repos = DecodeJSON(t, resp, []api.Repository{}) for _, repo := range repos { assert.False(t, repo.Private) } assert.NotContains(t, repoNames(repos), privateRepo.FullName()) assert.NotContains(t, repoNames(repos), privateRepo2.FullName()) // the total-count header must match the filtered page, otherwise it leaks the // number of hidden private repos assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count")) // the single-repo endpoint must not confirm a private repo for a public-only token req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(publicToken) MakeRequest(t, req, http.StatusNotFound) } func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) { t.Helper() for _, activity := range activities { assert.False(t, activity.IsPrivate) if activity.Repo != nil { assert.False(t, activity.Repo.Private) } } } // TestAPIRepoLimitedOwnerPublicOnly ensures a public-only token cannot reach a public repo // owned by a limited-visibility owner (which is not reachable anonymously). func TestAPIRepoLimitedOwnerPublicOnly(t *testing.T) { defer tests.PrepareTestEnv(t)() // limited_org (limited visibility) owns the non-private repo public_repo_on_limited_org const url = "/api/v1/repos/limited_org/public_repo_on_limited_org" // a public-only token is confined to genuinely public resources publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly) req := NewRequest(t, "GET", url).AddTokenAuth(publicOnlyToken) MakeRequest(t, req, http.StatusNotFound) // a normal token can still reach the limited owner's public repo token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository) req = NewRequest(t, "GET", url).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) }