Create users package
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2022-01-20 03:35:55 +01:00
parent c6b282fbcc
commit d4b7702bad
12 changed files with 55 additions and 40 deletions

36
users/user.go Normal file
View File

@@ -0,0 +1,36 @@
package users
import "golang.org/x/crypto/bcrypt"
type Role string
const (
RoleUnset Role = ""
RoleUser Role = "user"
RoleAdmin Role = "admin"
)
type User struct {
Username string `json:"username"`
HashedPassword []byte `json:"hashed_password"`
Roles []Role `json:"roles"`
}
type UserStore interface {
Get(username string) (*User, error)
Store(user *User) error
Delete(username string) error
}
func (u *User) ValidatePassword(password string) error {
return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(password))
}
func (u *User) SetPassword(password string) error {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.HashedPassword = hashed
return nil
}

37
users/user_test.go Normal file
View File

@@ -0,0 +1,37 @@
package users_test
import (
"math/rand"
"testing"
"git.t-juice.club/torjus/gpaste/users"
)
func TestUser(t *testing.T) {
t.Run("Password", func(t *testing.T) {
userMap := make(map[string]string)
for i := 0; i < 10; i++ {
userMap[randomString(8)] = randomString(16)
}
for username, password := range userMap {
user := &users.User{Username: username}
if err := user.SetPassword(password); err != nil {
t.Fatalf("Error setting password: %s", err)
}
if err := user.ValidatePassword(password); err != nil {
t.Fatalf("Error validating password: %s", err)
}
}
})
}
func randomString(length int) string {
const charset = "abcdefghijklmnopqrstabcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}

69
users/userstore_bolt.go Normal file
View File

@@ -0,0 +1,69 @@
package users
import (
"encoding/json"
"go.etcd.io/bbolt"
)
var keyUsers = []byte("users")
type BoltUserStore struct {
db *bbolt.DB
}
func NewBoltUserStore(path string) (*BoltUserStore, error) {
db, err := bbolt.Open(path, 0666, nil)
if err != nil {
return nil, err
}
if err := db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(keyUsers)
return err
}); err != nil {
return nil, err
}
return &BoltUserStore{db: db}, nil
}
func (s *BoltUserStore) Close() error {
return s.db.Close()
}
func (s *BoltUserStore) Get(username string) (*User, error) {
var user User
err := s.db.View(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(keyUsers)
rawUser := bkt.Get([]byte(username))
if err := json.Unmarshal(rawUser, &user); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &user, nil
}
func (s *BoltUserStore) Store(user *User) error {
return s.db.Update(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(keyUsers)
data, err := json.Marshal(user)
if err != nil {
return err
}
return bkt.Put([]byte(user.Username), data)
})
}
func (s *BoltUserStore) Delete(username string) error {
return s.db.Update(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(keyUsers)
return bkt.Delete([]byte(username))
})
}

View File

@@ -0,0 +1,27 @@
package users_test
import (
"path/filepath"
"testing"
"git.t-juice.club/torjus/gpaste/users"
)
func TestBoltUserStore(t *testing.T) {
tmpDir := t.TempDir()
newFunc := func() (func(), users.UserStore) {
tmpFile := filepath.Join(tmpDir, randomString(8))
store, err := users.NewBoltUserStore(tmpFile)
if err != nil {
t.Fatalf("Error creating store: %s", err)
}
cleanup := func() {
store.Close()
}
return cleanup, store
}
RunUserStoreTest(newFunc, t)
}

39
users/userstore_memory.go Normal file
View File

@@ -0,0 +1,39 @@
package users
import (
"fmt"
"sync"
)
type MemoryUserStore struct {
users map[string]*User
lock sync.Mutex
}
func NewMemoryUserStore() *MemoryUserStore {
return &MemoryUserStore{users: make(map[string]*User)}
}
func (s *MemoryUserStore) Get(username string) (*User, error) {
s.lock.Lock()
defer s.lock.Unlock()
user, ok := s.users[username]
if !ok {
return nil, fmt.Errorf("no such user: %s", username)
}
return user, nil
}
func (s *MemoryUserStore) Store(user *User) error {
s.lock.Lock()
defer s.lock.Unlock()
s.users[user.Username] = user
return nil
}
func (s *MemoryUserStore) Delete(username string) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.users, username)
return nil
}

View File

@@ -0,0 +1,15 @@
package users_test
import (
"testing"
"git.t-juice.club/torjus/gpaste/users"
)
func TestMemoryUserStore(t *testing.T) {
newFunc := func() (func(), users.UserStore) {
return func() {}, users.NewMemoryUserStore()
}
RunUserStoreTest(newFunc, t)
}

51
users/userstore_test.go Normal file
View File

@@ -0,0 +1,51 @@
package users_test
import (
"testing"
"git.t-juice.club/torjus/gpaste/users"
"github.com/google/go-cmp/cmp"
)
func RunUserStoreTest(newFunc func() (func(), users.UserStore), t *testing.T) {
t.Run("Basics", func(t *testing.T) {
cleanup, s := newFunc()
t.Cleanup(cleanup)
userMap := make(map[string]*users.User)
passwordMap := make(map[string]string)
for i := 0; i < 10; i++ {
username := randomString(8)
password := randomString(16)
passwordMap[username] = password
user := &users.User{
Username: username,
Roles: []users.Role{users.RoleAdmin},
}
if err := user.SetPassword(password); err != nil {
t.Fatalf("Error setting password: %s", err)
}
userMap[username] = user
}
for _, user := range userMap {
if err := s.Store(user); err != nil {
t.Fatalf("Error storing user: %s", err)
}
}
for k := range userMap {
user, err := s.Get(k)
if err != nil {
t.Errorf("Error getting user: %s", err)
}
if err := user.ValidatePassword(passwordMap[user.Username]); err != nil {
t.Errorf("Error verifying password: %s", err)
}
if !cmp.Equal(user, userMap[k]) {
t.Errorf("User mismatch: %s", cmp.Diff(user, userMap[k]))
}
}
})
}