mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-19 16:18:11 +00:00
refactor: carry the API token on the doer (#39690)
The token a request authenticated with now lives on the doer, so permission and visibility checks apply its scope from the doer alone. Replaces https://github.com/go-gitea/gitea/pull/39167, following https://github.com/go-gitea/gitea/pull/39167#issuecomment-6063839619. 1. Public-only tokens get 404 instead of 403 for hidden users, orgs and repos, like GitHub. 2. Org ownership, site admin, repo and org creation, generation and transfer endpoints reject public-only tokens. 3. API pull request responses set `head.repo` to `null` and `head.repo_id` to `-1` when the caller cannot see the head repository. 4. Public-only tokens see org memberships like anonymous callers, so concealed members stay hidden. 5. Site admin access in activity feeds, follower lists, org members, Actions run lists and packages needs a session or an `all`-scoped token. Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -436,12 +436,6 @@ type GetFeedsOptions struct {
|
||||
DontCount bool // do counting in GetFeeds
|
||||
}
|
||||
|
||||
func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.IncludePrivate = false
|
||||
}
|
||||
}
|
||||
|
||||
// ActivityReadable return whether doer can read activities of user
|
||||
func ActivityReadable(user, doer *user_model.User) bool {
|
||||
return !user.KeepActivityPrivate ||
|
||||
@@ -478,13 +472,13 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
||||
}
|
||||
|
||||
// check activity visibility for actor ( similar to activityReadable() )
|
||||
if opts.Actor == nil {
|
||||
if !user_model.DoerCanViewPrivate(opts.Actor) {
|
||||
cond = cond.And(builder.In("act_user_id",
|
||||
builder.Select("`user`.id").Where(
|
||||
builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic},
|
||||
).From("`user`"),
|
||||
))
|
||||
} else if !opts.Actor.IsAdmin {
|
||||
} else if !user_model.DoerIsSiteAdminFullAccess(opts.Actor) {
|
||||
uidCond := builder.Select("`user`.id").From("`user`").Where(
|
||||
builder.Eq{"keep_activity_private": false}.
|
||||
And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))).
|
||||
@@ -509,7 +503,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
||||
}
|
||||
|
||||
// check readable repositories by doer/actor
|
||||
if opts.Actor == nil || !opts.Actor.IsAdmin {
|
||||
if !user_model.DoerIsSiteAdminFullAccess(opts.Actor) {
|
||||
cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor)))
|
||||
}
|
||||
|
||||
@@ -539,7 +533,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.IncludePrivate {
|
||||
if !opts.IncludePrivate || user_model.IsPublicOnlyDoer(opts.Actor) {
|
||||
cond = cond.And(builder.Eq{"`action`.is_private": false})
|
||||
}
|
||||
if !opts.IncludeDeleted {
|
||||
|
||||
@@ -212,7 +212,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
|
||||
var err error
|
||||
var cond builder.Cond
|
||||
// if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition
|
||||
if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) {
|
||||
if opts.RequestedUser != nil && user_model.DoerCanViewPrivate(opts.Actor) && (user_model.DoerIsSiteAdminFullAccess(opts.Actor) || opts.Actor.ID == opts.RequestedUser.ID) {
|
||||
cond = builder.Eq{
|
||||
"user_id": opts.RequestedUser.ID,
|
||||
}.And(
|
||||
|
||||
@@ -283,6 +283,7 @@ func applyConditions(sess db.Session, opts *IssuesOptions) {
|
||||
sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
|
||||
}
|
||||
|
||||
sess.And(repo_model.DoerTokenRepoCond(opts.Doer))
|
||||
if opts.Doer != nil && !opts.Doer.IsAdmin {
|
||||
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
@@ -150,6 +151,8 @@ func TestUpdateIssueCols(t *testing.T) {
|
||||
|
||||
func TestIssues(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
publicOnlyUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
publicOnlyUser2.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:issue")
|
||||
for _, test := range []struct {
|
||||
Opts issues_model.IssuesOptions
|
||||
ExpectedIssueIDs []int64
|
||||
@@ -204,6 +207,15 @@ func TestIssues(t *testing.T) {
|
||||
},
|
||||
[]int64{11, 5, 9, 8, 3, 2, 1},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
RepoCond: builder.In("repo_id", 1, 2),
|
||||
Doer: publicOnlyUser2,
|
||||
IsPull: optional.Some(false),
|
||||
SortType: "oldest",
|
||||
},
|
||||
[]int64{1, 5},
|
||||
},
|
||||
} {
|
||||
issues, err := issues_model.Issues(t.Context(), &test.Opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -178,7 +178,7 @@ type FindOrgMembersOpts struct {
|
||||
}
|
||||
|
||||
func (opts FindOrgMembersOpts) PublicOnly() bool {
|
||||
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
|
||||
return !user_model.DoerCanViewPrivate(opts.Doer) || !(opts.IsDoerMember || user_model.DoerIsSiteAdminFullAccess(opts.Doer))
|
||||
}
|
||||
|
||||
// applyKeywordFilter adds keyword search conditions to session
|
||||
@@ -456,8 +456,8 @@ func ownerVisibilitySatisfiesDoer(orgOrUser, user *user_model.User) bool {
|
||||
|
||||
// HasOrgOrUserVisible tells if the given user can see the given org or user
|
||||
func HasOrgOrUserVisible(ctx context.Context, owner, doer *user_model.User) bool {
|
||||
return ownerVisibilitySatisfiesDoer(owner, doer) ||
|
||||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID))
|
||||
return user_model.DoerTokenAllowsOwner(doer, owner) && (ownerVisibilitySatisfiesDoer(owner, doer) ||
|
||||
(doer != nil && OrgFromUser(owner).HasMemberWithUserID(ctx, doer.ID)))
|
||||
}
|
||||
|
||||
// HasOrgsVisible tells if the given user can see at least one of the orgs provided
|
||||
|
||||
@@ -54,12 +54,6 @@ type FindOrgOptions struct {
|
||||
IncludeVisibility structs.VisibleType
|
||||
}
|
||||
|
||||
func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.IncludeVisibility = structs.VisibleTypePublic
|
||||
}
|
||||
}
|
||||
|
||||
func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
||||
cond := builder.Eq{"uid": userID}
|
||||
if !includePrivate {
|
||||
@@ -83,7 +77,7 @@ func (opts FindOrgOptions) ToOrders() string {
|
||||
}
|
||||
|
||||
func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType {
|
||||
if doer == nil || other == nil {
|
||||
if other == nil || !user_model.DoerCanViewPrivate(doer) {
|
||||
return structs.VisibleTypePublic
|
||||
}
|
||||
if doer.IsAdmin || doer.ID == other.ID {
|
||||
|
||||
@@ -123,7 +123,7 @@ func GetTotalUnreferencedBlobSize(ctx context.Context) (int64, error) {
|
||||
|
||||
// IsBlobAccessibleForUser tests if the user has access to the blob
|
||||
func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model.User) (bool, error) {
|
||||
if user.IsAdmin {
|
||||
if user_model.DoerIsSiteAdminFullAccess(user) {
|
||||
return true, nil
|
||||
}
|
||||
ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic}
|
||||
@@ -153,6 +153,9 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model
|
||||
Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}.
|
||||
And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))),
|
||||
)
|
||||
if !user_model.DoerCanViewPrivate(user) {
|
||||
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
|
||||
}
|
||||
|
||||
return db.GetEngine(ctx).
|
||||
Table("package_blob").
|
||||
|
||||
@@ -424,6 +424,9 @@ func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
// In most request paths, callers should use GetDoerRepoPermission instead.
|
||||
// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users.
|
||||
func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
if !repo_model.DoerTokenAllowsRepo(ctx, user, repo) {
|
||||
return PermissionNoAccess(), nil
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
@@ -675,10 +678,14 @@ func PermissionNoAccess() Permission {
|
||||
// permission on a repository. Producers and consumers must agree on it, so it lives here.
|
||||
func RepoUserPermissionCacheKey(repoID int64, doer *user_model.User) string {
|
||||
var doerID int64
|
||||
var doerExtData string
|
||||
if doer != nil {
|
||||
doerID = doer.ID
|
||||
if doer.ExtDoerData != nil {
|
||||
doerExtData = doer.ExtDoerData.EncodeToString()
|
||||
}
|
||||
return fmt.Sprintf("%d-%d", repoID, doerID)
|
||||
}
|
||||
return fmt.Sprintf("%d-%d-%s", repoID, doerID, doerExtData)
|
||||
}
|
||||
|
||||
// CanReadWorkflowCrossRepo checks whether the run can read workflow files from targetRepo.
|
||||
|
||||
@@ -272,4 +272,21 @@ func testGetDoerRepoPermission(t *testing.T) {
|
||||
individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, individualPerm, doerPerm)
|
||||
|
||||
t.Run("PublicOnlyTokenReachesOnlyPublicReposOfPublicOwners", func(t *testing.T) {
|
||||
publicOnlyUser := *regularUser
|
||||
publicOnlyUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:repository")
|
||||
publicOnlyAdmin := *unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
publicOnlyAdmin.ExtDoerData = publicOnlyUser.ExtDoerData
|
||||
for _, repoID := range []int64{2, 38} {
|
||||
perm, err := GetDoerRepoPermission(ctx, unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}), &publicOnlyAdmin)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, perm.HasAnyUnitAccessOrPublicAccess(), "repo %d", repoID)
|
||||
}
|
||||
|
||||
perm, err := GetDoerRepoPermission(ctx, repo1, &publicOnlyUser)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, perm.CanWrite(unit.TypeCode))
|
||||
assert.NotEqual(t, RepoUserPermissionCacheKey(repo1.ID, regularUser), RepoUserPermissionCacheKey(repo1.ID, &publicOnlyUser))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ func GetOrgRepositoryIDs(ctx context.Context, orgID int64) (repoIDs []int64, _ e
|
||||
type SearchTeamRepoOptions struct {
|
||||
db.ListOptions
|
||||
TeamID int64
|
||||
// PublicOnly restricts the result (and count) to non-private repositories.
|
||||
PublicOnly bool
|
||||
Actor *user_model.User
|
||||
}
|
||||
|
||||
func (opts *SearchTeamRepoOptions) toCond() builder.Cond {
|
||||
@@ -44,10 +43,7 @@ func (opts *SearchTeamRepoOptions) toCond() builder.Cond {
|
||||
Where(builder.Eq{"team_id": opts.TeamID}),
|
||||
))
|
||||
}
|
||||
if opts.PublicOnly {
|
||||
cond = cond.And(builder.Eq{"is_private": false})
|
||||
}
|
||||
return cond
|
||||
return cond.And(DoerTokenRepoCond(opts.Actor))
|
||||
}
|
||||
|
||||
// GetTeamRepositories returns paginated repositories in team of organization.
|
||||
|
||||
+32
-22
@@ -212,13 +212,6 @@ type SearchRepoOptions struct {
|
||||
OnlyShowRelevant bool
|
||||
}
|
||||
|
||||
func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.Private = false
|
||||
opts.AllLimited = false
|
||||
}
|
||||
}
|
||||
|
||||
// UserOwnedRepoCond returns user ownered repositories
|
||||
func UserOwnedRepoCond(userID int64) builder.Cond {
|
||||
return builder.Eq{
|
||||
@@ -370,6 +363,9 @@ func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond {
|
||||
|
||||
// SearchRepositoryCondition creates a query condition according search repository options
|
||||
func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond {
|
||||
if user_model.IsPublicOnlyDoer(opts.Actor) {
|
||||
opts.Private, opts.AllLimited = false, false
|
||||
}
|
||||
cond := builder.NewCond()
|
||||
|
||||
if opts.Private {
|
||||
@@ -663,6 +659,9 @@ func userAllPublicRepoCond(cond builder.Cond, ownerVisibilityLimit []structs.Vis
|
||||
|
||||
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
||||
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
|
||||
if user_model.IsPublicOnlyDoer(user) && !user.IsRestricted {
|
||||
return PublicRepoUnderPublicOwnerCond()
|
||||
}
|
||||
cond := builder.NewCond()
|
||||
|
||||
if user == nil || !user.IsRestricted || user.ID <= 0 {
|
||||
@@ -700,8 +699,7 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
||||
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||
}
|
||||
}
|
||||
|
||||
return cond
|
||||
return cond.And(DoerTokenRepoCond(user))
|
||||
}
|
||||
|
||||
// SearchRepositoryByName takes keyword and part of repository name to search,
|
||||
@@ -766,6 +764,20 @@ func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||
)
|
||||
}
|
||||
|
||||
func DoerTokenAllowsRepo(ctx context.Context, doer *user_model.User, repo *Repository) bool {
|
||||
if !user_model.IsPublicOnlyDoer(doer) {
|
||||
return true
|
||||
}
|
||||
return !repo.IsPrivate && repo.LoadOwner(ctx) == nil && repo.Owner.Visibility.IsPublic()
|
||||
}
|
||||
|
||||
func DoerTokenRepoCond(doer *user_model.User) builder.Cond {
|
||||
if user_model.IsPublicOnlyDoer(doer) {
|
||||
return PublicRepoUnderPublicOwnerCond()
|
||||
}
|
||||
return builder.NewCond()
|
||||
}
|
||||
|
||||
// NotPublicRepoUnderPublicOwnerCond complements PublicRepoUnderPublicOwnerCond. Spelled positively so
|
||||
// the owner subquery hashes the limited/private minority, not every public user.
|
||||
func NotPublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||
@@ -780,23 +792,21 @@ func NotPublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||
// - owner_id = ownerID: only that owner's repos.
|
||||
// - AccessibleRepositoryCondition(user, TypeActions): only repos whose Actions the user can read
|
||||
// (admin/owner teams are handled inside it; a site admin is not, callers must skip the filter for one).
|
||||
// - publicOnly (a public-only token): additionally limit to public repos under a public owner.
|
||||
func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User, publicOnly bool) builder.Cond {
|
||||
cond := builder.NewCond().And(
|
||||
func UserActionsAccessibleOwnerRepoCond(ownerID int64, user *user_model.User) builder.Cond {
|
||||
return builder.And(
|
||||
builder.Eq{"`repository`.owner_id": ownerID},
|
||||
AccessibleRepositoryCondition(user, unit.TypeActions),
|
||||
)
|
||||
if publicOnly {
|
||||
cond = cond.And(PublicRepoUnderPublicOwnerCond())
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// FindUserActionsAccessibleOwnerRepoIDsSubQuery returns a subquery selecting the repository IDs the user
|
||||
// can see for the given owner. Callers embed it in an `IN (...)` condition so that a large owner does not
|
||||
// can see for the given owner, nil if unrestricted. Callers embed it in an `IN (...)` condition so that a large owner does not
|
||||
// materialize every repo ID into the SQL statement, which could exceed database parameter limits.
|
||||
func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User, publicOnly bool) *builder.Builder {
|
||||
return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user, publicOnly))
|
||||
func FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID int64, user *user_model.User) *builder.Builder {
|
||||
if ownerID <= 0 || user_model.DoerIsSiteAdminFullAccess(user) {
|
||||
return nil
|
||||
}
|
||||
return builder.Select("id").From("repository").Where(UserActionsAccessibleOwnerRepoCond(ownerID, user))
|
||||
}
|
||||
|
||||
// GetUserRepositories returns a list of repositories of given user.
|
||||
@@ -806,10 +816,10 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor
|
||||
}
|
||||
|
||||
cond := builder.NewCond()
|
||||
if opts.Actor == nil {
|
||||
return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: Actor is needed but not given")
|
||||
if opts.OwnerID == 0 {
|
||||
return nil, 0, util.NewInvalidArgumentErrorf("GetUserRepositories: OwnerID is needed but not given")
|
||||
}
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID})
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}, DoerTokenRepoCond(opts.Actor))
|
||||
if !opts.Private {
|
||||
cond = cond.And(builder.Eq{"is_private": false})
|
||||
}
|
||||
|
||||
@@ -493,14 +493,21 @@ func TestFindUserActionsAccessibleOwnerRepoIDs(t *testing.T) {
|
||||
// org3 is a public org owning repo3 (private) and repo32 (public), both with the actions unit
|
||||
const orgID = 3
|
||||
|
||||
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, false))
|
||||
all, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, all, int64(3), "without public-only the private repo's actions are listed")
|
||||
|
||||
publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, user, true))
|
||||
scopedUser := *user
|
||||
scopedUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:repository")
|
||||
publicOnly, err := repo_model.SearchRepositoryIDsByCondition(t.Context(), repo_model.UserActionsAccessibleOwnerRepoCond(orgID, &scopedUser))
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, publicOnly, int64(3), "a public-only token must not list a private repo's actions")
|
||||
assert.Contains(t, publicOnly, int64(32), "a public repo under a public owner stays listed")
|
||||
|
||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
assert.Nil(t, repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(orgID, admin))
|
||||
admin.ExtDoerData = scopedUser.ExtDoerData
|
||||
assert.NotNil(t, repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(orgID, admin))
|
||||
}
|
||||
|
||||
// TestUserOrgUnitRepoCondTeamAuthorize pins team.authorize vs team_unit.access_mode
|
||||
|
||||
@@ -31,12 +31,6 @@ func (opts *StarredReposOptions) ToOrders() string {
|
||||
return "`repository`.id"
|
||||
}
|
||||
|
||||
func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.IncludePrivate = false
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *StarredReposOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"star.uid": opts.StarrerID,
|
||||
@@ -84,12 +78,6 @@ func (opts *WatchedReposOptions) ToOrders() string {
|
||||
return "`repository`.id"
|
||||
}
|
||||
|
||||
func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.IncludePrivate = false
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *WatchedReposOptions) ToConds() builder.Cond {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"watch.user_id": opts.WatcherID,
|
||||
|
||||
+8
-20
@@ -60,12 +60,6 @@ func (opts *SearchUserOptions) ToOrders() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) {
|
||||
if publicOnly {
|
||||
opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
|
||||
}
|
||||
}
|
||||
|
||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) db.Session {
|
||||
var cond builder.Cond
|
||||
cond = builder.In("type", opts.Types)
|
||||
@@ -177,23 +171,17 @@ func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _
|
||||
|
||||
// BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see
|
||||
func BuildCanSeeUserCondition(actor *User) builder.Cond {
|
||||
if actor != nil {
|
||||
// If Admin - they see all users!
|
||||
if !actor.IsAdmin {
|
||||
// Users can see an organization they are a member of
|
||||
cond := builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID}))
|
||||
canViewPrivate := DoerCanViewPrivate(actor)
|
||||
cond := builder.NewCond()
|
||||
if actor != nil && !actor.IsAdmin && (canViewPrivate || actor.IsRestricted) {
|
||||
cond = builder.In("`user`.id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": actor.ID}))
|
||||
if !actor.IsRestricted {
|
||||
// Not-Restricted users can see public and limited users/organizations
|
||||
cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
||||
}
|
||||
// Don't forget about self
|
||||
return cond.Or(builder.Eq{"`user`.id": actor.ID})
|
||||
cond = cond.Or(builder.Eq{"`user`.id": actor.ID})
|
||||
}
|
||||
|
||||
return nil
|
||||
if !canViewPrivate {
|
||||
cond = cond.And(builder.In("`user`.visibility", structs.VisibleTypePublic))
|
||||
}
|
||||
|
||||
// Force visibility for privacy
|
||||
// Not logged in - only public users
|
||||
return builder.In("`user`.visibility", structs.VisibleTypePublic)
|
||||
return cond
|
||||
}
|
||||
|
||||
+7
-8
@@ -175,7 +175,8 @@ type User struct {
|
||||
// When the user model is used as a doer (all existing code does so), the doer can have extra details.
|
||||
// * Actions task doer needs to bind to the task
|
||||
// * Deploy-key doer needs to bind to the key
|
||||
ExtDoerData ExtDoerData `xorm:"-"`
|
||||
// * API token doer needs to bind to the token and its scope
|
||||
ExtDoerData ExtDoerData `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// Meta defines the meta information of a user, to be stored in the K/V table
|
||||
@@ -445,11 +446,6 @@ func (u *User) IsTypeBot() bool {
|
||||
return u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
|
||||
func (u *User) IsTokenAccessAllowed() bool {
|
||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
||||
func (u *User) EmailTo() string {
|
||||
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
|
||||
@@ -1446,11 +1442,11 @@ func GetAdminUser(ctx context.Context) (*User, error) {
|
||||
}
|
||||
|
||||
func isUserVisibleToViewerCond(viewer *User) builder.Cond {
|
||||
if viewer != nil && viewer.IsAdmin {
|
||||
if DoerIsSiteAdminFullAccess(viewer) {
|
||||
return builder.NewCond()
|
||||
}
|
||||
|
||||
if viewer == nil || viewer.IsRestricted {
|
||||
if !DoerCanViewPrivate(viewer) || viewer.IsRestricted {
|
||||
return builder.Eq{
|
||||
"`user`.visibility": structs.VisibleTypePublic,
|
||||
}
|
||||
@@ -1484,6 +1480,9 @@ func isUserVisibleToViewerCond(viewer *User) builder.Cond {
|
||||
|
||||
// IsUserVisibleToViewer check if viewer is able to see user profile
|
||||
func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool {
|
||||
if !DoerTokenAllowsOwner(viewer, u) {
|
||||
return false
|
||||
}
|
||||
if viewer != nil && (viewer.IsAdmin || viewer.ID == u.ID) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
)
|
||||
|
||||
type ExtDoerData interface {
|
||||
@@ -44,3 +47,88 @@ func (e *extDoerDeployKey) DecodeFromString(s string) (err error) {
|
||||
e.DeployKeyID, err = strconv.ParseInt(idStr, 10, 64)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
CredentialAccessToken = "access-token"
|
||||
CredentialOAuth2Grant = "oauth2-grant"
|
||||
)
|
||||
|
||||
type extDoerToken struct {
|
||||
credential string // empty for package registry tokens
|
||||
scope auth.AccessTokenScope
|
||||
publicOnly bool
|
||||
}
|
||||
|
||||
var _ ExtDoerData = (*extDoerToken)(nil)
|
||||
|
||||
func NewTokenExtDoerData(credentialKind string, credentialID int64, scope auth.AccessTokenScope) ExtDoerData {
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
e := &extDoerToken{scope: scope, publicOnly: publicOnly || err != nil}
|
||||
if credentialKind != "" {
|
||||
e.credential = credentialKind + ":" + strconv.FormatInt(credentialID, 10)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *extDoerToken) EncodeToString() string {
|
||||
return e.credential + "|" + string(e.scope)
|
||||
}
|
||||
|
||||
func (e *extDoerToken) DecodeFromString(s string) error {
|
||||
credential, scope, ok := strings.Cut(s, "|")
|
||||
if !ok {
|
||||
return errors.New("invalid token doer data")
|
||||
}
|
||||
publicOnly, err := auth.AccessTokenScope(scope).PublicOnly()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.credential, e.scope, e.publicOnly = credential, auth.AccessTokenScope(scope), publicOnly
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDoerToken(u *User) *extDoerToken {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
token, _ := u.ExtDoerData.(*extDoerToken)
|
||||
return token
|
||||
}
|
||||
|
||||
func GetDoerTokenScope(u *User) (auth.AccessTokenScope, bool) {
|
||||
if token := getDoerToken(u); token != nil {
|
||||
return token.scope, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func IsPublicOnlyDoer(u *User) bool {
|
||||
token := getDoerToken(u)
|
||||
return token != nil && token.publicOnly
|
||||
}
|
||||
|
||||
func DoerCanViewPrivate(doer *User) bool {
|
||||
return doer != nil && !IsPublicOnlyDoer(doer)
|
||||
}
|
||||
|
||||
func DoerIsSiteAdminFullAccess(doer *User) bool {
|
||||
if !DoerCanViewPrivate(doer) || !doer.IsAdmin {
|
||||
return false
|
||||
}
|
||||
scope, hasScope := GetDoerTokenScope(doer)
|
||||
return !hasScope || scope == auth.AccessTokenScopeAll
|
||||
}
|
||||
|
||||
func DoerTokenAllowsOwner(doer, owner *User) bool {
|
||||
return !IsPublicOnlyDoer(doer) || owner.Visibility.IsPublic()
|
||||
}
|
||||
|
||||
func GetDoerCredential(u *User) string {
|
||||
if u == nil || u.ExtDoerData == nil {
|
||||
return ""
|
||||
}
|
||||
if token := getDoerToken(u); token != nil {
|
||||
return token.credential
|
||||
}
|
||||
return u.ExtDoerData.EncodeToString()
|
||||
}
|
||||
|
||||
@@ -115,15 +115,17 @@ func GetSystemUserByID(uid int64) *User {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDoerPermissionUser(ctx context.Context, id int64, extDoerData string) (u *User, _ error) {
|
||||
if id > 0 {
|
||||
return GetUserByID(ctx, id)
|
||||
func GetDoerPermissionUser(ctx context.Context, id int64, extDoerData string) (u *User, err error) {
|
||||
switch {
|
||||
case id > 0:
|
||||
if u, err = GetUserByID(ctx, id); err != nil || extDoerData == "" {
|
||||
return u, err
|
||||
}
|
||||
switch id {
|
||||
case ActionsUserID:
|
||||
u.ExtDoerData = &extDoerToken{}
|
||||
case id == ActionsUserID:
|
||||
u = NewActionsUser()
|
||||
u.ExtDoerData = &extDoerGiteaActions{}
|
||||
case DeployKeyUserID:
|
||||
case id == DeployKeyUserID:
|
||||
u = NewDeployKeyUser()
|
||||
u.ExtDoerData = &extDoerDeployKey{}
|
||||
default:
|
||||
|
||||
@@ -6,6 +6,8 @@ package user
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -37,3 +39,15 @@ func TestSystemUser(t *testing.T) {
|
||||
assert.Equal(t, int64(-1), uid)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
}
|
||||
|
||||
func TestGetDoerPermissionUserCarriesToken(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
u, err := GetDoerPermissionUser(t.Context(), 2, NewTokenExtDoerData(CredentialAccessToken, 9, "public-only,read:repository").EncodeToString())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, IsPublicOnlyDoer(u))
|
||||
assert.Equal(t, "access-token:9", GetDoerCredential(u))
|
||||
|
||||
_, err = GetDoerPermissionUser(t.Context(), 2, "access-token:9|bogus")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -215,6 +215,11 @@ func TestSearchUsers(t *testing.T) {
|
||||
|
||||
testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)},
|
||||
[]int64{24})
|
||||
|
||||
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
testUserSuccess(user_model.SearchUserOptions{Actor: restrictedUser}, []int64{29})
|
||||
restrictedUser.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:user")
|
||||
testUserSuccess(user_model.SearchUserOptions{Actor: restrictedUser}, []int64{29})
|
||||
}
|
||||
|
||||
func TestEmailNotificationPreferences(t *testing.T) {
|
||||
@@ -525,6 +530,10 @@ func TestIsUserVisibleToViewer(t *testing.T) {
|
||||
test(user20, user1, true)
|
||||
test(user31, user1, true)
|
||||
test(user33, user1, true)
|
||||
publicOnlyAdmin := *user1
|
||||
publicOnlyAdmin.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:user")
|
||||
test(user31, &publicOnlyAdmin, false)
|
||||
test(user33, &publicOnlyAdmin, false)
|
||||
|
||||
// non admin viewer
|
||||
test(user4, user4, true)
|
||||
|
||||
@@ -16,8 +16,6 @@ const (
|
||||
ContextDataKeySignedUser = "SignedUser"
|
||||
// ContextDataKeyImpersonator holds the admin acting as the signed-in user, if any.
|
||||
ContextDataKeyImpersonator = "Impersonator"
|
||||
// ContextDataKeyAuthCredential names the credential the request authenticated with, e.g. "access-token:42".
|
||||
ContextDataKeyAuthCredential = "AuthCredential"
|
||||
)
|
||||
|
||||
func GetContextData(c context.Context) reqctx.ContextData {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/perm"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
npm_module "gitea.dev/modules/packages/npm"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -41,50 +42,20 @@ import (
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if hasApiTokenScope {
|
||||
{ // request authenticated by a scoped token; enforce package scope restrictions
|
||||
scopeMatched := false
|
||||
var err error
|
||||
switch accessMode {
|
||||
case perm.AccessModeRead:
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage)
|
||||
if scope, ok := user_model.GetDoerTokenScope(ctx.Doer); ok {
|
||||
scopeMatched, err := scope.HasScope(auth_model.GetRequiredScopes(auth_model.GetScopeLevelFromAccessMode(accessMode), auth_model.AccessTokenScopeCategoryPackage)...)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error())
|
||||
return
|
||||
}
|
||||
case perm.AccessModeWrite:
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if !scopeMatched {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
|
||||
ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||
return
|
||||
}
|
||||
|
||||
// check if scope only applies to public resources
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly {
|
||||
// a public-only token must not reach limited-visibility owners either,
|
||||
// matching how orgs/users are enforced elsewhere in this file
|
||||
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
|
||||
ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
if ctx.Package.AccessMode < accessMode {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
|
||||
ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ func (a *Auth) Name() string {
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, _ auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
packageMeta, err := packages.ParseAuthorizationRequest(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
@@ -49,11 +49,9 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if packageMeta.Scope != "" {
|
||||
store.GetData()["ApiTokenScope"] = packageMeta.Scope
|
||||
u.ExtDoerData = user_model.NewTokenExtDoerData("", 0, packageMeta.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func Authenticate(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data)
|
||||
if has, err := packageScope.HasAnyScope(
|
||||
auth_model.AccessTokenScopeReadPackage,
|
||||
auth_model.AccessTokenScopeWritePackage,
|
||||
@@ -154,7 +154,7 @@ func CheckCredentials(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data)
|
||||
if has, err := packageScope.HasAnyScope(
|
||||
auth_model.AccessTokenScopeReadPackage,
|
||||
auth_model.AccessTokenScopeWritePackage,
|
||||
|
||||
@@ -153,7 +153,7 @@ func DetermineSupport(ctx *context.Context) {
|
||||
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
|
||||
func Authenticate(ctx *context.Context) {
|
||||
u := ctx.Doer
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
packageScope := auth_service.GetAccessScope(ctx.Doer, ctx.Data)
|
||||
if u == nil {
|
||||
if setting.Service.RequireSignInViewStrict {
|
||||
APIUnauthorizedError(ctx)
|
||||
|
||||
+34
-103
@@ -130,6 +130,7 @@ func sudo() func(ctx *context.APIContext) {
|
||||
|
||||
audit.Record(ctx, audit_model.UserImpersonation, user)
|
||||
|
||||
user.ExtDoerData = ctx.Doer.ExtDoerData
|
||||
ctx.Doer = user
|
||||
// keep the audit actor in step with the effective doer, and keep the admin attached to it
|
||||
ctx.Data[middleware.ContextDataKeyImpersonator] = ctx.Data[middleware.ContextDataKeySignedUser]
|
||||
@@ -219,11 +220,6 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.TokenCanAccessRepo(repo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,73 +239,16 @@ func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool,
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
if ctx.Package.AccessMode < accessMode {
|
||||
ctx.APIError(http.StatusForbidden, "user should have specific permission or be a site admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkTokenPublicOnly() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if !ctx.PublicOnly {
|
||||
return
|
||||
}
|
||||
|
||||
requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory)
|
||||
if !ok || len(requiredScopeCategories) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, category := range requiredScopeCategories {
|
||||
switch category {
|
||||
case auth_model.AccessTokenScopeCategoryRepository:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryIssue:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryOrganization:
|
||||
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
|
||||
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
|
||||
if orgPrivate || userOrgPrivate {
|
||||
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.IsPublic() {
|
||||
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.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryNotification:
|
||||
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
|
||||
return
|
||||
}
|
||||
case auth_model.AccessTokenScopeCategoryPackage:
|
||||
// a public-only token must not reach limited-visibility owners either,
|
||||
// matching the org/user public-only enforcement above
|
||||
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
|
||||
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 {
|
||||
if !user_model.IsPublicOnlyDoer(ctx.Doer) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -319,6 +258,10 @@ func rejectPublicOnly() func(ctx *context.APIContext) {
|
||||
|
||||
func contextAuthenticatedUser() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if !user_model.DoerTokenAllowsOwner(ctx.Doer, ctx.Doer) {
|
||||
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
|
||||
return
|
||||
}
|
||||
ctx.ContextUser = ctx.Doer
|
||||
}
|
||||
}
|
||||
@@ -333,7 +276,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
||||
}
|
||||
|
||||
// Need OAuth2 token to be present.
|
||||
scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
scope, scopeExists := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if !scopeExists {
|
||||
return
|
||||
}
|
||||
@@ -354,20 +297,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
||||
|
||||
if !allow {
|
||||
ctx.APIError(http.StatusForbidden, fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["requiredScopeCategories"] = requiredScopeCategories
|
||||
|
||||
// check if scope only applies to public resources
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusForbidden, "parsing public resource scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// assign to true so that those searching should only filter public repositories/users/organizations
|
||||
ctx.PublicOnly = publicOnly
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,6 +422,9 @@ func reqAnyRepoReader() func(ctx *context.APIContext) {
|
||||
// reqOrgOwnership user should be an organization owner, or a site admin
|
||||
func reqOrgOwnership() func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if rejectPublicOnly()(ctx); ctx.Written() { // org ownership reaches the org's private repos (hooks, teams, secrets)
|
||||
return
|
||||
}
|
||||
if ctx.IsUserSiteAdmin() {
|
||||
return
|
||||
}
|
||||
@@ -731,6 +664,10 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Org.Organization != nil && !user_model.DoerTokenAllowsOwner(ctx.Doer, ctx.Org.Organization.AsUser()) {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,13 +754,6 @@ func mustEnableWiki(ctx *context.APIContext) {
|
||||
// visibility is too permissive for reads, org ownership too strict for writes.
|
||||
func reqProjectsUnitAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
// "/users/{username}/projects" also accepts an organization, where checkTokenPublicOnly
|
||||
// does nothing because IsTokenAccessAllowed is false for orgs. Enforce it here, before
|
||||
// the admin bypass, so both spellings of the route answer alike.
|
||||
if ctx.PublicOnly && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic() {
|
||||
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
|
||||
return
|
||||
}
|
||||
if ctx.IsUserSiteAdmin() {
|
||||
return
|
||||
}
|
||||
@@ -1133,7 +1063,7 @@ func Routes() *web.Router {
|
||||
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
||||
|
||||
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||
}, context.UserAssignmentAPI(), individualPermsChecker)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
||||
|
||||
// Users (requires user scope)
|
||||
@@ -1151,7 +1081,7 @@ func Routes() *web.Router {
|
||||
m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos)
|
||||
|
||||
m.Get("/subscriptions", user.GetWatchedRepos)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||
}, context.UserAssignmentAPI(), individualPermsChecker)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Users (requires user scope)
|
||||
@@ -1249,7 +1179,7 @@ func Routes() *web.Router {
|
||||
m.Get("", user.IsStarring)
|
||||
m.Put("", user.Star)
|
||||
m.Delete("", user.Unstar)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, repoAssignment())
|
||||
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
|
||||
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
|
||||
@@ -1274,15 +1204,16 @@ func Routes() *web.Router {
|
||||
m.Get("", user.CheckUserBlock)
|
||||
m.Put("", user.BlockUser)
|
||||
m.Delete("", user.UnblockUser)
|
||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||
}, context.UserAssignmentAPI())
|
||||
}, rejectPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser())
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
m.Post("/org/{org}/repos",
|
||||
// FIXME: we need org in context
|
||||
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
|
||||
reqToken(),
|
||||
rejectPublicOnly(),
|
||||
bind(api.CreateRepoOption{}),
|
||||
repo.CreateOrgRepoDeprecated)
|
||||
|
||||
@@ -1303,12 +1234,12 @@ func Routes() *web.Router {
|
||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
|
||||
Delete(reqToken(), reqRepoDangerZone(), repo.Delete).
|
||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
||||
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
||||
m.Post("/generate", reqToken(), rejectPublicOnly(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
||||
m.Group("/transfer", func() {
|
||||
m.Post("", reqRepoDangerZone(), bind(api.TransferRepoOption{}), repo.Transfer)
|
||||
m.Post("/accept", repo.AcceptTransfer)
|
||||
m.Post("/reject", repo.RejectTransfer)
|
||||
}, reqToken())
|
||||
}, reqToken(), rejectPublicOnly())
|
||||
|
||||
// Adds the routes for secrets/variables and runner management
|
||||
addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction())
|
||||
@@ -1607,7 +1538,7 @@ func Routes() *web.Router {
|
||||
}, reqAdmin(), reqToken())
|
||||
|
||||
m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true), repo.DownloadArchive)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, repoAssignment())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||
|
||||
// Artifacts direct download endpoint authenticates via signed url
|
||||
@@ -1620,7 +1551,7 @@ func Routes() *web.Router {
|
||||
m.Combo("/notifications", reqToken()).
|
||||
Get(notify.ListRepoNotifications).
|
||||
Put(notify.ReadRepoNotifications)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, repoAssignment())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
|
||||
|
||||
// Issue (requires issue scope)
|
||||
@@ -1746,7 +1677,7 @@ func Routes() *web.Router {
|
||||
m.Group("/projects", func() {
|
||||
addProjectRoutes(m, reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived)
|
||||
}, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, repoAssignment())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
|
||||
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
||||
@@ -1769,15 +1700,15 @@ func Routes() *web.Router {
|
||||
})
|
||||
|
||||
m.Get("/", packages.ListPackages)
|
||||
}, 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))
|
||||
|
||||
// Organizations
|
||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
|
||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
||||
m.Group("/users/{username}/orgs", func() {
|
||||
m.Get("", reqToken(), org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), individualPermsChecker)
|
||||
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), rejectPublicOnly(), bind(api.CreateOrgOption{}), org.Create)
|
||||
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
|
||||
m.Group("/orgs/{org}", func() {
|
||||
m.Combo("").Get(org.Get).
|
||||
@@ -1785,7 +1716,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(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
||||
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
||||
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
|
||||
m.Group("/members", func() {
|
||||
m.Get("", reqToken(), org.ListMembers)
|
||||
@@ -1840,7 +1771,7 @@ func Routes() *web.Router {
|
||||
m.Delete("", org.UnblockUser)
|
||||
})
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam)
|
||||
@@ -1866,7 +1797,7 @@ func Routes() *web.Router {
|
||||
Put(reqToken(), reqTeamMembership(), org.AddTeamRepository).
|
||||
Delete(reqToken(), reqTeamMembership(), org.RemoveTeamRepository)
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken())
|
||||
|
||||
m.Group("/admin", func() {
|
||||
m.Group("/cron", func() {
|
||||
@@ -1921,7 +1852,7 @@ func Routes() *web.Router {
|
||||
m.Get("/runs", admin.ListWorkflowRuns)
|
||||
m.Get("/jobs", admin.ListWorkflowJobs)
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin(), rejectPublicOnly())
|
||||
|
||||
m.Group("/topics", func() {
|
||||
m.Get("/search", repo.TopicSearch)
|
||||
|
||||
@@ -82,7 +82,7 @@ func ListMembers(ctx *context.APIContext) {
|
||||
err error
|
||||
)
|
||||
|
||||
if ctx.Doer != nil {
|
||||
if user_model.DoerCanViewPrivate(ctx.Doer) {
|
||||
isMember, err = ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -150,7 +150,7 @@ func IsMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
if user_model.DoerCanViewPrivate(ctx.Doer) {
|
||||
userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@@ -42,7 +42,6 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
UserID: u.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -147,13 +146,6 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
|
||||
|
||||
op := api.OrganizationPermissions{}
|
||||
|
||||
// A public-only token must not disclose membership/permission details of a
|
||||
// non-public org, even for the token owner's own private orgs.
|
||||
if ctx.PublicOnly && !o.Visibility.IsPublic() {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
@@ -225,7 +217,6 @@ func GetAll(ctx *context.APIContext) {
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Visible: vMode,
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
|
||||
if err != nil {
|
||||
@@ -509,7 +500,6 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -544,13 +544,10 @@ func GetTeamRepos(ctx *context.APIContext) {
|
||||
|
||||
team := ctx.Org.Team
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
// A public-only token must not expose (or count) private repos, even when the
|
||||
// doer owning the token otherwise has access to them, so filter them out at the
|
||||
// query level to keep the returned page and the total-count header consistent.
|
||||
searchOpts := &repo_model.SearchTeamRepoOptions{
|
||||
ListOptions: listOptions,
|
||||
TeamID: team.ID,
|
||||
PublicOnly: ctx.PublicOnly,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
teamRepos, err := repo_model.GetTeamRepositories(ctx, searchOpts)
|
||||
if err != nil {
|
||||
@@ -619,12 +616,6 @@ func GetTeamRepo(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// A public-only token must not confirm the existence of a private repo.
|
||||
if !ctx.TokenCanAccessRepo(repo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
@@ -881,8 +872,6 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
// A public-only token must not receive private activity entries.
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -138,7 +138,6 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
|
||||
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
|
||||
Doer: ctx.Doer,
|
||||
PublicOnly: ctx.PublicOnly,
|
||||
OwnerName: ctx.FormString("owner"),
|
||||
TeamName: ctx.FormString("team"),
|
||||
})
|
||||
|
||||
@@ -1069,7 +1069,15 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
if !ctx.TokenCanAccessRepo(headRepo) {
|
||||
// user should have permission to read headRepo's codes
|
||||
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
|
||||
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
|
||||
ctx.APIErrorNotFound()
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1107,19 +1115,6 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// user should have permission to read headRepo's codes
|
||||
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
|
||||
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil
|
||||
}
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
|
||||
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
baseRef, err := common.ResolveRefWithSuffix(ctx, ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix)
|
||||
if err != nil {
|
||||
ctx.APIErrorAuto(err)
|
||||
@@ -1220,18 +1215,6 @@ func UpdatePullRequest(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = pr.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// a public-only token must not update (push into) a private head repo,
|
||||
// even when the base repo named in the route is public
|
||||
if !ctx.TokenCanAccessRepo(pr.HeadRepo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
|
||||
// so existing API clients keep getting a merge update.
|
||||
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
@@ -23,7 +24,7 @@ func canAccessReleaseDraft(ctx *context.APIContext) bool {
|
||||
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
|
||||
return false
|
||||
}
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if !hasApiTokenScope {
|
||||
// not API token request, the request is from a user session with write access
|
||||
return true
|
||||
|
||||
@@ -151,7 +151,6 @@ func Search(ctx *context.APIContext) {
|
||||
StarredByID: ctx.FormInt64("starredBy"),
|
||||
IncludeDescription: ctx.FormBool("includeDesc"),
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
if ctx.FormString("template") != "" {
|
||||
opts.Template = optional.Some(ctx.FormBool("template"))
|
||||
@@ -557,11 +556,6 @@ func GetByID(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ctx.TokenCanAccessRepo(repo) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@@ -1306,7 +1300,6 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,21 +21,8 @@ import (
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// actionsOwnerAccessibleRepoIDsSubQuery returns the sub-query restricting an owner-scoped actions
|
||||
// listing to the repos whose actions the caller can read, or nil when no restriction applies. A bare
|
||||
// org member must not be able to enumerate runs/jobs of repos they have no access to. A site admin may
|
||||
// skip the access filter, but a public-only token must stay confined to public repos even for an admin.
|
||||
func actionsOwnerAccessibleRepoIDsSubQuery(ctx *context.APIContext, ownerID int64) *builder.Builder {
|
||||
if ownerID > 0 && (ctx.Doer == nil || !ctx.Doer.IsAdmin || ctx.PublicOnly) {
|
||||
return repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID, ctx.Doer, ctx.PublicOnly)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListJobs lists jobs for api route validated ownerID and repoID
|
||||
// ownerID == 0 and repoID == 0 means all jobs
|
||||
// ownerID == 0 and repoID != 0 means all jobs for the given repo
|
||||
@@ -73,7 +60,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI
|
||||
opts.Statuses = append(opts.Statuses, values...)
|
||||
}
|
||||
|
||||
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
|
||||
opts.AccessibleRepoIDsSubQuery = repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(opts.OwnerID, ctx.Doer)
|
||||
|
||||
jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts)
|
||||
if err != nil {
|
||||
@@ -185,7 +172,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string)
|
||||
}
|
||||
excludePullRequests := ctx.FormBool("exclude_pull_requests")
|
||||
|
||||
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
|
||||
opts.AccessibleRepoIDsSubQuery = repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(opts.OwnerID, ctx.Doer)
|
||||
|
||||
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -1686,11 +1686,10 @@ func ListProjectColumnIssues(ctx *context.APIContext) {
|
||||
} else {
|
||||
// an owner-level board spans repositories, so filter to what this doer may see
|
||||
issuesOpts.Owner = scope.Owner
|
||||
if user_model.DoerCanViewPrivate(ctx.Doer) {
|
||||
issuesOpts.Doer = ctx.Doer
|
||||
issuesOpts.AllPublic = ctx.Doer == nil
|
||||
if ctx.PublicOnly {
|
||||
} else {
|
||||
issuesOpts.AllPublic = true
|
||||
issuesOpts.Doer = nil // a public-only token must not reach the doer's private repos
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
audit_model "gitea.dev/models/audit"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
@@ -129,7 +130,7 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||
t.Scope = scope
|
||||
|
||||
// a token-authenticated request must not mint a token with a broader scope than its own
|
||||
apiTokenScope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
apiTokenScope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if hasApiTokenScope {
|
||||
hasScope, err := apiTokenScope.CanCreateChildScope(scope)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,15 +19,13 @@ import (
|
||||
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
||||
opts := utils.GetListOptions(ctx)
|
||||
|
||||
searchOpts := repo_model.SearchRepoOptions{
|
||||
Actor: u,
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
OwnerID: u.ID,
|
||||
Actor: ctx.Doer,
|
||||
Private: private,
|
||||
ListOptions: opts,
|
||||
OrderBy: "id ASC",
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
@@ -112,7 +110,6 @@ func ListMyRepos(ctx *context.APIContext) {
|
||||
Private: ctx.IsSigned,
|
||||
IncludeDescription: true,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,9 +24,8 @@ func getStarredRepos(ctx *context.APIContext, user *user_model.User, private boo
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
StarrerID: user.ID,
|
||||
IncludePrivate: private,
|
||||
Actor: user,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -73,7 +73,6 @@ func Search(ctx *context.APIContext) {
|
||||
SearchByEmail: true,
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
var err error
|
||||
users, maxResults, err = user_model.SearchUsers(ctx, opts)
|
||||
if err != nil {
|
||||
@@ -208,7 +207,6 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
||||
Date: ctx.FormString("date"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
feeds, count, err := feed_service.GetFeeds(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,9 +22,8 @@ func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private boo
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
WatcherID: user.ID,
|
||||
IncludePrivate: private,
|
||||
Actor: user,
|
||||
Actor: ctx.Doer,
|
||||
}
|
||||
opts.ApplyPublicOnly(ctx.PublicOnly)
|
||||
|
||||
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,7 +32,6 @@ func ParseIssueFilterTypeIsPull(typ string) optional.Option[bool] {
|
||||
|
||||
type SearchIssuesRepoIDsOptions struct {
|
||||
Doer *user_model.User
|
||||
PublicOnly bool
|
||||
OwnerName string
|
||||
TeamName string
|
||||
}
|
||||
@@ -45,7 +44,6 @@ func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) (
|
||||
Collaborate: optional.None[bool](),
|
||||
Actor: opts.Doer,
|
||||
}
|
||||
searchOpts.ApplyPublicOnly(opts.PublicOnly)
|
||||
if opts.OwnerName != "" {
|
||||
owner, err := user_model.GetUserByName(ctx, opts.OwnerName)
|
||||
if err != nil {
|
||||
@@ -69,7 +67,7 @@ func SearchIssuesRepoIDs(ctx context.Context, opts SearchIssuesRepoIDsOptions) (
|
||||
allPublic = opts.OwnerName == ""
|
||||
cond := repo_model.SearchRepositoryCondition(searchOpts)
|
||||
if allPublic {
|
||||
if !searchOpts.Private {
|
||||
if !user_model.DoerCanViewPrivate(opts.Doer) {
|
||||
return []int64{0}, allPublic, nil // sees nothing beyond is_public, so skip the query
|
||||
}
|
||||
cond = cond.And(repo_model.NotPublicRepoUnderPublicOwnerCond()) // enumerating them scales with the instance
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
|
||||
func TestSearchIssuesRepoIDs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
publicOnlyUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
publicOnlyUser2.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,read:issue")
|
||||
|
||||
// the indexer's is_public covers repo 1 (public under a public owner) but misses repo 38 (public
|
||||
// under a limited org) and repo 40 (public under a private org)
|
||||
@@ -52,8 +54,7 @@ func TestSearchIssuesRepoIDs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "public-only token",
|
||||
doerID: 2,
|
||||
opts: SearchIssuesRepoIDsOptions{PublicOnly: true},
|
||||
opts: SearchIssuesRepoIDsOptions{Doer: publicOnlyUser2},
|
||||
allPublic: true,
|
||||
want: []int64{0},
|
||||
},
|
||||
|
||||
@@ -103,7 +103,7 @@ func InfoOAuth(ctx *context.Context) {
|
||||
// enforce the same user scope the REST API requires before returning identity
|
||||
// claims; OIDC access tokens map to the "all" scope, so standard OIDC clients
|
||||
// are unaffected and only explicitly-restricted tokens are rejected
|
||||
tokenScope, _ := ctx.Data["ApiTokenScope"].(auth.AccessTokenScope)
|
||||
tokenScope, _ := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if allowed, err := tokenScope.HasScope(auth.AccessTokenScopeReadUser); err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
@@ -121,17 +121,7 @@ func InfoOAuth(ctx *context.Context) {
|
||||
Picture: ctx.Doer.AvatarLink(ctx),
|
||||
}
|
||||
|
||||
var accessTokenScope auth.AccessTokenScope
|
||||
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil {
|
||||
accessTokenScope, _, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
|
||||
}
|
||||
}
|
||||
|
||||
// since version 1.22 does not verify if groups should be public-only,
|
||||
// onlyPublicGroups will be set only if 'public-only' is included in a valid scope
|
||||
onlyPublicGroups, _ := accessTokenScope.PublicOnly()
|
||||
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
|
||||
groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, user_model.IsPublicOnlyDoer(ctx.Doer))
|
||||
if err != nil {
|
||||
ctx.ServerError("Oauth groups for user", err)
|
||||
return
|
||||
|
||||
@@ -50,11 +50,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
|
||||
includePrivate = isOrgMember
|
||||
}
|
||||
|
||||
// a public-only API token must not surface private activity, even for its own owner
|
||||
if includePrivate && context.TokenIsPublicOnly(ctx) {
|
||||
includePrivate = false
|
||||
}
|
||||
|
||||
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctx.ContextUser,
|
||||
Actor: ctx.Doer,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// feed serves private repository content, mirroring checkDownloadTokenScope for
|
||||
// downloads. Returns false (and writes the response) when the token is denied.
|
||||
func checkRepoFeedTokenScope(ctx *context.Context) bool {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
return !ctx.Written()
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,6 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin
|
||||
if !goGetTokenCanReadRepo(ctx) {
|
||||
return def
|
||||
}
|
||||
// a public-only token may only reach genuinely public resources (a public repo under a public owner)
|
||||
if context.TokenIsPublicOnly(ctx) && (repo.IsPrivate || !repo.Owner.Visibility.IsPublic()) {
|
||||
return def
|
||||
}
|
||||
// the caller must be able to read the code and see the owner: a limited/private owner hides its repos
|
||||
// from anonymous/non-member callers even when the repo itself is public
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
@@ -121,7 +117,7 @@ func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) strin
|
||||
// always may; a token request may only when its scope grants repository read, so a PAT that was never
|
||||
// scoped for repositories cannot disclose the branch even if its owner can read the repo.
|
||||
func goGetTokenCanReadRepo(ctx *context.Context) bool {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if !hasApiTokenScope {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func GetWorkflowBadge(ctx *context.Context) {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
}
|
||||
|
||||
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||
context.CheckTokenScopes(ctx, repo, requiredScope)
|
||||
context.CheckTokenScopes(ctx, requiredScope)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
func checkDownloadTokenScope(ctx *context.Context) bool {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
return !ctx.Written()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/git/gitrepo"
|
||||
@@ -159,12 +160,12 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode))
|
||||
context.CheckRepoScopedToken(ctx, auth_model.GetScopeLevelFromAccessMode(accessMode))
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx.IsBasicAuth && ctx.Data["ApiTokenScope"] == nil && ctx.Doer.IsIndividual() {
|
||||
if _, isToken := user_model.GetDoerTokenScope(ctx.Doer); ctx.IsBasicAuth && !isToken && ctx.Doer.IsIndividual() {
|
||||
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if err == nil {
|
||||
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
|
||||
|
||||
@@ -433,7 +433,7 @@ func Home(ctx *context.Context) {
|
||||
|
||||
// a scoped or public-only API token authenticating this web request must still satisfy
|
||||
// the repository read scope before private repo content is served
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestArchivedIssues(t *testing.T) {
|
||||
ctx.Req.Form.Set("state", "open")
|
||||
|
||||
// Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
|
||||
repos, _, _ := repo_model.GetUserRepositories(t.Context(), repo_model.SearchRepoOptions{Actor: ctx.Doer})
|
||||
repos, _, _ := repo_model.GetUserRepositories(t.Context(), repo_model.SearchRepoOptions{OwnerID: ctx.Doer.ID})
|
||||
assert.Len(t, repos, 3)
|
||||
IsArchived := make(map[int64]bool)
|
||||
NumIssues := make(map[int64]int)
|
||||
|
||||
@@ -303,7 +303,7 @@ func ViewPackageVersion(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PackageVersionViewData"] = viewData
|
||||
ctx.Data["PackageVersionSetupManual"] = pkgSpec.RenderSetupManual(ctx, pd, viewData)
|
||||
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
|
||||
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite
|
||||
|
||||
hasRepositoryAccess := false
|
||||
if pd.Repository != nil {
|
||||
@@ -417,7 +417,7 @@ func PackageSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = pd.Package.Name
|
||||
ctx.Data["IsPackagesPage"] = true
|
||||
ctx.Data["PackageDescriptor"] = pd
|
||||
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
|
||||
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite
|
||||
|
||||
if pd.Package.RepoID > 0 {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, pd.Package.RepoID)
|
||||
|
||||
@@ -168,10 +168,6 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
date := ctx.FormString("date")
|
||||
pagingNum = setting.UI.FeedPagingNum
|
||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
// a public-only API token must not surface private activity, even for its own owner
|
||||
if showPrivate && context.TokenIsPublicOnly(ctx) {
|
||||
showPrivate = false
|
||||
}
|
||||
items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctx.ContextUser,
|
||||
Actor: ctx.Doer,
|
||||
|
||||
@@ -108,7 +108,7 @@ func CreateAccessToken(ctx *context.Context, owner *user_model.User) {
|
||||
// a token-authenticated request must not mint a token with a broader scope than its own, nor
|
||||
// drop the public-only restriction. Web routes accept basic-auth PATs/OAuth tokens too, so this
|
||||
// must mirror the REST API guard in routers/api/v1/user/app.go.
|
||||
apiTokenScope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
apiTokenScope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if hasApiTokenScope {
|
||||
hasScope, err := apiTokenScope.CanCreateChildScope(t.Scope)
|
||||
if err != nil {
|
||||
|
||||
@@ -276,7 +276,7 @@ func Repos(ctx *context.Context) {
|
||||
}
|
||||
|
||||
userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
Actor: ctxUser,
|
||||
OwnerID: ctxUser.ID,
|
||||
Private: true,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
@@ -300,7 +300,7 @@ func Repos(ctx *context.Context) {
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
ctx.Data["ReposMap"] = repos
|
||||
} else {
|
||||
repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
|
||||
repos, reposCount, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{OwnerID: ctxUser.ID, Private: true, ListOptions: opts})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepositories", err)
|
||||
return
|
||||
|
||||
+1
-1
@@ -930,7 +930,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
if ctx.Package.AccessMode < accessMode {
|
||||
ctx.NotFound(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Contex
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueEdited,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
@@ -120,7 +120,7 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
|
||||
// Merge pull request calls issue.changeStatus so we need to handle separately.
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
CommitID: commitID,
|
||||
@@ -240,7 +240,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
|
||||
payload := &api.PullRequestPayload{
|
||||
Action: action,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
Changes: changes,
|
||||
@@ -374,7 +374,7 @@ func (n *actionsNotifier) NewPullRequest(ctx context.Context, pull *issues_model
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueOpened,
|
||||
Index: pull.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pull, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pull, nil),
|
||||
Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, pull.Issue.Poster, nil),
|
||||
}).
|
||||
@@ -454,7 +454,7 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: api.HookIssueReviewed,
|
||||
Index: review.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil),
|
||||
Repository: convert.ToRepo(ctx, review.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, review.Reviewer, nil),
|
||||
Review: &api.ReviewPayload{
|
||||
@@ -488,7 +488,7 @@ func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *us
|
||||
WithPayload(&api.PullRequestPayload{
|
||||
Action: action,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, nil),
|
||||
RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
@@ -525,7 +525,7 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U
|
||||
// Merge pull request calls issue.changeStatus so we need to handle separately.
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: pr.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil),
|
||||
Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
Action: api.HookIssueClosed,
|
||||
@@ -695,7 +695,7 @@ func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *use
|
||||
Before: before,
|
||||
After: after,
|
||||
Index: pr.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil),
|
||||
Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
@@ -726,7 +726,7 @@ func (n *actionsNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe
|
||||
From: oldBranch,
|
||||
},
|
||||
},
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, nil),
|
||||
Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}).
|
||||
|
||||
@@ -50,23 +50,6 @@ func doerFromContext(ctx context.Context) *user_model.User {
|
||||
return nil
|
||||
}
|
||||
|
||||
// credentialFromContext returns the credential the surrounding request
|
||||
// authenticated with. It is dropped when the event is recorded for someone
|
||||
// other than the signed-in user, so an explicit actor is never tied to a
|
||||
// credential that is not theirs.
|
||||
func credentialFromContext(ctx context.Context, doer *user_model.User) string {
|
||||
data := middleware.GetContextData(ctx)
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
signedUser, _ := data[middleware.ContextDataKeySignedUser].(*user_model.User)
|
||||
if signedUser == nil || signedUser.ID != doer.ID {
|
||||
return ""
|
||||
}
|
||||
credential, _ := data[middleware.ContextDataKeyAuthCredential].(string)
|
||||
return credential
|
||||
}
|
||||
|
||||
// ImpersonatorFromContext resolves the admin acting as the doer, so an event
|
||||
// recorded during an impersonated session cannot be pinned on the impersonated
|
||||
// user alone. Returns nil for ordinary sessions.
|
||||
|
||||
@@ -106,25 +106,15 @@ func TestBuildEvent(t *testing.T) {
|
||||
e := buildEvent(context.Background(), RecordParams{
|
||||
Action: audit_model.UserCreate,
|
||||
Actor: actorRef(actions),
|
||||
ActorCredential: actorCredential(context.Background(), actions),
|
||||
ActorCredential: user_model.GetDoerCredential(actions),
|
||||
Scope: ScopeFromUser(u),
|
||||
})
|
||||
assert.Equal(t, user_model.ActionsUserID, e.ActorID)
|
||||
assert.Equal(t, "gitea-actions:42", e.ActorCredential)
|
||||
|
||||
key := user_model.NewDeployKeyUserWithKeyID(7)
|
||||
assert.Equal(t, "deploy-key:7", actorCredential(context.Background(), key))
|
||||
assert.Empty(t, actorCredential(context.Background(), doer))
|
||||
})
|
||||
|
||||
t.Run("CredentialFromRequest", func(t *testing.T) {
|
||||
ctx := newRequestContext(t, doer)
|
||||
middleware.GetContextData(ctx)[middleware.ContextDataKeyAuthCredential] = "access-token:9"
|
||||
assert.Equal(t, "access-token:9", actorCredential(ctx, doer))
|
||||
|
||||
// an event recorded for someone other than the signed-in user is not
|
||||
// tied to the credential of that request
|
||||
assert.Empty(t, actorCredential(ctx, u))
|
||||
assert.Equal(t, "deploy-key:7", user_model.GetDoerCredential(key))
|
||||
assert.Empty(t, user_model.GetDoerCredential(doer))
|
||||
})
|
||||
|
||||
t.Run("RequestInfo", func(t *testing.T) {
|
||||
|
||||
@@ -120,7 +120,7 @@ func RecordAs(ctx context.Context, doer *user_model.User, action audit_model.Act
|
||||
writeEvent(ctx, RecordParams{
|
||||
Action: action,
|
||||
Actor: actorRef(doer),
|
||||
ActorCredential: actorCredential(ctx, doer),
|
||||
ActorCredential: user_model.GetDoerCredential(doer),
|
||||
Impersonator: impersonatorRef(ImpersonatorFromContext(ctx), doer),
|
||||
Scope: scopeRef(scope),
|
||||
Metadata: metaPairs(metadata...),
|
||||
|
||||
@@ -25,20 +25,6 @@ func actorRef(doer *user_model.User) audit_model.EntityRef {
|
||||
return audit_model.EntityRef{Type: audit_model.ScopeUser, ID: doer.ID, Name: doer.Name}
|
||||
}
|
||||
|
||||
// actorCredential names what the actor acted with: the task or key behind a
|
||||
// system user, otherwise the token the surrounding request authenticated with.
|
||||
// An incident then traces one credential across every event it produced,
|
||||
// instead of stopping at the account that owns it.
|
||||
func actorCredential(ctx context.Context, doer *user_model.User) string {
|
||||
if doer == nil {
|
||||
return ""
|
||||
}
|
||||
if doer.ExtDoerData != nil {
|
||||
return doer.ExtDoerData.EncodeToString()
|
||||
}
|
||||
return credentialFromContext(ctx, doer)
|
||||
}
|
||||
|
||||
// impersonatorRef names the admin behind an impersonated session. It is dropped
|
||||
// when the actor is the admin themselves, so events an admin performs before
|
||||
// entering or after leaving an impersonation are not marked as impersonated.
|
||||
|
||||
@@ -115,7 +115,7 @@ func RecordScoped(ctx context.Context, owner *user_model.User, repo *repository_
|
||||
writeEvent(ctx, RecordParams{
|
||||
Action: action,
|
||||
Actor: actorRef(doer),
|
||||
ActorCredential: actorCredential(ctx, doer),
|
||||
ActorCredential: user_model.GetDoerCredential(doer),
|
||||
Scope: scope,
|
||||
Metadata: metaPairs(metadata...),
|
||||
})
|
||||
|
||||
+6
-20
@@ -28,9 +28,6 @@ var (
|
||||
// BasicMethodName is the constant name of the basic authentication method
|
||||
const (
|
||||
BasicMethodName = "basic"
|
||||
AccessTokenMethodName = "access_token"
|
||||
OAuth2TokenMethodName = "oauth2_token"
|
||||
ActionTokenMethodName = "action_token"
|
||||
DeployTokenMethodName = "deploy_token"
|
||||
)
|
||||
|
||||
@@ -86,9 +83,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
store.GetData()["LoginMethod"] = OAuth2TokenMethodName
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
u.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialOAuth2Grant, grantID, accessTokenScope)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -107,9 +102,7 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
|
||||
store.GetData()["LoginMethod"] = AccessTokenMethodName
|
||||
store.GetData()["ApiTokenScope"] = token.Scope
|
||||
setAuthCredential(store, credentialAccessToken, token.ID)
|
||||
u.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, token.ID, token.Scope)
|
||||
return u, nil
|
||||
} else if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
@@ -119,7 +112,6 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store
|
||||
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
|
||||
if err == nil && task != nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
|
||||
store.GetData()["LoginMethod"] = ActionTokenMethodName
|
||||
return user_model.NewActionsUserWithTaskID(task.ID), nil
|
||||
}
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
@@ -194,18 +186,12 @@ func validateTOTP(req *http.Request, u *user_model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAccessScope(store DataStore) auth_model.AccessTokenScope {
|
||||
if scope, hasApiTokenScope := store.GetData()["ApiTokenScope"].(auth_model.AccessTokenScope); hasApiTokenScope {
|
||||
func GetAccessScope(doer *user_model.User, store DataStore) auth_model.AccessTokenScope {
|
||||
if scope, ok := user_model.GetDoerTokenScope(doer); ok {
|
||||
return scope
|
||||
}
|
||||
switch store.GetData()["LoginMethod"] {
|
||||
case OAuth2TokenMethodName:
|
||||
fallthrough
|
||||
case BasicMethodName, AccessTokenMethodName:
|
||||
if store.GetData()["LoginMethod"] == BasicMethodName {
|
||||
return auth_model.AccessTokenScopeAll
|
||||
case ActionTokenMethodName:
|
||||
fallthrough
|
||||
default:
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/modules/web/middleware"
|
||||
)
|
||||
|
||||
// Credential kinds naming how a request authenticated, recorded so an audit
|
||||
// event can point at the token rather than only at its owner.
|
||||
const (
|
||||
credentialAccessToken = "access-token"
|
||||
credentialOAuth2Grant = "oauth2-grant"
|
||||
)
|
||||
|
||||
func setAuthCredential(store DataStore, kind string, id int64) {
|
||||
store.GetData()[middleware.ContextDataKeyAuthCredential] = kind + ":" + strconv.FormatInt(id, 10)
|
||||
}
|
||||
@@ -42,6 +42,5 @@ func (d *DeployToken) Verify(req *http.Request, _ http.ResponseWriter, store Dat
|
||||
log.Error("UpdateDeployKeyUpdated: %v", err)
|
||||
}
|
||||
|
||||
store.GetData()["LoginMethod"] = DeployTokenMethodName
|
||||
return user_model.NewDeployKeyUserWithKeyID(key.ID), nil
|
||||
}
|
||||
|
||||
+10
-9
@@ -107,8 +107,7 @@ func parseToken(req *http.Request) (string, bool) {
|
||||
}
|
||||
|
||||
// userFromToken returns the user corresponding to the OAuth token.
|
||||
// It will set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data)
|
||||
func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) {
|
||||
func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string) (*user_model.User, error) {
|
||||
// Let's see if token is valid.
|
||||
if strings.Contains(tokenSHA, ".") {
|
||||
// First attempt to decode an actions JWT, returning the actions user
|
||||
@@ -124,8 +123,7 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
if err != nil || !user.IsIndividual() {
|
||||
return nil, err
|
||||
}
|
||||
store.GetData()["ApiTokenScope"] = accessTokenScope
|
||||
setAuthCredential(store, credentialOAuth2Grant, grantID)
|
||||
user.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialOAuth2Grant, grantID, accessTokenScope)
|
||||
return user, nil
|
||||
}
|
||||
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
|
||||
@@ -144,22 +142,25 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS
|
||||
if err = auth_model.UpdateAccessToken(ctx, t); err != nil {
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
store.GetData()["ApiTokenScope"] = t.Scope
|
||||
setAuthCredential(store, credentialAccessToken, t.ID)
|
||||
return user_model.GetUserByID(ctx, t.UID)
|
||||
user, err := user_model.GetUserByID(ctx, t.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, t.ID, t.Scope)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Verify extracts the user ID from the OAuth token in the query parameters
|
||||
// or the "Authorization" header and returns the corresponding user object for that ID.
|
||||
// If verification is successful returns an existing user object.
|
||||
// Returns nil if verification fails.
|
||||
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
|
||||
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, _ DataStore, sess SessionStore) (*user_model.User, error) {
|
||||
token, ok := parseToken(req)
|
||||
if !ok {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
user, err := o.userFromToken(req.Context(), token, store)
|
||||
user, err := o.userFromToken(req.Context(), token)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/reqctx"
|
||||
"gitea.dev/services/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -23,16 +24,25 @@ func TestUserIDFromToken(t *testing.T) {
|
||||
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ds := make(reqctx.ContextData)
|
||||
|
||||
o := OAuth2{}
|
||||
u, err := o.userFromToken(t.Context(), token, ds)
|
||||
u, err := o.userFromToken(t.Context(), token)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, user_model.ActionsUserID, u.ID)
|
||||
taskID, ok := user_model.GetActionsUserTaskID(u)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, RunningTaskID, taskID)
|
||||
})
|
||||
|
||||
t.Run("Access token", func(t *testing.T) {
|
||||
token := &auth_model.AccessToken{UID: 2, Name: "public-only", Scope: "public-only,read:repository"}
|
||||
require.NoError(t, auth_model.NewAccessToken(t.Context(), token))
|
||||
|
||||
u, err := (&OAuth2{}).userFromToken(t.Context(), token.Token)
|
||||
require.NoError(t, err)
|
||||
scope, _ := user_model.GetDoerTokenScope(u)
|
||||
assert.Equal(t, token.Scope, scope)
|
||||
assert.Equal(t, fmt.Sprintf("access-token:%d", token.ID), user_model.GetDoerCredential(u))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckTaskIsRunning(t *testing.T) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/cache"
|
||||
@@ -47,14 +46,6 @@ type APIContext struct {
|
||||
Repo *Repository
|
||||
Org *APIOrganization
|
||||
Package *Package
|
||||
PublicOnly bool // Whether the request is for a public endpoint
|
||||
}
|
||||
|
||||
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
|
||||
// A public-only token cannot reach a private repo or a repo owned by a non-public (limited or
|
||||
// private) owner; any other token is unrestricted by this check.
|
||||
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
|
||||
return !ctx.PublicOnly || !publicOnlyTokenDeniedRepo(ctx, repo)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -37,7 +37,7 @@ func RedirectToUser(ctx *Base, doer *user_model.User, userName string, redirectU
|
||||
}
|
||||
|
||||
// Handle Visibility
|
||||
if user.Visibility != structs.VisibleTypePublic && doer == nil {
|
||||
if user.Visibility != structs.VisibleTypePublic && !user_model.DoerCanViewPrivate(doer) {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.HTTPError(http.StatusNotFound, "user does not exist")
|
||||
return
|
||||
|
||||
@@ -91,7 +91,7 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Handle Visibility
|
||||
if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned {
|
||||
if org.Visibility != structs.VisibleTypePublic && !user_model.DoerCanViewPrivate(ctx.Doer) {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
|
||||
@@ -110,6 +110,13 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string)) *Pack
|
||||
}
|
||||
|
||||
func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.AccessMode, error) {
|
||||
if !user_model.DoerTokenAllowsOwner(doer, pkgOwner) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
if user_model.DoerIsSiteAdminFullAccess(doer) {
|
||||
return perm.AccessModeOwner, nil
|
||||
}
|
||||
|
||||
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
|
||||
return perm.AccessModeNone, nil
|
||||
}
|
||||
|
||||
@@ -23,4 +23,9 @@ func TestDeterminePackageAccessModeForLimitedOwner(t *testing.T) {
|
||||
accessMode, err = determineAccessMode(&Base{}, owner, &user.User{ID: 3, IsActive: true, IsRestricted: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm.AccessModeNone, accessMode)
|
||||
|
||||
publicOnlyAdmin := &user.User{ID: 4, IsActive: true, IsAdmin: true, ExtDoerData: user.NewTokenExtDoerData(user.CredentialAccessToken, 1, "public-only,read:package")}
|
||||
accessMode, err = determineAccessMode(&Base{}, owner, publicOnlyAdmin)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, perm.AccessModeNone, accessMode)
|
||||
}
|
||||
|
||||
@@ -4,63 +4,21 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
)
|
||||
|
||||
// isOwnerHidden reports whether repo's owner is not publicly visible (a limited or private owner), so
|
||||
// the owner's repositories must be hidden from callers that may only reach genuinely public resources.
|
||||
func isOwnerHidden(ctx context.Context, repo *repo_model.Repository) bool {
|
||||
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
|
||||
return true // fail closed if the owner visibility can't be determined
|
||||
}
|
||||
return !repo.Owner.Visibility.IsPublic()
|
||||
}
|
||||
|
||||
// publicOnlyTokenDeniedRepo reports whether a public-only API token must be denied access to
|
||||
// repo. A public-only token may only reach genuinely public resources, so it is denied for
|
||||
// private repos and for repos owned by a non-public (limited or private) owner.
|
||||
func publicOnlyTokenDeniedRepo(ctx context.Context, repo *repo_model.Repository) bool {
|
||||
if repo == nil {
|
||||
return false
|
||||
}
|
||||
return repo.IsPrivate || isOwnerHidden(ctx, repo)
|
||||
}
|
||||
|
||||
// TokenIsPublicOnly reports whether the request is authenticated by a public-only API token. A
|
||||
// non-token request, or a token with no recorded scope, is not public-only.
|
||||
func TokenIsPublicOnly(ctx *Context) bool {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if !hasApiTokenScope {
|
||||
return false
|
||||
}
|
||||
publicOnly, _ := scope.PublicOnly()
|
||||
return publicOnly
|
||||
}
|
||||
|
||||
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
|
||||
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
|
||||
scope, hasApiTokenScope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
func CheckTokenScopes(ctx *Context, scopes ...auth_model.AccessTokenScope) {
|
||||
scope, hasApiTokenScope := user_model.GetDoerTokenScope(ctx.Doer)
|
||||
if !hasApiTokenScope {
|
||||
return
|
||||
}
|
||||
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("PublicOnly", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && publicOnlyTokenDeniedRepo(ctx, repo) {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err := scope.HasAnyScope(scopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasAnyScope", err)
|
||||
@@ -118,6 +76,6 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
}
|
||||
|
||||
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||
func CheckRepoScopedToken(ctx *Context, level auth_model.AccessTokenScopeLevel) {
|
||||
CheckTokenScopes(ctx, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,13 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (
|
||||
} else {
|
||||
errCb(http.StatusInternalServerError, fmt.Sprintf("GetUserByName: %v", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !user_model.DoerTokenAllowsOwner(doer, contextUser) {
|
||||
errCb(http.StatusNotFound, user_model.ErrUserRedirectNotExist{Name: username}.Error())
|
||||
return nil
|
||||
}
|
||||
return contextUser
|
||||
}
|
||||
|
||||
|
||||
+29
-12
@@ -22,10 +22,20 @@ import (
|
||||
"gitea.dev/services/gitdiff"
|
||||
)
|
||||
|
||||
// ToAPIPullRequest assumes following fields have been assigned with valid values:
|
||||
// ToAPIPullRequest hides a head repository the doer cannot see
|
||||
func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
|
||||
return toAPIPullRequest(ctx, pr, doer, false)
|
||||
}
|
||||
|
||||
// ToPayloadPullRequest keeps the head repository regardless of the doer, for webhook and Actions payloads
|
||||
func ToPayloadPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
|
||||
return toAPIPullRequest(ctx, pr, doer, true)
|
||||
}
|
||||
|
||||
// toAPIPullRequest assumes following fields have been assigned with valid values:
|
||||
// Required - Issue
|
||||
// Optional - Merger
|
||||
func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
|
||||
func toAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, isPayload bool) *api.PullRequest {
|
||||
var (
|
||||
baseBranch string
|
||||
headBranch string
|
||||
@@ -181,10 +191,14 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
}
|
||||
|
||||
// a hidden head repository is read from the base repository's PR ref
|
||||
headGitRepo := gitRepo
|
||||
exist = false
|
||||
if isPayload || pr.HeadRepoID == pr.BaseRepoID || p.HasAnyUnitAccessOrPublicAccess() {
|
||||
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
|
||||
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
|
||||
|
||||
headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo)
|
||||
headGitRepo, err = git.OpenRepository(ctx, pr.HeadRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository[%s]: %v", pr.HeadRepo.FullName(), err)
|
||||
return nil
|
||||
@@ -196,6 +210,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
log.Error("GetBranch[%s]: %v", pr.HeadBranch, err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Outer scope variables to be used in diff calculation
|
||||
var (
|
||||
@@ -419,7 +434,17 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
|
||||
}
|
||||
|
||||
// pull request head branch, both repository and branch could not exist
|
||||
if pr.HeadRepo != nil {
|
||||
if pr.HeadRepo != nil && pr.HeadRepoID != pr.BaseRepoID {
|
||||
p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
}
|
||||
if p.HasAnyUnitAccessOrPublicAccess() {
|
||||
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
|
||||
}
|
||||
}
|
||||
if apiPullRequest.Head.Repository != nil {
|
||||
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
|
||||
exist, err := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch)
|
||||
if err != nil {
|
||||
@@ -429,14 +454,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
|
||||
if exist {
|
||||
apiPullRequest.Head.Ref = pr.HeadBranch
|
||||
}
|
||||
if pr.HeadRepoID != pr.BaseRepoID {
|
||||
p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
}
|
||||
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
|
||||
}
|
||||
}
|
||||
if apiPullRequest.Head.Ref == "" {
|
||||
apiPullRequest.Head.Ref = pr.GetGitHeadRefName()
|
||||
|
||||
@@ -59,7 +59,7 @@ func GetListLockHandler(ctx *context.Context) {
|
||||
}
|
||||
repository.MustOwner(ctx)
|
||||
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func PostLockHandler(ctx *context.Context) {
|
||||
}
|
||||
repository.MustOwner(ctx)
|
||||
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Write)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func VerifyLockHandler(ctx *context.Context) {
|
||||
}
|
||||
repository.MustOwner(ctx)
|
||||
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -281,7 +281,7 @@ func UnLockHandler(ctx *context.Context) {
|
||||
}
|
||||
repository.MustOwner(ctx)
|
||||
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Write)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -473,9 +473,9 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
|
||||
}
|
||||
|
||||
if requireWrite {
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Write)
|
||||
} else {
|
||||
context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
|
||||
context.CheckRepoScopedToken(ctx, auth_model.Read)
|
||||
}
|
||||
|
||||
if ctx.Written() {
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati
|
||||
}
|
||||
|
||||
repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
Actor: org.AsUser(), Private: true, ListOptions: db.ListOptionsAll,
|
||||
OwnerID: org.ID, Private: true, ListOptions: db.ListOptionsAll,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -145,6 +145,9 @@ func CheckUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullReques
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
if !repo_model.DoerTokenAllowsRepo(ctx, user, pull.HeadRepo) { // whitelists and maintainer edits below skip the doer's repo permission
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// 1. check whether pull request enabled.
|
||||
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
||||
|
||||
@@ -122,6 +122,32 @@ func TestIsUserAllowedToUpdate(t *testing.T) {
|
||||
|
||||
pr3Poster := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
|
||||
|
||||
t.Run("PublicOnlyTokenCannotUpdatePrivateHeadEvenWhitelisted", func(t *testing.T) {
|
||||
pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
protectedBranch := &git_model.ProtectedBranch{
|
||||
RepoID: pr3.HeadRepoID,
|
||||
RuleName: pr3.HeadBranch,
|
||||
CanPush: true,
|
||||
EnableWhitelist: true,
|
||||
WhitelistUserIDs: []int64{pr3Poster.ID},
|
||||
}
|
||||
_, err := db.GetEngine(t.Context()).Insert(protectedBranch)
|
||||
require.NoError(t, err)
|
||||
defer db.DeleteByBean(t.Context(), protectedBranch)
|
||||
require.NoError(t, pr3.LoadHeadRepo(t.Context()))
|
||||
pr3.HeadRepo.IsPrivate = true
|
||||
|
||||
pushAllowed, _, _, err := checkUserAllowedToUpdate(t.Context(), pr3, pr3Poster)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pushAllowed)
|
||||
|
||||
publicOnlyPoster := *pr3Poster
|
||||
publicOnlyPoster.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:repository")
|
||||
pushAllowed, _, _, err = checkUserAllowedToUpdate(t.Context(), pr3, &publicOnlyPoster)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, pushAllowed)
|
||||
})
|
||||
|
||||
t.Run("MaintainerEditRespectsPosterPermissions", func(t *testing.T) {
|
||||
pr3 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
|
||||
pr3.AllowMaintainerEdit = true
|
||||
|
||||
@@ -265,7 +265,7 @@ func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesT
|
||||
return err
|
||||
}
|
||||
repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
|
||||
Actor: ctxUser,
|
||||
OwnerID: ctxUser.ID,
|
||||
Private: true,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
|
||||
@@ -398,7 +398,6 @@ func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User
|
||||
},
|
||||
Private: true,
|
||||
OwnerID: owner.ID,
|
||||
Actor: owner,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetUserRepositories: %w", err)
|
||||
|
||||
@@ -65,7 +65,7 @@ func (m *webhookNotifier) IssueClearLabels(ctx context.Context, doer *user_model
|
||||
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{
|
||||
Action: api.HookIssueLabelCleared,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
})
|
||||
@@ -179,7 +179,7 @@ func (m *webhookNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
|
||||
Action: action,
|
||||
Changes: changes,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func (m *webhookNotifier) IssueChangeTitle(ctx context.Context, doer *user_model
|
||||
From: oldTitle,
|
||||
},
|
||||
},
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
})
|
||||
@@ -257,7 +257,7 @@ func (m *webhookNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
|
||||
// Merge pull request calls issue.changeStatus so we need to handle separately.
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
CommitID: commitID,
|
||||
@@ -320,7 +320,7 @@ func (m *webhookNotifier) DeleteIssue(ctx context.Context, doer *user_model.User
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
|
||||
Action: api.HookIssueDeleted,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}); err != nil {
|
||||
@@ -365,7 +365,7 @@ func (m *webhookNotifier) NewPullRequest(ctx context.Context, pull *issues_model
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: pull.Issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
|
||||
Action: api.HookIssueOpened,
|
||||
Index: pull.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pull, pull.Issue.Poster),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pull, pull.Issue.Poster),
|
||||
Repository: convert.ToRepo(ctx, pull.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, pull.Issue.Poster, nil),
|
||||
}); err != nil {
|
||||
@@ -394,7 +394,7 @@ func (m *webhookNotifier) IssueChangeContent(ctx context.Context, doer *user_mod
|
||||
From: oldContent,
|
||||
},
|
||||
},
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
})
|
||||
@@ -436,7 +436,7 @@ func (m *webhookNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
|
||||
var pullRequest *api.PullRequest
|
||||
if c.Issue.IsPull {
|
||||
eventType = webhook_module.HookEventPullRequestComment
|
||||
pullRequest = convert.ToAPIPullRequest(ctx, c.Issue.PullRequest, doer)
|
||||
pullRequest = convert.ToPayloadPullRequest(ctx, c.Issue.PullRequest, doer)
|
||||
} else {
|
||||
eventType = webhook_module.HookEventIssueComment
|
||||
}
|
||||
@@ -471,7 +471,7 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
|
||||
log.Error("LoadPullRequest: %v", err)
|
||||
return
|
||||
}
|
||||
pullRequest = convert.ToAPIPullRequest(ctx, issue.PullRequest, doer)
|
||||
pullRequest = convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer)
|
||||
} else {
|
||||
eventType = webhook_module.HookEventIssueComment
|
||||
}
|
||||
@@ -512,7 +512,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
|
||||
var pullRequest *api.PullRequest
|
||||
if comment.Issue.IsPull {
|
||||
eventType = webhook_module.HookEventPullRequestComment
|
||||
pullRequest = convert.ToAPIPullRequest(ctx, comment.Issue.PullRequest, doer)
|
||||
pullRequest = convert.ToPayloadPullRequest(ctx, comment.Issue.PullRequest, doer)
|
||||
} else {
|
||||
eventType = webhook_module.HookEventIssueComment
|
||||
}
|
||||
@@ -597,7 +597,7 @@ func (m *webhookNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
|
||||
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{
|
||||
Action: api.HookIssueLabelUpdated,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
})
|
||||
@@ -639,7 +639,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
|
||||
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestMilestone, &api.PullRequestPayload{
|
||||
Action: hookAction,
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
})
|
||||
@@ -712,7 +712,7 @@ func (*webhookNotifier) MergePullRequest(ctx context.Context, doer *user_model.U
|
||||
// Merge pull request calls issue.changeStatus so we need to handle separately.
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: pr.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer),
|
||||
Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
Action: api.HookIssueClosed,
|
||||
@@ -740,7 +740,7 @@ func (m *webhookNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe
|
||||
From: oldBranch,
|
||||
},
|
||||
},
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, mode),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}); err != nil {
|
||||
@@ -777,7 +777,7 @@ func (m *webhookNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{
|
||||
Action: api.HookIssueReviewed,
|
||||
Index: review.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, review.Reviewer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, review.Reviewer),
|
||||
RequestedReviewer: convert.ToUser(ctx, review.Reviewer, nil),
|
||||
Repository: convert.ToRepo(ctx, review.Issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, review.Reviewer, nil),
|
||||
@@ -802,7 +802,7 @@ func (m *webhookNotifier) PullRequestReviewRequest(ctx context.Context, doer *us
|
||||
}
|
||||
apiPullRequest := &api.PullRequestPayload{
|
||||
Index: issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, issue.PullRequest, doer),
|
||||
RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
|
||||
Repository: convert.ToRepo(ctx, issue.Repo, permission),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
@@ -847,7 +847,7 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use
|
||||
Before: before,
|
||||
After: after,
|
||||
Index: pr.Issue.Index,
|
||||
PullRequest: convert.ToAPIPullRequest(ctx, pr, doer),
|
||||
PullRequest: convert.ToPayloadPullRequest(ctx, pr, doer),
|
||||
Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
}); err != nil {
|
||||
|
||||
@@ -281,10 +281,9 @@ func TestPackageGenericPublicOnlyTokenLimitedOwner(t *testing.T) {
|
||||
// a public-only read:package token (even the owner's own) must be refused
|
||||
publicOnlyToken := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", base+"/file.bin").AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
// same via the v1 package API surface (checkTokenPublicOnly)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/pkg/1.0.0", owner.Name)).AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// a normal read:package token still works, proving only public-only is restricted
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadPackage)
|
||||
|
||||
@@ -30,6 +30,7 @@ type projectScope struct {
|
||||
issueID int64
|
||||
// a second such issue, deliberately left off the board
|
||||
foreignIssueID int64
|
||||
publicReader string
|
||||
}
|
||||
|
||||
func TestAPIProjects(t *testing.T) {
|
||||
@@ -43,10 +44,12 @@ func TestAPIProjects(t *testing.T) {
|
||||
outsider := getUserToken(t, "user8", auth_model.AccessTokenScopeWriteIssue,
|
||||
auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeReadUser)
|
||||
|
||||
publicOnlyOutsider := getUserToken(t, "user8", auth_model.AccessTokenScopeReadIssue,
|
||||
auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
||||
for _, scope := range []projectScope{
|
||||
{"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11},
|
||||
{"Organization", "/api/v1/orgs/org3/projects", 16, 17},
|
||||
{"User", "/api/v1/user/projects", 1, 11},
|
||||
{"Repository", "/api/v1/repos/user2/repo1/projects", 1, 11, ""},
|
||||
{"Organization", "/api/v1/orgs/org3/projects", 16, 17, publicOnlyOutsider},
|
||||
{"User", "/api/v1/user/projects", 1, 11, ""},
|
||||
} {
|
||||
t.Run(scope.name, func(t *testing.T) {
|
||||
testProjectLifecycle(t, scope, token)
|
||||
@@ -215,6 +218,10 @@ func testProjectLifecycle(t *testing.T, scope projectScope, token string) {
|
||||
issues := *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{})
|
||||
require.Len(t, issues, 1)
|
||||
assert.Equal(t, scope.issueID, issues[0].ID)
|
||||
if scope.publicReader != "" {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[1])).AddTokenAuth(scope.publicReader)
|
||||
assert.Len(t, *DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &[]api.Issue{}), 1)
|
||||
}
|
||||
|
||||
// the sibling column must not report the same issue
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/columns/%d/issues", projectURL, columnIDs[0])).AddTokenAuth(token)
|
||||
@@ -288,7 +295,7 @@ func testAPIProjectVisibility(t *testing.T, outsider string) {
|
||||
auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue,
|
||||
auth_model.AccessTokenScopePublicOnly)
|
||||
for _, url := range []string{"/api/v1/orgs/private_org35/projects", "/api/v1/users/private_org35/projects"} {
|
||||
MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", url).AddTokenAuth(publicOnly), http.StatusNotFound)
|
||||
}
|
||||
|
||||
// a public org's board is readable by an outsider but not writable
|
||||
|
||||
@@ -44,6 +44,7 @@ func TestAPIUserReposPublicOnly(t *testing.T) {
|
||||
assert.False(t, repo.Private)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count"))
|
||||
}
|
||||
|
||||
func repoNames(repos []api.Repository) []string {
|
||||
@@ -78,12 +79,14 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assert.NotEmpty(t, activities)
|
||||
|
||||
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||
for _, tokenOwner := range []string{"user2", "user1"} {
|
||||
publicToken := getUserToken(t, tokenOwner, 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)
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
}
|
||||
|
||||
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
|
||||
@@ -100,7 +103,7 @@ func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
}
|
||||
|
||||
func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
|
||||
func TestAPIOrgMembershipPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// user2 is a member of the private org private_org35
|
||||
@@ -115,6 +118,14 @@ func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
|
||||
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) {
|
||||
|
||||
@@ -142,6 +142,34 @@ func TestAPIViewPulls(t *testing.T) {
|
||||
assert.Len(t, files, 1)
|
||||
}))
|
||||
}
|
||||
|
||||
t.Run("HeadRepoHiddenWhenInaccessible", func(t *testing.T) {
|
||||
session := loginUser(t, "user31")
|
||||
forkToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(forkToken), http.StatusAccepted)
|
||||
visible := DecodeJSON(t, MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/pulls", &api.CreatePullRequestOption{
|
||||
Head: "user31:branch2", Base: "master", Title: "pull from a private user's fork",
|
||||
}).AddTokenAuth(forkToken), http.StatusCreated), &api.PullRequest{})
|
||||
require.NotNil(t, visible.Head.Repository)
|
||||
require.NotEmpty(t, visible.Head.Sha)
|
||||
|
||||
prURL := fmt.Sprintf("/api/v1/repos/user2/repo1/pulls/%d", visible.Index)
|
||||
var listed []*api.PullRequest
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/repo1/pulls?poster=user31"), http.StatusOK), &listed)
|
||||
require.Len(t, listed, 1)
|
||||
for _, hidden := range []*api.PullRequest{
|
||||
DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", prURL), http.StatusOK), &api.PullRequest{}),
|
||||
listed[0],
|
||||
} {
|
||||
assert.Nil(t, hidden.Head.Repository)
|
||||
assert.EqualValues(t, -1, hidden.Head.RepoID)
|
||||
assert.Equal(t, visible.Head.Name, hidden.Head.Name)
|
||||
assert.Equal(t, visible.Head.Sha, hidden.Head.Sha)
|
||||
}
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Index: visible.Index})
|
||||
assert.NotNil(t, convert.ToPayloadPullRequest(t.Context(), pr, nil).Head.Repository)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIViewPullsByBaseHead(t *testing.T) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestUserHeatmap(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeAll)
|
||||
|
||||
fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local)
|
||||
timeutil.MockSet(fakeNow)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
@@ -32,10 +33,12 @@ func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
|
||||
auth_model.AccessTokenScopeWriteUser,
|
||||
)
|
||||
|
||||
t.Run("PrivateProfileForbidden", func(t *testing.T) {
|
||||
t.Run("PrivateProfileUnreachable", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
||||
require.NoError(t, db.Insert(t.Context(), &user_model.Redirect{LowerName: "old-user31", RedirectUserID: privateUser.ID}))
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/old-user31").AddTokenAuth(privateReadUserToken), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
})
|
||||
|
||||
|
||||
@@ -277,16 +277,16 @@ func TestAttachmentTokenScopes(t *testing.T) {
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateIssueAttachment",
|
||||
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "GlobalPublicReleaseAttachment",
|
||||
@@ -309,16 +309,16 @@ func TestAttachmentTokenScopes(t *testing.T) {
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateReleaseAttachment",
|
||||
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusNotFound,
|
||||
publicOnlyRepoStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ func TestDownloadRepoContentTokenScopes(t *testing.T) {
|
||||
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(ownerReadToken), tc.withScope)
|
||||
|
||||
publicOnlyStatus := http.StatusForbidden
|
||||
publicOnlyStatus := http.StatusNotFound
|
||||
if tc.publicOnlyOK {
|
||||
publicOnlyStatus = tc.withScope
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func testGitSmartHTTPTokenScopes(t *testing.T) {
|
||||
|
||||
t.Run("public-only scope rejects private repo", func(t *testing.T) {
|
||||
path := "/user2/repo2/info/refs?service=git-upload-pack"
|
||||
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser).
|
||||
AddTokenAuth(token)
|
||||
|
||||
@@ -61,41 +61,6 @@ func TestAPIPullUpdate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIPullUpdatePublicOnlyToken verifies that a public-only API token cannot
|
||||
// update (push into) a PR whose head repo is private, even when the base repo
|
||||
// named in the route is public.
|
||||
func TestAPIPullUpdatePublicOnlyToken(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
|
||||
pr := createOutdatedPR(t, user, org26)
|
||||
require.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
require.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
require.NoError(t, pr.LoadIssue(t.Context()))
|
||||
|
||||
// make the head repo private while the base repo stays public
|
||||
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(),
|
||||
&repo_model.Repository{ID: pr.HeadRepo.ID, IsPrivate: true}, "is_private"))
|
||||
|
||||
// a public-only write token must be refused (404), not perform the push
|
||||
publicOnlyToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// the head branch must still be outdated (no push happened)
|
||||
diffCount, err := git.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, diffCount.Behind)
|
||||
|
||||
// a normal write token still works, proving the guard is scope-specific
|
||||
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
func updateRepoPullRequestConfig(t *testing.T, repoID int64, update func(*repo_model.PullRequestsConfig)) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestRepoWebTokenScopes(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assertBasicAuthStatus(t, test.url, miscToken, http.StatusForbidden)
|
||||
assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusForbidden)
|
||||
assertBasicAuthStatus(t, test.url, publicOnlyToken, http.StatusNotFound)
|
||||
assertBasicAuthStatus(t, test.url, readToken, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestAPIWorkflowRun(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/user/actions", "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
|
||||
})
|
||||
t.Run("OrgRuns", func(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository)
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeAll)
|
||||
})
|
||||
t.Run("RepoRuns", func(t *testing.T) {
|
||||
testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
Reference in New Issue
Block a user