371 lines
9 KiB
Go
371 lines
9 KiB
Go
package indentalUserDB
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"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
|
|
}
|