Files
yovision/Sense/server/app/sense/identity/store.go
T

172 lines
4.3 KiB
Go

package identity
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"sort"
"strings"
"sync"
"time"
)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrBootstrapDone = errors.New("bootstrap already completed")
)
type Store interface {
BootstrapUser(context.Context, User) error
CreateUser(context.Context, User) error
UpdateUser(context.Context, User) error
FindUserByUsername(context.Context, string) (User, error)
FindUserByID(context.Context, string) (User, error)
ListUsers(context.Context) ([]User, error)
CreateSession(context.Context, Session) error
SessionActive(context.Context, string, time.Time) (bool, error)
RevokeSession(context.Context, string, time.Time) error
AppendAudit(context.Context, AuditEntry) error
ListAudit(context.Context, int) ([]AuditEntry, error)
}
type MemoryStore struct {
mu sync.RWMutex
users map[string]User
byName map[string]string
sessions map[string]Session
audit []AuditEntry
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{users: map[string]User{}, byName: map[string]string{}, sessions: map[string]Session{}}
}
func (s *MemoryStore) BootstrapUser(_ context.Context, user User) error {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.users) != 0 {
return ErrBootstrapDone
}
return s.createUserLocked(user)
}
func (s *MemoryStore) CreateUser(_ context.Context, user User) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.createUserLocked(user)
}
func (s *MemoryStore) createUserLocked(user User) error {
key := strings.ToLower(user.Username)
if _, exists := s.byName[key]; exists {
return ErrAlreadyExists
}
s.users[user.ID] = user
s.byName[key] = user.ID
return nil
}
func (s *MemoryStore) UpdateUser(_ context.Context, user User) error {
s.mu.Lock()
defer s.mu.Unlock()
previous, exists := s.users[user.ID]
if !exists {
return ErrNotFound
}
delete(s.byName, strings.ToLower(previous.Username))
s.users[user.ID] = user
s.byName[strings.ToLower(user.Username)] = user.ID
return nil
}
func (s *MemoryStore) FindUserByUsername(_ context.Context, username string) (User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
id, exists := s.byName[strings.ToLower(username)]
if !exists {
return User{}, ErrNotFound
}
return s.users[id], nil
}
func (s *MemoryStore) FindUserByID(_ context.Context, id string) (User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, exists := s.users[id]
if !exists {
return User{}, ErrNotFound
}
return user, nil
}
func (s *MemoryStore) ListUsers(_ context.Context) ([]User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]User, 0, len(s.users))
for _, user := range s.users {
users = append(users, user)
}
sort.Slice(users, func(i, j int) bool { return users[i].Username < users[j].Username })
return users, nil
}
func (s *MemoryStore) CreateSession(_ context.Context, session Session) error {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[session.ID] = session
return nil
}
func (s *MemoryStore) SessionActive(_ context.Context, id string, now time.Time) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
session, exists := s.sessions[id]
return exists && session.RevokedAt == nil && now.Before(session.ExpiresAt), nil
}
func (s *MemoryStore) RevokeSession(_ context.Context, id string, now time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
session, exists := s.sessions[id]
if !exists {
return ErrNotFound
}
session.RevokedAt = &now
s.sessions[id] = session
return nil
}
func (s *MemoryStore) AppendAudit(_ context.Context, entry AuditEntry) error {
s.mu.Lock()
defer s.mu.Unlock()
s.audit = append(s.audit, entry)
return nil
}
func (s *MemoryStore) ListAudit(_ context.Context, limit int) ([]AuditEntry, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if limit <= 0 || limit > 200 {
limit = 100
}
start := len(s.audit) - limit
if start < 0 {
start = 0
}
entries := append([]AuditEntry(nil), s.audit[start:]...)
for left, right := 0, len(entries)-1; left < right; left, right = left+1, right-1 {
entries[left], entries[right] = entries[right], entries[left]
}
return entries, nil
}
func newID(prefix string) string {
buffer := make([]byte, 16)
if _, err := rand.Read(buffer); err != nil {
panic("cryptographic random source unavailable")
}
return prefix + "_" + hex.EncodeToString(buffer)
}