github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/model/user.go (about)

     1  package model
     2  
     3  import (
     4  	"crypto/rand"
     5  	"errors"
     6  	"time"
     7  
     8  	"github.com/asaskevich/govalidator"
     9  	"github.com/hashicorp/go-multierror"
    10  	"golang.org/x/crypto/bcrypt"
    11  )
    12  
    13  // revive:disable:max-public-structs domain complexity
    14  
    15  var (
    16  	ErrUserNotFound = NotFoundError{errors.New("user not found")}
    17  
    18  	ErrUserNameExists      = ValidationError{errors.New("user with this name already exists")}
    19  	ErrUserNameEmpty       = ValidationError{errors.New("user name can't be empty")}
    20  	ErrUserNameTooLong     = ValidationError{errors.New("user name must not exceed 255 characters")}
    21  	ErrUserFullNameTooLong = ValidationError{errors.New("user full name must not exceed 255 characters")}
    22  	ErrUserEmailExists     = ValidationError{errors.New("user with this email already exists")}
    23  	ErrUserEmailInvalid    = ValidationError{errors.New("user email is invalid")}
    24  	ErrUserExternalChange  = ValidationError{errors.New("external users can't be modified")}
    25  	ErrUserPasswordEmpty   = ValidationError{errors.New("user password can't be empty")}
    26  	ErrUserPasswordTooLong = ValidationError{errors.New("user password must not exceed 255 characters")}
    27  	ErrUserPasswordInvalid = ValidationError{errors.New("invalid password")}
    28  	ErrUserDisabled        = ValidationError{errors.New("user disabled")}
    29  
    30  	// ErrCredentialsInvalid should be returned when details of the authentication
    31  	// failure should be hidden (e.g. when user or API key not found).
    32  	ErrCredentialsInvalid = AuthenticationError{errors.New("invalid credentials")}
    33  	// ErrPermissionDenied should be returned if the actor does not have
    34  	// sufficient permissions for the action.
    35  	ErrPermissionDenied = AuthorizationError{errors.New("permission denied")}
    36  )
    37  
    38  type User struct {
    39  	ID           uint    `gorm:"primarykey"`
    40  	Name         string  `gorm:"type:varchar(255);not null;default:null;index:,unique"`
    41  	Email        *string `gorm:"type:varchar(255);default:null;index:,unique"`
    42  	FullName     *string `gorm:"type:varchar(255);default:null"`
    43  	PasswordHash []byte  `gorm:"type:varchar(255);not null;default:null"`
    44  	Role         Role    `gorm:"not null;default:null"`
    45  	IsDisabled   *bool   `gorm:"not null;default:false"`
    46  
    47  	// IsExternal indicates that the user authenticity is confirmed by
    48  	// an external authentication provider (such as OAuth) and thus,
    49  	// only limited attributes of the user can be managed. In fact, only
    50  	// FullName and Email can be altered by the user, and Role and IsDisabled
    51  	// can be changed by an administrator. Name should never change.
    52  	// TODO(kolesnikovae):
    53  	//  Add an attribute indicating the provider (e.g OAuth/LDAP).
    54  	//  Can it be a tagged union (sum type)?
    55  	IsExternal *bool `gorm:"not null;default:false"`
    56  
    57  	// TODO(kolesnikovae): Add an attribute indicating whether the email is confirmed.
    58  	// IsEmailConfirmed *bool
    59  
    60  	// TODO(kolesnikovae): Add an attribute forcing user to change its password.
    61  	// IsPasswordChangeRequired *bool
    62  
    63  	// TODO(kolesnikovae): Implemented LastSeenAt updating.
    64  	LastSeenAt        *time.Time `gorm:"default:null"`
    65  	PasswordChangedAt time.Time
    66  	CreatedAt         time.Time
    67  	UpdatedAt         time.Time
    68  }
    69  
    70  // TokenUser represents a user info retrieved from the validated JWT token.
    71  type TokenUser struct {
    72  	Name string
    73  	Role Role
    74  }
    75  
    76  type CreateUserParams struct {
    77  	Name       string
    78  	Email      *string
    79  	FullName   *string
    80  	Password   string
    81  	Role       Role
    82  	IsExternal bool
    83  }
    84  
    85  func (p CreateUserParams) Validate() error {
    86  	var err error
    87  	if nameErr := ValidateUserName(p.Name); nameErr != nil {
    88  		err = multierror.Append(err, nameErr)
    89  	}
    90  	if p.Email != nil {
    91  		if emailErr := ValidateEmail(*p.Email); emailErr != nil {
    92  			err = multierror.Append(err, emailErr)
    93  		}
    94  	}
    95  	if p.FullName != nil {
    96  		if nameErr := ValidateUserFullName(*p.FullName); nameErr != nil {
    97  			err = multierror.Append(err, nameErr)
    98  		}
    99  	}
   100  	if pwdErr := ValidatePasswordRequirements(p.Password); pwdErr != nil {
   101  		err = multierror.Append(err, pwdErr)
   102  	}
   103  	if !p.Role.IsValid() {
   104  		err = multierror.Append(err, ErrRoleUnknown)
   105  	}
   106  	return err
   107  }
   108  
   109  type UpdateUserParams struct {
   110  	FullName   *string
   111  	Name       *string
   112  	Email      *string
   113  	Password   *string
   114  	Role       *Role
   115  	IsDisabled *bool
   116  }
   117  
   118  func (p UpdateUserParams) SetRole(r Role) UpdateUserParams {
   119  	p.Role = &r // revive:disable:modifies-value-receiver returns by value
   120  	return p
   121  }
   122  
   123  func (p UpdateUserParams) SetIsDisabled(d bool) UpdateUserParams {
   124  	p.IsDisabled = &d // revive:disable:modifies-value-receiver returns by value
   125  	return p
   126  }
   127  
   128  func (p UpdateUserParams) Validate() error {
   129  	var err error
   130  	if p.Name != nil {
   131  		if nameErr := ValidateUserName(*p.Name); nameErr != nil {
   132  			err = multierror.Append(err, nameErr)
   133  		}
   134  	}
   135  	if p.Email != nil {
   136  		if emailErr := ValidateEmail(*p.Email); emailErr != nil {
   137  			err = multierror.Append(err, emailErr)
   138  		}
   139  	}
   140  	if p.FullName != nil {
   141  		if nameErr := ValidateUserFullName(*p.FullName); nameErr != nil {
   142  			err = multierror.Append(err, nameErr)
   143  		}
   144  	}
   145  	if p.Password != nil {
   146  		if pwdErr := ValidatePasswordRequirements(*p.Password); pwdErr != nil {
   147  			err = multierror.Append(err, pwdErr)
   148  		}
   149  	}
   150  	if p.Role != nil && !p.Role.IsValid() {
   151  		err = multierror.Append(err, ErrRoleUnknown)
   152  	}
   153  	return err
   154  }
   155  
   156  type UpdateUserPasswordParams struct {
   157  	OldPassword string
   158  	NewPassword string
   159  }
   160  
   161  func (p UpdateUserPasswordParams) Validate() error {
   162  	return ValidatePasswordRequirements(p.NewPassword)
   163  }
   164  
   165  func IsUserDisabled(u User) bool {
   166  	if u.IsDisabled == nil {
   167  		return false
   168  	}
   169  	return *u.IsDisabled
   170  }
   171  
   172  func IsUserExternal(u User) bool {
   173  	if u.IsExternal == nil {
   174  		return false
   175  	}
   176  	return *u.IsExternal
   177  }
   178  
   179  func ValidateUserName(userName string) error {
   180  	// TODO(kolesnikovae): restrict allowed chars?
   181  	if len(userName) == 0 {
   182  		return ErrUserNameEmpty
   183  	}
   184  	if len(userName) > 255 {
   185  		return ErrUserNameTooLong
   186  	}
   187  	return nil
   188  }
   189  
   190  func ValidateUserFullName(fullName string) error {
   191  	if len(fullName) > 255 {
   192  		return ErrUserFullNameTooLong
   193  	}
   194  	return nil
   195  }
   196  
   197  func ValidateEmail(email string) error {
   198  	if !govalidator.IsEmail(email) {
   199  		return ErrUserEmailInvalid
   200  	}
   201  	return nil
   202  }
   203  
   204  func ValidatePasswordRequirements(p string) error {
   205  	// TODO(kolesnikovae): should be configurable.
   206  	if p == "" {
   207  		return ErrUserPasswordEmpty
   208  	}
   209  	if len(p) > 255 {
   210  		return ErrUserPasswordTooLong
   211  	}
   212  	return nil
   213  }
   214  
   215  func MustPasswordHash(password string) []byte {
   216  	h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
   217  	if err != nil {
   218  		panic(err)
   219  	}
   220  	return h
   221  }
   222  
   223  func VerifyPassword(hashed []byte, password string) error {
   224  	return bcrypt.CompareHashAndPassword(hashed, []byte(password))
   225  }
   226  
   227  func MustRandomPassword() string {
   228  	// TODO(kolesnikovae): should be compliant with the requirements.
   229  	p := make([]byte, 32)
   230  	if _, err := rand.Read(p); err != nil {
   231  		panic(err)
   232  	}
   233  	return string(p)
   234  }