template-monorepo/docs/identity-member-design.md

304 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.

# Identity / Member / Permission 跨模組架構
Gateway 三個業務模組(**auth** / **member** / **permission**)與外部 **ZITADEL**(身份)、**LDAP**(企業目錄)、**SCIM 2.0**(企業 provisioning的整合總覽。**內容只談跨模組層次的設計決策與關聯**;模組內部請看:
| 模組 | 文件 |
|------|------|
| auth | [`internal/model/auth/README.md`](../internal/model/auth/README.md) |
| member | [`internal/model/member/README.md`](../internal/model/member/README.md) |
| permission | [`internal/model/permission/README.md`](../internal/model/permission/README.md) |
| notification | [`internal/model/notification/README.md`](../internal/model/notification/README.md) |
分層公約:[`docs/model.md`](./model.md)。
---
## 1. 設計目標
| 目標 | 說明 |
|------|------|
| 統一身份 | ZITADEL 為 IdPOIDC、LDAP IdP、Social Login |
| 業務會員 | Gateway `member` 管 tenant-scoped profile |
| 細粒度授權 | Gateway `permission`Casbin RBAC + Permission TreeB2B 租戶可自定義 Role 並勾選 Permission |
| 對外 Token | Gateway 自簽 CloudEP JWTZITADEL OIDC token 只在 `/auth/token/exchange` 用一次 |
| 規模 | 全平台 100 萬+ 會員;單租戶可達 50 萬 |
| UID | 人類可讀帶租戶前綴(如 `ACME-10000003`);唯一鍵 `(tenant_id, uid)` |
---
## 2. 核心原則
1. **職責分離**
- `auth`你是誰Authentication
- `member`你的業務資料Profile
- `permission`你能做什麼Authorization
2. **LDAP 不做登入 bind**
登入驗證一律走 ZITADEL LDAP IdPGateway LDAP client 只做 Directory Syncread-only
3. **租戶隔離**
所有持久化資料以 `tenant_id` 為邊界JWT `tenant_id` 與請求資源不一致 → 403。
4. **B2B 權限自定義**
- 平台 seed 全局 Permission Tree`http_path` + `http_method`
- 租戶建自訂 Role從 Tree **勾選** Permission自動補 parent
- Casbin 以 `(tenant_id, role_key, path, method)` 比對,跨租戶同名角色互不污染
5. **身份驗證 vs 業務驗證分層**
| 級別 | 由誰負責 | 範例 |
|------|---------|------|
| 身份級 | ZITADEL Org Policy | 登入 MFA、註冊 email、忘記密碼、帳號鎖定 |
| 業務級 | Gateway `member` | 業務 email/phone OTP、TOTP step-up |
Gateway **不**讀 `ZITADEL.email_verified` 當業務守門;改讀 `member.business_email_verified`
6. **UseCase 原子性**
UseCase 為 atomic primitive禁止互相呼叫跨原子流程OTP → 寄信 → 驗碼 → flip flag一律在 `internal/logic/<module>/` 編排。
---
## 3. 模組依賴方向
```
handler → logic → model/{module}/domain/usecase介面
model/{module}/repository介面
MongoDB / Redis
logic 可同時呼叫多個 model 的 usecase編排
usecase 之間互不依賴
auth/logic → member.usecaseCreateUnverified / Activate / EnsureFromOIDC
auth/logic → permission.usecaseSyncRolesFromClaims未來
auth/logic → notification.NotifierOTP / 註冊信)
member/logic → notification.Notifier業務驗證、step-up OTP
permission → 獨立,不反向依賴
```
---
## 4. 外部系統分工
| 能力 | ZITADEL | auth | member | permission | notification |
|------|---------|------|--------|------------|---------------|
| OIDC / Social Login | ? | callback 換 JWT | EnsureFromOIDC | SyncRoles | — |
| 平台 Email + Password 註冊 | 建 userlocal | logic 編排 | LifecycleUseCase | — | 寄 OTP |
| 身份 MFA 強制 | ? Org Policy | — | — | — | — |
| LDAP 登入 | ? LDAP IdP | — | — | Group→Role | — |
| 對外 JWT | — | ? CloudEP | — | — | — |
| 業務 TOTP (Authenticator) | — | — | ? AES-GCM 存 secret | — | — |
| 業務 Email/Phone OTP | — | — | ? OTPUseCase | — | ? 模板 + 發送 |
| API 細粒度權限 | 粗 Role | — | — | ? Casbin | — |
| SCIM Users/Groups | 可同步 | — | 業務寫入 | Group→Role | — |
| Directory Sync | — | — | Worker | Group→Role | 同步異常告警 |
### 4.1 租戶 ? ZITADEL Org
```
1 CloudEP Tenant = 1 ZITADEL Organization = 1 資料隔離邊界
```
| 欄位 | 來源 | 用途 |
|------|------|------|
| `tenant_id` | Gateway 內部 | 分片鍵 / 授權邊界 |
| `tenant.org_id` | ZITADEL `org_id` | 身份系統對應 |
| `member.zitadel_user_id` | ZITADEL `sub` | 身份映射 |
| `member.uid` | `UIDGenerator` | 業務可讀主鍵 |
### 4.2 Tenant 建立Saga
```
1. POST /api/v1/admin/tenants
→ Mongo upsert tenants { status: provisioning, org_id: "" }
2. ZITADEL Mgmt.CreateOrganization(slug)
→ org_id
3. UPDATE tenants { org_id, status: active }
4. seed system roles + Casbin reload
失敗補償cron 每 5 分鐘):
- 步驟 2 失敗 → status=failed指數退避3 次後告警
- 步驟 3 失敗 → status=orphan_zitadel_org掃描補綁
```
### 4.3 租戶類型
| 類型 | 登入 | LDAP | 權限 |
|------|------|------|------|
| **B2C** | Email / Social | — | 系統預設 Role唯讀模板 |
| **B2B** | ZITADEL → LDAP IdP | ? | 完全自定義 Role + Permission |
| **Hybrid** | Social + LDAP | ? | B2B 自定義;外部客戶用 B2C 模板 |
---
## 5. UID 設計
| 規則 | 值 |
|------|----|
| 格式 | `{TenantUIDPrefix}-{Sequence}` |
| Prefix | 2~4 個大寫字母Tenant 建立時設,全域唯一) |
| 起始序號 | **10,000,000**7 位起跳,避免 `ACME-1` 這種短 UID |
| 唯一鍵 | `(tenant_id, uid)` |
| 範例 | `ACME-10000003` |
實作:`member.UIDGenerator` → Redis `INCR member:seq:{tenant}`;首次 INCR=1 時自動 `INCRBY 9_999_999` 補到起跳值。
---
## 6. Permission 模型
```
平台層 (Platform-wide) 租戶層 (per-tenant)
┌────────────────┐ ┌─────────┐
│ Permission │ ── 勾選 ──→ │ Role │
│ Catalog (Tree) │ │ │
└────────────────┘ │ ↓ 指派 │
│ UserRole│
│ │
│ ↑ 對映 │
│RoleMap │
└─────────┘
↓ LoadPolicy
Casbin Enforcer (Redis adapter)
CasbinRBAC Middleware
```
| 概念 | 說明 |
|------|------|
| Permission | 平台級節點(樹狀 dot notation。**Name 不可改**;廢棄走 `status=close` |
| Role | 租戶內角色,`(tenant_id, key)` unique**Key 不可改**(外部 IdP mapping 直接綁 Key |
| RolePermission | 全量取代式更新;自動補 parent permission |
| UserRole | `source` 區分 `manual` / `zitadel` / `ldap` / `scim``ReplaceForSource` 不洗 manual |
| RoleMapping | 外部 group/role → 內部 `role.key`,給 SyncFromX 翻譯 IdP claims |
| Casbin Rule | `[tenant, role.key, http_path, http_methods, perm.name]` |
**多 pod 同步**Redis Pub/Sub 即時 + 5 分鐘 cron 兜底(兜底建議在 svc 層排程,本模組不內建)。
---
## 7. Middleware 鏈
```
HTTP Request
[AuthJWT] ← 解析 Bearer → actor.WithActor(ctx, tenant, uid)
[CasbinRBAC] ← 必須在 AuthJWT 之後any-allow 過所有 open role
handler → logic → usecase
```
掛載方式:在 `.api``@server (middleware: AuthJWT,CasbinRBAC)` 宣告,`make gen-api` 會把 middleware 寫進 `routes.go`。**禁止** 在 `gateway.go``server.Use(...)` 全域掛。詳見 [`generate/api/README.md`](../generate/api/README.md#middleware-go-zero-正規手段)。
Actor 一律用 `internal/library/actor.WithActor` / `ActorFromContext`,不要在各 logic package 自定 `actorKey struct{}`
---
## 8. Notification 與業務驗證
> 詳細:[`internal/model/notification/README.md`](../internal/model/notification/README.md)、[`internal/model/member/README.md`](../internal/model/member/README.md)
| 流程 | 編排層 | 用到的 atomic |
|------|--------|---------------|
| 平台註冊 email OTP | `logic/auth.RegisterLogic` | `member.OTP.Generate` + `notifier.Send(verify_registration_email)` |
| 業務 email/phone 驗證 | `logic/member.startVerification` + `confirmVerification` | `VerifyRate``OTP.Generate``Notifier.Send``Profile.SetXxxVerified` |
| TOTP 綁定 / step-up | `logic/member.*_t_o_t_p_*` | `TOTP.StartEnroll` / `ConfirmEnroll` / `VerifyCode` |
> Notification 是 library-style domain由 ServiceContext 啟動 `RetryWorker`(異步重試 + DLQ。Provider chainSMTP / SES / Mitake / Mock依 `Sort` failover。
---
## 9. Rate Limit / Audit規劃
| 主題 | 現況 | 規劃 |
|------|------|------|
| OTP / 業務驗證 cooldown | ? 已實作(`member.VerifyRate`Redis SETNX + daily INCR | — |
| HTTP 全域 rate limit | 未實作 | 建議 go-zero middleware + Redis sliding-windowZSETkey = `rl:{dim}:{path}` |
| Audit Log | 未實作 | `audit_logs` collection獨立 DB 建議);`critical` 同步寫,`info` 批次TTL 90d |
---
## 10. 規模考量100 萬+ 會員)
| 項目 | 建議 |
|------|------|
| `members` 分片 | hashed `tenant_id`(單租戶 50 萬可走複合 unique index 不分片) |
| Casbin policy | 一 tenant 一 enforcerlazy 建立Redis Set 儲存 rule |
| JWT 黑名單 | 只黑名單已撤銷的 jtiTTL = 至自然過期 |
| Member list 查詢 | 強制帶 `tenant_id` + `(create_at, _id)` cursor 分頁 |
| Notification DLQ | 超過 `MaxRetry` 寫入 `notification_dlq`admin CLI 重試 |
---
## 11. 已決策事項
| 主題 | 決策 |
|------|------|
| Tenant 建立順序 | Gateway 先建草稿status=provisioning→ ZITADEL CreateOrg → 補 `org_id`;補償 cron |
| Permission name 改名 | 禁止(被 RolePermission / Casbin policy.name 引用);廢棄改 `status=close` |
| Role.Key 改名 | 禁止(外部 IdP mapping 直接綁) |
| `is_system` Role | 不可刪、不可改 status |
| `manual` source ReplaceForSource | 拒絕(防 SyncFromX 誤洗手動指派) |
| Gateway 統一註冊 | 取代 ZITADEL Hosted Pageemail + social 同 UXinvite 由 logic 驗 |
| Email / SMS OTP | 由 Gateway `notification` 自送,**不**轉 ZITADEL Notification |
| 業務 TOTP | 與 ZITADEL TOTP 獨立secret AES-GCM 存 Mongo |
| Email 驗證守門 | 讀 `member.business_email_verified`**不**讀 ZITADEL `email_verified` |
| UseCase 不互呼 | 強制:跨原子流程在 logic 編排model.md §6.1 |
| Casbin 多 pod 同步 | Redis Pub/Sub 即時 + 5min cron 兜底 |
| Permission Catalog 變更 | seed CLI`cmd/permission-seed`UI 不可直接改 |
---
## 12. 目錄結構
```
internal/
├── library/
│ ├── actor/ # ctx actor helper跨 module 共用 key
│ ├── crypto/ # AES-GCM cipherTOTP secret KEK
│ ├── errors/ # 8 碼 SSCCCDDD
│ ├── mongo/ # DocumentDB + cache
│ ├── redis/ # go-zero client + Pub/Sub
│ ├── validate/ # struct 驗證
│ └── zitadel/ # ZITADEL HTTP client
├── middleware/
│ ├── authjwt_middleware.go # 解 JWT → actor
│ └── casbinrbac_middleware.go # any-allow RBAC
├── model/
│ ├── auth/ # 邀請碼 / 註冊 metadata / OAuth session / JWT
│ ├── member/ # Tenant / Member / Identity / OTP / TOTP / UID
│ ├── notification/ # Email / SMS / 模板 / Worker / DLQ
│ └── permission/ # Permission Catalog / Role / RolePermission / Casbin
└── worker/
└── notification_retry/ # Notification RetryWorker runner
```
---
## 13. 設定檔
主要區塊(完整見 [`etc/README.md`](../etc/README.md) + `etc/gateway.dev.example.yaml`
```yaml
Mongo: {...}
Redis: {...}
Auth: # JWT secret / TTL / RegistrationSessionTTLSeconds
Member: # OTP / TOTP / Registration
Permission: # Casbin.Enabled / ModelPath / PolicyAdapter / Cache / Reload
Notification:# Email / SMS / Async / RatePerTenant
Zitadel: # Issuer / ServiceUserToken / Google* / DefaultOrgID
```
---
## 14. 進度速覽
| 模組 | 狀態 | 備註 |
|------|------|------|
| auth | ? 已實作 | 統一註冊 (Email + Google) / login / token refresh / logout |
| member | ? 已實作 | profile / 業務 email/phone OTP / TOTP 全流程 |
| permission | ? 已實作 | Catalog / Role CRUD / RolePermission / UserRole / RoleMapping / Casbin RBAC |
| notification | ? 核心完成 | Email/SMS sync+async + DLQHTTP admin API 未做 |
| LDAP Directory Sync | ? 未實作 | 依 SCIM / SyncFromX 推進 |
| SCIM 2.0 server | ? 未實作 | 依企業客戶需求 |
| Audit Log | ? 未實作 | 規劃 collection + TTL 90d |
| HTTP Rate Limit middleware | ? 未實作 | OTP cooldown 已有;全域待補 |