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

334 lines
12 KiB
Markdown
Raw Normal View History

# 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 + Rediscasbin 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 clientgo-zero 沒包 Subscribe且 Subscribe 會佔住 conn
---
## 關鍵流程
### 1. RolePermission 全量取代PUT /roles/:id/permissions
```mermaid
sequenceDiagram
API->>UC: Replace(tenantID, roleID, ids)
UC->>Roles: GetByIDtenant check
UC->>Perms: GetAllcatalog 全表)
UC->>UC: ids ⊆ catalog?
UC->>UC: getFullParentPermissionIDs(ids)
UC->>RP: SetForRoleDeleteMany + 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: GetByIDsunique 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 rolesany-allow
RBAC->>Enforcer: EnforceEx
end
RBAC-->>MW: Allow / Deny403
```
### 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 // RBACUseCaseMongo+Redis 全到位才有)
sc.PermissionRoleRepo // 給 SCIM / SyncFromX 等下游使用
```
未啟用 Casbin 時 `PermissionRBAC == nil``Check()` 永遠 denymiddleware 視 `AllowMissingActor` 決定放行或拒絕。
---
## HTTP API`/api/v1/permissions`
| Method | Path | Middleware | 說明 |
|--------|------|------------|------|
| GET | `/catalog` | AuthJWT | 全局 Catalogtree/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 rolecatalog 已存在)
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 CLIidempotent | 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)。