package auth import ( "context" "database/sql" "fmt" "log/slog" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // KeyStore 持久化 API key 的哈希(迁移 0016_api_keys;K1)。 // // 它**只接受哈希**:本接口没有任何接受明文的入口,因此「明文落库」在这条路径上 // 连写错都写不出来(明文只在 APIKey.Generate 的返回值里出现一次)。 // *SQLiteKeyStore 是生产实现,测试可换成内存桩。实现必须对并发使用安全。 type KeyStore interface { // LoadUnrevoked 返回全部未吊销 key 的 hash。没有任何记录时返回空切片 // (不是错误):那就是「还没签过 key」的诚实状态。 LoadUnrevoked(ctx context.Context) ([]string, error) // Insert 记录一把新 key 的 hash。hash 为空是非法调用,返回错误。 Insert(ctx context.Context, keyHash string) error // Revoke 把 hash 对应的行标记为已吊销(写 revoked_at)。未知或已吊销的 // hash 是幂等的空操作,不返回错误。 Revoke(ctx context.Context, keyHash string) error // RevokeAll 把全部未吊销的行标记为已吊销,返回受影响的行数。 RevokeAll(ctx context.Context) (int, error) } // keyStoreTimeLayout 是落盘时间戳的格式(与迁移 0016 的 strftime 默认值同形: // UTC RFC3339Nano)。 const keyStoreTimeLayout = time.RFC3339Nano // API key 持久化的 SQL(全参数化,项目规范 §十)。表由迁移 0016_api_keys 建立。 const ( selectUnrevokedAPIKeysSQL = `SELECT key_hash FROM api_keys WHERE revoked_at IS NULL` insertAPIKeySQL = `INSERT INTO api_keys (key_hash) VALUES (?)` revokeAPIKeySQL = `UPDATE api_keys SET revoked_at = ? WHERE key_hash = ? AND revoked_at IS NULL` revokeAllAPIKeysSQL = `UPDATE api_keys SET revoked_at = ? WHERE revoked_at IS NULL` ) // SQLiteKeyStore 把 API key 的哈希落盘在 SQLite 的 api_keys 表上。它只写 hash: // 明文既不经过本类型、也不进任何 SQL 参数。凭底层的 *sql.DB 对并发使用安全。 type SQLiteKeyStore struct { db *sql.DB log *slog.Logger } // NewSQLiteKeyStore 在 db 上构建 SQLite key 存储。db 为 nil 时返回 ErrNilDB。 // // 它刻意不自建 DDL:迁移 0016_api_keys 是这份 schema 的唯一权威路径(与 // capability/registry.SQLitePolicyStore 同一口径),因此表缺失时读写会返回明确的 // 错误(调用方据此让 Generate 响亮地失败),而不是悄悄降级成「只在内存里成功、 // 重启全部失效」的假成功 —— 那正是本修复要消灭的缺陷。log 可为 nil。 func NewSQLiteKeyStore(db *sql.DB, log *slog.Logger) (*SQLiteKeyStore, error) { if db == nil { return nil, fmt.Errorf("auth: api key store: %w", ErrNilDB) } if log == nil { log = slog.Default() } return &SQLiteKeyStore{db: db, log: logger.WithSource(log, "auth/apikey")}, nil } // LoadUnrevoked 返回 api_keys 里全部未吊销行的 key_hash。空表返回空切片。 // 日志只记条数(哈希与明文都不打)。 func (s *SQLiteKeyStore) LoadUnrevoked(ctx context.Context) ([]string, error) { if s == nil || s.db == nil { return nil, fmt.Errorf("auth: api key store: not configured") } rows, err := s.db.QueryContext(ctx, selectUnrevokedAPIKeysSQL) if err != nil { return nil, fmt.Errorf("auth: load api key hashes: %w", err) } defer func() { _ = rows.Close() }() out := make([]string, 0) for rows.Next() { var hash string if err := rows.Scan(&hash); err != nil { return nil, fmt.Errorf("auth: scan api key hash: %w", err) } out = append(out, hash) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("auth: iterate api key hashes: %w", err) } s.log.Debug("api keys loaded from store", "count", len(out)) return out, nil } // Insert 落盘一把新 key 的哈希。只有 hash 进 SQL,name/last_used_at 留 NULL // (当前签发入口没有命名参数,最后一用时间不做同步写库)。 func (s *SQLiteKeyStore) Insert(ctx context.Context, keyHash string) error { if s == nil || s.db == nil { return fmt.Errorf("auth: api key store: not configured") } if strings.TrimSpace(keyHash) == "" { return fmt.Errorf("auth: insert api key: empty key hash") } if _, err := s.db.ExecContext(ctx, insertAPIKeySQL, keyHash); err != nil { return fmt.Errorf("auth: insert api key: %w", err) } return nil } // Revoke 把 hash 对应的未吊销行标记为已吊销。找不到行(未知或已吊销)不报错: // 吊销是幂等操作,重复吊销同一把 key 应当成功。 func (s *SQLiteKeyStore) Revoke(ctx context.Context, keyHash string) error { if s == nil || s.db == nil { return fmt.Errorf("auth: api key store: not configured") } if _, err := s.db.ExecContext(ctx, revokeAPIKeySQL, formatKeyStoreTime(time.Now()), keyHash); err != nil { return fmt.Errorf("auth: revoke api key: %w", err) } return nil } // RevokeAll 把全部未吊销行标记为已吊销并返回受影响行数。删除(而非标记)会丢掉 // 「什么时候废掉的」这一审计信息,因此这里坚持标记。 func (s *SQLiteKeyStore) RevokeAll(ctx context.Context) (int, error) { if s == nil || s.db == nil { return 0, fmt.Errorf("auth: api key store: not configured") } res, err := s.db.ExecContext(ctx, revokeAllAPIKeysSQL, formatKeyStoreTime(time.Now())) if err != nil { return 0, fmt.Errorf("auth: revoke all api keys: %w", err) } n, err := res.RowsAffected() if err != nil { return 0, fmt.Errorf("auth: revoke all api keys: %w", err) } return int(n), nil } // formatKeyStoreTime 把 t 渲染成 UTC RFC3339Nano 以备落盘。 func formatKeyStoreTime(t time.Time) string { return t.UTC().Format(keyStoreTimeLayout) }