initial commit
This commit is contained in:
commit
f48fa210bb
49 changed files with 4058 additions and 0 deletions
33
pkg/db/blacklist.go
Normal file
33
pkg/db/blacklist.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import "git.giftfish.de/ston1th/jwt/v3"
|
||||
|
||||
const blacklistPrefix = "blacklist/"
|
||||
|
||||
func (db *DB) Add(sig string, exp int64) error {
|
||||
return db.store.Set(blacklistPrefix+sig, exp)
|
||||
}
|
||||
|
||||
func (db *DB) Remove(sig string) error {
|
||||
return db.store.Delete(blacklistPrefix + sig)
|
||||
}
|
||||
|
||||
func (db *DB) Check(sig string) (ok bool) {
|
||||
return db.store.Get(blacklistPrefix+sig, nil) == nil
|
||||
}
|
||||
|
||||
func (db *DB) Map() (list jwt.BlacklistMap, err error) {
|
||||
list = make(jwt.BlacklistMap)
|
||||
err = db.store.ForEachPrefix(blacklistPrefix, func(k string, v []byte) error {
|
||||
var exp int64
|
||||
err := db.store.Unmarshal(v, &exp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list[k] = exp
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
69
pkg/db/db.go
Normal file
69
pkg/db/db.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"git.giftfish.de/ston1th/goacc/pkg/core"
|
||||
"git.giftfish.de/ston1th/goacc/pkg/store"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
const (
|
||||
defUser = "admin"
|
||||
defEmail = "admin@goacc"
|
||||
defPassword = "goacc"
|
||||
storeFile = "store.db"
|
||||
bcryptCost = 13
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
log logr.Logger
|
||||
admin bool
|
||||
store store.Store
|
||||
}
|
||||
|
||||
func NewPlain(log logr.Logger, cfg *core.Config) (db *DB, err error) {
|
||||
db = &DB{log: log, admin: cfg.Admin}
|
||||
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||
//err = godrop.Unveil(dbFile, "rwc")
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
db.store, err = store.NewBoltStore(dbFile, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func New(log logr.Logger, cfg *core.Config) (db *DB, err error) {
|
||||
db, err = NewPlain(log, cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.UserExists(defUser) != nil {
|
||||
err = db.CreateUser(defUser, defEmail, defPassword, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = db.UnlockUser(defUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.RunMigrations()
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Dump(w io.Writer) error {
|
||||
return db.store.Dump(w)
|
||||
}
|
||||
|
||||
func (db *DB) Restore(r io.Reader) error {
|
||||
return db.store.Restore(r)
|
||||
}
|
||||
|
||||
func (db *DB) Close() error {
|
||||
return db.store.Close()
|
||||
}
|
||||
21
pkg/db/helper.go
Normal file
21
pkg/db/helper.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"time"
|
||||
)
|
||||
|
||||
func validatePassword(pw string) (b []byte) {
|
||||
hash := sha256.New()
|
||||
hash.Write(b)
|
||||
b = hash.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
const timeFmt = "2006-01-02 15:04:05"
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
13
pkg/db/lockout.go
Normal file
13
pkg/db/lockout.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
const lockoutPrefix = "lockout/"
|
||||
|
||||
func (db *DB) lockout(username, created string) error {
|
||||
return db.store.Set(lockoutPrefix+username+created, nil)
|
||||
}
|
||||
|
||||
func (db *DB) LockedOut(username, created string) bool {
|
||||
return db.store.Get(lockoutPrefix+username+created, nil) == nil
|
||||
}
|
||||
60
pkg/db/migration.go
Normal file
60
pkg/db/migration.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.giftfish.de/ston1th/goacc/pkg/store"
|
||||
)
|
||||
|
||||
const (
|
||||
versionKey = "version/version"
|
||||
currentVersion = 0
|
||||
)
|
||||
|
||||
type migrator func(*DB, int) error
|
||||
|
||||
var migrators = []migrator{
|
||||
func(_ *DB, _ int) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func (db *DB) RunMigrations() (err error) {
|
||||
log := db.log.WithName("migration")
|
||||
var version int
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err == store.ErrKeyNotFound {
|
||||
err = db.store.Set(versionKey, currentVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Info("db versions", "current", version, "new", currentVersion)
|
||||
if currentVersion < version {
|
||||
return errors.New("incompatible versions")
|
||||
}
|
||||
if currentVersion == version {
|
||||
log.Info("nothing to do")
|
||||
}
|
||||
for version < currentVersion {
|
||||
v := version + 1
|
||||
log.Info("running", "version", v)
|
||||
err = migrators[v](db, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Set(versionKey, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
version = v
|
||||
log.Info("finished", "version", v)
|
||||
}
|
||||
return
|
||||
}
|
||||
220
pkg/db/user.go
Normal file
220
pkg/db/user.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.giftfish.de/ston1th/goacc/pkg/core"
|
||||
"git.giftfish.de/ston1th/goacc/pkg/otp"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const userPrefix = "user/"
|
||||
|
||||
var (
|
||||
errUserLocked = errors.New("db: user locked")
|
||||
errUserLogin = errors.New("db: wrong login")
|
||||
errUserPin = errors.New("db: wrong pin")
|
||||
errUserExists = errors.New("db: user already exist")
|
||||
errNameReserved = errors.New("db: name is reserved")
|
||||
|
||||
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
)
|
||||
|
||||
func (db *DB) GetUsers() (users core.Users, err error) {
|
||||
err = db.store.ForEachPrefix(userPrefix, func(k string, _ []byte) error {
|
||||
u, err := db.GetUser(k)
|
||||
u.Password = ""
|
||||
if err == nil {
|
||||
users = append(users, u)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Sort(users)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserWithoutPassword(username string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
u.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSecret(username string) (secret string, err error) {
|
||||
var user *core.User
|
||||
user, err = db.GetUser(username)
|
||||
secret = user.Secret
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUser(username string) (u *core.User, err error) {
|
||||
u = new(core.User)
|
||||
err = db.store.Get(userPrefix+username, u)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UserExists(username string) error {
|
||||
return db.store.Get(userPrefix+username, nil)
|
||||
}
|
||||
|
||||
func (db *DB) CreateUser(username, email, password string, admin bool) error {
|
||||
username = userRe.ReplaceAllString(username, "")
|
||||
if err := db.UserExists(username); err == nil {
|
||||
return errUserExists
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.store.Set(userPrefix+username, &core.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: string(hash),
|
||||
Created: strconv.Itoa(int(jwt.Now())),
|
||||
Admin: admin,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) Login(username, password string) (u *core.User, err error) {
|
||||
if username == defUser && !db.admin {
|
||||
err = errors.New("db: user disabled")
|
||||
return
|
||||
}
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserLogin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Totp(username, pin string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if !otp.Validate(pin, u.Secret) {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserPin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UnlockUser(username string) (err error) {
|
||||
var u *core.User
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserSecret(username, secret string) error {
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Secret = secret
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserPassword(username, password string) error {
|
||||
if password == "" {
|
||||
return errors.New("empty password")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) ResetAdmin() error {
|
||||
return db.UpdateUserPassword(defUser, defPassword)
|
||||
}
|
||||
|
||||
func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
|
||||
if username == defUser && !admin {
|
||||
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Admin = admin
|
||||
if password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
}
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteUser(username string) error {
|
||||
if username == defUser {
|
||||
return errors.New("user '" + defUser + "' cannot be deleted")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.store.Delete(userPrefix + username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.lockout(username, u.Created)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue