304 lines
12 KiB
Markdown
304 lines
12 KiB
Markdown
# 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 為 IdP(OIDC、LDAP IdP、Social Login) |
|
||
| 業務會員 | Gateway `member` 管 tenant-scoped profile |
|
||
| 細粒度授權 | Gateway `permission`(Casbin RBAC + Permission Tree);B2B 租戶可自定義 Role 並勾選 Permission |
|
||
| 對外 Token | Gateway 自簽 CloudEP JWT;ZITADEL 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 IdP;Gateway LDAP client 只做 Directory Sync(read-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.usecase(CreateUnverified / Activate / EnsureFromOIDC)
|
||
auth/logic → permission.usecase(SyncRolesFromClaims,未來)
|
||
auth/logic → notification.Notifier(OTP / 註冊信)
|
||
member/logic → notification.Notifier(業務驗證、step-up OTP)
|
||
permission → 獨立,不反向依賴
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 外部系統分工
|
||
|
||
| 能力 | ZITADEL | auth | member | permission | notification |
|
||
|------|---------|------|--------|------------|---------------|
|
||
| OIDC / Social Login | ? | callback 換 JWT | EnsureFromOIDC | SyncRoles | — |
|
||
| 平台 Email + Password 註冊 | 建 user(local) | 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 chain:SMTP / 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-window(ZSET),key = `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 一 enforcer,lazy 建立;Redis Set 儲存 rule |
|
||
| JWT 黑名單 | 只黑名單已撤銷的 jti,TTL = 至自然過期 |
|
||
| 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 Page;email + social 同 UX,invite 由 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 cipher(TOTP 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 + DLQ;HTTP admin API 未做 |
|
||
| LDAP Directory Sync | ? 未實作 | 依 SCIM / SyncFromX 推進 |
|
||
| SCIM 2.0 server | ? 未實作 | 依企業客戶需求 |
|
||
| Audit Log | ? 未實作 | 規劃 collection + TTL 90d |
|
||
| HTTP Rate Limit middleware | ? 未實作 | OTP cooldown 已有;全域待補 |
|