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:
silverwindandwxiaoguang authored and GitHub committed 2026-10-19 00:34:01 +00:00
1 parent 7e62dda791
commit 5264304a34
96 files changed
+583 -686

No files matched your search

+4 -10
View File
@@ -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 {
+1 -1
View File
@@ -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(
+1
View File
@@ -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()))
}
+12
View File
@@ -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)
+3 -3
View File
@@ -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
+1 -7
View File
@@ -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 {
+4 -1
View File
@@ -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").
+8 -1
View File
@@ -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))
})
}
+2 -6
View File
@@ -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
View File
@@ -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})
}
+9 -2
View File
@@ -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
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+88
View File
@@ -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()
}
+9 -7
View File
@@ -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:
+14
View File
@@ -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)
}
+9
View File
@@ -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)