# 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
Redis adapter)] Casbin -- Check --> Middleware[CasbinRBAC Middleware] ``` - Permission **平台 seed 全局**(`cmd/permission-seed`),租戶不可新增;只能勾選。 - Role / RolePermission / UserRole **租戶獨立**;同名 role 可在不同租戶共存。 - Role.Key 一旦建立 **不可改**;外部 IdP(ZITADEL / 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 client(go-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 = nil(Check 永遠 deny) end Mod->>Mod: New {Permission, Role, RolePermission, UserRole, RoleMapping, AuthorizationQuery} Mod-->>Boot: *Module(7 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: ok(fire-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
※ source=manual 紀錄不動 UC->>RBAC: BroadcastReload(tenant) ``` ### 6.7 LoadPolicy(Casbin 規則載入) ```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 roles(any-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 enforcer(Mongo+Redis 全到位才有) sc.PermissionRoleRepo // 給 SCIM / SyncFromX 等下游使用 ``` 未啟用 Casbin 時 `PermissionRBAC == nil`,`Check()` 永遠 deny;middleware 會拒絕所有請求(除非 `AllowMissingActor=true`)。 --- ## 9. HTTP API(前綴 `/api/v1/permissions`) | Method | Path | Handler | 說明 | |--------|------|---------|------| | GET | `/catalog` | `getPermissionCatalog` | 全局 Catalog(tree=true 取樹狀) | | GET | `/me` | `getMePermissions` | 當前 user 的 role / permission map | | GET | `/roles` | `listRoles` | 租戶角色清單 | | POST | `/roles` | `createRole` | 建立角色(key 不可改) | | PATCH | `/roles/:id` | `updateRole` | 更新 display_name / status(system 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 role(catalog 已存在) go run ./cmd/permission-seed -f etc/gateway.dev.yaml -tenant TEN-100001 -skip-catalog # 7) 強制全部 pod 重載 policy(HTTP) 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 一個 enforcer,lazy 建 | 比一個 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 端不直接改 catalog;seed JSON 是 SoT | --- ## 15. 後續工作 | 項目 | 預估 | |------|------| | Platform admin allowlist + audit log | 後續 | | RoleMapping 用 SyncFromX 落地(Zitadel / LDAP / SCIM)| 隨對應 SyncFromX usecase 推進 | | Policy reload cron worker(5 min) | 取自 svc 啟動 ticker | | Role permission 編輯 UI(不在 Gateway 內,由前端取資) | 前端 | | 細粒度欄位過濾(`.plain_code` 變體) | logic 層額外查 sub-permission |