Enterprise Authorization Architecture
本文說明如何使用 OpenFGA 設計企業級的權限系統,涵蓋組織架構、表單權限、多地區管理,並說明如何與 Zitadel、Cerbos 搭配使用。
系統職責分離
在開始之前,先釐清三個系統各自負責的事情:
| 系統 | 職責 | 核心問題 |
|---|---|---|
| Zitadel | Authentication 認證 | 你是誰? |
| OpenFGA | Authorization 授權 | 你跟資源的關係? |
| Cerbos | Policy 條件判斷 | 你符合條件嗎? |
整體架構如下:
組織架構設計
組織層級關係
企業的 OrgTree 通常包含以下層級:
OpenFGA Model 定義
model
schema 1.1
type employee
relations
define reports_to: [employee]
define supervisor: reports_to or supervisor from reports_to
type bg
relations
define admin: [employee]
define member: [employee] or admin or member from child_bu
type bu
relations
define parent: [bg]
define admin: [employee] or admin from parent
define member: [employee] or admin or member from child_dept
define bg_admin: admin from parent
type department
relations
define parent_bu: [bu]
define parent_dept: [department]
define manager: [employee]
define direct_member: [employee]
define member: direct_member or manager or member from child_dept
define admin: manager or admin from parent_bu or admin from parent_dept
define can_view: member or admin
資料對照表
| OrgTree 欄位 | OpenFGA 關係 | 範例 |
|---|---|---|
| BU.bg_id | bu → parent → bg | bu:mobile → bg:consumer |
| Dept.bu_id | department → parent_bu → bu | dept:rd → bu:mobile |
| Dept.parent_dept_id | department → parent_dept → department | dept:team_a → dept:rd |
| Employee.dept_id | employee → direct_member → department | alice → dept:rd |
| Employee.is_manager | employee → manager → department | alice → dept:rd |
| Employee.manager_id | employee → reports_to → employee | bob → alice |
地理層級設計
除了組織架構,表單系統通常還需要地區與廠區的概念:
Model 定義:
type region
relations
define admin: [employee]
define member: [employee] or admin
type plant
relations
define parent: [region]
define admin: [employee] or admin from parent
define member: [employee] or admin
define region_admin: admin from parent
表單權限設計
表單關係模型
一張表單可能關聯到多個地區、多個廠區:
表單 Model 定義
type form
relations
# 基本關係,支援多對多
define region: [region]
define plant: [plant]
define creator: [employee]
define approver: [employee]
# 權限推導
define plant_admin: admin from plant
define region_admin: admin from region
define creator_supervisor: supervisor from creator
# 最終權限
define can_view: creator or approver or plant_admin or region_admin or creator_supervisor
define can_edit: creator or plant_admin
define can_approve: [employee] but not creator
權限矩陣
| 角色 | can_view | can_edit | can_approve |
|---|---|---|---|
| 開單人員 | ✓ | ✓ | ✗ |
| 已簽核人員 | ✓ | ✗ | ✗ |
| 待簽核人員 | ✓ | ✗ | ✓ |
| 開單人主管鏈 | ✓ | ✗ | 視規則 |
| 廠區管理者 | ✓ | ✓ | 視規則 |
| 地區管理者 | ✓ | ✗ | 視規則 |
同步程式實作
OrgTree 同步
class OrgTreeSyncer:
def __init__(self, fga_client):
self.fga = fga_client
def sync_bu(self, bu):
self.fga.write(
user=f"bu:{bu.id}",
relation="parent",
object=f"bg:{bu.bg_id}"
)
def sync_department(self, dept):
if dept.parent_dept_id:
self.fga.write(
user=f"department:{dept.id}",
relation="parent_dept",
object=f"department:{dept.parent_dept_id}"
)
else:
self.fga.write(
user=f"department:{dept.id}",
relation="parent_bu",
object=f"bu:{dept.bu_id}"
)
def sync_employee(self, emp):
self.fga.write(
user=f"employee:{emp.id}",
relation="direct_member",
object=f"department:{emp.dept_id}"
)
if emp.is_manager:
self.fga.write(
user=f"employee:{emp.id}",
relation="manager",
object=f"department:{emp.dept_id}"
)
if emp.manager_id:
self.fga.write(
user=f"employee:{emp.id}",
relation="reports_to",
object=f"employee:{emp.manager_id}"
)
表單同步
def create_form_tuples(form):
tuples = []
for region_id in form.region_ids:
tuples.append({
"user": f"region:{region_id}",
"relation": "region",
"object": f"form:{form.id}"
})
for plant_id in form.plant_ids:
tuples.append({
"user": f"plant:{plant_id}",
"relation": "plant",
"object": f"form:{form.id}"
})
tuples.append({
"user": f"employee:{form.creator_id}",
"relation": "creator",
"object": f"form:{form.id}"
})
return tuples
def on_approval_complete(form_id, approver_id):
fga.write(
user=f"employee:{approver_id}",
relation="approver",
object=f"form:{form_id}"
)
Zitadel 整合
建議設定
Zitadel 負責認證,權限交給 OpenFGA,因此 Zitadel 設定可以簡化:
Organization: default
Project: your-app
Roles:
- user
- admin
權限檢查流程
API 實作
from fastapi import Depends, HTTPException
async def check_form_permission(
form_id: str,
permission: str,
current_user: User = Depends(get_current_user)
):
allowed = await fga.check(
user=f"employee:{current_user.id}",
relation=permission,
object=f"form:{form_id}"
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied")
@app.get("/forms/{form_id}")
async def get_form(
form_id: str,
_: None = Depends(lambda: check_form_permission(form_id, "can_view"))
):
return await form_service.get(form_id)
Cerbos 的定位
與 OpenFGA 的差異
| 項目 | OpenFGA | Cerbos |
|---|---|---|
| 模型 | ReBAC 關係型 | PBAC 政策型 |
| 核心問題 | A 和 B 有什麼關係? | A 符合什麼條件? |
| 資料儲存 | 儲存關係 tuple | 不儲存,即時運算 |
| 適合場景 | 組織架構、文件協作 | 屬性條件、商業規則 |
與 Camunda DMN 的比較
Cerbos 本質上類似於專門用於權限判斷的 DMN:
| DMN | Cerbos | |
|---|---|---|
| 問題 | 結果是什麼? | 可不可以? |
| 輸出 | 多元 | 二元 |
| 用途 | 通用商業規則 | 專注授權 |
何時需要 Cerbos
當你的權限邏輯需要基於屬性條件判斷時:
# Cerbos Policy 範例
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: form
rules:
- actions: ["edit"]
effect: EFFECT_ALLOW
roles: ["user"]
condition:
match:
all:
- expr: request.resource.attr.creator_id == request.principal.id
- expr: request.resource.attr.status == "draft"
- actions: ["approve"]
effect: EFFECT_ALLOW
roles: ["director", "vp", "ceo"]
condition:
match:
expr: request.resource.attr.amount > 1000000
整合使用
async def check_permission(user, action, form):
# Step 1: OpenFGA 檢查關係
has_relationship = await openfga.check(
user=f"employee:{user.id}",
relation=f"can_{action}",
object=f"form:{form.id}"
)
if not has_relationship:
return False
# Step 2: Cerbos 檢查條件
decision = await cerbos.check(
principal={"id": user.id, "roles": user.roles},
resource={"kind": "form", "id": form.id, "attr": {
"status": form.status,
"amount": form.amount
}},
action=action
)
return decision.is_allowed()
總結
根據需求選擇適合的組合:
| 需求 | 建議方案 |
|---|---|
| 只需要關係型權限 | Zitadel + OpenFGA |
| 需要條件判斷 | Zitadel + OpenFGA + Cerbos |
| 需要決策引擎 | 加入 Camunda DMN |
對於大多數表單系統,Zitadel + OpenFGA 已經足夠。等到有明確的條件判斷需求時,再加入 Cerbos 也不遲。