mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09: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
+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