package auth import ( "crypto/rand" "encoding/base64" "fmt" "log/slog" "sync" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // APIKey manages a bounded, in-memory constant table of API keys. 按裁决 // 15.4.3(主人原话:「key 始终给超级管理员权限」),表里**不再绑定角色**: // 任何已登记且未吊销的 key 鉴权后一律 owner 级 —— key 用于管理员调试,甚至 // 生产环境救火,因此不做权限分级。Keys are generated with crypto/rand and // stored by their string form. This suits the current single-binary milestone; // production should persist keys (e.g. hashed) and support revocation. type APIKey struct { mu sync.RWMutex cfg config.AuthConfig keys map[string]struct{} log *slog.Logger } // NewAPIKey returns an empty key manager. cfg is accepted for constructor // parity and future persistence settings; keys are runtime-generated. func NewAPIKey(cfg config.AuthConfig) *APIKey { return &APIKey{ cfg: cfg, keys: make(map[string]struct{}), log: logger.WithSource(slog.Default(), source), } } // Generate 铸造一个新 key 并登记进表,返回**仅此一次**可见的明文(绝不记录到 // 日志)。role 是可选形参:传入时只校验它是不是已知角色名(未知即报 // ErrInvalidRole),**不参与存储、不影响授权** —— 按裁决 15.4.3,key 表已无 // 角色维度,任何有效 key 都是超管级(见 Lookup)。 func (k *APIKey) Generate(role ...Role) (string, error) { if len(role) > 0 && !role[0].Valid() { return "", fmt.Errorf("%w: %q", ErrInvalidRole, role[0]) } raw := make([]byte, keyLength) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("auth: generate api key: %w", err) } key := base64.RawURLEncoding.EncodeToString(raw) k.mu.Lock() k.keys[key] = struct{}{} k.mu.Unlock() return key, nil } // Verify reports whether key is a known, unrevoked API key. func (k *APIKey) Verify(key string) bool { _, ok := k.Lookup(key) return ok } // Lookup 报告 key 是否已登记,并返回其授权角色。按裁决 15.4.3,**任何**已登记 // 的 key 都是超管(owner)级。key 不在表中时 ok 为 false,调用方必须据此拒绝。 func (k *APIKey) Lookup(key string) (Role, bool) { k.mu.RLock() defer k.mu.RUnlock() _, ok := k.keys[key] return RoleOwner, ok } // Revoke removes key from the constant table. func (k *APIKey) Revoke(key string) { k.mu.Lock() defer k.mu.Unlock() delete(k.keys, key) } // RevokeAll removes every currently registered API key and returns how many were // removed. It is the administrative "revoke all" operation: after a call the // table is empty, so no previously issued key can authenticate. func (k *APIKey) RevokeAll() int { k.mu.Lock() defer k.mu.Unlock() n := len(k.keys) k.keys = make(map[string]struct{}) return n } // RevokeByMask removes every key whose masked identifier (the same derived id // List returns) matches mask. It returns true when at least one key was revoked. // The mask is lossy, so it is matched by recomputing it for each key; the table // is small so a linear scan is fine. An empty or nil-returning call is a no-op. func (k *APIKey) RevokeByMask(mask string) bool { if mask == "" { return false } k.mu.Lock() defer k.mu.Unlock() revoked := false for key := range k.keys { if maskKey(key) == mask { delete(k.keys, key) revoked = true } } return revoked } // List returns a summary of every currently registered API key for the admin // panel. Key material is never returned; each identifier is masked so the panel // can tell keys apart without exposing the secret. The in-memory store keeps no // creation time, so CreatedAt is nil for every entry. 每条的角色恒为 owner // (裁决 15.4.3:key 不做角色分级)。 func (k *APIKey) List() []UserSummary { k.mu.RLock() defer k.mu.RUnlock() out := make([]UserSummary, 0, len(k.keys)) for key := range k.keys { out = append(out, UserSummary{ID: maskKey(key), Username: "apikey", Role: RoleOwner}) } return out } // MaskKey renders a key's leading and trailing groups so the panel can identify // an entry without revealing the secret. Short keys are fully hidden. It is the // exported form of maskKey so callers (e.g. the admin issue handler) can return // the same masked id the List page shows. func MaskKey(key string) string { return maskKey(key) } // maskKey renders a key's leading and trailing groups so the panel can identify // an entry without revealing the secret. Short keys are fully hidden. func maskKey(key string) string { if len(key) <= 8 { return "••••" } return key[:4] + "••••" + key[len(key)-4:] }