## v0.9.20260325_144654 ### Features - API Key Authentication System - Job Worker System - V2 Backup Versioning ### Bug Fixes - get_processor_results_by_job column mapping Co-authored-by: OpenCode
10 KiB
10 KiB
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 細節
// Before
impl GiteaScope {
pub fn from_str(s: &str) -> Option<Self> { ... }
}
// After: Option A - Rename
impl GiteaScope {
pub fn parse(s: &str) -> Option<Self> { ... }
}
// After: Option B - Implement FromStr
impl std::str::FromStr for GiteaScope {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { ... }
}
AKO-CODE-02 細節
// 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<i64>,
service_name: Option<&str>,
permissions: &serde_json::Value,
expires_at: Option<DateTime<Utc>>,
) -> Result<i64>
// 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<i64>,
pub service_name: Option<&'a str>,
pub permissions: &'a serde_json::Value,
pub expires_at: Option<DateTime<Utc>>,
}
pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result<i64>
AKO-CODE-03 細節
#[async_trait]
pub trait ExternalTokenStore<T> {
async fn create(&self, record: T) -> Result<i64>;
async fn get_by_label(&self, label: &str) -> Result<Option<T>>;
async fn list(&self) -> Result<Vec<T>>;
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 細節
// 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 細節
use moka::future::Cache;
use std::time::Duration;
pub struct ApiKeyCache {
cache: Cache<String, CachedApiKey>,
}
pub struct CachedApiKey {
pub record: ApiKeyRecord,
pub cached_at: chrono::DateTime<chrono::Utc>,
}
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<ApiKeyRecord> {
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 細節
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 細節
use moka::future::Cache;
pub struct RateLimiter {
attempts: Cache<String, AttemptInfo>,
max_attempts: u32,
window_seconds: u64,
}
pub struct AttemptInfo {
pub count: u32,
pub first_attempt: chrono::DateTime<chrono::Utc>,
pub locked_until: Option<chrono::DateTime<chrono::Utc>>,
}
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 細節
// 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
}
]
}
momentry api-key batch-create --file keys.json
AKO-FEAT-05 細節
pub struct WebhookConfig {
pub url: String,
pub secret: String,
pub events: Vec<WebhookEvent>,
}
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 |
環境變數
# 效能
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 模組