directory/internal/store/user.go

208 lines
4.9 KiB
Go

package store
import (
"crypto/rand"
"errors"
"net/http"
"time"
"github.com/oklog/ulid/v2"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
ID string `gorm:"primaryKey"`
Username string `gorm:"unique"`
Email *string
PasswordHash string
CreatedAt *time.Time
LastLogin *time.Time
LastSeen *time.Time
LastFailedAuth *time.Time
RecentFailedAuthCount int
LastSeenIP *string
Disabled bool
Attributes []UserAttribute `gorm:"foreignKey:UserID"`
db *gorm.DB `gorm:"-"`
}
func (User) TableName() string {
return UsersTableName
}
func (u *User) IsDisabled() bool {
return u.Disabled
}
func (s *Store) AddUser(username, password string) (*User, error) {
var hashedPassword string
if password != "" {
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Error().Err(err).Msg("unable to hash password")
return nil, err
}
hashedPassword = string(hashedPasswordBytes)
}
userID := ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader).String()
createdAt := time.Now()
user := &User{
ID: userID,
Username: username,
PasswordHash: hashedPassword,
CreatedAt: &createdAt,
db: s.db,
}
result := s.db.Create(user)
if result.Error != nil {
log.Error().
Str("username", username).
Err(result.Error).
Msg("unable to insert user into db")
return nil, result.Error
}
log.Debug().
Str("username", username).
Msg("user added to db")
return user, nil
}
func (s *Store) FindUserByUsername(username string) (*User, error) {
if len(username) < 3 {
return nil, errors.New("usernames are at least 3 characters")
}
var user User
result := s.db.Where("username = ? AND disabled = ?", username, false).First(&user)
if result.Error != nil {
log.Error().Err(result.Error).Msg("unable to find user")
return nil, result.Error
}
user.db = s.db
return &user, nil
}
func (s *Store) FindUserByID(userID string) (*User, error) {
var user User
result := s.db.Where("id = ? and disabled = ?", userID, false).First(&user)
if result.Error != nil {
log.Error().Err(result.Error).Msg("unable to find user")
return nil, result.Error
}
user.db = s.db
return &user, nil
}
func (s *Store) LogFailedAuth(UserID string, r *http.Request) {
// FIXME implement
}
func (s *Store) LogSuccessfulAuth(UserID string, r *http.Request) {
now := time.Now()
u, err := s.FindUserByID(UserID)
if err != nil {
panic("unable to find user")
}
s.db.Model(u).Update("LastSeen", now)
//s.db.Model(u).Update("LastSeenIP", FIXME)
}
func (s *Store) AuthenticateUser(username, password, rootPasswordHash string, r *http.Request) (bool, *User, error) {
if username == "root" {
user, err := s.FindUserByUsername(username)
if err != nil {
// we probably need to create the root user in the db
// and set 'user' because it's returned
user, err = s.AddUser("root", "*")
if err != nil {
log.Error().Err(err).Msg("unable to create root user")
return false, nil, err
}
}
// we also ignore if the root user in the database is disabled,
// because root can always log in using the env var password
// even if the root user exists in the db, we still use the root
// password from the environment var, and ignore the root password
// hash in the database
err = bcrypt.CompareHashAndPassword([]byte(rootPasswordHash), []byte(password))
if err != nil {
s.LogFailedAuth(user.ID, r)
return false, nil, nil
}
s.LogSuccessfulAuth(user.ID, r)
return true, user, nil
}
// FIXME update user record when auth fails
user, err := s.FindUserByUsername(username)
if err != nil {
return false, nil, err
}
if user == nil {
return false, nil, nil
}
if len(password) < 8 {
s.LogFailedAuth(user.ID, r)
return false, nil, nil
}
// how long are bcrypt hashes anyway?
if len(user.PasswordHash) < 10 {
s.LogFailedAuth(user.ID, r)
return false, nil, nil
}
if user.Disabled {
s.LogFailedAuth(user.ID, r)
return false, nil, nil
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
s.LogFailedAuth(user.ID, r)
return false, nil, nil
}
log.Error().Err(err).Msg("password comparison error")
return false, nil, err
}
s.LogSuccessfulAuth(user.ID, r)
return true, user, nil
}
func (u *User) Disable() error {
u.Disabled = true
result := u.db.Save(u)
if result.Error != nil {
log.Error().
Str("email", *u.Email).
Err(result.Error).
Msg("unable to disable user")
return result.Error
}
log.Debug().
Str("email", *u.Email).
Msg("user disabled")
return nil
}
func (u *User) IsAdmin() bool {
return u.HasAttribute("admin")
}
func (u *User) IsSuperAdmin() bool {
return u.HasAttribute("superadmin")
}