比較 RBAC ABAC
- RBAC 適合簡單、靜態、角色固定的系統。
- ABAC 適合複雜、多租戶、大規模企業,特別是金融、雲端、政府。
- AWS / GCP / Azure / Okta 這些雲端大廠,早就全面上 ABAC,因為它能撐住全球規模的多租戶需求。
🏢 已採用 ABAC 的大型系統 / 平台
-
AWS (Amazon Web Services IAM Policies)
- IAM policy 就是 ABAC:可以寫
Condition
,檢查user.tag == resource.tag
才能存取。 - 範例:只有標籤
project=fanpokka
的使用者才能操作同標籤的 S3 bucket。
- IAM policy 就是 ABAC:可以寫
-
Google Cloud IAM
- 採「角色 + 條件」混合:IAM Conditions 允許設定 基於屬性的規則(時間、資源名稱、requester 屬性)。
-
Microsoft Azure
- Azure Role-Based Access Control 支援 條件存取 (Conditional Access),其實就是 ABAC:例如「只有從公司網路登入的使用者才能存取資源」。
-
Okta / Auth0 (大型身份服務商)
- 支援 基於使用者屬性 (department, group, app_metadata) 的動態授權,等於 ABAC。
-
美國國防部 / NIST 標準
- ABAC 是 NIST 800-162 的正式標準,被美國政府和軍方大量採用,特別是需要 零信任架構 (Zero Trust) 的系統。
-
金融核心系統 (銀行/保險)
- 很多新一代核心系統會用 ABAC,因為可以描述「交易金額 > 1,000,000 時必須是經理級才能批核」這種動態條件,RBAC 無法處理。
項目 | RBAC (Role-Based Access Control) | ABAC (Attribute-Based Access Control) |
---|---|---|
核心概念 | 角色 (Role) 決定權限 | 屬性 (Attribute) + 規則 (Policy) 決定權限 |
授權方式 | 使用者被指派一個或多個角色;角色內建一組權限 | 使用者屬性 + 資料屬性 + 環境屬性 → 經 Policy 引擎判斷 |
適合規模 | 組織結構簡單、角色固定 | 複雜組織、跨部門、多條件存取控制 |
例子 | - Admin 可讀寫所有資料 - 店員只能存取自己門市 |
- 如果 user.dept = 資料.dept,允許讀取 - 如果 user.role=Manager 且 request.time < 晚上 10 點,允許修改 |
優點 | - 簡單直觀 - 易於實作 - 適合小系統 |
- 精細化控制 - 高度彈性 - 支援動態條件(時間、地點、設備) |
缺點 | - 角色爆炸 (role explosion):要管理數百個角色 - 無法表達動態條件 |
- 複雜度高 - 需要 Policy Engine 或規則系統 - 測試/維護成本大 |
典型應用 | ERP 權限管理、傳統後台系統 | 金融業、政府、雲端平台 (多租戶 SaaS)、零信任 (Zero Trust) |
改成「泛化模型」:N 層樹 + ABAC 策略
1) 資料模型(N 層樹,不綁業務詞)
核心:org_units
作為可任意深度的組織樹(樹高不限、型別只是 metadata)
-
tenants
:企業/甲方(最上層租戶) -
org_units
:租戶內的組織節點(樹狀,可任意深度)id, tenant_id, parent_id, path, type, attrs
type
只作建議型別(如 brand/region/store/online/department),不參與硬性邏輯path
用 ltree(Postgres)或文字 path(1.3.5
)利於「含子層」查詢
-
channels
:每個 LINE OA(或其他渠道),綁到某一個 org_unit(最常綁門市/品牌) -
業務資料(members/events/messages/coupons)都存:
tenant_id, org_unit_id, channel_id
這樣:
- 「層數」跟你無關(N 層)
- 「名稱」只是展示,不動到資料結構
- Channel 自然落在某個 org_unit 之下(例如門市或品牌)
表結構示意
路徑索引:Postgres
ltree
+ GIST/GIN,可支援path @> 'a.b'
(包含子層)查詢。
2) 權限與隔離(從「等級」改為「規則」)
把 strict/relaxed/none
改成 策略化(Policy-based):
用 ABAC(基於屬性) 或 OPA(Open Policy Agent)/自製 Policy Engine 來表達。
Membership 與 Policy
判斷邏輯(簡化)
- 每個請求經過中介層,得到
tenant_id
與「可視子樹集合」。 - 查詢自動注入:
org_units.path <@ any(allowed_paths)
(或org_unit_id in (...)
經由展開)
更彈性的共享
- 不是「同 root 就能看」,而是 Membership 可以有多個
org_unit_scope
(一個行銷主管可以同時看到北區與南區)。 - 若要跨租戶(代理商帳號),則該 user 會有多個
tenant_id
的 membership。
DB 層保險:RLS
-
為
members/events/...
開啟 RLS:USING (tenant_id = current_setting('app.tenant_id')::uuid AND org_unit_id = ANY(current_setting('app.allowed_ou_ids')::uuid[]))
-
每個請求前
SET app.tenant_id
,SET app.allowed_ou_ids = '{...}'
(由 membership 決定)。
好處:
- 不再被 rootId 綁死
- 可以設定「只看這兩棵子樹」或「看整棵」
- 能滿足大型連鎖、代理商、多品牌集團等任意組織
3) Webhook 映射(每個 Channel 唯一路徑)
維持之前要求:
- 以
hookKey
O(1) 查到:channel_id, tenant_id, org_unit_id, secret
- 驗簽後把事件丟入 queue,事件上打:
tenant_id/org_unit_id/channel_id
- 日後報表就能依
channel
或org_unit
任意聚合
為什麼 Channel 綁 org_unit?
- 因為同一租戶下可能有「門市用一個 OA、電商用另一個 OA」;各自對應不同 org_unit 更直覺。
4) API 擴充(泛化命名,避免業務詞寫死)
把特定詞(brand/region/department)改為抽象資源:org-units
。
- 查樹:
GET /tenants/{tenantId}/org-units?parentId=&type=&q=...
- 指定 Channel 掛在哪個 org-unit:
POST /tenants/{tenantId}/channels { orgUnitId, ... }
- 查會員/訊息/報表都能帶
orgUnitId
或channelId
;或includeDescendants=true
。
查詢例
5) 報表維度
以 tenant_id, org_unit_id, channel_id, sender
為標準維度。
- 任何 KPI 支援
groupBy = [day | orgUnit | channel | sender | segment]
- 若
includeDescendants=true
,後端展開org_units.path <@ target.path
做整棵匯總 - 可以在 DWH 建 物化視圖 加速(
org_unit_path
+channel_id
維度)
6) 過度專化版本的額外風險
- 遷移地獄:一旦某客戶要第 5 層,就得改 schema/查詢/索引/權限;成本高昂。
- 權限邏輯不夠表達力:
strict/relaxed
無法覆蓋「跨兩個非相鄰區域共享」。 - 不同產業不適配:醫療、教育、政府採購、品牌代理都會有不同結構,寫死詞彙難賣。
- rootId 大範圍掃描:開發者容易用 rootId 做懶人查詢,出現越權風險和性能問題。
7) 過渡策略(從 4 層 → 泛化)
-
先把現有四層映射到
org_units
:- brand → type='brand'
- region → type='region'
- channel → type='channel'
- department → type='department'
-
新專案只用
org_units
,不再創建新固定層表。 -
建置 membership.scope 改存 ltree path;API 支援
includeDescendants
。 -
用 DB migration 把舊資料加上
org_unit_id/path
。 -
報表 ETL 改用
org_unit_path
聚合。
8) 範例:RLS 與查詢(Postgres)
中介層(偽碼)
子樹查詢(含後代)
9) 測試要點(把「泛化」測到底)
- ✅ 同一租戶、不同 OU:看得到自己 OU,但看不到旁支 OU
- ✅ membership 綁「兩棵不同子樹」:兩邊都看得到,其他都看不到
- ✅ includeDescendants:有無包含子層結果不同
- ✅ channel 綁在任意 OU:Webhook 事件正確落到該 OU,報表能 groupBy channel
- ✅ RLS 生效:移除中介層
WHERE
後仍無法越權讀取
10) 總結
把四層固定結構,改成「N 層 org_units + ABAC」。
Channel 綁 org_unit;資料一律帶tenant_id/org_unit_id/channel_id
。
隔離靠 membership 的「子樹 scope」+資料庫 RLS 雙保險。
這樣才泛化可賣、可維護、可長期演進。
以下備注,詳細說明什麼是 ABAC
好 👍 我幫你把 「N 層 org_units + ABAC」 這個概念,講成比較淺顯又工程師能懂的版本。
🧩 什麼是「N 層 org_units」?
👉 簡單說:把租戶內的組織結構做成一棵樹,可以有任意層級,不要寫死只有 4 層。
org_units
就是「組織單位(Organization Units)」。- 每個
org_unit
可以是品牌、分公司、區域、門市、部門… 名字不重要。 - 層數不限(N 層),可以是 2 層、3 層、5 層都行。
範例
再換一個產業 → 醫療院所:
➡️ 不同產業有不同層級,但因為是「N 層樹」,架構不用改,只要填不同的 org_unit。
🛡️ 什麼是「ABAC」?
👉 Attribute-Based Access Control = 基於屬性的存取控制
傳統做法是 RBAC(Role-Based Access Control):
- 角色 = 權限 → 例如「Admin 可以看全部」「店員只能看自己的店」
- 缺點:一旦組織複雜,就要創造一堆角色,很快失控。
ABAC 改成「判斷屬性」:
- 使用者有屬性(user.id, user.roles, user.org_scope)
- 資源有屬性(data.tenant_id, data.org_unit_path, data.channel_id)
- 請求有屬性(operation=read/write, time=2025-09-10)
➡️ 系統判斷規則:是否允許 user 對 resource 做 operation
這些規則就是「Policy」。
範例
規則:
user.tenant_id == data.tenant_id
→ 同租戶才可讀取data.org_unit_path 在 user.org_scope 子樹裡
→ 只能看自己分店與其子部門operation == 'read'
→ 行銷可以看資料,但不能刪除
效果:
- 北區經理登入 →
user.org_scope = "北區"
→ 可以看「台北店」和「客服部」 - 高雄經理登入 →
user.org_scope = "南區"
→ 看不到北區的任何資料 - 平台管理員 →
user.org_scope = "*" / role=platform-admin
→ 可看全部