12 KiB
12 KiB
Permission 模組
Gateway 多租戶 B2B 自定義 RBAC:平台級 Permission Catalog + 租戶級 Role / RolePermission / UserRole / RoleMapping + Casbin enforcer。
- 規格書(Data Dictionary、API 端點欄位、Casbin model)→
SDD.md - 跨模組總覽 →
docs/identity-member-design.md§6 - 本 README = 流程圖 + curl + ServiceContext 速查
TL;DR
flowchart LR
subgraph Platform["平台層"]
Catalog[Permission Catalog]
end
subgraph Tenant["租戶層"]
Role[Role]
RP[RolePermission]
UR[UserRole]
RM[RoleMapping]
end
Catalog -- 勾選 --> RP --> Role
Role -- 指派 --> UR
Role -- 對應 --> RM
Tenant -- LoadPolicy --> Casbin[(Casbin Enforcer<br/>Redis adapter)]
Casbin -- Check --> Middleware[CasbinRBAC Middleware]
- Permission 平台 seed 全局(
cmd/permission-seed);租戶不可新增、只能勾選 - Role / RolePermission / UserRole 租戶獨立;同名 role 跨租戶共存
- Role.Key 一旦建立 不可改(外部 IdP mapping 直接綁 key)
- 多 pod 同步:Redis Pub/Sub 即時 + 5min cron 兜底
核心概念
| 概念 | 簡述 | 關鍵欄位 |
|---|---|---|
| Permission | 平台級權限節點(樹狀,dot notation) | name 唯一、http_methods + http_path 命中 Casbin policy |
| Role | 租戶內角色 | (tenant_id, key) unique;is_system=true 不可刪 |
| RolePermission | Role 勾選的 Permission | 自動補齊 parent permission |
| UserRole | 使用者被指派的角色 | source ∈ {manual / zitadel / ldap / scim} |
| RoleMapping | 外部 group → 內部 Role.Key | SyncFromX 用來翻譯 IdP claims |
| Casbin Rule | 物化後規則 | [tenant, role.key, http_path, http_methods, perm.name] |
Permission Tree 範例
member.info.management ← 分類(無 HTTP)
├── member.basic.info ← 二級分類
│ ├── member.info.select GET /api/v1/members/me
│ └── member.info.update PATCH /api/v1/members/me
└── member.admin.list GET /api/v1/members
permission.role.management ← 分類
├── permission.role.read GET /api/v1/permissions/roles
├── permission.role.write POST/PUT/DELETE /api/v1/permissions/roles*
└── permission.assign.write POST/DELETE /api/v1/permissions/users/*/roles*
分類節點(無
http_path)不會寫入 Casbin policy;只是 UI 樹狀渲染 + parent closure 用。
目錄結構
internal/model/permission/
├── README.md
├── config/ # CasbinConfig / CacheConfig / ReloadConfig
├── domain/
│ ├── const.go # BSON 欄位、Casbin / Role.Key 規則
│ ├── errors.go
│ ├── redis.go # casbin / user_roles / role_perms key
│ ├── entity/ # Permission / Role / RolePermission / UserRole / RoleMapping
│ ├── enum/ # Status / PermissionType / RoleSource
│ ├── repository/ # 5 個 repo 介面 + Casbin adapter port
│ └── usecase/ # 7 個 usecase 介面 + DTO
├── repository/ # Mongo + Redis(casbin policy Set)
├── usecase/ # 7 個 atomic(含 permission_tree / rbac)
└── seed/
├── catalog.go # embed + Apply + DefaultSystemRoles
└── catalog.json # 平台 seed
7 個 UseCase
| UseCase | 主要方法 |
|---|---|
PermissionUseCase |
GetCatalogTree / List / UpsertCatalog / UpdateStatus |
RoleUseCase |
Create / Get / List / Update / Delete |
RolePermissionUseCase |
List / Replace(含 parent closure + Pub/Sub reload) |
UserRoleUseCase |
Assign / Revoke / List / ReplaceForSource |
RoleMappingUseCase |
Upsert / Delete / GetByExternal / List |
AuthorizationQueryUseCase |
Me |
RBACUseCase |
Check / LoadPolicy / LoadAllPolicies / BroadcastReload / Pub/Sub 訂閱 |
資料儲存
MongoDB
| Collection | 索引 |
|---|---|
permissions |
name uniq、parent、status、type |
roles |
(tenant_id, key) uniq、(tenant_id, is_system) |
role_permissions |
(tenant_id, role_id, permission_id) uniq、(tenant_id, permission_id) |
user_roles |
(tenant_id, uid, role_id) uniq、(tenant_id, role_id)、(tenant_id, uid, source) |
role_mappings |
(tenant_id, external_source, external_key) uniq、(tenant_id, internal_role_id) |
啟動建索引:permrepo.EnsureMongoIndexes(已掛在 cmd/mongo-index)。
Redis
| Key | 內容 | TTL | 由誰寫 |
|---|---|---|---|
permission:casbin:rules:{tenant_id} |
Set of JSON Casbin rules | 永久 | RBAC.LoadPolicy / BroadcastReload |
perm:user_roles:{tenant_id}:{uid} |
role keys 快取(預留) | Cache.UserRolesTTLSeconds |
— |
perm:role_perms:{tenant_id}:{role_id} |
permission names 快取(預留) | Cache.RolePermsTTLSeconds |
— |
(channel) casbin:reload |
Pub/Sub payload {tenant_id, ts} |
— | RBAC.BroadcastReload |
Redis Set + JSON 是為了 SaveAll 用 pipelined
DEL + SADD原子更新;Pub/Sub 走獨立 go-redis client(go-zero 沒包 Subscribe,且 Subscribe 會佔住 conn)。
關鍵流程
1. RolePermission 全量取代(PUT /roles/:id/permissions)
sequenceDiagram
API->>UC: Replace(tenantID, roleID, ids)
UC->>Roles: GetByID(tenant check)
UC->>Perms: GetAll(catalog 全表)
UC->>UC: ids ⊆ catalog?
UC->>UC: getFullParentPermissionIDs(ids)
UC->>RP: SetForRole(DeleteMany + InsertMany 原子)
UC->>RBAC: BroadcastReload(tenantID)
UC-->>API: nil
2. SyncFromX(外部 IdP 同步)
sequenceDiagram
Sync->>Map: GetByExternal(tenant, source, externalKey)
Map-->>Sync: RoleMapping(internal_role_key)
Note over Sync: 收齊 IdP 端 → keys
Sync->>UC: ReplaceForSource(tenant, uid, source, [roleKeys])
UC->>UC: 阻擋 source==manual(防誤洗)
UC->>URR: DeleteMany source + BulkInsert
UC->>RBAC: BroadcastReload(tenant)
3. LoadPolicy + Check
sequenceDiagram
Trigger->>RBAC: LoadPolicy(tenantID)
RBAC->>Roles: ListByTenant
RBAC->>RP: ListByRoles(roleIDs)
RBAC->>Perms: GetByIDs(unique perm ids)
RBAC->>RBAC: 過濾 IsLeaf() && Status=open
RBAC->>Enforcer: ClearPolicy + AddPolicies
RBAC->>Adapter: SaveAll → Redis pipelined DEL+SADD
MW->>RBAC: Check{tenant, uid, path, method}
RBAC->>URR: ListByUser → ListByTenantAndIDs(過濾 status=open)
loop role in roles(any-allow)
RBAC->>Enforcer: EnforceEx
end
RBAC-->>MW: Allow / Deny(403)
4. Pub/Sub 多 Pod Reload
sequenceDiagram
PodA->>PodA: RolePermission.Replace + LoadPolicy
PodA->>Redis: PUBLISH casbin:reload {tenant, ts}
Redis-->>PodB: 推 message
Redis-->>PodC: 推 message
PodB->>PodB: handleReload → LoadPolicy(tenant)
PodC->>PodC: handleReload → LoadPolicy(tenant)
Note over PodB,PodC: 2-3ms 內三個 pod 同步
兜底:每個 pod 可定時
LoadAllPolicies(5min cron,建議 svc 層或 cron-worker 觸發;本模組不內建)。
Casbin 模型(etc/rbac.conf)
[request_definition]
r = tenant, role, path, method
[policy_definition]
p = tenant, role, path, methods, name
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.tenant == p.tenant && r.role == p.role && keyMatch2(r.path, p.path) && regexMatch(r.method, p.methods)
keyMatch2:支援/api/v1/members/*regexMatch:GET|POST|PATCH多 method 同 policy- 平台 Admin bypass 不寫在 matcher,由 middleware 預檢(保留 audit)
ServiceContext 注入
sc.PermissionCatalog // PermissionUseCase
sc.PermissionRole // RoleUseCase(含 system role 防呆)
sc.PermissionRolePermission // RolePermissionUseCase
sc.PermissionUserRole // UserRoleUseCase
sc.PermissionRoleMapping // RoleMappingUseCase
sc.PermissionAuthQuery // AuthorizationQueryUseCase
sc.PermissionRBAC // RBACUseCase(Mongo+Redis 全到位才有)
sc.PermissionRoleRepo // 給 SCIM / SyncFromX 等下游使用
未啟用 Casbin 時 PermissionRBAC == nil,Check() 永遠 deny;middleware 視 AllowMissingActor 決定放行或拒絕。
HTTP API(/api/v1/permissions)
| Method | Path | Middleware | 說明 |
|---|---|---|---|
| GET | /catalog |
AuthJWT | 全局 Catalog(tree/list) |
| GET | /me |
AuthJWT | 當前 user roles + permissions |
| GET | /roles |
AuthJWT+Casbin | 租戶角色清單 |
| POST | /roles |
AuthJWT+Casbin | 建立角色 |
| PATCH | /roles/:id |
AuthJWT+Casbin | 更新 display_name / status |
| DELETE | /roles/:id |
AuthJWT+Casbin | 刪角色(system / 仍有指派 → 拒絕) |
| GET | /roles/:id/permissions |
AuthJWT+Casbin | 角色 permission 集合 |
| PUT | /roles/:id/permissions |
AuthJWT+Casbin | 全量取代 + parent closure + reload |
| GET | /users/:uid/roles |
AuthJWT+Casbin | 使用者 role |
| POST | /users/:uid/roles |
AuthJWT+Casbin | 指派 |
| DELETE | /users/:uid/roles/:role_id |
AuthJWT+Casbin | 撤銷 |
| GET / PUT / DELETE | /role-mappings |
AuthJWT+Casbin | 外部映射 CRUD |
| POST | /policy/reload |
AuthJWT+Casbin | 強制重載(單 tenant 或 *) |
完整 schema 見 generate/api/permission.api。
設定(etc/gateway.dev.yaml)
Permission:
Casbin:
Enabled: false # 預設關閉
ModelPath: etc/rbac.conf
PolicyAdapter: auto # auto / redis / mongo
Cache:
UserRolesTTLSeconds: 300
RolePermsTTLSeconds: 300
CatalogTTLSeconds: 600
Reload:
Channel: casbin:reload
DebounceMilliseconds: 200
HeartbeatSeconds: 60
CLI / 操作
# 建索引
make mongo-index
# Catalog seed(全平台)
go run ./cmd/permission-seed -f etc/gateway.dev.yaml
# Catalog + 為 tenant seed 5 個 system role
go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001
# 只 reseed tenant role(catalog 已存在)
go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001 -skip-catalog
# 強制全 pod 重載 policy
curl -X POST http://localhost:8888/api/v1/permissions/policy/reload \
-H "Authorization: Bearer $TOKEN" \
-d '{"tenant_id": "*"}'
預設 5 個 system role:tenant_owner / tenant_admin / member_manager / member / viewer(seed/catalog.go::DefaultSystemRoles)。
設計約束(速查)
| 議題 | 決策 | 原因 |
|---|---|---|
Permission name 改名 |
禁止 | 被 RolePermission / Casbin policy.name 引用;廢棄改 status=close |
Role key 改名 |
禁止 | 外部 IdP mapping 直接綁 key |
is_system 刪除 / 改 status |
拒絕 | 平台預設角色保留 |
manual source ReplaceForSource |
拒絕 | 防 SyncFromX 誤洗手動指派 |
| 多 pod 同步 | Pub/Sub 即時 + 5min cron 兜底 | 即時 + reboot 不漏 |
| Pub/Sub client | 獨立 go-redis | go-zero 沒包 Subscribe |
| Catalog 改動 | seed CLI(idempotent) | catalog.json 是 SoT |
測試
# 單元
go test ./internal/model/permission/...
# 整合(需 Mongo + Redis)
make deps-up
go test -tags=integration ./internal/model/permission/...
# E2E(含 Casbin enforcement)
make e2e-casbin
E2E 細節:docs/e2e-testing.md。