123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- // Copyright 2020 The Gogs Authors. All rights reserved.
- // Use of this source code is governed by a MIT-style
- // license that can be found in the LICENSE file.
- package db
- import (
- "fmt"
- "strings"
- "time"
- "github.com/go-macaron/binding"
- "github.com/pkg/errors"
- "gorm.io/gorm"
- "gogs.io/gogs/internal/auth"
- "gogs.io/gogs/internal/cryptoutil"
- "gogs.io/gogs/internal/errutil"
- )
- // UsersStore is the persistent interface for users.
- //
- // NOTE: All methods are sorted in alphabetical order.
- type UsersStore interface {
- // Authenticate validates username and password via given login source ID.
- // It returns ErrUserNotExist when the user was not found.
- //
- // When the "loginSourceID" is negative, it aborts the process and returns
- // ErrUserNotExist if the user was not found in the database.
- //
- // When the "loginSourceID" is non-negative, it returns ErrLoginSourceMismatch
- // if the user has different login source ID than the "loginSourceID".
- //
- // When the "loginSourceID" is positive, it tries to authenticate via given
- // login source and creates a new user when not yet exists in the database.
- Authenticate(username, password string, loginSourceID int64) (*User, error)
- // Create creates a new user and persists to database.
- // It returns ErrUserAlreadyExist when a user with same name already exists,
- // or ErrEmailAlreadyUsed if the email has been used by another user.
- Create(username, email string, opts CreateUserOpts) (*User, error)
- // GetByEmail returns the user (not organization) with given email.
- // It ignores records with unverified emails and returns ErrUserNotExist when not found.
- GetByEmail(email string) (*User, error)
- // GetByID returns the user with given ID. It returns ErrUserNotExist when not found.
- GetByID(id int64) (*User, error)
- // GetByUsername returns the user with given username. It returns ErrUserNotExist when not found.
- GetByUsername(username string) (*User, error)
- }
- var Users UsersStore
- // NOTE: This is a GORM create hook.
- func (u *User) BeforeCreate(tx *gorm.DB) error {
- if u.CreatedUnix == 0 {
- u.CreatedUnix = tx.NowFunc().Unix()
- u.UpdatedUnix = u.CreatedUnix
- }
- return nil
- }
- // NOTE: This is a GORM query hook.
- func (u *User) AfterFind(tx *gorm.DB) error {
- u.Created = time.Unix(u.CreatedUnix, 0).Local()
- u.Updated = time.Unix(u.UpdatedUnix, 0).Local()
- return nil
- }
- var _ UsersStore = (*users)(nil)
- type users struct {
- *gorm.DB
- }
- type ErrLoginSourceMismatch struct {
- args errutil.Args
- }
- func (err ErrLoginSourceMismatch) Error() string {
- return fmt.Sprintf("login source mismatch: %v", err.args)
- }
- func (db *users) Authenticate(login, password string, loginSourceID int64) (*User, error) {
- login = strings.ToLower(login)
- var query *gorm.DB
- if strings.Contains(login, "@") {
- query = db.Where("email = ?", login)
- } else {
- query = db.Where("lower_name = ?", login)
- }
- user := new(User)
- err := query.First(user).Error
- if err != nil && err != gorm.ErrRecordNotFound {
- return nil, errors.Wrap(err, "get user")
- }
- var authSourceID int64 // The login source ID will be used to authenticate the user
- createNewUser := false // Whether to create a new user after successful authentication
- // User found in the database
- if err == nil {
- // Note: This check is unnecessary but to reduce user confusion at login page
- // and make it more consistent from user's perspective.
- if loginSourceID >= 0 && user.LoginSource != loginSourceID {
- return nil, ErrLoginSourceMismatch{args: errutil.Args{"expect": loginSourceID, "actual": user.LoginSource}}
- }
- // Validate password hash fetched from database for local accounts.
- if user.IsLocal() {
- if user.ValidatePassword(password) {
- return user, nil
- }
- return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login, "userID": user.ID}}
- }
- authSourceID = user.LoginSource
- } else {
- // Non-local login source is always greater than 0.
- if loginSourceID <= 0 {
- return nil, auth.ErrBadCredentials{Args: map[string]interface{}{"login": login}}
- }
- authSourceID = loginSourceID
- createNewUser = true
- }
- source, err := LoginSources.GetByID(authSourceID)
- if err != nil {
- return nil, errors.Wrap(err, "get login source")
- }
- if !source.IsActived {
- return nil, errors.Errorf("login source %d is not activated", source.ID)
- }
- extAccount, err := source.Provider.Authenticate(login, password)
- if err != nil {
- return nil, err
- }
- if !createNewUser {
- return user, nil
- }
- // Validate username make sure it satisfies requirement.
- if binding.AlphaDashDotPattern.MatchString(extAccount.Name) {
- return nil, fmt.Errorf("invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", extAccount.Name)
- }
- return Users.Create(extAccount.Name, extAccount.Email, CreateUserOpts{
- FullName: extAccount.FullName,
- LoginSource: authSourceID,
- LoginName: extAccount.Login,
- Location: extAccount.Location,
- Website: extAccount.Website,
- Activated: true,
- Admin: extAccount.Admin,
- })
- }
- type CreateUserOpts struct {
- FullName string
- Password string
- LoginSource int64
- LoginName string
- Location string
- Website string
- Activated bool
- Admin bool
- }
- type ErrUserAlreadyExist struct {
- args errutil.Args
- }
- func IsErrUserAlreadyExist(err error) bool {
- _, ok := err.(ErrUserAlreadyExist)
- return ok
- }
- func (err ErrUserAlreadyExist) Error() string {
- return fmt.Sprintf("user already exists: %v", err.args)
- }
- type ErrEmailAlreadyUsed struct {
- args errutil.Args
- }
- func IsErrEmailAlreadyUsed(err error) bool {
- _, ok := err.(ErrEmailAlreadyUsed)
- return ok
- }
- func (err ErrEmailAlreadyUsed) Email() string {
- email, ok := err.args["email"].(string)
- if ok {
- return email
- }
- return "<email not found>"
- }
- func (err ErrEmailAlreadyUsed) Error() string {
- return fmt.Sprintf("email has been used: %v", err.args)
- }
- func (db *users) Create(username, email string, opts CreateUserOpts) (*User, error) {
- err := isUsernameAllowed(username)
- if err != nil {
- return nil, err
- }
- _, err = db.GetByUsername(username)
- if err == nil {
- return nil, ErrUserAlreadyExist{args: errutil.Args{"name": username}}
- } else if !IsErrUserNotExist(err) {
- return nil, err
- }
- _, err = db.GetByEmail(email)
- if err == nil {
- return nil, ErrEmailAlreadyUsed{args: errutil.Args{"email": email}}
- } else if !IsErrUserNotExist(err) {
- return nil, err
- }
- user := &User{
- LowerName: strings.ToLower(username),
- Name: username,
- FullName: opts.FullName,
- Email: email,
- Passwd: opts.Password,
- LoginSource: opts.LoginSource,
- LoginName: opts.LoginName,
- Location: opts.Location,
- Website: opts.Website,
- MaxRepoCreation: -1,
- IsActive: opts.Activated,
- IsAdmin: opts.Admin,
- Avatar: cryptoutil.MD5(email),
- AvatarEmail: email,
- }
- user.Rands, err = GetUserSalt()
- if err != nil {
- return nil, err
- }
- user.Salt, err = GetUserSalt()
- if err != nil {
- return nil, err
- }
- user.EncodePassword()
- return user, db.DB.Create(user).Error
- }
- var _ errutil.NotFound = (*ErrUserNotExist)(nil)
- type ErrUserNotExist struct {
- args errutil.Args
- }
- func IsErrUserNotExist(err error) bool {
- _, ok := err.(ErrUserNotExist)
- return ok
- }
- func (err ErrUserNotExist) Error() string {
- return fmt.Sprintf("user does not exist: %v", err.args)
- }
- func (ErrUserNotExist) NotFound() bool {
- return true
- }
- func (db *users) GetByEmail(email string) (*User, error) {
- email = strings.ToLower(email)
- if len(email) == 0 {
- return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
- }
- // First try to find the user by primary email
- user := new(User)
- err := db.Where("email = ? AND type = ? AND is_active = ?", email, UserIndividual, true).First(user).Error
- if err == nil {
- return user, nil
- } else if err != gorm.ErrRecordNotFound {
- return nil, err
- }
- // Otherwise, check activated email addresses
- emailAddress := new(EmailAddress)
- err = db.Where("email = ? AND is_activated = ?", email, true).First(emailAddress).Error
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- return nil, ErrUserNotExist{args: errutil.Args{"email": email}}
- }
- return nil, err
- }
- return db.GetByID(emailAddress.UID)
- }
- func (db *users) GetByID(id int64) (*User, error) {
- user := new(User)
- err := db.Where("id = ?", id).First(user).Error
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- return nil, ErrUserNotExist{args: errutil.Args{"userID": id}}
- }
- return nil, err
- }
- return user, nil
- }
- func (db *users) GetByUsername(username string) (*User, error) {
- user := new(User)
- err := db.Where("lower_name = ?", strings.ToLower(username)).First(user).Error
- if err != nil {
- if err == gorm.ErrRecordNotFound {
- return nil, ErrUserNotExist{args: errutil.Args{"name": username}}
- }
- return nil, err
- }
- return user, nil
- }
|