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

334 lines
12 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 模組
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)。