fix(auth): repair persistence layer and stop storing plaintext keys
- Store SHA-256 digests (key_hash + key_preview) instead of raw keys, in both the in-memory store and Postgres; migrate legacy plaintext rows in place and drop the plaintext column. - Fix TEXT[] scans that failed at runtime (missing pq.Array) in GetKey/ListKeys/LoadAllKeys. - Load persisted keys at startup (InitializeFromDatabase was never called) and fall back to the database on cache miss, so issued keys survive restarts. - Make HybridKeyStore.CreateKey genuinely write-through: cache is only updated after a successful DB write. - Guard nil database backend (auth enabled without DB previously panicked on key creation) and degrade to in-memory operation. - Persist bootstrap admin keys when a database is configured. - Record real audit-log details as JSON and log audit failures instead of silently dropping them; add DB pool limits and ping timeout. - Sentinel errors matched with errors.Is; unit tests for hashing, write-through ordering, DB fallback, and nil-DB operation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+207
-193
@@ -3,34 +3,41 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/igodwin/notifier/internal/logging"
|
||||
"github.com/lib/pq"
|
||||
_ "github.com/lib/pq" // PostgreSQL driver
|
||||
)
|
||||
|
||||
// KeyStoreDB provides persistent storage for API keys using PostgreSQL
|
||||
// It acts as the backend for the in-memory cache
|
||||
// KeyStoreDB provides persistent storage for API keys using PostgreSQL.
|
||||
// It acts as the backend for the in-memory cache. Only SHA-256 digests of
|
||||
// keys are persisted — the raw secret never reaches the database.
|
||||
type KeyStoreDB struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewKeyStoreDB creates a new database-backed key store
|
||||
func NewKeyStoreDB(dbURL string) (*KeyStoreDB, error) {
|
||||
func NewKeyStoreDB(dbURL string, logger *logging.Logger) (*KeyStoreDB, error) {
|
||||
db, err := sql.Open("postgres", dbURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(30 * time.Minute)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
ks := &KeyStoreDB{db: db}
|
||||
ks := &KeyStoreDB{db: db, logger: logger}
|
||||
|
||||
// Initialize schema
|
||||
if err := ks.initializeSchema(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
||||
}
|
||||
@@ -38,14 +45,15 @@ func NewKeyStoreDB(dbURL string) (*KeyStoreDB, error) {
|
||||
return ks, nil
|
||||
}
|
||||
|
||||
// initializeSchema creates the necessary tables and indexes if they don't exist
|
||||
// initializeSchema creates the necessary tables and indexes if they don't
|
||||
// exist, and migrates legacy plaintext-key rows to hashed storage.
|
||||
func (ks *KeyStoreDB) initializeSchema() error {
|
||||
// Create tables
|
||||
tableSchema := `
|
||||
-- API Keys table
|
||||
-- API Keys table (key_hash is the SHA-256 digest of the raw key)
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(255) UNIQUE NOT NULL,
|
||||
key_hash VARCHAR(64) UNIQUE NOT NULL,
|
||||
key_preview VARCHAR(16) NOT NULL DEFAULT '',
|
||||
name VARCHAR(255) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
roles TEXT[] DEFAULT '{}',
|
||||
@@ -73,9 +81,11 @@ func (ks *KeyStoreDB) initializeSchema() error {
|
||||
return fmt.Errorf("failed to create tables: %w", err)
|
||||
}
|
||||
|
||||
// Create indexes separately (PostgreSQL syntax)
|
||||
if err := ks.migrateLegacyPlaintextKeys(); err != nil {
|
||||
return fmt.Errorf("failed to migrate legacy plaintext keys: %w", err)
|
||||
}
|
||||
|
||||
indexSchema := `
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_client_id ON api_keys(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_expires ON api_keys(expires_at);
|
||||
@@ -90,12 +100,87 @@ func (ks *KeyStoreDB) initializeSchema() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveKey persists an API key to the database
|
||||
// migrateLegacyPlaintextKeys upgrades tables created by earlier versions that
|
||||
// stored the raw key in a "key" column: it adds the hash columns, hashes each
|
||||
// plaintext key in place, then drops the plaintext column entirely.
|
||||
func (ks *KeyStoreDB) migrateLegacyPlaintextKeys() error {
|
||||
var hasLegacyColumn bool
|
||||
err := ks.db.QueryRow(`
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'api_keys' AND column_name = 'key'
|
||||
)`).Scan(&hasLegacyColumn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect schema: %w", err)
|
||||
}
|
||||
if !hasLegacyColumn {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := ks.db.Exec(`
|
||||
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS key_hash VARCHAR(64);
|
||||
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS key_preview VARCHAR(16) NOT NULL DEFAULT '';
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add hash columns: %w", err)
|
||||
}
|
||||
|
||||
rows, err := ks.db.Query(`SELECT id, key FROM api_keys WHERE key_hash IS NULL AND key IS NOT NULL`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read legacy keys: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type legacyRow struct {
|
||||
id int
|
||||
key string
|
||||
}
|
||||
var legacy []legacyRow
|
||||
for rows.Next() {
|
||||
var r legacyRow
|
||||
if err := rows.Scan(&r.id, &r.key); err != nil {
|
||||
return fmt.Errorf("failed to scan legacy key: %w", err)
|
||||
}
|
||||
legacy = append(legacy, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range legacy {
|
||||
if _, err := ks.db.Exec(
|
||||
`UPDATE api_keys SET key_hash = $1, key_preview = $2 WHERE id = $3`,
|
||||
HashKey(r.key), keyPreview(r.key), r.id,
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to hash legacy key id=%d: %w", r.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Drop the plaintext column and enforce uniqueness on the digest.
|
||||
if _, err := ks.db.Exec(`
|
||||
ALTER TABLE api_keys DROP COLUMN key;
|
||||
ALTER TABLE api_keys ALTER COLUMN key_hash SET NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to finalize hash migration: %w", err)
|
||||
}
|
||||
|
||||
if ks.logger != nil {
|
||||
ks.logger.Infof("Migrated %d legacy plaintext API key(s) to hashed storage", len(legacy))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveKey persists an API key to the database. Only the digest and preview
|
||||
// are stored; apiKey.Key (the raw secret) is never written.
|
||||
func (ks *KeyStoreDB) SaveKey(ctx context.Context, key *APIKey, createdBy string) error {
|
||||
if key.KeyHash == "" {
|
||||
return fmt.Errorf("refusing to save key without digest")
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO api_keys (key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
INSERT INTO api_keys (key_hash, key_preview, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT (key_hash) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
roles = EXCLUDED.roles,
|
||||
is_active = EXCLUDED.is_active,
|
||||
@@ -104,10 +189,11 @@ func (ks *KeyStoreDB) SaveKey(ctx context.Context, key *APIKey, createdBy string
|
||||
`
|
||||
|
||||
_, err := ks.db.ExecContext(ctx, query,
|
||||
key.Key,
|
||||
key.KeyHash,
|
||||
key.KeyPreview,
|
||||
key.Name,
|
||||
key.ClientID,
|
||||
pq.Array(key.Roles), // Convert Go slice to PostgreSQL array
|
||||
pq.Array(key.Roles),
|
||||
key.CreatedAt,
|
||||
key.LastUsedAt,
|
||||
key.ExpiresAt,
|
||||
@@ -120,8 +206,7 @@ func (ks *KeyStoreDB) SaveKey(ctx context.Context, key *APIKey, createdBy string
|
||||
return fmt.Errorf("failed to save key: %w", err)
|
||||
}
|
||||
|
||||
// Log to audit trail
|
||||
ks.logAudit(ctx, key.Key, "created", createdBy, map[string]interface{}{
|
||||
ks.logAudit(ctx, key.KeyHash, "created", createdBy, map[string]interface{}{
|
||||
"client_id": key.ClientID,
|
||||
"roles": key.Roles,
|
||||
})
|
||||
@@ -129,50 +214,54 @@ func (ks *KeyStoreDB) SaveKey(ctx context.Context, key *APIKey, createdBy string
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKey retrieves an API key from the database
|
||||
func (ks *KeyStoreDB) GetKey(ctx context.Context, keyStr string) (*APIKey, error) {
|
||||
query := `
|
||||
SELECT key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit
|
||||
FROM api_keys
|
||||
WHERE key = $1
|
||||
`
|
||||
|
||||
// scanKey scans a single api_keys row into an APIKey.
|
||||
func scanKey(scanner interface{ Scan(...interface{}) error }) (*APIKey, error) {
|
||||
var key APIKey
|
||||
var roles []string
|
||||
|
||||
err := ks.db.QueryRowContext(ctx, query, keyStr).Scan(
|
||||
&key.Key,
|
||||
err := scanner.Scan(
|
||||
&key.KeyHash,
|
||||
&key.KeyPreview,
|
||||
&key.Name,
|
||||
&key.ClientID,
|
||||
&roles,
|
||||
pq.Array(&roles),
|
||||
&key.CreatedAt,
|
||||
&key.LastUsedAt,
|
||||
&key.ExpiresAt,
|
||||
&key.IsActive,
|
||||
&key.RateLimit,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key.Roles = roles
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// ListKeys retrieves all API keys for a client
|
||||
func (ks *KeyStoreDB) ListKeys(ctx context.Context, clientID string) ([]*APIKey, error) {
|
||||
query := `
|
||||
SELECT key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit
|
||||
FROM api_keys
|
||||
WHERE client_id = $1 AND is_active = true
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
const keyColumns = `key_hash, key_preview, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit`
|
||||
|
||||
rows, err := ks.db.QueryContext(ctx, query, clientID)
|
||||
// GetKeyByHash retrieves an API key from the database by its digest
|
||||
func (ks *KeyStoreDB) GetKeyByHash(ctx context.Context, keyHash string) (*APIKey, error) {
|
||||
row := ks.db.QueryRowContext(ctx,
|
||||
`SELECT `+keyColumns+` FROM api_keys WHERE key_hash = $1`, keyHash)
|
||||
|
||||
key, err := scanKey(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ListKeys retrieves all active API keys for a client
|
||||
func (ks *KeyStoreDB) ListKeys(ctx context.Context, clientID string) ([]*APIKey, error) {
|
||||
rows, err := ks.db.QueryContext(ctx,
|
||||
`SELECT `+keyColumns+` FROM api_keys
|
||||
WHERE client_id = $1 AND is_active = true
|
||||
ORDER BY created_at DESC`, clientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list keys: %w", err)
|
||||
}
|
||||
@@ -180,36 +269,20 @@ func (ks *KeyStoreDB) ListKeys(ctx context.Context, clientID string) ([]*APIKey,
|
||||
|
||||
var keys []*APIKey
|
||||
for rows.Next() {
|
||||
var key APIKey
|
||||
var roles []string
|
||||
|
||||
err := rows.Scan(
|
||||
&key.Key,
|
||||
&key.Name,
|
||||
&key.ClientID,
|
||||
&roles,
|
||||
&key.CreatedAt,
|
||||
&key.LastUsedAt,
|
||||
&key.ExpiresAt,
|
||||
&key.IsActive,
|
||||
&key.RateLimit,
|
||||
)
|
||||
key, err := scanKey(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan key: %w", err)
|
||||
}
|
||||
|
||||
key.Roles = roles
|
||||
keys = append(keys, &key)
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
// DeactivateKey disables an API key
|
||||
func (ks *KeyStoreDB) DeactivateKey(ctx context.Context, keyStr string, deactivatedBy string) error {
|
||||
query := `UPDATE api_keys SET is_active = false WHERE key = $1`
|
||||
|
||||
result, err := ks.db.ExecContext(ctx, query, keyStr)
|
||||
// DeactivateKeyByHash disables an API key identified by its digest
|
||||
func (ks *KeyStoreDB) DeactivateKeyByHash(ctx context.Context, keyHash string, deactivatedBy string) error {
|
||||
result, err := ks.db.ExecContext(ctx,
|
||||
`UPDATE api_keys SET is_active = false WHERE key_hash = $1`, keyHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deactivate key: %w", err)
|
||||
}
|
||||
@@ -223,31 +296,25 @@ func (ks *KeyStoreDB) DeactivateKey(ctx context.Context, keyStr string, deactiva
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
|
||||
ks.logAudit(ctx, keyStr, "deactivated", deactivatedBy, nil)
|
||||
ks.logAudit(ctx, keyHash, "deactivated", deactivatedBy, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLastUsed updates the last_used_at timestamp
|
||||
func (ks *KeyStoreDB) UpdateLastUsed(ctx context.Context, keyStr string) error {
|
||||
query := `UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP WHERE key = $1`
|
||||
|
||||
_, err := ks.db.ExecContext(ctx, query, keyStr)
|
||||
// UpdateLastUsed updates the last_used_at timestamp for a key digest
|
||||
func (ks *KeyStoreDB) UpdateLastUsed(ctx context.Context, keyHash string) error {
|
||||
_, err := ks.db.ExecContext(ctx,
|
||||
`UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP WHERE key_hash = $1`, keyHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update last used: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAllKeys loads all active keys into memory for caching
|
||||
func (ks *KeyStoreDB) LoadAllKeys(ctx context.Context) ([]*APIKey, error) {
|
||||
query := `
|
||||
SELECT key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit
|
||||
FROM api_keys
|
||||
WHERE is_active = true AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||
`
|
||||
|
||||
rows, err := ks.db.QueryContext(ctx, query)
|
||||
rows, err := ks.db.QueryContext(ctx,
|
||||
`SELECT `+keyColumns+` FROM api_keys
|
||||
WHERE is_active = true AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load keys: %w", err)
|
||||
}
|
||||
@@ -255,53 +322,52 @@ func (ks *KeyStoreDB) LoadAllKeys(ctx context.Context) ([]*APIKey, error) {
|
||||
|
||||
var keys []*APIKey
|
||||
for rows.Next() {
|
||||
var key APIKey
|
||||
var roles []string
|
||||
|
||||
err := rows.Scan(
|
||||
&key.Key,
|
||||
&key.Name,
|
||||
&key.ClientID,
|
||||
&roles,
|
||||
&key.CreatedAt,
|
||||
&key.LastUsedAt,
|
||||
&key.ExpiresAt,
|
||||
&key.IsActive,
|
||||
&key.RateLimit,
|
||||
)
|
||||
key, err := scanKey(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan key: %w", err)
|
||||
}
|
||||
|
||||
key.Roles = roles
|
||||
keys = append(keys, &key)
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
// logAudit logs a key operation to the audit trail
|
||||
func (ks *KeyStoreDB) logAudit(ctx context.Context, keyStr string, action string, performedBy string, details map[string]interface{}) {
|
||||
// Get key ID
|
||||
// logAudit logs a key operation to the audit trail. Failures don't abort the
|
||||
// calling operation but are logged rather than silently dropped.
|
||||
func (ks *KeyStoreDB) logAudit(ctx context.Context, keyHash string, action string, performedBy string, details map[string]interface{}) {
|
||||
var keyID int
|
||||
err := ks.db.QueryRowContext(ctx, "SELECT id FROM api_keys WHERE key = $1", keyStr).Scan(&keyID)
|
||||
if err != nil {
|
||||
return // Silently fail audit logging
|
||||
if err := ks.db.QueryRowContext(ctx,
|
||||
"SELECT id FROM api_keys WHERE key_hash = $1", keyHash).Scan(&keyID); err != nil {
|
||||
ks.warnf("audit log: failed to resolve key for action=%s: %v", action, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the action
|
||||
detailsJSON := "{}"
|
||||
detailsJSON := []byte("{}")
|
||||
if len(details) > 0 {
|
||||
// Simple JSON encoding (could use jsonb package for robustness)
|
||||
detailsJSON = fmt.Sprintf(`{"event": "%s"}`, action)
|
||||
encoded, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
ks.warnf("audit log: failed to encode details for action=%s: %v", action, err)
|
||||
} else {
|
||||
detailsJSON = encoded
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO api_key_audit_log (key_id, action, performed_by, details)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`
|
||||
if _, err := ks.db.ExecContext(ctx,
|
||||
`INSERT INTO api_key_audit_log (key_id, action, performed_by, details) VALUES ($1, $2, $3, $4)`,
|
||||
keyID, action, performedBy, detailsJSON); err != nil {
|
||||
ks.warnf("audit log: failed to record action=%s: %v", action, err)
|
||||
}
|
||||
}
|
||||
|
||||
ks.db.ExecContext(ctx, query, keyID, action, performedBy, detailsJSON)
|
||||
func (ks *KeyStoreDB) warnf(format string, args ...interface{}) {
|
||||
if ks.logger != nil {
|
||||
ks.logger.Warnf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Ping verifies database connectivity (used by readiness checks).
|
||||
func (ks *KeyStoreDB) Ping(ctx context.Context) error {
|
||||
return ks.db.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
@@ -309,101 +375,55 @@ func (ks *KeyStoreDB) Close() error {
|
||||
return ks.db.Close()
|
||||
}
|
||||
|
||||
// GetAuditLog retrieves audit log entries for a key
|
||||
func (ks *KeyStoreDB) GetAuditLog(ctx context.Context, keyStr string, limit int) ([]map[string]interface{}, error) {
|
||||
query := `
|
||||
SELECT al.action, al.performed_by, al.performed_at, al.details
|
||||
FROM api_key_audit_log al
|
||||
JOIN api_keys ak ON al.key_id = ak.id
|
||||
WHERE ak.key = $1
|
||||
ORDER BY al.performed_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := ks.db.QueryContext(ctx, query, keyStr, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit log: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var action, performedBy, details string
|
||||
var performedAt time.Time
|
||||
|
||||
err := rows.Scan(&action, &performedBy, &performedAt, &details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs = append(logs, map[string]interface{}{
|
||||
"action": action,
|
||||
"performed_by": performedBy,
|
||||
"performed_at": performedAt,
|
||||
"details": details,
|
||||
})
|
||||
}
|
||||
|
||||
return logs, rows.Err()
|
||||
// GetAuditLog retrieves audit log entries for a key digest
|
||||
func (ks *KeyStoreDB) GetAuditLog(ctx context.Context, keyHash string, limit int) ([]map[string]interface{}, error) {
|
||||
return ks.auditLogQuery(ctx,
|
||||
`SELECT al.action, al.performed_by, al.performed_at, al.details
|
||||
FROM api_key_audit_log al
|
||||
JOIN api_keys ak ON al.key_id = ak.id
|
||||
WHERE ak.key_hash = $1
|
||||
ORDER BY al.performed_at DESC
|
||||
LIMIT $2`, keyHash, limit)
|
||||
}
|
||||
|
||||
// GetKeyByName retrieves an API key by its name
|
||||
func (ks *KeyStoreDB) GetKeyByName(ctx context.Context, name string) (*APIKey, error) {
|
||||
query := `
|
||||
SELECT key, name, client_id, roles, created_at, last_used_at, expires_at, is_active, rate_limit
|
||||
FROM api_keys
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
var key APIKey
|
||||
var roles []string
|
||||
|
||||
err := ks.db.QueryRowContext(ctx, query, name).Scan(
|
||||
&key.Key,
|
||||
&key.Name,
|
||||
&key.ClientID,
|
||||
pq.Array(&roles),
|
||||
&key.CreatedAt,
|
||||
&key.LastUsedAt,
|
||||
&key.ExpiresAt,
|
||||
&key.IsActive,
|
||||
&key.RateLimit,
|
||||
)
|
||||
row := ks.db.QueryRowContext(ctx,
|
||||
`SELECT `+keyColumns+` FROM api_keys WHERE name = $1`, name)
|
||||
|
||||
key, err := scanKey(row)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key by name: %w", err)
|
||||
}
|
||||
|
||||
key.Roles = roles
|
||||
return &key, nil
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeactivateKeyByName disables an API key by its name
|
||||
func (ks *KeyStoreDB) DeactivateKeyByName(ctx context.Context, name string, deactivatedBy string) error {
|
||||
// First get the key to find its raw key for cache invalidation and audit
|
||||
key, err := ks.GetKeyByName(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ks.DeactivateKey(ctx, key.Key, deactivatedBy)
|
||||
return ks.DeactivateKeyByHash(ctx, key.KeyHash, deactivatedBy)
|
||||
}
|
||||
|
||||
// GetAuditLogByName retrieves audit log entries for a key identified by name
|
||||
func (ks *KeyStoreDB) GetAuditLogByName(ctx context.Context, name string, limit int) ([]map[string]interface{}, error) {
|
||||
query := `
|
||||
SELECT al.action, al.performed_by, al.performed_at, al.details
|
||||
FROM api_key_audit_log al
|
||||
JOIN api_keys ak ON al.key_id = ak.id
|
||||
WHERE ak.name = $1
|
||||
ORDER BY al.performed_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
return ks.auditLogQuery(ctx,
|
||||
`SELECT al.action, al.performed_by, al.performed_at, al.details
|
||||
FROM api_key_audit_log al
|
||||
JOIN api_keys ak ON al.key_id = ak.id
|
||||
WHERE ak.name = $1
|
||||
ORDER BY al.performed_at DESC
|
||||
LIMIT $2`, name, limit)
|
||||
}
|
||||
|
||||
rows, err := ks.db.QueryContext(ctx, query, name, limit)
|
||||
func (ks *KeyStoreDB) auditLogQuery(ctx context.Context, query string, ident string, limit int) ([]map[string]interface{}, error) {
|
||||
rows, err := ks.db.QueryContext(ctx, query, ident, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit log: %w", err)
|
||||
}
|
||||
@@ -414,8 +434,7 @@ func (ks *KeyStoreDB) GetAuditLogByName(ctx context.Context, name string, limit
|
||||
var action, performedBy, details string
|
||||
var performedAt time.Time
|
||||
|
||||
err := rows.Scan(&action, &performedBy, &performedAt, &details)
|
||||
if err != nil {
|
||||
if err := rows.Scan(&action, &performedBy, &performedAt, &details); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -429,8 +448,3 @@ func (ks *KeyStoreDB) GetAuditLogByName(ctx context.Context, name string, limit
|
||||
|
||||
return logs, rows.Err()
|
||||
}
|
||||
|
||||
// Custom errors
|
||||
var (
|
||||
ErrKeyNotFound = fmt.Errorf("API key not found")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user