0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-14 10:57:54 +02:00

Fix public only

This commit is contained in:
Lunny Xiao 2026-04-05 17:20:33 -07:00
parent e47c6135dd
commit b6ff8e14a3
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
12 changed files with 256 additions and 47 deletions

View File

@ -250,50 +250,62 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
} }
// public Only permission check // public Only permission check
switch { for _, category := range requiredScopeCategories {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository): switch category {
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { case auth_model.AccessTokenScopeCategoryRepository:
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos") if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue): }
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { case auth_model.AccessTokenScopeCategoryIssue:
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues") if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization): }
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic { case auth_model.AccessTokenScopeCategoryOrganization:
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
} return
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic { }
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser): }
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { case auth_model.AccessTokenScopeCategoryUser:
ctx.APIError(http.StatusForbidden, "token scope is limited to public users") if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub): }
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { case auth_model.AccessTokenScopeCategoryActivityPub:
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub") if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification): }
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { case auth_model.AccessTokenScopeCategoryNotification:
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications") if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
} return
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage): }
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { case auth_model.AccessTokenScopeCategoryPackage:
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages") if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
return ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
} }
} }
} }
} }
func rejectPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
}
}
// if a token is being used for auth, we check that it contains the required scope // if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods // if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@ -966,7 +978,7 @@ func Routes() *web.Router {
m.Combo("/threads/{id}"). m.Combo("/threads/{id}").
Get(reqToken(), notify.GetThread). Get(reqToken(), notify.GetThread).
Patch(reqToken(), notify.ReadThread) Patch(reqToken(), notify.ReadThread)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
// Users (requires user scope) // Users (requires user scope)
m.Group("/users", func() { m.Group("/users", func() {
@ -1597,7 +1609,7 @@ func Routes() *web.Router {
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) }, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
// Organizations // Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() { m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs) m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)

View File

@ -22,7 +22,6 @@ func NewAvailable(ctx *context.APIContext) {
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/NotificationCount" // "$ref": "#/responses/NotificationCount"
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID, UserID: ctx.Doer.ID,
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},

View File

@ -28,10 +28,14 @@ import (
func listUserOrgs(ctx *context.APIContext, u *user_model.User) { func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
includeVisibility := organization.DoerViewOtherVisibility(ctx.Doer, u)
if ctx.PublicOnly {
includeVisibility = api.VisibleTypePublic
}
opts := organization.FindOrgOptions{ opts := organization.FindOrgOptions{
ListOptions: listOptions, ListOptions: listOptions,
UserID: u.ID, UserID: u.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u), IncludeVisibility: includeVisibility,
} }
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts) orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil { if err != nil {
@ -464,7 +468,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
includePrivate := false includePrivate := false
if ctx.IsSigned { if ctx.IsSigned && !ctx.PublicOnly {
if ctx.Doer.IsAdmin { if ctx.Doer.IsAdmin {
includePrivate = true includePrivate = true
} else { } else {

View File

@ -567,6 +567,10 @@ func GetByID(ctx *context.APIContext) {
} }
return return
} }
if ctx.PublicOnly && repo.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil { if err != nil {

View File

@ -18,6 +18,9 @@ import (
// listUserRepos - List the repositories owned by the given user. // listUserRepos - List the repositories owned by the given user.
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx) opts := utils.GetListOptions(ctx)
if ctx.PublicOnly {
private = false
}
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: u, Actor: u,
@ -79,7 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
private := ctx.IsSigned private := ctx.IsSigned && !ctx.PublicOnly
listUserRepos(ctx, ctx.ContextUser, private) listUserRepos(ctx, ctx.ContextUser, private)
} }
@ -103,11 +106,12 @@ func ListMyRepos(ctx *context.APIContext) {
// "200": // "200":
// "$ref": "#/responses/RepositoryList" // "$ref": "#/responses/RepositoryList"
private := ctx.IsSigned && !ctx.PublicOnly
opts := repo_model.SearchRepoOptions{ opts := repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx), ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer, Actor: ctx.Doer,
OwnerID: ctx.Doer.ID, OwnerID: ctx.Doer.ID,
Private: ctx.IsSigned, Private: private,
IncludeDescription: true, IncludeDescription: true,
} }

View File

@ -20,6 +20,9 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has // getStarredRepos returns the repos that the user with the specified userID has
// starred // starred
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) { func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
if ctx.PublicOnly {
private = false
}
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{ starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx), ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID, StarrerID: user.ID,

View File

@ -203,7 +203,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) && !ctx.PublicOnly
listOptions := utils.GetListOptions(ctx) listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{ opts := activities_model.GetFeedsOptions{

View File

@ -18,6 +18,9 @@ import (
// getWatchedRepos returns the repos that the user with the specified userID is watching // getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) { func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
if ctx.PublicOnly {
private = false
}
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{ watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx), ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID, WatcherID: user.ID,

View File

@ -212,3 +212,23 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread) assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned) assert.False(t, apiNL[0].Pinned)
} }
func TestAPINotificationPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/notifications").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", "/api/v1/notifications/new").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}

View File

@ -0,0 +1,96 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/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)
req := NewRequest(t, "GET", "/api/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
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")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(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.StatusForbidden)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.Empty(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.Empty(t, activities)
}

View File

@ -155,3 +155,44 @@ func TestMyOrgs(t *testing.T) {
}, },
}, orgs) }, orgs)
} }
func TestMyOrgsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var orgs []*api.Organization
DecodeJSON(t, resp, &orgs)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
assert.Equal(t, []*api.Organization{
{
ID: 17,
Name: org17.Name,
UserName: org17.Name,
FullName: org17.FullName,
Email: org17.Email,
AvatarURL: org17.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{
ID: 3,
Name: org3.Name,
UserName: org3.Name,
FullName: org3.FullName,
Email: org3.Email,
AvatarURL: org3.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
}, orgs)
}

View File

@ -155,3 +155,26 @@ func TestAPIStarDisabled(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden) MakeRequest(t, req, http.StatusForbidden)
}) })
} }
func TestAPIStarPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/starred").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
}