334 lines
12 KiB
Markdown
334 lines
12 KiB
Markdown
# Permission 模組
|
||
|
||
Gateway 多租戶 **B2B 自定義 RBAC**:平台級 Permission Catalog + 租戶級 Role / RolePermission / UserRole / RoleMapping + Casbin enforcer。
|
||
|
||
- **規格書**(Data Dictionary、API 端點欄位、Casbin model)→ [`SDD.md`](./SDD.md)
|
||
- **跨模組總覽** → [`docs/identity-member-design.md`](../../../docs/identity-member-design.md) §6
|
||
- 本 README = 流程圖 + curl + ServiceContext 速查
|
||
|
||
---
|
||
|
||
## TL;DR
|
||
|
||
```mermaid
|
||
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)
|
||
|
||
```mermaid
|
||
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 同步)
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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`)
|
||
|
||
```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/*`
|
||
- `regexMatch`:`GET|POST|PATCH` 多 method 同 policy
|
||
- 平台 Admin bypass 不寫在 matcher,由 middleware 預檢(保留 audit)
|
||
|
||
---
|
||
|
||
## ServiceContext 注入
|
||
|
||
```go
|
||
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`)
|
||
|
||
```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 / 操作
|
||
|
||
```bash
|
||
# 建索引
|
||
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 |
|
||
|
||
---
|
||
|
||
## 測試
|
||
|
||
```bash
|
||
# 單元
|
||
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`](../../../docs/e2e-testing.md)。
|