# FR-062 License 控管機制 — 全案 SUMMARY（含 STG/POC 雙環境部署）

> 產出日期：2026-08-10 · 由 `fr062-LOG.md` 四個 block 濃縮，非憑記憶重建
> living 現況見同資料夾 `fr062-STATE.md`；決策軌跡與教訓全文見 `fr062-LOG.md`

## 一句話成果

Guidant AI 上線銷售所需的 License 控管機制全案完成：**公司自建簽發端（License Center，獨立 repo 獨立 DB，私鑰不出機）＋產品端只驗不簽**的信任模型落地，涵蓋簽章引擎、按模組授權、到期狀態機與通知、執法與唯讀降級、序號開通與機器指紋綁定、簽發 Web 後台，並於 2026-08-10 完成 **STG 與 POC 雙環境部署**與請照實測。

---

## 全案階段回顧

| 階段 | 內容 | 狀態 |
|---|---|---|
| **.1 簽發端地基** | Ed25519 簽章引擎＋CLI＋`plans`/`license_issuance` DB；照檔格式歷經三次演進（v2 信封 zlib+base64 打包·非加密 → v3 `.license` PEM 風格 armor → v2.1 三段 `send_email` 旗標） | 完成 |
| **.2 產品端驗照** | `config.tenant_licenses` 資料模型、root 後台授權管理、FE 授權狀態頁；照綁頂層租戶、整條讀照鏈過租戶樹 resolve | 完成 |
| **.3 狀態機＋橫幅** | 每日排程 cron 推進 notify→grace→readonly，conditional UPDATE 冪等；FE 全域到期橫幅；root tenant 特判豁免 | 完成 |
| **.4 執法進場** | `common/authz` 第六軸＋全域唯讀 gate＋選單/任務型態動態過濾；兩支環境開關可整體關閉；`/license/` 前綴整段豁免防死鎖 | 完成 |
| **.5 開通** | 離線（機器碼→簽照→上傳）與線上（序號＋指紋一鍵換照）兩路；線上路徑 LC 開放公開 API，BE 代打並完整驗章 | 完成 |
| **.6 簽發 Web 後台** | Tabler UI（pin 版入 static/vendor，不依賴 CDN）：客戶總覽、簽發/換發表單、詳情頁、照檔下載、客戶授權時間線 | 完成 |
| **.7 通知＋migration** | notify/grace/readonly 三段到期信（收件人＝三旗標聯集＋root fallback）；既有租戶發照 SOP 與護欄腳本（DEV 演練） | 完成 |
| **.8 角色矩陣 license 維度**（驗收回饋追加） | `ui-routes` 附 `is_licensed`、FE 未授權能力點反灰空框、`roles` 寫入端守門、provisioning 第三道扣除、選單結構調整 | 完成 |
| **.9 手動停權獨立欄位**（追加） | 停權升為獨立欄位，cron 不再覆寫人為決策；API 回傳旗標；FE 文案與狀態快取失效 | 完成 |
| **.10 產品後台向 LC 請照**（追加） | LC API token 白名單＋管理頁＋三支內部簽發 API；產品 BE `lc-issue`/`lc-extend`/`lc/plans`；FE 請照 dialog——SaaS 續約從此免人工搬檔 | 完成 |

---

## 今日（2026-08-10）重點

### 三輪追加開發

- **FR-062.8**：執法上線後發現角色權限矩陣完全沒有 license 維度——只買基礎包的租戶，管理員仍勾得下未購買模組的能力點且真的入庫。採業界 show-and-disable（反灰＋原因），存量資料走 evaluate-time 抑制不做破壞性移除。初版把存量項渲染成「打勾＋標籤」，驗收後推翻為反灰空框。
- **FR-062.9**：手動停權與到期狀態混用，導致橫幅顯示錯誤且人為停權會被 cron 自動撤銷。停權改為獨立欄位，cron 只管到期軸。migration 回填時**刻意不寫 `suspend_reason`**——該欄會直接顯示給客戶。
- **FR-062.10**：SaaS 續約原本要 admin 在 LC 簽發下載、再切回產品上傳，兩趟手動搬檔。改為產品後台帶 API token 直打 LC 內部簽發 API，回照仍走本地完整驗章與既有落地漏斗。驗收當下另追加三卡（LC 總覽開通狀態三態化、同客戶分組＋排序、搜尋＋篩選）。

### STG / POC 雙環境部署

每環境依序走：套 migration → keygen → 公鑰進 BE code → BE 重部署 → LC 起服務 → 建 API token → 請照實測。

- **三環境簽發鑰各自獨立**（決策者要求）：DEV `2ce3bb59a6f5a2ce` / STG `04b1e65f100c6c8f` / POC `f3b562a6d9755527`，三把公鑰共存於 `common/license/public_keys.py`，私鑰各留該機。STG 首把因 passphrase 對不上作廢重產。
- **POC 首發簽出 `deployment_mode` 記載錯誤的照**（BE 未傳、LC 套預設 saas），修為請照時帶本站台 config；兩側 license 資料依決策者指示清空重發。

---

## Commits（僅 FR-062 相關）

### BE（`compliance-manager-be`，main，已 push）

今日 16 支：
```
8ca60a81 docs 授權管理使用指南（平台管理員 + 租戶使用者兩視角）
8c82e8e6 fix .10 請照未指定 deployment_mode 時帶本站台 DEPLOYMENT_MODE
358cb601 feat POC 簽發鑰 f3b562a6d9755527 進 PUBLIC_KEYS
d7cc0394 fix STG 簽發鑰換為二次 keygen 的 04b1e65f100c6c8f
de96ed92 feat group-license 選單群組沉底 sort 45→90
214b72f6 feat STG 簽發鑰進 PUBLIC_KEYS（首把，後作廢）
a14f2070 feat .10 CM-1177 產品 BE 向 LC 請照 app service + route
543ff6dd docs .10 CM-1179 design.md §4.7 圖 4 修正＋設計摘要
bde8fc1b fix .9 CM-1173 migration 回填不寫 suspend_reason
d725ade1 feat .9 CM-1173 手動停權升獨立欄位＋API 旗標
e63649c8 docs .8 T-8.5 補登 design.md §5＋統計校正 27→28
d0de3baf feat .8 CM-1171 選單結構調整（is_platform 正解）
6802da99 feat CM-1169 POST/PUT /roles 寫入端 license 守門
50652ec1 feat .8 CM-1170 建租戶 seed 第三道扣除未授權模組
9c5a1d71 feat .8 CM-1167 GET /ui-routes 附 is_licensed
37cc661b docs .8 子需求補登 design.md §5＋features README
```
先前階段（.1~.7 與驗收回饋期）另有約 35 支，見 `git log --oneline | grep FR-062`。

### FE（`compliance-manager-fe`，main，已 push）

今日 9 支 FR-062 相關：
```
d00ba1b feat .10 請照 dialog 補照型態選項（admin 可發 Trial）
48041e2 fix .9 CM-1183 追加：登出前先等 chatbot session 清完
0f734d0 fix .9 CM-1183 登出後不再誤打 license status
2a1d3c5 fix .10 CM-1178 授權歷程補 lc_issue/lc_extend 翻譯
21677cd feat .10 CM-1178 授權管理頁「向 LC 請照」dialog
7462cab feat .9 CM-1174 停權文案與狀態快取失效
5b76fae fix .8 CM-1168 未授權能力點一律不打勾
3263045 feat .8 CM-1171 角色矩陣平台頁判定改吃 is_platform
c8dab0c feat .8 CM-1168 角色矩陣未授權反灰＋顯示原因
```
另有 SaaS 版文案/入口三支（`7b767ca`/`1c2a46d`/`4ed56cd`）與 `4d24a1a` error-code 補譯。

### LC（`license_center`，main，已 push）

今日 8 支：
```
d001b76 docs LC repo CLAUDE.md（架構守則、安全鐵律、與主產品介面）
3671e38 docs License Center 部署手冊——以 STG 實戰部署為藍本
72d61ba feat 綁定位址環境變數化 LICENSE_CENTER_BIND_HOST
4122456 feat .10 CM-1182 客戶總覽搜尋框＋下拉篩選
4b45d81 fix  .10 CM-1181 客戶名稱欄補表頭排序（原實作漏掛 sort）
cb9a73d feat .10 CM-1181 客戶總覽同客戶視覺分組＋List.js 排序
1464d91 fix  .10 CM-1180 開通狀態三態化
e622350 feat .10 CM-1176 API token 白名單＋管理頁＋內部簽發 API
```

---

## 行為差異（使用者看得到的變化）

| 對象 | 變化 |
|---|---|
| 平台管理員（root） | 授權管理頁可**直接向 License Center 請照**（新簽／展延／Trial），不必再去 LC 下載檔案再回來上傳；每租戶列有授權歷程時間線 |
| 平台管理員 | **手動停權成為獨立軸**——停權不會被每日 cron 依到期日自動撤銷，狀態顯示與到期狀態不再互相干擾 |
| 所有使用者 | 側邊選單新增獨立的**「授權管理」群組**（沉底 sort=90）；未完成的「自定義儀表板」入口關閉 |
| 租戶管理員 | 角色權限矩陣中，**未購買模組的能力點反灰並顯示原因**，不再看得到也勾得下去；繞過 UI 直打 API 亦回 403 |
| LC 操作者 | 客戶總覽：開通狀態改**三態**（無序號照不再誤標未開通）、**同客戶視覺分組**、表頭可排序、有搜尋框與下拉篩選 |
| LC 操作者 | 新增 **API token 管理頁**（新增／列表／停用），發照紀錄可看出由哪個環境請發 |

---

## 規範文件清單

| 文件 | 狀態 |
|---|---|
| `docs/features/FR-062-2608-license-management/design.md` | 已回寫至最新（§4.7 圖 4 修正＋.10 設計摘要、§5 拆分表 28 子任務、.8 三段裁示 callout）；**.9 手動停權尚未在 design.md 留段落，屬已知缺口** |
| `docs/features/FR-062-2608-license-management/discussion.md` | 已含實作後記 |
| `license_center/docs/deployment-guide.html` | 新增，以 STG 實戰部署為藍本（systemd unit、環境變數、keygen 與公鑰接線） |
| `license_center/CLAUDE.md` | 新增（架構守則、安全鐵律、與主產品介面） |
| BE `docs/.../授權管理使用指南` | 由平行 runner 於 `8ca60a81` 補入（平台管理員＋租戶使用者兩視角） |
| `docs/specs/current/` 頁面 spec | 由平行 runner 處理中 |
| `existing-tenant-license-migration-sop.md` | 保留，轉為 DEV 演練與離線備援用途（STG/POC 實際改走「向 LC 請照」） |

---

## 已知 follow-up

1. **三 repo push**：本次已完成（HEAD == origin/main）；未來新 commit 仍等決策者明示
2. **v1.14.0 發版未完成**：`release/v1.14.0` branch 存在但落後 main 2 commit，**release note 未寫、`pyproject.toml` 仍為 1.13.0** — 要發版走 `version-bump` skill
3. **STG 兩租戶正式發照**：BE/LC 已就緒，發照動作由決策者手動執行（現有 2 張為部署驗證照）
4. **雲端 LC 公網補驗**：TLS／網域／防火牆，清單在 CM-1130 卡內；目前 LC 皆內網 :5062
5. **正式環境簽發鑰**：產出後必須先進 `common/license/public_keys.py` 才 build 出貨 image
6. **`design.md` 缺 FR-062.9 段落**：手動停權獨立欄位的設計未落 design.md
7. **CM-1143 遺留模組整鏈退役**（cruise-project／resource／report＋jedi-resource-store）：Not started，全案後另議
8. **未套的三支 migration**（決策者裁示這次不動）：`2026-07-20-ssp-shared-metadata-DEV-cleanup`、`2026-08-01-fr058-22-openscap-content-path-advanced`、`2026-08-03-fr060-1-detection-profile-split`

---

## 部署 handover

### 三環境座標

| | DEV（本機/188） | STG（192.168.50.188） | POC（192.168.50.189） |
|---|---|---|---|
| 產品 DB | `guidant_ai_dev` | `guidant_ai_stg` | `guidant_ai_poc` |
| LC DB | `license_center_dev` | `license_center_stg` | `license_center_poc` |
| 產品 BE | 本機 :8000 | `/opt/compliance-manager-be` :8000 | `/opt/compliance-manager-be` :8000 |
| LC | 本機 :5062 | `/opt/license_center`，systemd `license-center.service`（enabled，BIND_HOST=0.0.0.0）:5062 | `/opt/license_center` :5062 |
| FE | dev :5180 | nginx `/var/www/html/audit-manager` :80 | nginx `/var/www/html/audit-manager` :80 |
| 簽發鑰 kid | `2ce3bb59a6f5a2ce` | `04b1e65f100c6c8f` | `f3b562a6d9755527` |
| LC API token | `BE-dev-T2-test`(dev) | `Staging`(stg) | `GuidantAI-POC`(production) |
| FR-062 migration | 7 支 | 7 支 | 7 支 |

DB 連線一律 `-p 25432 -U cmmgr`，密碼請查 `.env` 的 `DB_SECRET`。LC 管理密碼與 passphrase 在各機 LC `.env`。

### 上正式環境的檢查項

1. **簽發鑰先於 image**：正式環境 keygen 後，公鑰 kid 必須先進 `common/license/public_keys.py` 並 commit，**才 build 出貨 image**。順序顛倒＝產品收不到正式照，報「未知的 kid」
2. **私鑰保管**：正式私鑰須進密碼管理器＋兩處站外備份。**私鑰遺失＝無法簽續約**，且 passphrase 是 keygen 當下設定的檔案解鎖密碼，事後改設定檔無效
3. **請照必帶 `deployment_mode`**：不讓 LC 用預設值猜（POC 曾因此簽出錯誤照）
4. **三環境公鑰共存不取代**：新增 kid 不刪舊 kid，否則既有環境的照全部驗不過
5. **執法總開關**：`LICENSE_ENFORCEMENT_ENABLED` / `LICENSE_READONLY_GATE_ENABLED` 預設 true，上線前確認符合預期
6. **root tenant 永不持照**：root 無條件豁免執法，正式環境不應為 root 發照
