644 lines
24 KiB
Markdown
644 lines
24 KiB
Markdown
|
|
# 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 一旦建立 **不可改**;外部 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<br/>※ 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 |
|