template-monorepo/internal/model/permission/README.md

644 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Permission Module
> 本模組提供 Gateway 多租戶 **B2B 自定義 RBAC**:平台級 Permission Catalog + 租戶級 Role / RolePermission / UserRole / RoleMapping搭配 Casbin enforcer 進行 HTTP path/method 授權。設計參考 `docs/identity-member-design.md` §6 / §7.3 / §13。
---
## 0. TL;DR
```mermaid
flowchart LR
subgraph Platform["平台層 (Platform-wide)"]
Catalog[Permission Catalog]
end
subgraph Tenant["租戶層 (per-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 一旦建立 **不可改**;外部 IdPZITADEL / LDAP / SCIM以 Key 作對應。
- 多 pod 同步:**Redis Pub/Sub 即時通知 + 5min cron 兜底**。
---
## 1. 核心概念
| 概念 | 簡述 | 關鍵欄位 |
|------|------|----------|
| **Permission** | 平台級權限節點樹狀dot notation | `name` 唯一、`http_methods` + `http_path` 命中 Casbin policy |
| **Role** | 租戶內的角色 | `tenant_id + key` unique`is_system=true` 不可刪 |
| **RolePermission** | Role 勾選了哪些 Permission | 自動補齊 parent permission ID |
| **UserRole** | 使用者被指派的角色(多角色) | `source` 區分 manual / zitadel / ldap / scim |
| **RoleMapping** | 外部 group/role → 內部 Role.Key | SyncFromX 用來翻譯 IdP claims |
| **Casbin Policy** | 物化後的授權規則Redis Set | `(tenant, role, path, methods, name)` |
### 1.1 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
└── member.admin.read GET /api/v1/members/:uid
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 用。
---
## 2. 目錄結構
```
internal/model/permission/
├── README.md # 本文件
├── config/
│ └── config.go # CasbinConfig / CacheConfig / ReloadConfig
├── domain/
│ ├── const.go # BSON 欄位 / Casbin / Role.Key 規則
│ ├── errors.go # 模組共用 sentinel errors
│ ├── redis.go # Redis key helpers (casbin / user_roles / role_perms)
│ ├── entity/
│ │ ├── permission.go # Permission catalog node
│ │ ├── role.go
│ │ ├── role_permission.go
│ │ ├── user_role.go
│ │ └── role_mapping.go
│ ├── enum/
│ │ ├── status.go # open / close + Permissions map
│ │ ├── permission_type.go # backend_user / frontend_user
│ │ └── role_source.go # manual / zitadel / ldap / scim
│ ├── repository/ # 介面(+ Casbin adapter port
│ │ ├── permission.go
│ │ ├── role.go
│ │ ├── role_permission.go
│ │ ├── user_role.go
│ │ ├── role_mapping.go
│ │ └── casbin_adapter.go
│ └── usecase/ # 介面 + DTO
│ ├── permission.go
│ ├── role.go
│ ├── role_permission.go
│ ├── user_role.go
│ ├── role_mapping.go
│ ├── rbac.go
│ └── authorization_query.go
├── repository/ # Mongo + Redis 實作
│ ├── index.go # EnsureMongoIndexes + bsonOpSet
│ ├── permission_mongo.go
│ ├── role_mongo.go
│ ├── role_permission_mongo.go
│ ├── user_role_mongo.go
│ ├── role_mapping_mongo.go
│ └── casbin_redis.go # tenant-scoped policy Redis Set
├── usecase/ # atomic primitives (7)
│ ├── module.go # NewModuleFromParam
│ ├── errors.go # wrapRepoErr → errs.For(code.Permission)
│ ├── permission_tree.go # buildTree / filterOpenNodes / parent closure
│ ├── permission_usecase.go
│ ├── role_usecase.go
│ ├── role_permission_usecase.go
│ ├── user_role_usecase.go
│ ├── role_mapping_usecase.go
│ ├── authorization_query_usecase.go
│ └── rbac_usecase.go # Casbin enforcer + LoadPolicy + Pub/Sub reload
└── seed/
├── catalog.go # embed + Apply + DefaultSystemRoles
└── catalog.json # 平台 seed 資料
```
---
## 3. 模組依賴
```mermaid
flowchart TD
Logic[logic/permission] --> SVC[svc.ServiceContext]
SVC --> AuthQ[AuthorizationQueryUseCase]
SVC --> Perm[PermissionUseCase]
SVC --> Role[RoleUseCase]
SVC --> RolePerm[RolePermissionUseCase]
SVC --> UserRole[UserRoleUseCase]
SVC --> Mapping[RoleMappingUseCase]
SVC --> RBAC[RBACUseCase]
AuthQ --> RoleR[(roles)]
AuthQ --> PermR[(permissions)]
AuthQ --> RPR[(role_permissions)]
AuthQ --> URR[(user_roles)]
Perm --> PermR
Role --> RoleR
Role --> URR
RolePerm --> RPR
RolePerm --> RoleR
RolePerm --> PermR
UserRole --> URR
UserRole --> RoleR
Mapping --> RMR[(role_mappings)]
Mapping --> RoleR
RBAC --> RoleR
RBAC --> PermR
RBAC --> RPR
RBAC --> URR
RBAC --> Adapter[Casbin Redis Adapter]
Adapter --> Redis[(Redis)]
RBAC --> Pub[Redis Pub/Sub]
```
---
## 4. UseCase 介面7 個)
| UseCase | 主要方法 | 注入 |
|---------|----------|------|
| `PermissionUseCase` | `GetCatalogTree` / `List` / `UpsertCatalog` / `UpdateStatus` | PermissionRepository |
| `RoleUseCase` | `Create` / `Get` / `List` / `Update` / `Delete` | Role + RolePermission + UserRole |
| `RolePermissionUseCase` | `List` / `Replace` | Role + Permission + RolePermission + Reloader |
| `UserRoleUseCase` | `Assign` / `Revoke` / `List` / `ReplaceForSource` | Role + UserRole + Reloader |
| `RoleMappingUseCase` | `Upsert` / `Delete` / `GetByExternal` / `List` | Role + RoleMapping |
| `AuthorizationQueryUseCase` | `Me` | Role + Permission + RolePermission + UserRole |
| `RBACUseCase` | `Check` / `LoadPolicy` / `LoadAllPolicies` / `BroadcastReload` / `Start/StopReloadSubscriber` | All repos + Redis |
---
## 5. 資料儲存
### 5.1 MongoDB
| Collection | 索引 | 用途 |
|------------|------|------|
| `permissions` | `name`(uniq) / `parent` / `status` / `type` | 平台 Permission Catalog樹狀 |
| `roles` | `(tenant_id, key)`(uniq) / `(tenant_id, is_system)` | 租戶角色 |
| `role_permissions` | `(tenant_id, role_id, permission_id)`(uniq) / `(tenant_id, permission_id)` | Role↔Permission 多對多 |
| `user_roles` | `(tenant_id, uid, role_id)`(uniq) / `(tenant_id, role_id)` / `(tenant_id, uid, source)` | User↔Role 多對多 |
| `role_mappings` | `(tenant_id, external_source, external_key)`(uniq) / `(tenant_id, internal_role_id)` | 外部 group → 內部 Role |
啟動時呼叫 `permrepo.EnsureMongoIndexes(ctx, &c.Mongo)`(已掛在 `cmd/mongo-index`)。
### 5.2 Redis Key
| Key | 內容 | TTL | 由誰寫 |
|-----|------|-----|--------|
| `permission:casbin:rules:{tenant_id}` | Set of JSON-encoded `[]string` rules | 永久 | `RBACUseCase.LoadPolicy` / `BroadcastReload` |
| `perm:user_roles:{tenant_id}:{uid}` | List of role keys讀取快取預留 | `Cache.UserRolesTTLSeconds` | 預留 |
| `perm:role_perms:{tenant_id}:{role_id}` | List of permission names預留 | `Cache.RolePermsTTLSeconds` | 預留 |
| `permission:tree:open` | 序列化的全局 open tree預留 | `Cache.CatalogTTLSeconds` | 預留 |
| (channel) `casbin:reload` | Pub/Sub payload `{tenant_id, ts}` | — | `RBACUseCase.BroadcastReload` |
> Redis Set + JSON 編碼是為了讓 SaveAll 用 pipelined `DEL + SADD` 一致性更新Pub/Sub 走獨立 go-redis clientgo-zero 沒有 Subscribe詳見 `internal/library/redis/pubsub.go`。
---
## 6. 核心流程時序圖
### 6.1 NewModuleFromParam — 模組組裝
```mermaid
sequenceDiagram
participant Boot as svc.NewServiceContext
participant Mod as permission.NewModuleFromParam
participant Cfg as config.Defaults()
participant Repo as Mongo Repos (5)
participant Casbin as RBACUseCase
participant Redis as PolicyAdapter
Boot->>Mod: FactoryParam{MongoConf, Redis, Config}
Mod->>Cfg: cfg = Config.Defaults()
Mod->>Repo: NewPermission/Role/.../RoleMapping Repository
Note over Mod: 若已注入 repo測試跳過
alt cfg.Casbin.Enabled && Redis 有
Mod->>Casbin: NewRBACUseCase(repos+Redis)
Casbin-->>Mod: rbacUC
Mod->>Redis: RedisAdapterFactory = NewCasbinRedisAdapter
Mod->>Mod: reloader = rbacUC.BroadcastReload
else 無 Redis 或 Disabled
Mod->>Mod: rbacUC = nilCheck 永遠 deny
end
Mod->>Mod: New {Permission, Role, RolePermission, UserRole, RoleMapping, AuthorizationQuery}
Mod-->>Boot: *Module7 usecases + 5 repos
```
### 6.2 Permission Catalog Seed
```mermaid
sequenceDiagram
participant CLI as cmd/permission-seed
participant Cfg as config.Mongo
participant Idx as permrepo.EnsureMongoIndexes
participant Seed as seed.Apply
participant Cat as Permissions
participant Roles as Roles + RolePermissions
CLI->>Cfg: load -f etc/gateway.dev.yaml
CLI->>Idx: 建立 5 collections 索引
CLI->>Seed: Apply(perms, roles, rolePerms, opts)
alt SkipCatalog == false
Seed->>Cat: 第一輪 UpsertByName不含 parent
Seed->>Cat: GetAll → 建 name→ID index
Seed->>Cat: 第二輪 UpsertByName補 parent ID
end
loop opts.TenantIDs
Seed->>Roles: GetByKey or Insert is_system role
Seed->>Roles: SetForRole(roleID, [permIDs]) ← 全量取代
end
Seed-->>CLI: Report{ catalog, roles, role_perms }
CLI-->>CLI: stdout summary
```
> 預設 5 個 system role`tenant_owner` / `tenant_admin` / `member_manager` / `member` / `viewer`,定義於 `seed/catalog.go::DefaultSystemRoles`。
### 6.3 Role 建立 / 更新 / 刪除
```mermaid
sequenceDiagram
participant API as POST/PATCH/DELETE /permissions/roles
participant Logic as logic.permission.*
participant UC as RoleUseCase
participant Repo as RoleRepository
participant URR as UserRoleRepository
API->>Logic: req + actor (tenant_id, uid)
Logic->>UC: Create / Update / Delete
alt Create
UC->>UC: validateRoleKey^[a-z][a-z0-9._-]+$、不可 system./platform_
UC->>Repo: Insert(role) ← unique (tenant_id, key)
else Update
UC->>Repo: GetByID
UC->>UC: 阻擋 is_system 改 status
UC->>Repo: FindOneAndUpdate
else Delete
UC->>Repo: GetByID
UC->>UC: 阻擋 is_system
UC->>URR: ListByRole仍有指派 → 拒絕)
UC->>Repo: DeleteByRole(role_perms)
UC->>Repo: Delete(role)
end
UC-->>Logic: role
Logic-->>API: types.RoleData
```
### 6.4 RolePermission 全量取代PUT /roles/:id/permissions
```mermaid
sequenceDiagram
participant API as PUT /permissions/roles/:id/permissions
participant Logic as logic.replaceRolePermissions
participant UC as RolePermissionUseCase
participant Roles as RoleRepository
participant Perms as PermissionRepository
participant RP as RolePermissionRepository
participant RBAC as RBACUseCase
API->>Logic: req{ID, PermissionIDs}
Logic->>UC: Replace(tenantID, roleID, ids)
UC->>Roles: GetByID驗證 tenant 一致)
UC->>Perms: GetAll拿到 catalog 全表)
UC->>UC: 檢查 ids ⊆ catalog
UC->>UC: getFullParentPermissionIDs(ids, all)
UC->>RP: SetForRole(tenantID, roleID, closure)
Note over RP: DeleteMany + InsertMany 原子化
UC->>RBAC: BroadcastReload(tenantID)
RBAC-->>UC: okfire-and-forget
UC-->>Logic: nil
Logic-->>API: 200 OK
```
### 6.5 UserRole 指派 / 撤銷
```mermaid
sequenceDiagram
participant API as POST /permissions/users/:uid/roles
participant UC as UserRoleUseCase
participant Roles as RoleRepository
participant URR as UserRoleRepository
participant RBAC as RBACUseCase
API->>UC: Assign{tenant, uid, role_id, source=manual}
UC->>Roles: GetByID (tenant scope check)
UC->>URR: Insert(unique tenant+uid+role)
UC->>RBAC: BroadcastReload(tenant)
UC-->>API: UserRole
```
### 6.6 SyncFromX 流程(外部 IdP 來源同步)
```mermaid
sequenceDiagram
participant Sync as auth/provisioning
participant UC as UserRoleUseCase
participant Map as RoleMappingUseCase
participant Roles as RoleRepository
participant URR as UserRoleRepository
participant RBAC as RBACUseCase
Sync->>Map: GetByExternal(tenant, source=zitadel, externalKey)
Map-->>Sync: RoleMapping(internal_role_key)
Note over Sync: 收齊 IdP 端所有 roles → keys
Sync->>UC: ReplaceForSource(tenant, uid, source=zitadel, [roleKeys])
UC->>UC: 阻擋 source==manual防誤洗
loop key in roleKeys
UC->>Roles: GetByKey (skip 不存在的)
end
UC->>URR: ReplaceForSource(tenant, uid, source, [roleIDs])
Note over URR: DeleteMany source=zitadel + BulkInsert<br/>※ source=manual 紀錄不動
UC->>RBAC: BroadcastReload(tenant)
```
### 6.7 LoadPolicyCasbin 規則載入)
```mermaid
sequenceDiagram
participant Trigger as Replace / Reload / Boot
participant RBAC as RBACUseCase
participant Roles as RoleRepository
participant RP as RolePermissionRepository
participant Perms as PermissionRepository
participant Enf as casbin.SyncedEnforcer
participant Adp as Redis Adapter
Trigger->>RBAC: LoadPolicy(tenantID)
RBAC->>Roles: ListByTenant
RBAC->>RP: ListByRoles(roleIDs)
RBAC->>Perms: GetByIDs(unique perm ids)
RBAC->>RBAC: 過濾 IsLeaf() && Status=open
RBAC->>RBAC: rules = [tenant, role.key, http_path, http_methods, perm.name]
RBAC->>Enf: ClearPolicy + AddPolicies
RBAC->>Adp: SaveAll(tenant, rules) ← Redis pipelined DEL+SADD
RBAC-->>Trigger: nil
```
### 6.8 Check授權檢查
```mermaid
sequenceDiagram
participant MW as middleware.CasbinRBAC
participant Logic as ActorFromContext
participant RBAC as RBACUseCase
participant URR as UserRoleRepository
participant Roles as RoleRepository
participant Enf as casbin.SyncedEnforcer
MW->>Logic: actor (tenant, uid)
MW->>RBAC: Check{tenant, uid, path, method}
RBAC->>RBAC: enforcerFor(tenant)lazy clone model + AddPolicies
RBAC->>URR: ListByUser(tenant, uid)
RBAC->>Roles: ListByTenantAndIDs過濾 status=open
loop role in rolesany-allow
RBAC->>Enf: EnforceEx(tenant, role.key, path, method)
alt allow
RBAC-->>MW: CheckResult{Allow=true, MatchedRoleKey, MatchedPolicyRow}
end
end
MW->>MW: result.Allow ? next : 403 (errs.AuthForbidden)
```
### 6.9 Pub/Sub 多 Pod Reload
```mermaid
sequenceDiagram
participant PodA as Pod A (Replace)
participant Redis
participant PodB as Pod B (Subscribe)
participant PodC as Pod C (Subscribe)
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 觸發)。掃 Redis `permission:casbin:rules:*` key 推導 tenant 列表。
### 6.10 GET /permissions/me前端選單渲染
```mermaid
sequenceDiagram
participant Front as Frontend
participant API as GET /permissions/me
participant UC as AuthorizationQueryUseCase
participant URR as UserRoleRepository
participant Roles as RoleRepository
participant RP as RolePermissionRepository
participant Perms as PermissionRepository
Front->>API: Bearer JWT
API->>UC: Me(tenant, uid, includeTree)
UC->>URR: ListByUser
UC->>Roles: ListByTenantAndIDs過濾 status=open
UC->>RP: ListByRoles(roleIDs)
UC->>Perms: GetByIDs(unique perm ids)
UC->>UC: permission map = name→status
alt includeTree
UC->>UC: buildPermissionTree + filterOpenNodes
end
UC-->>API: { uid, tenant_id, roles, permissions, tree? }
API-->>Front: 200 OK
```
---
## 7. Casbin 模型(`etc/rbac.conf`
```ini
[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/*` 萬用 path
- `regexMatch``GET|POST|PATCH` 多 method 同一 policy
- 平台 Admin bypass 不寫進 matcher由 middleware 預檢(保留 audit
---
## 8. ServiceContext 注入
```go
sc.PermissionCatalog // Permission catalog reader (tree / list / status)
sc.PermissionRole // Role CRUD含 system role 防呆)
sc.PermissionRolePermission // Replace含 parent closure
sc.PermissionUserRole // Assign / Revoke / ReplaceForSource
sc.PermissionRoleMapping // 外部 group → Role.Key
sc.PermissionAuthQuery // GET /me 用
sc.PermissionRBAC // Casbin enforcerMongo+Redis 全到位才有)
sc.PermissionRoleRepo // 給 SCIM / SyncFromX 等下游使用
```
未啟用 Casbin 時 `PermissionRBAC == nil``Check()` 永遠 denymiddleware 會拒絕所有請求(除非 `AllowMissingActor=true`)。
---
## 9. HTTP API前綴 `/api/v1/permissions`
| Method | Path | Handler | 說明 |
|--------|------|---------|------|
| GET | `/catalog` | `getPermissionCatalog` | 全局 Catalogtree=true 取樹狀) |
| GET | `/me` | `getMePermissions` | 當前 user 的 role / permission map |
| GET | `/roles` | `listRoles` | 租戶角色清單 |
| POST | `/roles` | `createRole` | 建立角色key 不可改) |
| PATCH | `/roles/:id` | `updateRole` | 更新 display_name / statussystem role 限制) |
| DELETE | `/roles/:id` | `deleteRole` | 刪角色system / 仍有指派 → 拒絕) |
| GET | `/roles/:id/permissions` | `getRolePermissions` | 角色目前的 permission 集合 |
| PUT | `/roles/:id/permissions` | `replaceRolePermissions` | 全量取代 + 補 parent + Pub/Sub reload |
| GET | `/users/:uid/roles` | `listUserRoles` | 使用者目前指派的 role |
| POST | `/users/:uid/roles` | `assignUserRole` | 指派角色source 預設 manual |
| DELETE | `/users/:uid/roles/:role_id` | `revokeUserRole` | 撤銷單一角色 |
| GET | `/role-mappings` | `listRoleMappings` | 外部映射列表(分頁) |
| PUT | `/role-mappings` | `upsertRoleMapping` | Upsert 外部 group → Role.Key |
| DELETE | `/role-mappings` | `deleteRoleMapping` | 刪除外部映射 |
| POST | `/policy/reload` | `reloadPolicy` | 強制重載(單租戶或 `*` |
完整錯誤碼註解參見 `generate/api/permission.api`,由 `make gen-doc` 出 OpenAPI。
---
## 10. 設定範例(`etc/gateway.dev.example.yaml`
```yaml
Permission:
Casbin:
Enabled: false # 預設關閉,啟用後 RBAC enforcement 生效
ModelPath: etc/rbac.conf
PolicyAdapter: auto # auto / redis / mongo
Cache:
UserRolesTTLSeconds: 300
RolePermsTTLSeconds: 300
CatalogTTLSeconds: 600
Reload:
Channel: casbin:reload
DebounceMilliseconds: 200
HeartbeatSeconds: 60
```
---
## 11. CLI / 操作指南
```bash
# 1) 建索引
make mongo-index
# 2) 撰寫 / 修改 catalog
$EDITOR internal/model/permission/seed/catalog.json
# 3) 全平台 seed catalog不為任何 tenant 建 role
go run ./cmd/permission-seed -f etc/gateway.dev.yaml
# 4) 同時為 dev tenant seed 5 個 system role
go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001
# 5) 多租戶
go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001,TEN-100002
# 6) 只 reseed tenant rolecatalog 已存在)
go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001 -skip-catalog
# 7) 強制全部 pod 重載 policyHTTP
curl -X POST http://localhost:8888/api/v1/permissions/policy/reload \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: TEN-100001" -H "X-UID: TEN-100001-OWNER" \
-d '{"tenant_id": "*"}'
```
---
## 12. 中介層middleware/casbin_rbac.go
**現況:** middleware 已寫好,但 **尚未掛入 routes.go**(避免影響現有 dev 模式)。要啟用:
```go
import perm "gateway/internal/middleware"
server.AddRoutes(routes,
rest.WithMiddlewares(
[]rest.Middleware{
middleware.CloudEPJWT(serverCtx.AuthToken), // 已存在
middleware.CasbinRBAC(serverCtx.PermissionRBAC, middleware.CasbinRBACOptions{
AllowMissingActor: false,
SkipPaths: map[string]struct{}{
"/api/v1/health": {},
},
}),
}...,
),
rest.WithPrefix("/api/v1/members"),
)
```
要先:
1. 跑 seed CLI 把 catalog + system role 建好
2. 為平台 admin tenant 建 `platform_super_admin` role + bypass allowlist
3. 開啟 `Permission.Casbin.Enabled = true`
4. 設好 `Permission.Reload.Channel`(多 pod 才需要)
---
## 13. 測試
```bash
# 全模組 unit test
go test ./internal/model/permission/...
# 含整合(需要 Mongo + Redis 在 docker compose 起著)
make deps-up
go test -tags=integration ./internal/model/permission/...
```
---
## 14. 設計權衡 / 注意事項
| 議題 | 決策 | 原因 |
|------|------|------|
| Permission `name` 改名 | **禁止** | 被 RolePermission、UI i18n、Casbin policy.name 引用;廢棄走 `status=close` 然後新建 |
| Role `key` 改名 | **禁止** | 外部 IdP mapping 直接綁 key改名會切斷映射 |
| `is_system` role 刪除 | 拒絕 | 平台預設角色保留 |
| `is_system` role 改 status | 拒絕 | 維持平台預期行為 |
| `manual` source ReplaceForSource | 拒絕 | 防 SyncFromX 誤洗手動指派 |
| Permission 有 `*` 萬用 path | 不建議裸 `*`;至少帶資源根 | 防 keyMatch2 貪婪命中跨資源 |
| Casbin 多 enforcer | 一 tenant 一個 enforcerlazy 建 | 比一個 enforcer + filtered policy 簡單,且記憶體可預測 |
| 多 pod 同步 | Pub/Sub 即時 + 5min cron 兜底 | 即時通知 + reboot 不漏 |
| Pub/Sub client | 獨立 go-redis不走 go-zero pool | go-zero 沒包 Subscribe且 Subscribe 會佔住 conn |
| Permission Catalog 改動 | seed CLI 即可idempotent | UI 端不直接改 catalogseed JSON 是 SoT |
---
## 15. 後續工作
| 項目 | 預估 |
|------|------|
| Platform admin allowlist + audit log | 後續 |
| RoleMapping 用 SyncFromX 落地Zitadel / LDAP / SCIM| 隨對應 SyncFromX usecase 推進 |
| Policy reload cron worker5 min | 取自 svc 啟動 ticker |
| Role permission 編輯 UI不在 Gateway 內,由前端取資) | 前端 |
| 細粒度欄位過濾(`.plain_code` 變體) | logic 層額外查 sub-permission |