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 }