mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-10 09:41:52 +02:00
Merge e6a18723baaece10cf97bd679cdf458a050d7100 into 0a3aaeafe7bef9d6935422f4b91c77c216c01b21
This commit is contained in:
commit
b596cd6e88
@ -250,50 +250,62 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// public Only permission check
|
||||
switch {
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
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 {
|
||||
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 {
|
||||
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")
|
||||
return
|
||||
}
|
||||
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
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 {
|
||||
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 {
|
||||
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() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
|
||||
return
|
||||
for _, category := range requiredScopeCategories {
|
||||
switch category {
|
||||
case auth_model.AccessTokenScopeCategoryRepository:
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryIssue:
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
|
||||
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")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryUser:
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryActivityPub:
|
||||
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryNotification:
|
||||
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
|
||||
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 not being used, reqToken will enforce other sign in methods
|
||||
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
|
||||
@ -965,7 +977,9 @@ func Routes() *web.Router {
|
||||
m.Combo("/threads/{id}").
|
||||
Get(reqToken(), notify.GetThread).
|
||||
Patch(reqToken(), notify.ReadThread)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
|
||||
// notifications API should not be used with public-only tokens, as notifications are mixed with both public and private repositories
|
||||
// if a token is used with notifications API, it should be required to have the notification scope, and the token should not be public-only
|
||||
|
||||
// Users (requires user scope)
|
||||
m.Group("/users", func() {
|
||||
@ -1601,7 +1615,7 @@ func Routes() *web.Router {
|
||||
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
||||
|
||||
// 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.Get("", reqToken(), org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
|
||||
@ -35,10 +35,14 @@ import (
|
||||
|
||||
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
includeVisibility := organization.DoerViewOtherVisibility(ctx.Doer, u)
|
||||
if ctx.PublicOnly {
|
||||
includeVisibility = api.VisibleTypePublic
|
||||
}
|
||||
opts := organization.FindOrgOptions{
|
||||
ListOptions: listOptions,
|
||||
UserID: u.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
|
||||
IncludeVisibility: includeVisibility,
|
||||
}
|
||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
@ -471,7 +475,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
includePrivate := false
|
||||
if ctx.IsSigned {
|
||||
if ctx.IsSigned && !ctx.PublicOnly {
|
||||
if ctx.Doer.IsAdmin {
|
||||
includePrivate = true
|
||||
} else {
|
||||
|
||||
@ -570,6 +570,10 @@ func GetByID(ctx *context.APIContext) {
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
@ -18,6 +18,9 @@ import (
|
||||
// listUserRepos - List the repositories owned by the given user.
|
||||
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
||||
opts := utils.GetListOptions(ctx)
|
||||
if ctx.PublicOnly {
|
||||
private = false
|
||||
}
|
||||
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
Actor: u,
|
||||
@ -79,7 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
private := ctx.IsSigned
|
||||
private := ctx.IsSigned && !ctx.PublicOnly
|
||||
listUserRepos(ctx, ctx.ContextUser, private)
|
||||
}
|
||||
|
||||
@ -103,11 +106,12 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
|
||||
private := ctx.IsSigned && !ctx.PublicOnly
|
||||
opts := repo_model.SearchRepoOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
Private: ctx.IsSigned,
|
||||
Private: private,
|
||||
IncludeDescription: true,
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,9 @@ import (
|
||||
// getStarredRepos returns the repos that the user with the specified userID has
|
||||
// starred
|
||||
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{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
StarrerID: user.ID,
|
||||
|
||||
@ -203,7 +203,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$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)
|
||||
|
||||
opts := activities_model.GetFeedsOptions{
|
||||
|
||||
@ -18,6 +18,9 @@ import (
|
||||
|
||||
// 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) {
|
||||
if ctx.PublicOnly {
|
||||
private = false
|
||||
}
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
WatcherID: user.ID,
|
||||
|
||||
@ -209,3 +209,23 @@ func TestAPINotificationPUT(t *testing.T) {
|
||||
assert.True(t, apiNL[0].Unread)
|
||||
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)
|
||||
}
|
||||
|
||||
96
tests/integration/api_public_only_test.go
Normal file
96
tests/integration/api_public_only_test.go
Normal 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)
|
||||
}
|
||||
@ -154,3 +154,44 @@ func TestMyOrgs(t *testing.T) {
|
||||
},
|
||||
}, 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)
|
||||
}
|
||||
|
||||
@ -153,3 +153,26 @@ func TestAPIStarDisabled(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user