跳至主要内容

Enterprise Authorization Architecture

本文說明如何使用 OpenFGA 設計企業級的權限系統,涵蓋組織架構、表單權限、多地區管理,並說明如何與 Zitadel、Cerbos 搭配使用。

系統職責分離

在開始之前,先釐清三個系統各自負責的事情:

系統職責核心問題
ZitadelAuthentication 認證你是誰?
OpenFGAAuthorization 授權你跟資源的關係?
CerbosPolicy 條件判斷你符合條件嗎?

整體架構如下:

組織架構設計

組織層級關係

企業的 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_idbu → parent → bgbu:mobile → bg:consumer
Dept.bu_iddepartment → parent_bu → budept:rd → bu:mobile
Dept.parent_dept_iddepartment → parent_dept → departmentdept:team_a → dept:rd
Employee.dept_idemployee → direct_member → departmentalice → dept:rd
Employee.is_manageremployee → manager → departmentalice → dept:rd
Employee.manager_idemployee → reports_to → employeebob → 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_viewcan_editcan_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 的差異

項目OpenFGACerbos
模型ReBAC 關係型PBAC 政策型
核心問題A 和 B 有什麼關係?A 符合什麼條件?
資料儲存儲存關係 tuple不儲存,即時運算
適合場景組織架構、文件協作屬性條件、商業規則

與 Camunda DMN 的比較

Cerbos 本質上類似於專門用於權限判斷的 DMN:

DMNCerbos
問題結果是什麼?可不可以?
輸出多元二元
用途通用商業規則專注授權

何時需要 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 也不遲。