# API Key Management 優化計畫 | 項目 | 內容 | |------|------| | 版本 | V1.0 | | 日期 | 2026-03-21 | | 狀態 | 規劃中 | --- ## 版本歷史 | 版本 | 日期 | 目的 | 操作人 | 工具/模型 | |------|------|------|--------|-----------| | V1.0 | 2026-03-21 | 創建優化計畫 | OpenCode | - | --- ## 任務編碼規則 ``` AKO-{類別}-{序號} AKO = API Key Optimization 類別: - CODE = 程式碼品質 - PERF = 效能優化 - SEC = 安全性 - FEAT = 功能增強 - DOC = 文件 ``` --- ## Phase 1: 程式碼品質 (CODE) | 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | |------|------|------|--------|----------|------| | AKO-CODE-01 | 修復 from_str 警告 | 重命名為 `parse_scope` 或實作 `FromStr` trait | 🔴 高 | 0.5h | ⏳ 待辦 | | AKO-CODE-02 | 函數參數重構 | 使用 Config struct 減少參數數量 | 🔴 高 | 1h | ⏳ 待辦 | | AKO-CODE-03 | 抽象 CRUD Trait | 建立 `ExternalTokenStore` trait 統一 Gitea/n8n | 🟡 中 | 3h | ⏳ 待辦 | | AKO-CODE-04 | 錯誤處理統一 | 使用 `thiserror` 定義自訂錯誤類型 | 🟡 中 | 2h | ⏳ 待辦 | ### AKO-CODE-01 細節 ```rust // Before impl GiteaScope { pub fn from_str(s: &str) -> Option { ... } } // After: Option A - Rename impl GiteaScope { pub fn parse(s: &str) -> Option { ... } } // After: Option B - Implement FromStr impl std::str::FromStr for GiteaScope { type Err = (); fn from_str(s: &str) -> Result { ... } } ``` ### AKO-CODE-02 細節 ```rust // Before pub async fn create_api_key( &self, key_id: &str, key_hash: &str, key_prefix: &str, name: &str, key_type: &str, user_id: Option, service_name: Option<&str>, permissions: &serde_json::Value, expires_at: Option>, ) -> Result // After pub struct CreateApiKeyConfig<'a> { pub key_id: &'a str, pub key_hash: &'a str, pub key_prefix: &'a str, pub name: &'a str, pub key_type: &'a str, pub user_id: Option, pub service_name: Option<&'a str>, pub permissions: &'a serde_json::Value, pub expires_at: Option>, } pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result ``` ### AKO-CODE-03 細節 ```rust #[async_trait] pub trait ExternalTokenStore { async fn create(&self, record: T) -> Result; async fn get_by_label(&self, label: &str) -> Result>; async fn list(&self) -> Result>; async fn delete(&self, label: &str) -> Result<()>; async fn update_verification(&self, label: &str) -> Result<()>; } ``` --- ## Phase 2: 效能優化 (PERF) | 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | |------|------|------|--------|----------|------| | AKO-PERF-01 | 連線池配置外部化 | 使用環境變數控制 max_connections | 🟡 中 | 0.5h | ⏳ 待辦 | | AKO-PERF-02 | API Key 驗證快取 | 使用 Moka 快取減少資料庫查詢 | 🔴 高 | 2h | ⏳ 待辦 | | AKO-PERF-03 | 批次查詢優化 | 合併多次查詢為單一 SQL | 🟡 中 | 1h | ⏳ 待辦 | | AKO-PERF-04 | 非同步日誌寫入 | 使用 channel 非同步寫入審計日誌 | 🟢 低 | 2h | ⏳ 待辦 | ### AKO-PERF-01 細節 ```rust // Before let pool_options = PgPoolOptions::new() .max_connections(10) .acquire_timeout(std::time::Duration::from_secs(60)); // After let max_conn = std::env::var("DB_MAX_CONNECTIONS") .unwrap_or_else(|_| "10".to_string()) .parse() .unwrap_or(10); let pool_options = PgPoolOptions::new() .max_connections(max_conn) .acquire_timeout(std::time::Duration::from_secs(60)); ``` ### AKO-PERF-02 細節 ```rust use moka::future::Cache; use std::time::Duration; pub struct ApiKeyCache { cache: Cache, } pub struct CachedApiKey { pub record: ApiKeyRecord, pub cached_at: chrono::DateTime, } impl ApiKeyCache { pub fn new(ttl_seconds: u64, max_capacity: u64) -> Self { Self { cache: Cache::builder() .time_to_live(Duration::from_secs(ttl_seconds)) .max_capacity(max_capacity) .build(), } } pub async fn get(&self, key_hash: &str) -> Option { self.cache.get(key_hash).await.map(|c| c.record) } pub async fn insert(&self, key_hash: String, record: ApiKeyRecord) { self.cache.insert(key_hash, CachedApiKey { record, cached_at: chrono::Utc::now(), }).await; } pub async fn invalidate(&self, key_hash: &str) { self.cache.invalidate(key_hash).await; } } ``` --- ## Phase 3: 安全性 (SEC) | 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | |------|------|------|--------|----------|------| | AKO-SEC-01 | Constant-time 比較 | 使用 `subtle` crate 防止 timing attack | 🔴 高 | 0.5h | ⏳ 待辦 | | AKO-SEC-02 | Rate Limiter | 限制驗證失敗重試次數 | 🔴 高 | 2h | ⏳ 待辦 | | AKO-SEC-03 | IP 黑名單 | 支援封鎖特定 IP | 🟡 中 | 1.5h | ⏳ 待辦 | | AKO-SEC-04 | 審計日誌加密 | 敏感欄位加密儲存 | 🟡 中 | 2h | ⏳ 待辦 | | AKO-SEC-05 | Key 強度檢查 | 驗證建立的 Key 符合強度要求 | 🟢 低 | 1h | ⏳ 待辦 | ### AKO-SEC-01 細節 ```rust use subtle::ConstantTimeEq; // Before if stored_hash == computed_hash { // valid } // After if bool::from(stored_hash.as_bytes().ct_eq(computed_hash.as_bytes())) { // valid } ``` ### AKO-SEC-02 細節 ```rust use moka::future::Cache; pub struct RateLimiter { attempts: Cache, max_attempts: u32, window_seconds: u64, } pub struct AttemptInfo { pub count: u32, pub first_attempt: chrono::DateTime, pub locked_until: Option>, } impl RateLimiter { pub async fn check(&self, identifier: &str) -> Result<()> { if let Some(info) = self.attempts.get(identifier).await { if let Some(locked_until) = info.locked_until { if chrono::Utc::now() < locked_until { anyhow::bail!("Account locked until {}", locked_until); } } } Ok(()) } pub async fn record_failure(&self, identifier: &str) -> Result<()> { let mut info = self.attempts.get(identifier).await .unwrap_or(AttemptInfo { count: 0, first_attempt: chrono::Utc::now(), locked_until: None, }); info.count += 1; if info.count >= self.max_attempts { info.locked_until = Some( chrono::Utc::now() + chrono::Duration::seconds(self.window_seconds as i64) ); } self.attempts.insert(identifier.to_string(), info).await; Ok(()) } pub async fn record_success(&self, identifier: &str) { self.attempts.invalidate(identifier).await; } } ``` --- ## Phase 4: 功能增強 (FEAT) | 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | |------|------|------|--------|----------|------| | AKO-FEAT-01 | 批量建立 Key | 支援 JSON 檔案批量匯入 | 🟡 中 | 3h | ⏳ 待辦 | | AKO-FEAT-02 | 批量撤銷 Key | 支援條件式批量撤銷 | 🟡 中 | 2h | ⏳ 待辦 | | AKO-FEAT-03 | Key 匯出 | 匯出 Key 列表(不含明文) | 🟢 低 | 1.5h | ⏳ 待辦 | | AKO-FEAT-04 | Key 匯入 | 匯入 Key 元數據 | 🟢 低 | 1.5h | ⏳ 待辦 | | AKO-FEAT-05 | Webhook 通知 | 異常發生時發送 Webhook | 🟡 中 | 3h | ⏳ 待辦 | | AKO-FEAT-06 | Email 通知 | Key 到期前提醒 | 🟢 低 | 4h | ⏳ 待辦 | | AKO-FEAT-07 | 統計報表 | 生成使用統計報表 | 🟢 低 | 2h | ⏳ 待辦 | | AKO-FEAT-08 | 清理過期記錄 | 自動清理過期的 Key 記錄 | 🟢 低 | 1h | ⏳ 待辦 | ### AKO-FEAT-01 細節 ```json // keys.json { "keys": [ { "name": "ci-service-1", "key_type": "service", "permissions": ["read", "write"], "ttl_days": 90 }, { "name": "ci-service-2", "key_type": "service", "permissions": ["read"], "ttl_days": 180 } ] } ``` ```bash momentry api-key batch-create --file keys.json ``` ### AKO-FEAT-05 細節 ```rust pub struct WebhookConfig { pub url: String, pub secret: String, pub events: Vec, } pub enum WebhookEvent { KeyCreated, KeyRevoked, KeyExpired, AnomalyDetected, RotationRequired, } pub struct WebhookNotifier { client: Client, config: WebhookConfig, } impl WebhookNotifier { pub async fn notify(&self, event: WebhookEvent, payload: serde_json::Value) -> Result<()> { if !self.config.events.contains(&event) { return Ok(()); } let signature = self.sign(&payload); self.client.post(&self.config.url) .header("X-Webhook-Signature", signature) .json(&serde_json::json!({ "event": event, "timestamp": chrono::Utc::now(), "payload": payload, })) .send() .await?; Ok(()) } } ``` --- ## Phase 5: 文件 (DOC) | 編碼 | 任務 | 描述 | 優先級 | 預估工時 | 狀態 | |------|------|------|--------|----------|------| | AKO-DOC-01 | API 文件自動生成 | 使用 `utoipa` 生成 OpenAPI | 🟢 低 | 3h | ⏳ 待辦 | | AKO-DOC-02 | CHANGELOG.md | 建立變更日誌 | 🟢 低 | 1h | ⏳ 待辦 | | AKO-DOC-03 | 架構圖 | 添加系統架構圖 | 🟢 低 | 2h | ⏳ 待辦 | | AKO-DOC-04 | 整合測試文件 | 記錄整合測試流程 | 🟢 低 | 1h | ⏳ 待辦 | --- ## 總工時估算 | Phase | 工時 | 任務數 | |-------|------|--------| | CODE | 6.5h | 4 | | PERF | 5.5h | 4 | | SEC | 7h | 5 | | FEAT | 18h | 8 | | DOC | 7h | 4 | | **總計** | **44h** | **25** | --- ## 環境變數 ```bash # 效能 DB_MAX_CONNECTIONS=10 CACHE_TTL_SECONDS=300 CACHE_MAX_CAPACITY=10000 # 安全 RATE_LIMIT_MAX_ATTEMPTS=5 RATE_LIMIT_WINDOW_SECONDS=900 # 通知 WEBHOOK_URL=https://example.com/webhook WEBHOOK_SECRET=your-secret ``` --- ## 參考文件 - `docs/API_KEY_MANAGEMENT.md` - API Key 管理系統設計 - `docs/PENDING_ISSUES.md` - 待解決問題追蹤 - `src/core/api_key/` - API Key 模組