186 lines
4.3 KiB
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
|
|
}
|