---
title: "FR-132 舊庫升級排練：逐角色、逐帳號比對"
---

# 舊庫升級排練：逐角色、逐帳號比對

拿一份沒有任何 FR-132 異動的舊庫，走客戶升級的正式路徑（`scripts/init/migrate.sh`）升到現版，逐角色、逐帳號比對升級前後「能做的事」。新裝環境永遠會過，會出事的是帶著舊資料升級，這份紀錄回答的是：**舊客戶升級後，每個人原本做得到的事還做得到嗎？有沒有人多拿到不該拿的？**

## 結論

- **零遺失**：15 個角色、21 個能登入的帳號，升級前持有的每一顆舊權限，升級後都還在（不抽樣，全部逐一比）。
- **多出來的只有兩類，都是已裁的設計**：
  1. CM-2556 補授的新拆權限（B 類 27 顆給所有非管理員角色、A 類 `asset.device.export` 給有 `device.read` 的角色）。
  2. CM-2588 補授的 `grc.audit.read`（舊名 `audit.read`）——一般成員與兩個零權限 E2E 角色因此多了「我的稽核管理」側欄。
- **側欄**：除上述「我的稽核管理」外，管理員的舊「角色管理」頁換成新「角色」「指派」兩頁；其餘頁面前後完全一致。
- **系統管理員轉換**：4 個 `is_admin` 角色全數轉成 `*.*.*`，**沒有未轉的 is_admin 角色**。非 root 租戶的管理員展開後 168 顆＝全集 208 扣平台層 40 顆，沒有拿到任何平台權限。
- **正式路徑本身**：25 支主線 migration 依 manifest 行序一次套完、無錯；套件輪 0 支；第三輪宣告 208 顆。重跑一次待套 0、角色 pattern 不變（冪等）。
- **沒有踩到順序依賴**：manifest 行序就是可用順序。

## 驗收打折（照實標）

| 項目 | 打折在哪 | 影響 |
|---|---|---|
| 舊庫來源 | 190 是**我們自己的 E2E 安裝版**，不是真實客戶庫。基線 1.22.0、BE image 1.25.1，資料是 e2e 跑出來的真流程 | 角色結構只有出貨預設四種＋兩個零權限測試角色，**沒有客戶自建、權限東拼西湊的角色**——CM-2562 的缺口（升級回補腳本仍寫舊表，非全集管理員角色拿不到新權限）在這份庫上**驗不到**，因為 190 沒有「is_admin 但不是全集」的角色 |
| 升級前選單 | 現版程式連不上升級前的庫（缺 `roles.is_system` 等新欄位），所以升級前選單不是登入取得，而是用舊版規則（舊表 role_capabilities × route_capabilities 的 ANY／ALL／無門檻，加授權照過濾）在升級前副本上**用 SQL 重算** | 已用 190 `api_logs` 裡唯一一筆真實 `/user/web-menu` 回應（blsadmin）對過，重算結果 37 個節點**完全相同**；其餘帳號的「升級前」是重算值 |
| 升級後選單 | 臨時 BE 起在 8010，**不起背景排程**、Redis 用本機另起的隔離實例（`.env` 指向的 189 Redis 與 8000 的 DEV BE 共用，展開快取鍵會互相污染） | 選單與權限判定走的是正式程式碼，排程不影響這兩者 |
| 按鈕 | 沒有開瀏覽器逐頁截圖。改比「前端判斷按鈕用的那份權限清單」（`/users/me/capabilities` 的 names）——升級後每個帳號實際登入取回，轉回舊名後與升級前逐帳號比 | 證明的是「按鈕判斷的依據」前後一致（零遺失），不是像素截圖 |
| blsalex | 密碼與共用測試密碼不同，沒有登入 | 該帳號角色是「一般成員」，同角色另有 9 個帳號已驗 |
| 第 26 支 migration | 排練進行中，另一棒（CM-2535 轉真擋）在同一工作目錄新增了 `2026-10-08-fr132-enforce-all-log-only.sql`（尚未 commit）。排練庫先套完 25 支做比對，之後把它當第 26 支補套 | 它只改 `enforce_mode`，套前套後角色 pattern 完全相同；套完後抽測 API 實擋結果與權限集合一致 |

## 怎麼做的

**舊庫**：190 唯讀 `pg_dump`（`guidant_ai_190_20261008_1548_before_FR-132_rehearsal.dump`，md5 `a15fde8c08cc3bbaeb66107309337313`），restore 到本機 `localhost:5432`：

| 庫名 | 用途 |
|---|---|
| `guidant_ai_rehearsal` | 升級後（跑過 migrate.sh），**保留給首腦抽查** |
| `guidant_ai_rehearsal_pre` | 升級前原樣副本（重算升級前選單用），保留 |

restore 零錯誤。升級前：285 支已套（主線 211＋套件 74）、0 支 FR-132；4 個租戶、15 個角色、capabilities 123 顆舊名。

**套件 SQL 攤平**：本機 jedi 套件是 path 模式，`flatten_pkg_migrations.sh` 只掃得到 site-packages 內實體安裝的 3 支，攤出來的套件 manifest 不完整、第三輪宣告也會缺。做法是在 scratchpad 建一個假 site-packages（把 path 模式套件的目錄接進來），攤平到 scratchpad 的 SQL 目錄，**repo 內的 `scripts/sql/packages/` 沒動**。攤出 74 支，與 190 已套的套件 migration 檔名集合完全相同；能力點宣告 208 顆。

**升級**：`migrate.sh` 先 dry-run（主線待套 25、套件 0、第三輪 208），再 `MIGRATE_APPLY=1`。過程 NOTICE：

- 改名自檢通過：204 個三段式
- 完整名 pattern 轉入 574 筆；自檢 13 個角色 pattern 展開與舊表完全一致
- 補授（CM-2556）：A 類 9 筆、B 類 306 筆、C 類 0 筆，適用 11 個角色
- 補授 `grc.audit.read`（CM-2588）：5 個角色
- 回收 `workspace.feedback.export`：0 個角色
- 第三輪：upsert 208、root 新增 0、租戶側新增 0

**比對口徑**：

- 角色層：升級前每角色 `role_capabilities` 的舊名集合；升級後以 `jedi_iam.authz.pattern.match_patterns`（判定用的同一支）展開 `role_capability_patterns`，root 判定與排除清單照正式規則，經 `capabilities.legacy_name` 轉回舊名比。升級時刪掉的 5 顆（`workflow.*` 四顆、`bulletin.read`）從預期集合扣掉。
- 帳號層：臨時 BE 實際登入每個帳號，取 `/user/web-menu`（側欄）與 `/users/me/capabilities`（按鈕判斷用的權限清單）。
- 指派：`user_roles` 升級前後只差 `org_unit_id` 被清空（`fr132-user-roles-tenant-scope-clear-org-unit` 的設計），其餘欄位逐列相同。

## 每個租戶選到的系統管理員

| 租戶 | 選為系統管理員（`*.*.*`）的角色 | 升級前能力點 | 升級後展開 |
|---|---|---|---|
| 1 Guidant.AI（root） | role 2 Administrator | 123（含 5 顆被刪的舊名） | 208（全集，含平台層） |
| 3 Billows | role 3 System Manager | 103（含 `bulletin.read`） | 168（全集扣平台 40） |
| 4 E2E_security_tenantA | role 8 System Manager | 103 | 168 |
| 5 E2E_security_tenantOther | role 12 System Manager | 103 | 168 |

**未轉的 is_admin 角色：無。** 每個租戶只有一個 is_admin 角色，且都等於該租戶全集。

## 逐角色比對（全部 15 個）

「多出（舊名）」＝升級後展開轉回舊名、升級前沒有的；「新拆」＝沒有舊名的新權限（升級前不存在，不算遺失也不算越權）。

| 角色 | 租戶 | 升級前 | 遺失 | 多出（舊名） | 新拆 | 判定 |
|---|---|---|---|---|---|---|
| 2 Administrator | 1 | 123 | 0 | 0 | 90 | 一致（`*.*.*`） |
| 3 System Manager | 3 | 103 | 0 | 0 | 66 | 一致（`*.*.*`） |
| 8 System Manager | 4 | 103 | 0 | 0 | 66 | 一致（`*.*.*`） |
| 12 System Manager | 5 | 103 | 0 | 0 | 66 | 一致（`*.*.*`） |
| 4／9／13 一般成員 | 3／4／5 | 14 | 0 | `audit.read` | 28 | 多出屬 CM-2588 設計 |
| 5／10／14 稽核人員 | 3／4／5 | 23 | 0 | 0 | 28 | 一致 |
| 6／11／15 唯讀檢視者 | 3／4／5 | 13 | 0 | 0 | 28 | 一致 |
| 31 E2E_fixed_nocap | 3 | 0 | 0 | `audit.read` | 27 | 多出屬 CM-2588／CM-2556 B 類設計 |
| 32 E2E_fixed_root_nocap | 1 | 0 | 0 | `audit.read` | 36 | 同上；root 租戶故 B 類含平台層 9 顆 |

**新拆權限補到哪些角色**（CM-2556 三類）：

- 一般成員／稽核人員／唯讀檢視者（9 個）：B 類 27 顆＋A 類 `asset.device.export`（來源 `asset.device.read`）。
- 兩個零權限 E2E 角色：只有 B 類（27 顆；root 租戶那個多 9 顆平台層，共 36）。
- C 類（空殼／全新功能只補 is_admin）：0 筆——4 個 is_admin 角色都已是 `*.*.*`，本來就涵蓋。

B 類的意思是「升級前這些端點只驗登入、任何人都做得到」，所以零權限角色升級後拿到 `grc.job.delete` 之類並不是越權，是把原本就做得到的事寫明。真擋開了之後它們仍做得到，與升級前一致。

**零權限角色值得決策者知道的一點**：`E2E_fixed_nocap` 升級前「什麼權限都沒有」，但當時任何登入者都能打那 27 支只驗登入的端點；升級後這件事被寫成明確權限，畫面上看起來像是「多了 27 顆」。客戶若有類似「刻意給空角色」的帳號，升級後在角色頁會第一次看到這些權限，可以自行拿掉。

## 逐帳號側欄比對

升級前＝舊規則＋授權照過濾重算；升級後＝臨時 BE 登入取 `/user/web-menu`。只比側欄頁（內頁不上側欄，兩邊都排除）。

| 帳號 | 租戶 | 前 | 後 | 少了 | 多了 |
|---|---|---|---|---|---|
| admin | 1 | 41 | 42 | `/auth/role-manage` | `/access/roles`、`/access/assignments` |
| blsadmin、blsit、E2E_fixed_member_a | 3 | 37 | 38 | 同上 | 同上 |
| blsfrank、E2E_fixed_sub_admin、E2E_fixed_sub_pm | 4 | 37 | 38 | 同上 | 同上 |
| E2E_fixed_oth_admin | 5 | 10 | 10 | — | — |
| blspan（稽核人員） | 3 | 15 | 15 | — | — |
| E2E_fixed_readonly_b（唯讀） | 3 | 16 | 16 | — | — |
| blsemp1、blsshino、E2E_fixed_emp_a／emp_b／mail_html／mfa（一般成員） | 3 | 13 | 14 | — | `/project/audit-manage` |
| E2E_fixed_sub_emp／sub_quota／sub_rpm（一般成員） | 4 | 13 | 14 | — | `/project/audit-manage` |
| E2E_fixed_nocap | 3 | 3 | 4 | — | `/project/audit-manage` |
| E2E_fixed_root_nocap | 1 | 3 | 4 | — | `/project/audit-manage` |

- 「角色管理」→「角色」「指派」：A.5 存取管理前端的設計。
- 一般成員多「我的稽核管理」：CM-2588（決策者 2026-10-08 裁乙，補授 `grc.audit.read` 給所有會進專案的角色）的直接效果。驗收條件「舊帳號選單不變」在這一項**不成立，是預期的不成立**。
- 租戶 5 管理員只有 10 頁：租戶 5 是頂層租戶但**沒有授權照**，授權過濾（FR-062，早於 FR-132）把需授權模組的頁全濾掉。升級前後相同，不是升級造成。重算升級前選單時漏套授權過濾，這個帳號會看起來「少 27 頁」，是比較口徑的假紅。

## 逐帳號權限清單比對（按鈕判斷的依據）

`/users/me/capabilities` 的 names 轉回舊名後與升級前逐帳號比：**21 個帳號遺失皆為 0**；多出的舊名只有一般成員與兩個零權限帳號的 `audit.read`，同上。

轉真擋（第 26 支）套完後抽測 API：一般成員、稽核人員打角色選單與 AI 呼叫紀錄回 403（升級前就沒有這兩顆）、打專案列表與我的任務通過守門（升級前有 `project.read`）；管理員全數通過。

## 給後續的備忘

- CM-2562（`migrate-capability-grants.sql:46,72` 仍寫舊表）這份庫驗不到，要等一份有「is_admin 但非全集」角色的真實客戶庫，或在排練庫手造一個這樣的角色再跑一次第三輪。
- 本機要跑 `migrate.sh` 排練，套件 SQL 不能直接用 repo 內 `scripts/sql/packages/`（path 模式只攤到 3 支）。build 機上是 wheel 安裝，不受影響。
- 排練庫兩支都留著；不要了直接 `DROP DATABASE guidant_ai_rehearsal; DROP DATABASE guidant_ai_rehearsal_pre;`。
