feat: Initial v0.9 release with API Key authentication
## 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
This commit is contained in:
399
docs/API_KEY_OPTIMIZATION.md
Normal file
399
docs/API_KEY_OPTIMIZATION.md
Normal file
@@ -0,0 +1,399 @@
|
||||
# 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<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 細節
|
||||
|
||||
```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<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 細節
|
||||
|
||||
```rust
|
||||
#[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 細節
|
||||
|
||||
```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<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 細節
|
||||
|
||||
```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<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 細節
|
||||
|
||||
```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<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** |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
```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 模組
|
||||
Reference in New Issue
Block a user