mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 12:59:11 -05:00
7cabc5670d
A remote user (UserTypeRemoteUser) is a placeholder that can be promoted to a regular user (UserTypeIndividual). It represents users that exist somewhere else. Although the UserTypeRemoteUser already exists in Forgejo, it is neither used or documented. A new login type / source (Remote) is introduced and set to be the login type of remote users. Type UserTypeRemoteUser LogingType Remote The association between a remote user and its counterpart in another environment (for instance another forge) is via the OAuth2 login source: LoginName set to the unique identifier relative to the login source LoginSource set to the identifier of the remote source For instance when migrating from GitLab.com, a user can be created as if it was authenticated using GitLab.com as an OAuth2 authentication source. When a user authenticates to Forejo from the same authentication source and the identifier match, the remote user is promoted to a regular user. For instance if 43 is the ID of the GitLab.com OAuth2 login source, 88 is the ID of the Remote loging source, and 48323 is the identifier of the foo user: Type UserTypeRemoteUser LogingType Remote LoginName 48323 LoginSource 88 Email (empty) Name foo Will be promoted to the following when the user foo authenticates to the Forgejo instance using GitLab.com as an OAuth2 provider. All users with a LoginType of Remote and a LoginName of 48323 are examined. If the LoginSource has a provider name that matches the provider name of GitLab.com (usually just "gitlab"), it is a match and can be promoted. The email is obtained via the OAuth2 provider and the user set to: Type UserTypeIndividual LogingType OAuth2 LoginName 48323 LoginSource 43 Email foo@example.com Name foo Note: the Remote login source is an indirection to the actual login source, i.e. the provider string my be set to a login source that does not exist yet.
178 lines
5.5 KiB
Go
178 lines
5.5 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/container"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/structs"
|
|
|
|
"xorm.io/builder"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
// SearchUserOptions contains the options for searching
|
|
type SearchUserOptions struct {
|
|
db.ListOptions
|
|
|
|
Keyword string
|
|
Type UserType
|
|
UID int64
|
|
LoginName string // this option should be used only for admin user
|
|
SourceID int64 // this option should be used only for admin user
|
|
OrderBy db.SearchOrderBy
|
|
Visible []structs.VisibleType
|
|
Actor *User // The user doing the search
|
|
SearchByEmail bool // Search by email as well as username/full name
|
|
|
|
SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set
|
|
|
|
IsActive optional.Option[bool]
|
|
IsAdmin optional.Option[bool]
|
|
IsRestricted optional.Option[bool]
|
|
IsTwoFactorEnabled optional.Option[bool]
|
|
IsProhibitLogin optional.Option[bool]
|
|
IncludeReserved bool
|
|
|
|
ExtraParamStrings map[string]string
|
|
}
|
|
|
|
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
|
var cond builder.Cond
|
|
if opts.Type == UserTypeIndividual {
|
|
cond = builder.In("type", UserTypeIndividual, UserTypeRemoteUser)
|
|
} else {
|
|
cond = builder.Eq{"type": opts.Type}
|
|
}
|
|
if opts.IncludeReserved {
|
|
if opts.Type == UserTypeIndividual {
|
|
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
|
|
builder.Eq{"type": UserTypeBot},
|
|
).Or(
|
|
builder.Eq{"type": UserTypeRemoteUser},
|
|
)
|
|
} else if opts.Type == UserTypeOrganization {
|
|
cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved})
|
|
}
|
|
}
|
|
|
|
if len(opts.Keyword) > 0 {
|
|
lowerKeyword := strings.ToLower(opts.Keyword)
|
|
keywordCond := builder.Or(
|
|
builder.Like{"lower_name", lowerKeyword},
|
|
builder.Like{"LOWER(full_name)", lowerKeyword},
|
|
)
|
|
if opts.SearchByEmail {
|
|
keywordCond = keywordCond.Or(builder.Like{"LOWER(email)", lowerKeyword})
|
|
}
|
|
|
|
cond = cond.And(keywordCond)
|
|
}
|
|
|
|
// If visibility filtered
|
|
if len(opts.Visible) > 0 {
|
|
cond = cond.And(builder.In("visibility", opts.Visible))
|
|
}
|
|
|
|
cond = cond.And(BuildCanSeeUserCondition(opts.Actor))
|
|
|
|
if opts.UID > 0 {
|
|
cond = cond.And(builder.Eq{"id": opts.UID})
|
|
}
|
|
|
|
if opts.SourceID > 0 {
|
|
cond = cond.And(builder.Eq{"login_source": opts.SourceID})
|
|
}
|
|
if opts.LoginName != "" {
|
|
cond = cond.And(builder.Eq{"login_name": opts.LoginName})
|
|
}
|
|
|
|
if opts.IsActive.Has() {
|
|
cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()})
|
|
}
|
|
|
|
if opts.IsAdmin.Has() {
|
|
cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()})
|
|
}
|
|
|
|
if opts.IsRestricted.Has() {
|
|
cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()})
|
|
}
|
|
|
|
if opts.IsProhibitLogin.Has() {
|
|
cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()})
|
|
}
|
|
|
|
e := db.GetEngine(ctx)
|
|
if !opts.IsTwoFactorEnabled.Has() {
|
|
return e.Where(cond)
|
|
}
|
|
|
|
// 2fa filter uses LEFT JOIN to check whether a user has a 2fa record
|
|
// While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed.
|
|
// There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now):
|
|
// (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch)
|
|
if opts.IsTwoFactorEnabled.Value() {
|
|
cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL"))
|
|
} else {
|
|
cond = cond.And(builder.Expr("two_factor.uid IS NULL"))
|
|
}
|
|
|
|
return e.Join("LEFT OUTER", "two_factor", "two_factor.uid = `user`.id").
|
|
Where(cond)
|
|
}
|
|
|
|
// SearchUsers takes options i.e. keyword and part of user name to search,
|
|
// it returns results in given range and number of total results.
|
|
func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ int64, _ error) {
|
|
sessCount := opts.toSearchQueryBase(ctx)
|
|
defer sessCount.Close()
|
|
count, err := sessCount.Count(new(User))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("count: %w", err)
|
|
}
|
|
|
|
if len(opts.OrderBy) == 0 {
|
|
opts.OrderBy = db.SearchOrderByAlphabetically
|
|
}
|
|
|
|
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
|
|
defer sessQuery.Close()
|
|
if opts.PageSize > 0 {
|
|
sessQuery = db.SetSessionPagination(sessQuery, opts)
|
|
}
|
|
|
|
// the sql may contain JOIN, so we must only select User related columns
|
|
sessQuery = sessQuery.Select("`user`.*")
|
|
users = make([]*User, 0, opts.PageSize)
|
|
return users, count, sessQuery.Find(&users)
|
|
}
|
|
|
|
// 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})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Force visibility for privacy
|
|
// Not logged in - only public users
|
|
return builder.In("`user`.visibility", structs.VisibleTypePublic)
|
|
}
|