172 lines
4.3 KiB
Go
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)
|
|
}
|