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

45 KiB
Raw Blame History

Identity / Member / Permission 模組設計草稿

狀態Draft待 Review
適用專案Portal API GatewayPGW
參考實作app-cloudep-permission-serverCasbin RBAC、Permission Tree、Role/RolePermission
最後更新2026-05-19
前提:全新 Gateway module不考慮舊版 member-server 遷移。

本文件描述 Gateway 內 authmemberpermission 三個業務模組的目標架構,整合 ZITADEL(身份)、LDAP(企業目錄)、SCIM 2.0(企業 provisioning支援 多租戶百萬級會員(含單租戶 50 萬)。

模組分層與程式碼撰寫規範見 model.md


目錄

  1. 設計目標與原則
  2. 模組全景
  3. 外部系統分工
  4. auth 模組
  5. member 模組
  6. permission 模組B2B 自定義)
  7. API 規劃
  8. Middleware 鏈
  9. 核心流程
  10. LDAP 與 SCIM
  11. 可讀 UID 設計
  12. 資料模型與索引
  13. Redis Key 命名
  14. 規模與性能100 萬+ / 單租戶 50 萬)
  15. 目錄結構
  16. 設定檔
  17. 實施順序
  18. 待決策事項

1. 設計目標與原則

1.1 目標

目標 說明
統一身份 ZITADEL 作為 IdP含 LDAP IdP、Social Login
業務會員 Gateway member 模組管理 tenant-scoped profile
細粒度授權 Gateway permission 模組(Casbin RBAC + Permission Tree每個 B2B 租戶可自定義 Role 並勾選 Permission
Token go-zero JWT 驗證 + Redis 黑名單(只黑名單 JWT
企業整合 SCIM 2.0 + LDAP Directory SyncAD + OpenLDAP
規模 全平台 100 萬+ 會員;單租戶可達 50 萬
UID 人類可讀、可口述,非 UUID / 亂碼

1.2 核心原則

  1. 職責分離

    • auth你是誰Authentication
    • member你的業務資料是什麼Profile
    • permission你能做什麼Authorization
  2. LDAP 不做登入 bind

    • 登入驗證由 ZITADEL LDAP IdP 處理
    • Gateway 的 LDAP client 僅供 Directory Syncread-only
  3. Token Exchange

    • 對外 API 只接受 Gateway 簽發的 CloudEP JWT
    • ZITADEL OIDC token 僅在 /auth/token/exchange 使用一次
  4. 租戶隔離

    • 所有持久化資料以 tenant_id 為邊界
    • JWT tenant_id 與請求資源必須一致
  5. B2B 權限自定義(參考 app-cloudep-permission-server

    • 平台 seed 全局 Permission Treehttp_path / http_method
    • 租戶建立自訂 Role從 Tree 勾選 PermissionRolePermission + 自動補 parent
    • API 授權由 Casbin 比對 (role.Name, path, method)
    • B2C 租戶仅用 seed 模板

2. 模組全景

┌─────────────────────────────────────────────────────────────────┐
│                     Portal API Gateway (go-zero)                   │
├─────────────────────────────────────────────────────────────────┤
│  generate/api/                                                   │
│    auth.api · member.api · permission.api · tenant.api · scim.api│
├─────────────────────────────────────────────────────────────────┤
│  internal/middleware/                                            │
│    jwt_revoke · casbin_rbac · scim_auth · tenant_context           │
├─────────────────────────────────────────────────────────────────┤
│  internal/model/                                                 │
│    auth/        → Token 簽發、換票、登出、黑名單、auth_gen         │
│    member/      → Profile、Identity、Tenant、UID、Directory Sync │
│    permission/  → Casbin RBAC、Permission Tree、RoleB2B 自定義)│
├─────────────────────────────────────────────────────────────────┤
│  internal/library/                                               │
│    zitadel/ · ldap/ · uid/ · casbin/RBAC enforcer 封裝)        │
├─────────────────────────────────────────────────────────────────┤
│  internal/worker/                                                │
│    directory_sync/                                               │
└─────────────────────────────────────────────────────────────────┘
         │                    │                    │
         ▼                    ▼                    ▼
     MongoDB               Redis              ZITADEL
   (profile/role)      (cache/blacklist)    (identity/LDAP IdP)

2.1 模組依賴方向

handler → logic → model/{auth|member|permission}/usecaseinterface
                      ↓
                repository → MongoDB / Redis

logic 不 import entity / repository見 model.md

auth      → memberEnsureMember
auth      → permissionSyncRolesFromClaims
member    → auth停權時 RevokeAllForUser
permission → member可選驗證 uid 存在)

3. 外部系統分工

能力 ZITADEL Gateway auth Gateway member Gateway permission
註冊 / 登入 換票 EnsureMember SyncRoles
密碼 / MFA / 忘記密碼
Google / LINE / Apple IdP
LDAP 登入 LDAP IdP Group→Role 映射
Access / Refresh Token對外 CloudEP JWT
JWT 黑名單 Redis
業務 UID
Profile
會員列表 / 狀態 需授權
API 細粒度權限 粗粒度 Role Casbin RBACpath + method
SCIM Users/Groups 可同步 業務寫入 Group→Role
LDAP Directory Sync Worker Group→Role

3.1 多租戶對應

1 CloudEP Tenant  =  1 ZITADEL Organization  =  1 資料隔離邊界
欄位 來源 用途
tenant_id ZITADEL org_id 分片鍵、授權邊界
identity_id ZITADEL sub 身份映射
uid Member 模組產生 業務會員 IDACME-ODWXGYBK

3.2 租戶類型

類型 登入 LDAP 權限
B2C Email / Social 系統預設 Role不可或不常自定義
B2B ZITADEL → LDAP IdP 完全自定義 Role + Permission
Hybrid Social + LDAP B2B 自定義 + 外部客戶用預設 Role

4. auth 模組

路徑:internal/model/auth/

4.1 職責

  • 驗證 ZITADEL OIDC tokenid_token / authorization_code + PKCE
  • 編排 member.EnsureMemberpermission.SyncRolesFromClaims
  • 簽發 CloudEP JWTaccess + refresh
  • 登出jti 黑名單
  • 批量失效:auth_gen(停權 / 改密碼 / 權限強制刷新)

4.2 UseCase 介面

type TokenUseCase interface {
    Exchange(ctx context.Context, req *ExchangeRequest) (*TokenPair, error)
    Refresh(ctx context.Context, req *RefreshRequest) (*TokenPair, error)
    Logout(ctx context.Context, req *LogoutRequest) error
    RevokeAllForUser(ctx context.Context, tenantID, uid string) error
}

4.3 CloudEP JWT Claims

type Claims struct {
    jwt.RegisteredClaims          // 含 jti, exp, iat
    TenantID string `json:"tenant_id"`
    UID      string `json:"uid"`
    Roles    string `json:"roles"`     // 逗號分隔 Role.NameCasbin enforce 用
    Typ      string `json:"typ"`       // access | refresh
    AuthGen  int64  `json:"auth_gen"`  // 批量失效代號
}

4.4 JWT 設定go-zero

Auth:
  AccessSecret: ${JWT_ACCESS_SECRET}
  AccessExpire: 900          # 15 分鐘

RefreshAuth:
  AccessSecret: ${JWT_REFRESH_SECRET}
  AccessExpire: 604800       # 7 天

.api 受保護路由:

@server(jwt: Auth, middleware: JwtRevokeMiddleware)

4.5 黑名單策略(只黑名單 JWT

單 Token 撤銷(登出)

Key:   auth:jwt:bl:{jti}
Value: 1
TTL:   token 剩餘有效時間exp - now

登出時同時黑名單 access + refresh 的 jti。

批量失效(停權 / 改密碼 / SCIM deactivate / 角色強刷)

Key:   auth:gen:{tenant_id}:{uid}
Value: 整數,預設 1事件發生時 INCR

Middleware 檢查:token.auth_gen >= redis.auth_gen,否則 401。

JWT 內不放全部 permission避免 token 過大);批量失效用 auth_gen,單次登出用 jti 黑名單。

4.6 Middleware 檢查順序

1. go-zero JWT 驗簽 + exp
2. typ == "access"(受保護 API
3. NOT EXISTS auth:jwt:bl:{jti}
4. claims.auth_gen >= redis auth:gen:{tenant}:{uid}
5. 注入 contexttenant_id, uid, roles

5. member 模組

路徑:internal/model/member/

5.1 職責

  • 會員 Profile CRUDtenant-scoped
  • Identity 映射(zitadel_subuid
  • Tenant metadata 與 LDAP 同步設定
  • UID 產生(可讀格式)
  • SCIM 業務寫入User + externalId = uid
  • Directory Sync WorkerAD + OpenLDAP
  • 會員狀態active / suspended→ 通知 auth 撤銷 token

5.2 UseCase 介面

type ProvisioningUseCase interface {
    EnsureMember(ctx context.Context, req *EnsureMemberRequest) (*MemberDTO, error)
}

type ProfileUseCase interface {
    GetByUID(ctx context.Context, req *GetMemberRequest) (*MemberDTO, error)
    Update(ctx context.Context, req *UpdateMemberRequest) (*MemberDTO, error)
    List(ctx context.Context, req *ListMembersRequest) (*ListMembersResponse, error)
}

type AdminUseCase interface {
    UpdateStatus(ctx context.Context, req *UpdateStatusRequest) error
}

type TenantUseCase interface {
    Create(ctx context.Context, req *CreateTenantRequest) (*TenantDTO, error)
    ResolveBySlug(ctx context.Context, slug string) (*TenantDTO, error)
    ConfigureLDAP(ctx context.Context, req *ConfigureLDAPRequest) error
}

type ScimUseCase interface {
    CreateUser(ctx context.Context, req *ScimCreateUserRequest) (*ScimUserDTO, error)
    GetUser(ctx context.Context, req *ScimGetUserRequest) (*ScimUserDTO, error)
    PatchUser(ctx context.Context, req *ScimPatchUserRequest) (*ScimUserDTO, error)
    DeleteUser(ctx context.Context, req *ScimDeleteUserRequest) error
    // Groups供 SCIM Group → Role 映射)
    PatchGroup(ctx context.Context, req *ScimPatchGroupRequest) error
}

type DirectorySyncUseCase interface {
    SyncTenant(ctx context.Context, tenantID string) (*SyncResult, error)
}

5.3 會員狀態

狀態 語意 副作用
unverified 尚未完成業務驗證
active 正常使用
suspended 停權 auth.RevokeAllForUser

6. permission 模組B2B 自定義,參考 permission-server

路徑:internal/model/permission/

本節吸收 app-cloudep-permission-server 已驗證的設計:Casbin + Redis RBACPermission Tree父子繼承HTTP Path/Method 綁定
與舊 permission-server 的差異Token 簽發/驗證/黑名單移至 Gateway auth 模組;ClientID 改為 tenant_id;支援多租戶 B2B 各自定義 Role。

6.1 設計目標

能力 說明
Permission Tree 全局權限樹(平台 seed父子節點繼承父節點關閉則子節點不可用
Casbin RBAC (role, http_path, http_method) 做 API 授權path 支援 keyMatch2 萬用字元
B2B 自定義 Role 每個租戶建立自訂 Role從全局 Catalog 勾選 Permission不可自創 Permission 字串)
UserRole 租戶 + uid + role支援多角色JWT 帶 role namesCasbin 逐一檢查)
RolePermission 勾選子權限時自動補齊父權限 ID沿用 permission-server 的 getFullParentPermissionIDs
Policy 同步 MongoDB → Casbin Policy → Redis定時 LoadPolicy + 變更時觸發 reload
外部映射 ZITADEL Role / LDAP Group / SCIM Group → 租戶內部 Role
細粒度擴展 同一 API 可掛 .plain_code 子權限(如明碼查詢),沿用舊設計

6.2 與 app-cloudep-permission-server 對照

permission-server Gateway permission 模組 備註
TokenService auth 模組 JWT 不再放 permission-server
PermissionService(空) 完整 HTTP API 直接在 Gateway 暴露
entity.Permission 沿用 + tenant_id 不適用(全局 Catalog Permission 為平台級
entity.Role.ClientID Role.TenantID 租戶隔離
entity.Role.UID Role.CreatorUID 建立者,可選
entity.Role.Name Role.Name Casbin policy 的 role 欄位
Casbin Enforcer RBACUseCase + Redis Adapter 沿用
PermissionTree usecase/permission_tree.go 沿用
AdminRoleUID / GodDog PlatformSuperAdminUID 平台超級管理員 bypass
permission.Type enum.PermissionType BackendUser / FrontendUser

6.3 核心概念

Permission全局樹  平台定義,含 name / http_path / http_method / parent / status / type
Role租戶自定義    租戶建立的命名角色,如 sales_supervisor、tenant_admin
RolePermission       Role ↔ Permission ID 多對多;勾選時自動補父節點
UserRole             uid ↔ Role一 user 可多 role
RoleMapping          外部 Group/Role → 內部 Role.Name
Casbin Policy        p, {role.Name}, {http_path}, {http_method}, {permission.Name}

6.4 Permission Entity全局 Catalog

沿用 permission-server 的 entity.Permission 結構MongoDB collectionpermission

type Permission struct {
    ID         primitive.ObjectID
    Parent     string             // 父權限 IDObjectID hex空 = 掛 root
    Name       string             // 唯一語意名dot notation如 member.info.select
    HTTPMethod string             // GET / POST / PATCH / DELETE / PUT分類節點可為空
    HTTPPath   string             // 如 /api/v1/members/*;分類節點可為空
    Status     enum.Status        // open | close
    Type       enum.PermissionType // backend_user | frontend_user後台 / 前台菜單)
    CreateAt   int64
    UpdateAt   int64
}

命名規則dot notation與 permission-server 一致)

{domain}.{module}.{action}
{domain}.{module}.{action}.{variant}   # 如 .plain_code

Permission Tree 範例seed 草案)

member.info.management          # 一級:會員資訊管理(分類,無 HTTP
├── member.basic.info           # 二級:基礎資訊
│   ├── member.info.select      # GET  /api/v1/members/me
│   ├── member.info.update      # PATCH /api/v1/members/me
│   └── member.info.select.plain_code  # GET /api/v1/members明碼欄位
├── member.admin.list           # GET  /api/v1/members
├── member.admin.read           # GET  /api/v1/members/:uid
├── member.admin.update         # PATCH /api/v1/members/:uid
└── member.admin.status         # PATCH /api/v1/members/:uid/status

permission.role.management      # 一級:角色權限管理
├── permission.role.read        # GET  /api/v1/permissions/roles
├── permission.role.write       # POST/PUT/DELETE roles
├── permission.assign.write     # POST/DELETE user roles
└── permission.catalog.read     # GET  /api/v1/permissions/catalog

tenant.management
├── tenant.read
├── tenant.ldap.write
└── tenant.sync.trigger

scim.management
├── scim.users.write
└── scim.groups.write

system.management               # 平台級
└── system.tenant.create

分類節點(無 http_path)供 UI 樹狀勾選;葉節點才寫入 Casbin Policy。
新增 Permission 走平台 seed migration租戶不可自行新增 Permission 名稱。

Permission Tree 行為(沿用 permission-server

  1. filterOpenNodes:父節點 status=close → 整棵子樹不可用
  2. getFullParentPermissionIDs:勾選子權限 → 自動加入所有父節點 ID
  3. getFullParentPermission:查 Role 權限 → 回傳含父節點的完整 permission name → status map供前端 UI

6.5 Role EntityB2B 租戶自定義)

type Role struct {
    ID         primitive.ObjectID
    TenantID   string             // 租戶 ID= 舊 ClientID
    Name       string             // 角色名稱租戶內唯一Casbin enforce 用此值
    CreatorUID string             // 建立者 uid= 舊 Role.UID可選
    Status     enum.Status        // open | close
    IsSystem   bool               // 系統 seed 的預設角色B2B 可改 Permission 但不可刪除 Owner
    CreateAt   int64
    UpdateAt   int64
}
// Index: { tenant_id, name } unique

B2B 自定義規則

  1. 租戶可 CRUD 自訂 Roleis_system=false
  2. 系統 seed 的預設 Roleis_system=true)可修改 Permission 集合,tenant_owner 不可刪
  3. Role 綁定的 Permission 必須是全局 Catalog 中 status=open 的節點
  4. 租戶不可勾選 system.* 權限(除非平台另行開啟)
  5. 至少保留一個 Role 含 permission.role.write,避免租戶自鎖

預設 Role 模板(建立 B2B tenant 時 seed

Name 說明 預設勾選Permission Name
tenant_owner 租戶擁有者 system.* 外全部 open 節點
tenant_admin 租戶管理員 member., permission., tenant., scim.
member_manager 會員管理 member.admin.list, member.admin.read, member.admin.status
member 一般會員 member.info.select, member.info.update
viewer 唯讀 member.info.select

B2B 管理員範例:

建立 Rolesales_supervisor
勾選member.admin.list, member.admin.read
指派POST /permissions/users/{uid}/roles { "role_name": "sales_supervisor" }
→ RolePermission.Create → getFullParentPermissionIDs 自動補 parent
→ LoadPolicy 刷新 Casbin

6.6 UserRole / RolePermission

type UserRole struct {
    TenantID string
    UID      string
    RoleID   string   // Role._id hex
    Source   enum.RoleSource  // manual | zitadel | ldap | scim
    CreateAt int64
    UpdateAt int64
}
// Index: { tenant_id, uid, role_id } unique
// Index: { tenant_id, uid }

type RolePermission struct {
    RoleID       string
    PermissionID string
    CreateAt     int64
    UpdateAt     int64
}
// Index: { role_id, permission_id } unique

舊 permission-server 的 UserRole 為一 user 一 roleUpdate 覆蓋);新設計支援多角色Middleware 對每個 role name 做 Casbin enforce任一 allow 即通過。

6.7 Casbin RBAC核心授權引擎

模型檔 etc/rbac.conf(沿用 permission-server

[request_definition]
r = role, path, method

[policy_definition]
p = role, path, methods, name

[policy_effect]
e = some(where (p.eft == allow))

[role_definition]
g = _, _

[matchers]
m = g(r.role, p.role) && keyMatch2(r.path, p.path) && regexMatch(r.method, p.methods) \
    || r.role == "${PlatformSuperAdminUID}"
  • keyMatch2:支援 /api/v1/members/* 萬用 path
  • regexMatch:支援 GET|POST 多 method 寫在同一 policy
  • SuperAdmin bypass:平台維運 uid 全放行(對應舊 GodDog

Policy 載入(RBACUseCase.LoadPolicy

1. permissionRepo.GetAll → GeneratePermissionTree → filterOpenNodes
2. roleRepo.All(tenant_id) → 每個 role 取 rolePermissionRepo.Get
3. 對每個 (role, permission) 若 http_path + http_method 非空:
     enforcer.AddPolicy(role.Name, permission.HTTPPath, permission.HTTPMethod, permission.Name)
4. adapter.SavePolicy → Redis Listcasbin rules
5. enforcer.LoadPolicy()

授權檢查(RBACUseCase.Check

// 輸入roleName, requestPath, requestMethod
ok, policy, err := enforcer.EnforceEx(roleName, path, method)

// 回傳 CheckRolePermissionStatus
//   Allow: bool
//   PermissionName: string   // 命中的 permission.Name
//   PlainCode: bool           // 是否有 .plain_code 子權限GET 時額外查)

Policy 同步策略

觸發 動作
RolePermission 變更 該 tenant LoadPolicy
Permission status 變更(平台) 全局 LoadPolicy
定時 cron如 5min SyncPolicy 兜底
Gateway 啟動 初始 LoadPolicy

Redis 儲存 Casbin rulespermission:casbin:rulesList of JSON rbac.Rule

6.8 UseCase 介面

// --- Casbin 授權(核心)---
type RBACUseCase interface {
    Check(ctx context.Context, req *CheckRequest) (*CheckResult, error)
    LoadPolicy(ctx context.Context, tenantID string) error
    LoadAllPolicies(ctx context.Context) error
    SyncPolicy(ctx context.Context, interval time.Duration)
}

type CheckRequest struct {
    TenantID string
    UID      string
    Path     string   // 實際請求 path
    Method   string   // 實際 HTTP method
}

type CheckResult struct {
    Allow          bool
    PermissionName string
    PlainCode      bool
    MatchedRole    string
}

// --- Permission Catalog平台級---
type PermissionUseCase interface {
    All(ctx context.Context, status *enum.Status) ([]PermissionDTO, error)
    FilterAll(ctx context.Context) ([]PermissionDTO, error)  // 樹狀過濾 open 節點
    Insert(ctx context.Context, req *CreatePermissionRequest) error  // 平台 Admin
    Update(ctx context.Context, id string, req *UpdatePermissionRequest) error
}

// --- Role租戶級B2B 自定義)---
type RoleUseCase interface {
    List(ctx context.Context, req *ListRolesRequest) ([]RoleDTO, int64, error)
    All(ctx context.Context, tenantID string) ([]RoleDTO, error)
    GetByID(ctx context.Context, tenantID, id string) (*RoleDTO, error)
    Create(ctx context.Context, req *CreateRoleRequest) error
    Update(ctx context.Context, id string, req *UpdateRoleRequest) error
    Delete(ctx context.Context, tenantID, id string) error
}

// --- RolePermission ---
type RolePermissionUseCase interface {
    Get(ctx context.Context, roleID string) (enum.Permissions, error)  // name → open/close
    Create(ctx context.Context, roleID string, perms enum.Permissions) error
    Delete(ctx context.Context, roleID string, perms enum.Permissions) error
}

// --- UserRole ---
type UserRoleUseCase interface {
    GetByUID(ctx context.Context, tenantID, uid string) ([]UserRoleDTO, error)
    Assign(ctx context.Context, tenantID, uid, roleID string) error
    Revoke(ctx context.Context, tenantID, uid, roleID string) error
    Replace(ctx context.Context, tenantID, uid string, roleIDs []string) error
}

// --- 外部映射 ---
type RoleMappingUseCase interface {
    List(ctx context.Context, tenantID string) ([]RoleMappingDTO, error)
    Upsert(ctx context.Context, req *UpsertRoleMappingRequest) error
    Delete(ctx context.Context, tenantID, id string) error
    SyncFromZitadelClaims(ctx context.Context, req *SyncFromZitadelRequest) error
    SyncFromScimGroup(ctx context.Context, req *SyncFromScimGroupRequest) error
    SyncFromLDAPGroups(ctx context.Context, req *SyncFromLDAPGroupsRequest) error
}

// --- 聚合查詢(前端菜单)---
type AuthorizationQueryUseCase interface {
    GetMyPermissions(ctx context.Context, tenantID, uid string) (enum.Permissions, error)
    GetMyRoles(ctx context.Context, tenantID, uid string) ([]string, error)
}

6.9 Middleware 授權流程

RequestJWT 已驗證)
  1. 取 ctx.tenant_id, ctx.uid
  2. userRoleUC.GetByUID → []Role.Name
  3. 對每個 roleName
       rbacUC.Check(tenantID, uid, roleName, r.URL.Path, r.Method)
  4. 任一 Allow=true → 通過,注入 ctx.permission_name, ctx.plain_code
  5. 全否 → 403 Forbidden
  6. SuperAdmin uid → 直接通過

Logic 層可讀 ctx.plain_code 決定是否回傳明碼欄位(沿用 permission-server 的 PlainCode 模式)。

6.10 外部 Group / Role 映射

type RoleMapping struct {
    TenantID       string
    ExternalSource enum.RoleSource  // zitadel | ldap | scim
    ExternalKey    string           // ZITADEL role / LDAP group DN / SCIM group id
    InternalRole   string           // 租戶 Role.Name
}
// Index: { tenant_id, external_source, external_key } unique
來源 ExternalKey 範例 映射到
ZITADEL org_admin tenant_admin
LDAP (AD) CN=CloudEP-Admins,OU=Groups,DC=acme,DC=com 租戶自訂 Role.Name
LDAP (OpenLDAP) cn=admins,ou=groups,dc=acme,dc=com 租戶自訂 Role.Name
SCIM Group group-uuid-xxx 租戶自訂 Role.Name

由 B2B 租戶管理員在後台設定(需命中 permission.role.write 對應 API

6.11 權限變更生效

事件 動作
RolePermission Create/Delete LoadPolicy(tenant_id)
Role Create/Update/Delete LoadPolicy(tenant_id)
UserRole Assign/Revoke 可選 INCR auth:gen(立即踢下线)
SCIM / LDAP Group 變更 更新 user_roles → LoadPolicy + INCR auth_gen
Permission status 變更(平台) LoadAllPolicies()

6.12 B2C vs B2B 權限策略

租戶類型 Role 自定義 Permission 勾選
B2C 不可(只用 seed 模板) 固定
B2B 完全自定義 從全局 Catalog 自由勾選
Hybrid B2B 部分可自定義 依 tenant 設定

7. API 規劃

檔案:generate/api/

7.1 auth.api公開

Method Path 說明
POST /api/v1/auth/token/exchange ZITADEL token → CloudEP JWT
POST /api/v1/auth/token/refresh 刷新 JWT
POST /api/v1/auth/logout 登出jti 黑名單)

7.2 member.api需 JWT + Casbin

Method Path Casbin 命中 Permission示例
GET /api/v1/members/me member.info.select
PATCH /api/v1/members/me member.info.update
GET /api/v1/members member.admin.list
GET /api/v1/members/:uid member.admin.read
PATCH /api/v1/members/:uid member.admin.update
PATCH /api/v1/members/:uid/status member.admin.status

授權由 Casbin 比對實際 path + method 決定,非硬編碼 permission 字串。

7.3 permission.api需 JWT + Casbin

Method Path 說明
GET /api/v1/permissions/catalog 全局 Permission Treeopen 節點)
GET /api/v1/permissions/me 當前用户的 permission name → status map
GET /api/v1/permissions/roles 列出租戶 Role
POST /api/v1/permissions/roles 建立 RoleB2B
PUT /api/v1/permissions/roles/:id 更新 Role
DELETE /api/v1/permissions/roles/:id 刪除 Role
GET /api/v1/permissions/roles/:id/permissions 取得 Role 勾選的 Permission
PUT /api/v1/permissions/roles/:id/permissions 更新 Role 勾選PermissionTree 驗證 + 補 parent
GET /api/v1/permissions/users/:uid/roles 查用户角色
POST /api/v1/permissions/users/:uid/roles 指派 Role { "role_id": "..." }
DELETE /api/v1/permissions/users/:uid/roles/:role_id 撤銷 Role
GET /api/v1/permissions/role-mappings 外部 Group 映射列表
PUT /api/v1/permissions/role-mappings 新增/更新映射
POST /api/v1/permissions/policy/reload 手動觸發 LoadPolicy平台 Admin

7.4 tenant.api平台 / 租戶 Admin

Method Path Casbin 命中 Permission示例
POST /api/v1/admin/tenants system.tenant.create
GET /api/v1/admin/tenants/:tenant_id tenant.read
PUT /api/v1/admin/tenants/:tenant_id/ldap tenant.ldap.write
POST /api/v1/admin/tenants/:tenant_id/directory-sync tenant.sync.trigger

7.5 scim.apiSCIM Bearer Token非 JWT

/scim/v2/tenants/{tenant_id}/Users
/scim/v2/tenants/{tenant_id}/Groups

認證:Authorization: Bearer {tenant_scim_token}hash 存於 tenant 設定)

SCIM 請求授權可透過 tenant 級 token 隱含 scim:*,或細分 scim users/groups permission。


8. Middleware 鏈

8.1 一般受保護 API

Request
  → go-zero JWT 驗簽
  → JwtRevokeMiddlewarejti 黑名單 + auth_gen
  → TenantContextMiddleware校驗 tenant_id 一致)
  → CasbinRBACMiddlewarerole names × path × method → Allow
  → handler → logic → usecase

8.2 CasbinRBACMiddleware

// 伪代码
roles := userRoleUC.GetRoleNames(ctx, tenantID, uid)
for _, roleName := range roles {
    result, _ := rbacUC.Check(ctx, &CheckRequest{
        TenantID: tenantID, UID: uid,
        RoleName: roleName, Path: r.URL.Path, Method: r.Method,
    })
    if result.Allow {
        ctx = withPermissionName(ctx, result.PermissionName)
        ctx = withPlainCode(ctx, result.PlainCode)
        next(w, r)
        return
    }
}
// SuperAdmin bypass
if uid == cfg.PlatformSuperAdminUID { next(w, r); return }
httpx.Error(w, forbidden)

8.3 SCIM API

Request
  → ScimAuthMiddlewaretenant_scim_token
  → TenantContextMiddleware
  → handler

8.4 Logic 層補充授權

Casbin 處理 API 級 授權。Logic 內可追加 資源級 判斷:

  • member.info.select vs 查他人:若 path 含 :uid 且 uid ≠ caller需命中 member.admin.read
  • PlainCodeLogic 讀 ctx.plain_code,決定是否回傳明碼欄位

9. 核心流程

9.1 登入 / 換票

Client → ZITADEL OIDC Login含 LDAP IdP
Client → POST /auth/token/exchange { tenant_slug, id_token }
  1. zitadel.VerifyIDToken
  2. tenant.ResolveBySlug → 校驗 org_id
  3. member.EnsureMember → uid如 ACME-ODWXGYBK
  4. permission.SyncFromZitadelClaims → user_roles
  5. auth.IssueTokenPairrole names 逗號分隔, auth_gen
Client ← { access_token, refresh_token, uid }

9.2 受保護 API

Client → GET /api/v1/members/me (Bearer access_jwt)
  1. JWT + 黑名單 + auth_gen
  2. CasbinRBACMiddleware → Check(role, "/api/v1/members/me", "GET")
  3. member.GetByUID

9.3 B2B 自定義 Role + 勾選 Permission

Tenant Admin → PUT /permissions/roles/{id}/permissions
  { "member.admin.list": "open", "member.admin.read": "open" }
  → RolePermissionUC.Create
  → PermissionTree.getFullParentPermissionIDs自動補 parent
  → RBACUC.LoadPolicy(tenant_id)

Tenant Admin → POST /permissions/users/{uid}/roles
  { "role_id": "..." }
  → UserRoleUC.Assign

9.4 停權

Admin → PATCH /members/:uid/status { status: "suspended" }
  → member.UpdateStatus
  → auth.RevokeAllForUserINCR auth_gen

10. LDAP 與 SCIM

10.1 三條 Provisioning 路徑

路徑 適用 說明
SCIM → ZITADEL → Gateway 有 HR / Entra ID / Okta 企業推送用户
ZITADEL LDAP IdP 用戶登入時 JIT 首次登入建立 member
Directory Sync Worker 無 SCIM 的 AD / OpenLDAP 定時同步 + 離職偵測

10.2 LDAP 設定AD + OpenLDAP

type TenantLDAPConfig struct {
    TenantID     string
    Type         string   // "ad" | "openldap"
    Host         string
    Port         int
    UseTLS       bool
    BaseDN       string
    BindDN       string   // encrypted
    BindPassword string   // encrypted
    UserFilter   string
    GroupFilter  string
    AttrMap      LDAPAttrMap
}

type LDAPAttrMap struct {
    Username    string // AD: sAMAccountName / LDAP: uid
    Email       string // mail
    DisplayName string // displayName / cn
    Phone       string // telephoneNumber
    ExternalID  string // objectGUID / entryUUID
    Groups      string // memberOf
}

10.3 SCIM

  • externalId = Member UIDACME-ODWXGYBK
  • SCIM Groups PATCH → permission.SyncFromScimGroup
  • SCIM deactivate → member.suspended + auth.RevokeAllForUser

11. 可讀 UID 設計

11.1 格式

{TenantPrefix}-{Body}

範例ACME-ODWXGYBK
  • TenantPrefix2~4 位大寫(來自 tenant.UIDPrefix / slug 縮寫)
  • Body6~8 位大寫字母(自訂字母表,排除易混淆字元)
  • 不要 UUID、不要純數字、不要 base64 亂碼

11.2 字母表(沿用 invited_code 風格)

O D W X Y G B C H E F A Q I J L M N Z K P V R S T
25 字符,無 0/O/1/I 混淆問題)

11.3 產生Bucket 取號,支援單租戶 50 萬)

Redis: member:seq:{tenant_id}
每次 INCRBY 500 取一個 bucket
bucket 內 sequential 編碼 → Body

唯一索引:{ tenant_id, uid } unique


12. 資料模型與索引

12.1 Collections

Collection 模組 說明
members member Profile
identities member zitadel_sub ↔ uid
tenants member 租戶 metadata
tenant_ldap_configs member LDAP 同步設定(加密)
permissions permission 全局 Permission Tree平台 seed
roles permission 租戶 Roletenant_id + name
role_permissions permission Role ↔ Permission ID
user_roles permission uid ↔ Role支援多角色
role_mappings permission 外部 Group ↔ Role.Name

12.2 主要索引

// members
{ tenant_id: 1, uid: 1 }                          // unique
{ tenant_id: 1, zitadel_user_id: 1 }              // unique
{ tenant_id: 1, member_status: 1, create_at: -1 }

// identities
{ tenant_id: 1, zitadel_user_id: 1 }              // unique
{ tenant_id: 1, uid: 1 }
{ tenant_id: 1, external_id: 1 }

// permissions全局
{ name: 1 }                                       // unique
{ parent: 1, status: 1 }
{ http_path: 1, http_method: 1 }                  // sparse

// roles
{ tenant_id: 1, name: 1 }                       // unique
{ tenant_id: 1, status: 1 }

// role_permissions
{ role_id: 1, permission_id: 1 }                // unique

// user_roles
{ tenant_id: 1, uid: 1, role_id: 1 }            // unique
{ tenant_id: 1, uid: 1 }

// role_mappings
{ tenant_id: 1, external_source: 1, external_key: 1 }  // unique

12.3 分片鍵100 萬+

Shard Key: { tenant_id: 1, uid: 1 }

單租戶 50 萬會集中在同一 chunkMongoDB 仍可承受;若預期單租戶千萬級再評估 hash 二次分片。


13. Redis Key 命名

authinternal/model/auth/redis.go

auth:jwt:bl:{jti}                  # 單 token 黑名單TTL = 剩餘壽命
auth:gen:{tenant_id}:{uid}         # 批量失效代號

memberinternal/model/member/redis.go

member:profile:{tenant_id}:{uid}   # profile cacheTTL 5~15min
member:sub:{tenant_id}:{sub}       # zitadel_sub → uidTTL 1h
member:seq:{tenant_id}             # UID bucket counter

permissioninternal/model/permission/redis.go

permission:casbin:rules              # Casbin policy rulesList of JSON
permission:tree:open                 # 可選open 節點 cache
perm:role_perms:{tenant_id}:{role_id}  # role → permission namesTTL 30min
perm:user_roles:{tenant_id}:{uid}      # uid → role namesTTL 5min

14. 規模與性能100 萬+ / 單租戶 50 萬)

項目 策略
Gateway 無狀態,水平擴展
MongoDB Sharding + Replica Set讀走 secondary
ListMembers Cursor 分頁,禁止 deep offset
Authorize Casbin EnforceEx内存 + Redis policy
LoadPolicy 变更时增量cron 5min 全量兜底
JWT → UID Redis cache 1h
Directory Sync 500 users / batchrate limit ZITADEL API
Access Token TTL 15min降低撤銷窗口

容量粗估

100 萬 members × ~2KB  ≈ 2GB不含 index
indexes               ≈ 1~2GB
→ 單集群可承受,建議 3 node replica set 起跳

15. 目錄結構

gateway/
├── generate/api/
│   ├── auth.api
│   ├── member.api
│   ├── permission.api
│   ├── tenant.api
│   └── scim.api
│
├── internal/
│   ├── middleware/
│   │   ├── jwt_revoke.go
│   │   ├── tenant_context.go
│   │   ├── require_permission.go
│   │   └── scim_auth.go
│   │
│   ├── library/
│   │   ├── zitadel/
│   │   │   ├── oidc.go
│   │   │   └── management.go
│   │   ├── ldap/
│   │   │   ├── client.go
│   │   │   └── attrmap.go
│   │   ├── casbin/                 # Enforcer 初始化 helper
│   │   └── uid/
│   │       ├── encode.go
│   │       └── generator.go
│   │
│   ├── model/
│   │   ├── auth/
│   │   │   └── ...
│   │   ├── member/
│   │   │   └── ...
│   │   └── permission/
│   │       ├── entity/
│   │       │   ├── permission.go
│   │       │   ├── role.go
│   │       │   ├── user_role.go
│   │       │   ├── role_permission.go
│   │       │   └── role_mapping.go
│   │       ├── enum/
│   │       │   ├── status.go
│   │       │   └── permission_type.go
│   │       ├── repository/
│   │       │   ├── permission.go
│   │       │   ├── role.go
│   │       │   ├── user_role.go
│   │       │   ├── role_permission.go
│   │       │   ├── role_mapping.go
│   │       │   └── casbin_redis_adapter.go   # 沿用 permission-server
│   │       ├── usecase/
│   │       │   ├── permission_tree.go        # 沿用 permission-server
│   │       │   ├── rbac.go                   # Casbin LoadPolicy / Check
│   │       │   ├── permission.go
│   │       │   ├── role.go
│   │       │   ├── role_permission.go
│   │       │   ├── user_role.go
│   │       │   ├── role_mapping.go
│   │       │   └── authorization_query.go
│   │       ├── rbac/
│   │       │   └── rule.go                   # Casbin Rule struct
│   │       ├── config/
│   │       ├── errors.go
│   │       ├── redis.go
│   │       └── mock/
│   │
│   └── worker/
│       ├── directory_sync/
│       └── policy_sync/            # 可選:定時 LoadPolicy
│
├── etc/
│   ├── gateway.yaml
│   └── rbac.conf                   # Casbin 模型(沿用 permission-server
│
└── docs/
    ├── model.md
    └── identity-member-design.md   # 本文件

16. 設定檔

etc/gateway.yaml 擴充草案:

Name: gateway
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: ${JWT_ACCESS_SECRET}
  AccessExpire: 900

RefreshAuth:
  AccessSecret: ${JWT_REFRESH_SECRET}
  AccessExpire: 604800

Zitadel:
  Issuer: https://id.example.com
  ClientID: ${ZITADEL_CLIENT_ID}
  JWKSUrl: https://id.example.com/oauth/v2/keys
  MgmtURL: https://id.example.com/management/v1
  MgmtToken: ${ZITADEL_MGMT_TOKEN}

Mongo:
  # 見 internal/library/mongo 設定

Redis:
  Host: 127.0.0.1:6379
  Type: node

Member:
  DefaultLanguage: zh-tw
  DefaultCurrency: TWD

Permission:
  RBACModelPath: etc/rbac.conf
  PolicySyncInterval: 5m
  PlatformSuperAdminUID: ${PLATFORM_SUPER_ADMIN_UID}  # 對應舊 GodDog bypass
  CacheTTLSeconds: 300

17. 實施順序

階段 內容 產出
P0 目錄骨架、entity、redis key、config 可啟動、可連 Mongo/Redis
P1 UID generator + EnsureMember + token exchange 可登入取得 JWT + 可讀 UID
P2 JWT middleware + jti 黑名單 + auth_gen + logout/refresh 完整 Token 生命週期
P3 Permission seed + PermissionTree + Casbin RBAC + Redis Adapter 可 LoadPolicy / Check
P4 member profile API + 預設 Role seed + CasbinRBACMiddleware /members/me + API 授權生效
P5 RolePermission + UserRole + B2B Role CRUD + Permission 勾選 API 租戶完全自定義
P6 Tenant 建立 + ZITADEL CreateOrg + LDAP 設定 多租戶
P7 Directory Sync WorkerAD + OpenLDAP 企業目錄同步
P8 SCIM 2.0 endpoint + Group 映射 企業 provisioning
P9 壓測100 萬 seed、sharding、調優 上線準備

18. 待決策事項

# 議題 選項 備註
1 UID 格式 ACME-ODWXGYBK vs 純 ODWXGYBK 建議带前缀
2 SCIM 路由 /scim/v2/tenants/{id} vs 獨立子域名
3 ZITADEL 部署 Self-hosted vs Cloud 影響 LDAP 網路
4 權限變更生效 仅清 cache vs 强制 INCR auth_gen 建议重要變更 INCR
5 B2C 租戶 是否允許自定義 Role 建议 B2C 只用 seed 模板B2B 完全自定義
6 Refresh Token 是否輪換 + 舊 jti 黑名單 建议輪換
7 UserRole 一 user 多 role vs 單 role 建议多 roleCasbin 逐一 Check
8 PlatformSuperAdminUID 固定 uid vs 平台 Role 建议保留 bypass uid + 后续可移除

附錄 AServiceContext 組裝草案

type ServiceContext struct {
    Config config.Config
    Validator validate.Validate

    // library clients
    Zitadel *zitadel.Client

    // usecases
    AuthUC           authusecase.TokenUseCase
    MemberProvUC     memberusecase.ProvisioningUseCase
    MemberProfileUC  memberusecase.ProfileUseCase
    MemberAdminUC    memberusecase.AdminUseCase
    TenantUC         memberusecase.TenantUseCase
    ScimUC           memberusecase.ScimUseCase

    // permission usecases對齊 permission-server 拆分)
    PermRBACUC       permusecase.RBACUseCase
    PermUC           permusecase.PermissionUseCase
    RoleUC           permusecase.RoleUseCase
    RolePermUC       permusecase.RolePermissionUseCase
    UserRoleUC       permusecase.UserRoleUseCase
    RoleMappingUC    permusecase.RoleMappingUseCase
    AuthQueryUC      permusecase.AuthorizationQueryUseCase
}

附錄 Cpermission-server 遷移對照(程式碼級)

permission-server 檔案 Gateway 目標 遷移方式
pkg/usecase/permission_tree.go model/permission/usecase/permission_tree.go 幾乎原樣搬移
pkg/usecase/casbin_redis_rbac.go model/permission/usecase/rbac.go tenant_id 過濾
pkg/repository/casbin_redis_adapter.go model/permission/repository/casbin_redis_adapter.go 原樣搬移
pkg/domain/rbac/rule.go model/permission/rbac/rule.go 原樣搬移
etc/rbac.conf etc/rbac.conf 原樣搬移
pkg/usecase/role.go model/permission/usecase/role.go ClientIDTenantID
pkg/usecase/role_permission.go model/permission/usecase/role_permission.go 原樣搬移
pkg/usecase/user_role.go model/permission/usecase/user_role.go 改支援多角色
pkg/usecase/token.go model/auth/usecase/token.go 不在 permission 模組
generate/database/seeders/*_permission* generate/database/seeders/ 或 Mongo seed 改為 Gateway seed job

附錄 B與 model.md 的關係

  • 本文件:做什麼架構、流程、API、權限模型
  • model.md怎麼寫entity / repository / usecase 程式碼規範)

實作時兩份文件搭配使用。


修訂紀錄

日期 版本 說明
2026-05-19 0.1.0 初稿auth + member + permissionB2B 自定義)+ ZITADEL/LDAP/SCIM
2026-05-19 0.2.0 對齊 app-cloudep-permission-serverCasbin RBAC、Permission Tree、Role/RolePermission