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

12 KiB
Raw Permalink Blame History

Permission 模組

Gateway 多租戶 B2B 自定義 RBAC:平台級 Permission Catalog + 租戶級 Role / RolePermission / UserRole / RoleMapping + Casbin enforcer。

  • 規格書Data Dictionary、API 端點欄位、Casbin modelSDD.md
  • 跨模組總覽docs/identity-member-design.md §6
  • 本 README = 流程圖 + curl + ServiceContext 速查

TL;DR

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) uniqueis_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、parentstatustype
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

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 同步)

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

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

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 可定時 LoadAllPolicies5min cron建議 svc 層或 cron-worker 觸發;本模組不內建)。


Casbin 模型(etc/rbac.conf

[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/*
  • regexMatchGET|POST|PATCH 多 method 同 policy
  • 平台 Admin bypass 不寫在 matcher由 middleware 預檢(保留 audit

ServiceContext 注入

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 == nilCheck() 永遠 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

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 / 操作

# 建索引
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 roletenant_owner / tenant_admin / member_manager / member / viewerseed/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

測試

# 單元
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