package sso import ( "encoding/json" "errors" "strconv" "sync" "git.tcp.direct/tcp.direct/bitcask-mirror" "golang.org/x/crypto/bcrypt" "git.tcp.direct/kayos/tcpd-sso/config" ) // UserDB is our account database for password authentication. type UserDB struct { Authenticator DB *bitcask.Bitcask mu *sync.RWMutex } // NewUserDB returns a new UserDB with a bitcask database that stores it's data in path. //goland:noinspection GoUnusedExportedFunction func NewUserDB(path string) (db *UserDB, err error) { var b *bitcask.Bitcask if b, err = bitcask.Open(path); err != nil { return } db = &UserDB{DB: b, mu: &sync.RWMutex{}} return } // update is not thread safe, the caller must handle locking. func (users *UserDB) update(u *User) error { var ( ubytes []byte err error ) if ubytes, err = json.Marshal(u); err != nil { return err } return users.DB.Put([]byte(u.UserID), ubytes) } // Register registers a new user into our database. func (users *UserDB) Register(user, pass string) error { if users.UserExists(user) { return errors.New("username already exists: " + user) } if len(pass) < config.MinPasswordLength { return errors.New("password must be at least 5 characters") } u := &User{UserID: user, PassHash: HashPassword(pass), GlobalAdmin: false} return users.update(u) } // Delete removes a user from our database. func (users *UserDB) Delete(user string) error { users.mu.Lock() defer users.mu.Unlock() u, err := users.GetUser(user) if err != nil { return err } return users.DB.Delete([]byte(u.UserID)) } // PasswordLogin attempts to validate the provided credentials against our UserDB. func (users *UserDB) PasswordLogin(user, pass string) (*User, error) { var u *User var err error users.mu.RLock() defer users.mu.RUnlock() if u, err = users.GetUser(user); err != nil { return nil, err } if !CheckPasswordHash(pass, u.PassHash) { return nil, errors.New("incorrect credentials") } return u, nil } // ChangePassword hashes their new password and replaces their old password hash. func (users *UserDB) ChangePassword(user, newpass string) error { var ( u *User err error ) users.mu.Lock() defer users.mu.Unlock() if len(newpass) < config.MinPasswordLength { return errors.New("password must be at least " + strconv.Itoa(config.MinPasswordLength) + " characters") } users.mu.Lock() defer users.mu.Unlock() if u, err = users.GetUser(user); err != nil { return err } u.PassHash = HashPassword(newpass) return users.update(u) } // UserExists checks for the existence of the given username in our database. func (users *UserDB) UserExists(user string) bool { _, err := users.GetUser(user) return err == nil } // GetUser iterates through all User instances in the database and returns a pointer to the one that matches the requested username. func (users *UserDB) GetUser(user string) (usr *User, err error) { var ( usrbytes []byte ) users.mu.Lock() defer users.mu.Unlock() switch { case users.DB.Len() < 1: return &User{}, errors.New("no users in database") case !users.DB.Has([]byte(user)): return nil, errors.New("user does not exist: " + user) default: usrbytes, err = users.DB.Get([]byte(user)) } if err != nil { return nil, err } u := &User{} if err := json.Unmarshal(usrbytes, u); err != nil { return nil, err } return u, nil } // MakeGlobalAdmin marks a user as a Global Administrator, granting them all permissions. func (users *UserDB) MakeGlobalAdmin(user string) error { var u *User var err error users.mu.Lock() defer users.mu.Unlock() if u, err = users.GetUser(user); err != nil { return err } u.GlobalAdmin = true return users.update(u) } // IsActive returns if the user is allowed to access their account. func (users *UserDB) IsActive(user string) (bool, error) { u, err := users.GetUser(user) if err != nil { return false, err } return u.Active, nil } // HashPassword hashes the given password string using bcrypt. func HashPassword(password string) string { var bytes []byte var err error if bytes, err = bcrypt.GenerateFromPassword([]byte(password), 14); err != nil { panic(err) } return string(bytes) } // CheckPasswordHash checks if a given password string, when hashed, matches the given hash. func CheckPasswordHash(password string, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil }