mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-24 07:49:34 +02: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:
1 parent
7e62dda791
commit
5264304a34
96 files changed
+583
-686
No files matched your search
@@ -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,
|
||||
|
||||
+11
-23
@@ -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}))
|
||||
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})
|
||||
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 {
|
||||
cond = cond.Or(builder.In("`user`.visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
||||
}
|
||||
|
||||
return nil
|
||||
cond = cond.Or(builder.Eq{"`user`.id": actor.ID})
|
||||
}
|
||||
|
||||
// Force visibility for privacy
|
||||
// Not logged in - only public users
|
||||
return builder.In("`user`.visibility", structs.VisibleTypePublic)
|
||||
if !canViewPrivate {
|
||||
cond = cond.And(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)
|
||||
}
|
||||
switch id {
|
||||
case ActionsUserID:
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user