298 lines
6.7 KiB
Go
298 lines
6.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// HybridAuthStore combines file-based and database authentication
|
|
// Falls back to file config if database is not available
|
|
type HybridAuthStore struct {
|
|
fileStore *Config
|
|
dbStore *DatabaseAuthStore
|
|
useDB bool
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewHybridAuthStore creates a hybrid auth store
|
|
func NewHybridAuthStore(config *Config, dbPath string) (*HybridAuthStore, error) {
|
|
hybrid := &HybridAuthStore{
|
|
fileStore: config,
|
|
useDB: false,
|
|
}
|
|
|
|
// Try to initialize database store
|
|
if dbPath != "" {
|
|
dbStore, err := NewDatabaseAuthStore(dbPath)
|
|
if err != nil {
|
|
log.Printf("Failed to initialize database auth store, falling back to file: %v", err)
|
|
} else {
|
|
hybrid.dbStore = dbStore
|
|
hybrid.useDB = true
|
|
log.Printf("Using database authentication store")
|
|
}
|
|
}
|
|
|
|
// If database is available, migrate file-based keys to database
|
|
if hybrid.useDB && config.Enabled && len(config.APIKeys) > 0 {
|
|
if err := hybrid.migrateFileToDatabase(context.Background()); err != nil {
|
|
log.Printf("Failed to migrate file keys to database: %v", err)
|
|
}
|
|
}
|
|
return hybrid, nil
|
|
}
|
|
|
|
// ValidateAPIKey validates an API key using either database or file store
|
|
func (h *HybridAuthStore) ValidateAPIKey(ctx context.Context, key string) (*User, error) {
|
|
h.mu.RLock()
|
|
useDB := h.useDB
|
|
h.mu.RUnlock()
|
|
|
|
if useDB {
|
|
user, err := h.dbStore.ValidateAPIKey(ctx, key)
|
|
if err == nil {
|
|
return user, nil
|
|
}
|
|
// Fallback to file store if database fails
|
|
}
|
|
|
|
// Always try file store as fallback
|
|
if h.fileStore != nil {
|
|
user, err := h.fileStore.ValidateAPIKey(key)
|
|
if err == nil {
|
|
return user, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid API key")
|
|
}
|
|
|
|
// CreateAPIKey creates an API key using the preferred store
|
|
func (h *HybridAuthStore) CreateAPIKey(
|
|
ctx context.Context,
|
|
userID string,
|
|
keyHash string,
|
|
admin bool,
|
|
roles []string,
|
|
permissions map[string]bool,
|
|
expiresAt *time.Time,
|
|
) error {
|
|
h.mu.RLock()
|
|
useDB := h.useDB
|
|
h.mu.RUnlock()
|
|
|
|
if useDB {
|
|
err := h.dbStore.CreateAPIKey(ctx, userID, keyHash, admin, roles, permissions, expiresAt)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
// If database fails, fall back to file store
|
|
log.Printf("Database key creation failed, using file store: %v", err)
|
|
return h.createFileAPIKey(userID, keyHash, admin, roles, permissions)
|
|
}
|
|
|
|
// Use file store
|
|
return h.createFileAPIKey(userID, keyHash, admin, roles, permissions)
|
|
}
|
|
|
|
// createFileAPIKey creates an API key in the file store
|
|
func (h *HybridAuthStore) createFileAPIKey(
|
|
userID string,
|
|
keyHash string,
|
|
admin bool,
|
|
roles []string,
|
|
permissions map[string]bool,
|
|
) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.fileStore.APIKeys == nil {
|
|
h.fileStore.APIKeys = make(map[Username]APIKeyEntry)
|
|
}
|
|
|
|
h.fileStore.APIKeys[Username(userID)] = APIKeyEntry{
|
|
Hash: APIKeyHash(keyHash),
|
|
Admin: admin,
|
|
Roles: roles,
|
|
Permissions: permissions,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RevokeAPIKey revokes an API key
|
|
func (h *HybridAuthStore) RevokeAPIKey(ctx context.Context, userID string) error {
|
|
h.mu.RLock()
|
|
useDB := h.useDB
|
|
h.mu.RUnlock()
|
|
|
|
if useDB {
|
|
err := h.dbStore.RevokeAPIKey(ctx, userID)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
log.Printf("Database key revocation failed: %v", err)
|
|
}
|
|
|
|
// Remove from file store
|
|
h.mu.Lock()
|
|
delete(h.fileStore.APIKeys, Username(userID))
|
|
h.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListUsers returns all users from the active store
|
|
func (h *HybridAuthStore) ListUsers(ctx context.Context) ([]UserInfo, error) {
|
|
h.mu.RLock()
|
|
useDB := h.useDB
|
|
h.mu.RUnlock()
|
|
|
|
if useDB {
|
|
records, err := h.dbStore.ListUsers(ctx)
|
|
if err == nil {
|
|
users := make([]UserInfo, len(records))
|
|
for i, record := range records {
|
|
users[i] = UserInfo{
|
|
UserID: record.UserID,
|
|
Admin: record.Admin,
|
|
KeyHash: record.KeyHash,
|
|
Created: record.CreatedAt,
|
|
Expires: record.ExpiresAt,
|
|
Revoked: record.RevokedAt,
|
|
}
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
log.Printf("Database user listing failed: %v", err)
|
|
}
|
|
|
|
// Use file store
|
|
return h.listFileUsers()
|
|
}
|
|
|
|
// listFileUsers returns users from file store
|
|
func (h *HybridAuthStore) listFileUsers() ([]UserInfo, error) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
var users []UserInfo
|
|
for username, entry := range h.fileStore.APIKeys {
|
|
users = append(users, UserInfo{
|
|
UserID: string(username),
|
|
Admin: entry.Admin,
|
|
KeyHash: string(entry.Hash),
|
|
Created: time.Now(), // File store doesn't track creation time
|
|
})
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// migrateFileToDatabase migrates file-based keys to database
|
|
func (h *HybridAuthStore) migrateFileToDatabase(ctx context.Context) error {
|
|
if h.fileStore == nil || len(h.fileStore.APIKeys) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Use context to check for cancellation during migration
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
// Continue with migration
|
|
}
|
|
|
|
log.Printf("Migrating %d API keys from file to database...", len(h.fileStore.APIKeys))
|
|
|
|
for username, entry := range h.fileStore.APIKeys {
|
|
userID := string(username)
|
|
err := h.dbStore.CreateAPIKey(
|
|
ctx,
|
|
userID,
|
|
string(entry.Hash),
|
|
entry.Admin,
|
|
entry.Roles,
|
|
entry.Permissions,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Failed to migrate key for user %s: %v", userID, err)
|
|
continue
|
|
}
|
|
log.Printf("Migrated key for user: %s", userID)
|
|
}
|
|
|
|
log.Printf("Migration completed. Consider removing keys from config file.")
|
|
return nil
|
|
}
|
|
|
|
// SwitchToDatabase attempts to switch to database authentication
|
|
func (h *HybridAuthStore) SwitchToDatabase(dbPath string) error {
|
|
dbStore, err := NewDatabaseAuthStore(dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create database store: %w", err)
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Close existing database if any
|
|
if h.dbStore != nil {
|
|
_ = h.dbStore.Close()
|
|
}
|
|
|
|
h.dbStore = dbStore
|
|
h.useDB = true
|
|
|
|
// Migrate existing keys
|
|
if h.fileStore.Enabled && len(h.fileStore.APIKeys) > 0 {
|
|
if err := h.migrateFileToDatabase(context.Background()); err != nil {
|
|
log.Printf("Migration warning: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the database connection
|
|
func (h *HybridAuthStore) Close() error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.dbStore != nil {
|
|
return h.dbStore.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDatabaseStats returns database statistics
|
|
func (h *HybridAuthStore) GetDatabaseStats(ctx context.Context) (map[string]interface{}, error) {
|
|
h.mu.RLock()
|
|
useDB := h.useDB
|
|
h.mu.RUnlock()
|
|
|
|
if !useDB {
|
|
return map[string]interface{}{
|
|
"store_type": "file",
|
|
"users": len(h.fileStore.APIKeys),
|
|
}, nil
|
|
}
|
|
|
|
users, err := h.dbStore.ListUsers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"store_type": "database",
|
|
"users": len(users),
|
|
"path": "db/fetch_ml.db",
|
|
}, nil
|
|
}
|