quartzgun/indentalUserDB/indentalUserDB.go

372 lines
9 KiB
Go
Raw Permalink Normal View History

package indentalUserDB
import (
"encoding/base64"
"errors"
"fmt"
2024-11-28 17:31:07 +00:00
"forge.lightcrystal.systems/nilix/quartzgun/auth"
"forge.lightcrystal.systems/nilix/quartzgun/cookie"
"golang.org/x/crypto/bcrypt"
"os"
"strconv"
"strings"
"sync"
"time"
)
type IndentalUserDB struct {
Users map[string]*auth.User
Basis string
mtx sync.RWMutex
}
func CreateIndentalUserDB(filePath string) *IndentalUserDB {
u, err := readDB(filePath)
if err == nil {
return &IndentalUserDB{
Users: u,
Basis: filePath,
}
} else {
return &IndentalUserDB{
Users: map[string]*auth.User{},
Basis: filePath,
}
}
}
func (self *IndentalUserDB) InitiateSession(user string, password string, ttl int) (string, error) {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return "", errors.New("User not in DB")
}
if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(password)) != nil {
self.mtx.RUnlock()
return "", errors.New("Incorrect password")
}
self.mtx.RUnlock()
sessionId := cookie.GenToken(64)
self.mtx.Lock()
self.Users[user].Session = sessionId
self.Users[user].LoginTime = time.Now()
self.Users[user].LastSeen = time.Now()
self.mtx.Unlock()
// SetData calls Lock & Unlock and writes DB to file
self.SetData(user, "token_expiry", strconv.Itoa(ttl))
return sessionId, nil
}
func (self *IndentalUserDB) GrantToken(user, password string, ttl int) (string, error) {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return "", errors.New("User not in DB")
}
if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(password)) != nil {
self.mtx.RUnlock()
return "", errors.New("Incorrect password")
}
self.mtx.RUnlock()
sessionId := cookie.GenToken(64)
self.mtx.Lock()
self.Users[user].Session = sessionId
self.Users[user].LoginTime = time.Now()
self.Users[user].LastSeen = time.Now()
self.mtx.Unlock()
// SetData calls Lock & Unlock and writes DB to file
self.SetData(user, "token_expiry", strconv.Itoa(ttl))
return base64.StdEncoding.EncodeToString([]byte(user + "\n" + sessionId)), nil
}
func (self *IndentalUserDB) ValidateUser(user string, sessionId string) (bool, error) {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return false, errors.New("User not in DB")
}
validated := self.Users[user].Session == sessionId
self.mtx.RUnlock()
// GetData takes RLock
expiry, err3 := self.GetData(user, "token_expiry")
expiryInt, err4 := strconv.ParseInt(expiry.(string), 10, 64)
self.mtx.RLock()
expiryTime := self.Users[user].LastSeen.Add(time.Minute * time.Duration(expiryInt))
self.mtx.RUnlock()
if validated {
if err3 == nil && err4 == nil && time.Now().After(expiryTime) {
self.EndSession(user)
return true, errors.New("Cookie or token expired")
} else {
self.mtx.Lock()
defer self.mtx.Unlock()
self.Users[user].LastSeen = time.Now()
writeDB(self.Basis, self.Users)
}
}
return validated, nil
}
func (self *IndentalUserDB) ValidateToken(token string) (bool, error) {
data, err := base64.StdEncoding.DecodeString(token)
if err == nil {
parts := strings.Split(string(data), "\n")
if len(parts) == 2 {
return self.ValidateUser(parts[0], parts[1])
}
}
return false, errors.New("Token was not in a valid format: b64(USER\nSESSION)")
}
func (self *IndentalUserDB) ValidateTokenWithScopes(token string, scopes map[string]string) (bool, error) {
data, err := base64.StdEncoding.DecodeString(token)
if err == nil {
parts := strings.Split(string(data), "\n")
n := 0
for k, v := range scopes {
s, _ := self.GetData(parts[0], k)
if s.(string) == v {
n++
}
}
validated, err2 := self.ValidateToken(token)
if validated {
if n == len(scopes) {
return validated, nil
} else {
return false, errors.New("User does not have the proper scopes")
}
} else {
return validated, err2
}
}
return false, err
}
func (self *IndentalUserDB) EndSession(user string) error {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return errors.New("User not in DB")
}
self.mtx.RUnlock()
self.mtx.Lock()
defer self.mtx.Unlock()
self.Users[user].Session = ""
self.Users[user].LastSeen = time.Now()
writeDB(self.Basis, self.Users)
return nil
}
func (self *IndentalUserDB) DeleteUser(user string) error {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return errors.New("User not in DB")
}
self.mtx.RUnlock()
self.mtx.Lock()
defer self.mtx.Unlock()
delete(self.Users, user)
writeDB(self.Basis, self.Users)
return nil
}
func (self *IndentalUserDB) ChangePassword(user string, password string, oldPassword string) error {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return errors.New("User not in DB")
}
if bcrypt.CompareHashAndPassword([]byte(self.Users[user].Pass), []byte(oldPassword)) != nil {
self.mtx.RUnlock()
return errors.New("Incorrect password")
}
self.mtx.RUnlock()
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 10)
self.mtx.Lock()
defer self.mtx.Unlock()
self.Users[user].Pass = string(hash[:])
writeDB(self.Basis, self.Users)
return nil
}
func (self *IndentalUserDB) AddUser(user string, password string) error {
self.mtx.RLock()
if _, exists := self.Users[user]; exists {
self.mtx.RUnlock()
return errors.New("User already in DB")
}
self.mtx.RUnlock()
hash, _ := bcrypt.GenerateFromPassword([]byte(password), 10)
self.mtx.Lock()
defer self.mtx.Unlock()
self.Users[user] = &auth.User{
Name: user,
Pass: string(hash[:]),
LastSeen: time.UnixMicro(0),
LoginTime: time.UnixMicro(0),
Session: "",
Data: map[string]interface{}{},
}
writeDB(self.Basis, self.Users)
return nil
}
func (self *IndentalUserDB) GetLastLoginTime(user string) (time.Time, error) {
self.mtx.RLock()
self.mtx.RUnlock()
if usr, exists := self.Users[user]; exists {
return usr.LoginTime, nil
}
return time.UnixMicro(0), errors.New("User not in DB")
}
func (self *IndentalUserDB) GetLastTimeSeen(user string) (time.Time, error) {
self.mtx.RLock()
self.mtx.RUnlock()
if usr, exists := self.Users[user]; exists {
return usr.LastSeen, nil
}
return time.UnixMicro(0), errors.New("User not in DB")
}
func (self *IndentalUserDB) SetData(user string, key string, value interface{}) error {
self.mtx.RLock()
if _, exists := self.Users[user]; !exists {
self.mtx.RUnlock()
return errors.New("User not in DB")
}
self.mtx.RUnlock()
self.mtx.Lock()
defer self.mtx.Unlock()
self.Users[user].Data[key] = value
writeDB(self.Basis, self.Users)
return nil
}
func (self *IndentalUserDB) GetData(user string, key string) (interface{}, error) {
self.mtx.RLock()
defer self.mtx.RUnlock()
if _, usrExists := self.Users[user]; !usrExists {
return nil, errors.New("User not in DB")
}
data, exists := self.Users[user].Data[key]
if !exists {
return nil, errors.New("Key not found in user data")
}
return data, nil
}
const timeFmt = "2006-01-02T15:04Z"
func readDB(filePath string) (map[string]*auth.User, error) {
f, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
fileData := string(f[:])
users := map[string]*auth.User{}
lines := strings.Split(fileData, "\n")
indentLevel := ""
var name string
var pass string
var session string
var loginTime time.Time
var lastSeen time.Time
var data map[string]interface{}
for _, l := range lines {
if strings.HasPrefix(l, indentLevel) {
switch indentLevel {
case "":
name = l
indentLevel = "\t"
case "\t":
if strings.Contains(l, ":") {
kvp := strings.Split(l, ":")
k := strings.TrimSpace(kvp[0])
v := strings.TrimSpace(strings.Join(kvp[1:], ":"))
switch k {
case "pass":
pass = v
case "session":
session = v
case "loginTime":
loginTime, _ = time.Parse(timeFmt, v)
case "lastSeen":
lastSeen, _ = time.Parse(timeFmt, v)
}
} else {
data = map[string]interface{}{}
indentLevel = "\t\t"
}
case "\t\t":
if strings.Contains(l, ":") {
kvp := strings.Split(l, ":")
k := strings.TrimSpace(kvp[0])
v := strings.TrimSpace(kvp[1])
data[k] = v
}
}
} else {
if indentLevel != "\t\t" {
panic("Malformed indental file")
} else {
users[name] = &auth.User{
Name: name,
Pass: pass,
Session: session,
LoginTime: loginTime,
LastSeen: lastSeen,
Data: data,
}
indentLevel = ""
}
}
}
return users, nil
}
func writeDB(filePath string, users map[string]*auth.User) error {
f, err := os.Create(filePath)
if err != nil {
return err
}
defer f.Close()
for _, user := range users {
f.WriteString(fmt.Sprintf("%s\n\tpass: %s\n\tsession: %s\n\tloginTime: %s\n\tlastSeen: %s\n\tdata\n",
user.Name,
user.Pass,
user.Session,
user.LoginTime.UTC().Format(timeFmt),
user.LastSeen.UTC().Format(timeFmt)))
for k, v := range user.Data {
f.WriteString(fmt.Sprintf("\t\t%s: %s\n", k, v))
}
f.WriteString("\n")
}
f.Sync()
return nil
}