tcpd-sso/db.go

186 lines
4.3 KiB
Go

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
}