# 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//` 編排。 --- ## 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 已有;全域待補 |