229 lines
5.9 KiB
Go
229 lines
5.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// HybridKeyStore combines in-memory cache with persistent database backend
|
|
// Write-through strategy: writes go to DB first, then cache is updated
|
|
// This ensures consistency: if DB write fails, cache is not updated
|
|
type HybridKeyStore struct {
|
|
cache *APIKeyStore // In-memory cache for fast lookups
|
|
db *KeyStoreDB // Database backend for persistence
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewHybridKeyStore creates a new hybrid key store
|
|
func NewHybridKeyStore(cache *APIKeyStore, db *KeyStoreDB) *HybridKeyStore {
|
|
return &HybridKeyStore{
|
|
cache: cache,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// InitializeFromDatabase loads all keys from database into cache at startup
|
|
func (h *HybridKeyStore) InitializeFromDatabase(ctx context.Context) error {
|
|
keys, err := h.db.LoadAllKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load keys from database: %w", err)
|
|
}
|
|
|
|
for _, key := range keys {
|
|
h.cache.keys[key.Key] = key
|
|
rateLimit := key.RateLimit
|
|
if rateLimit <= 0 {
|
|
rateLimit = 100 // Default rate limit
|
|
}
|
|
h.cache.rateLimits[key.Key] = &RateLimiter{
|
|
maxRequests: rateLimit,
|
|
window: time.Minute,
|
|
resetTime: time.Now().Add(time.Minute),
|
|
count: 0,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateKey generates a new API key and persists it
|
|
// Returns error if database write fails
|
|
func (h *HybridKeyStore) CreateKey(ctx context.Context, clientID string, roles []string, rateLimit int, expiresIn *time.Duration, createdBy string) (*APIKey, error) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Generate random key in memory
|
|
apiKey, err := h.generateKey(clientID, roles, rateLimit, expiresIn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Write to database first (consistency)
|
|
if err := h.db.SaveKey(ctx, apiKey, createdBy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update cache after successful DB write
|
|
h.cache.keys[apiKey.Key] = apiKey
|
|
h.cache.rateLimits[apiKey.Key] = &RateLimiter{
|
|
maxRequests: rateLimit,
|
|
window: time.Minute,
|
|
resetTime: time.Now().Add(time.Minute),
|
|
count: 0,
|
|
}
|
|
|
|
return apiKey, nil
|
|
}
|
|
|
|
// ValidateKey checks if a key is valid
|
|
// Checks cache first for performance, falls back to database if cache miss
|
|
func (h *HybridKeyStore) ValidateKey(keyStr string) (*APIKey, error) {
|
|
// Check cache first (fast path)
|
|
h.cache.mu.RLock()
|
|
key, exists := h.cache.keys[keyStr]
|
|
h.cache.mu.RUnlock()
|
|
|
|
if exists {
|
|
if h.isKeyValid(key) {
|
|
return key, nil
|
|
}
|
|
return nil, fmt.Errorf("key is inactive or expired")
|
|
}
|
|
|
|
// Cache miss - this is normal in distributed deployments
|
|
// Could implement database fallback here if needed:
|
|
// key, err := h.db.GetKey(context.Background(), keyStr)
|
|
// But for now, rely on cache being populated at startup
|
|
|
|
return nil, fmt.Errorf("API key not found")
|
|
}
|
|
|
|
// ListKeys returns all active keys for a client
|
|
func (h *HybridKeyStore) ListKeys(ctx context.Context, clientID string) ([]*APIKey, error) {
|
|
return h.db.ListKeys(ctx, clientID)
|
|
}
|
|
|
|
// DeactivateKey deactivates a key in both cache and database
|
|
func (h *HybridKeyStore) DeactivateKey(ctx context.Context, keyStr string, deactivatedBy string) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Remove from cache first
|
|
h.cache.mu.Lock()
|
|
delete(h.cache.keys, keyStr)
|
|
delete(h.cache.rateLimits, keyStr)
|
|
h.cache.mu.Unlock()
|
|
|
|
// Update database
|
|
return h.db.DeactivateKey(ctx, keyStr, deactivatedBy)
|
|
}
|
|
|
|
// UpdateLastUsed updates the last used timestamp in database
|
|
// Cache is not updated to avoid contention
|
|
func (h *HybridKeyStore) UpdateLastUsed(ctx context.Context, keyStr string) error {
|
|
return h.db.UpdateLastUsed(ctx, keyStr)
|
|
}
|
|
|
|
// CheckRateLimit checks if a key has exceeded its rate limit
|
|
func (h *HybridKeyStore) CheckRateLimit(keyStr string) (bool, error) {
|
|
h.cache.mu.RLock()
|
|
defer h.cache.mu.RUnlock()
|
|
|
|
limiter, exists := h.cache.rateLimits[keyStr]
|
|
if !exists {
|
|
return false, fmt.Errorf("rate limiter not found")
|
|
}
|
|
|
|
// Check if we're under the rate limit
|
|
if limiter.maxRequests == 0 {
|
|
return true, nil // Unlimited
|
|
}
|
|
|
|
limiter.mu.Lock()
|
|
defer limiter.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
if now.After(limiter.resetTime) {
|
|
limiter.resetTime = now.Add(limiter.window)
|
|
limiter.count = 0
|
|
}
|
|
|
|
if limiter.count >= limiter.maxRequests {
|
|
return false, nil
|
|
}
|
|
|
|
limiter.count++
|
|
return true, nil
|
|
}
|
|
|
|
// GetAuditLog retrieves audit log for a key
|
|
func (h *HybridKeyStore) GetAuditLog(ctx context.Context, keyStr string, limit int) ([]map[string]interface{}, error) {
|
|
return h.db.GetAuditLog(ctx, keyStr, limit)
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (h *HybridKeyStore) Close() error {
|
|
return h.db.Close()
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// generateKey creates an APIKey with cryptographic random bytes
|
|
func (h *HybridKeyStore) generateKey(clientID string, roles []string, rateLimit int, expiresIn *time.Duration) (*APIKey, error) {
|
|
apiKey, err := h.cache.CreateKey(clientID, roles, rateLimit, expiresIn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return apiKey, nil
|
|
}
|
|
|
|
// isKeyValid checks if a key is currently valid
|
|
func (h *HybridKeyStore) isKeyValid(key *APIKey) bool {
|
|
if !key.IsActive {
|
|
return false
|
|
}
|
|
|
|
if key.ExpiresAt != nil && time.Now().After(*key.ExpiresAt) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// SyncCache performs a full cache refresh from database
|
|
// Useful for multi-instance deployments where keys may be created elsewhere
|
|
func (h *HybridKeyStore) SyncCache(ctx context.Context) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
keys, err := h.db.LoadAllKeys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to sync cache: %w", err)
|
|
}
|
|
|
|
// Clear cache
|
|
h.cache.mu.Lock()
|
|
h.cache.keys = make(map[string]*APIKey)
|
|
h.cache.rateLimits = make(map[string]*RateLimiter)
|
|
|
|
// Repopulate cache
|
|
for _, key := range keys {
|
|
h.cache.keys[key.Key] = key
|
|
rateLimit := key.RateLimit
|
|
if rateLimit <= 0 {
|
|
rateLimit = 100
|
|
}
|
|
h.cache.rateLimits[key.Key] = &RateLimiter{
|
|
maxRequests: rateLimit,
|
|
window: time.Minute,
|
|
resetTime: time.Now().Add(time.Minute),
|
|
count: 0,
|
|
}
|
|
}
|
|
h.cache.mu.Unlock()
|
|
|
|
return nil
|
|
}
|