# Permission Service

# Permission Service — SRS/SDD Document

| Version | Version Date | Editor | Memo |
|---------|--------------|--------|------|
| 1.0.0 | 2026/05/21 | Gateway Team | 初版：Gateway Permission 模組 SDD（對齊 frontend Permission Service SDD 格式） |
| 1.1.0 | — | — | 新增 category 樹狀 Permission Catalog |
| 1.2.0 | — | — | 多租戶 B2B RBAC + Casbin + RoleMapping |

---

## 1. Introduction

### 1.1 Purpose

Permission 模組提供 Gateway **多租戶 B2B 自定義 RBAC**：平台級 Permission Catalog + 租戶級 Role / RolePermission / UserRole / RoleMapping，搭配 Casbin enforcer 進行 HTTP path/method 授權，並支援外部 IdP（ZITADEL / LDAP / SCIM）角色映射同步。

### 1.2 Scope

**範圍內（Multiple tenants）：**

- 平台 Permission Catalog（樹狀，dot notation）
- 租戶 Role CRUD（含 system role 防呆）
- Role ↔ Permission 多對多（含 parent closure）
- User ↔ Role 多對多（source: manual / zitadel / ldap / scim）
- 外部 group → 內部 Role.Key 映射（RoleMapping）
- Casbin policy 物化（Redis Set）+ 多 pod Pub/Sub reload
- GET /permissions/me 前端選單渲染

**範圍外：**

- JWT 簽發 → Auth 模組
- 會員資料 → Member 模組
- Platform admin bypass → middleware 預檢（保留 audit）

### 1.3 Definitions, Acronyms, and Abbreviation

| 縮寫 | 說明 |
|------|------|
| **RBAC** | Role-Based Access Control |
| **Permission** | 平台級權限節點（可為分類或 leaf API 權限） |
| **Role** | 租戶內角色，`key` 唯一且不可改 |
| **RolePermission** | Role 勾選的 Permission 集合 |
| **UserRole** | 使用者被指派的角色 |
| **RoleMapping** | 外部 IdP group/role → 內部 Role.Key |
| **Casbin** | 授權引擎，policy 存 Redis |
| **Leaf Permission** | 有 http_path + http_methods 的可 enforcement 節點 |

### 1.4 Technologies to be used

| 項目 | 技術 |
|------|------|
| Application Language | Go 1.22+ |
| Framework | go-zero |
| Cache / Policy Store | Redis（Casbin rules Set + Pub/Sub） |
| Database | MongoDB |
| Authorization Engine | Casbin（`keyMatch2` + `regexMatch`） |
| API Codegen | goctl / `.api` 檔 |

### 1.5 Overview

```mermaid
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 --> Casbin[(Casbin Enforcer<br/>Redis adapter)]
    Casbin --> MW[CasbinRBAC Middleware]
```

- Permission **平台 seed 全局**，租戶不可新增，只能勾選
- Role / RolePermission / UserRole **租戶獨立**
- Role.Key 一旦建立 **不可改**（外部 IdP 對應用）
- 多 pod 同步：**Redis Pub/Sub 即時 + cron 兜底**

---

## 2. System Overview

Permission 模組是 Gateway 授權子系統：

1. 平台維護 Permission Catalog（`cmd/permission-seed`）
2. 租戶管理員建立 Role、勾選 Permission
3. 指派 UserRole（手動或 SyncFromX 同步）
4. RBACUseCase 物化 Casbin policy → middleware 檢查 `(tenant, role.key, path, method)`

前端透過 `GET /permissions/me` 取得當前使用者的 role keys 與 permission map（含可選 tree）。

---

## 3. System Architecture

### 3.1 System Architecture

```mermaid
flowchart TD
    Logic[logic/permission] --> SVC[svc.ServiceContext]
    SVC --> AuthQ[AuthorizationQueryUseCase]
    SVC --> Perm[PermissionUseCase]
    SVC --> Role[RoleUseCase]
    SVC --> RolePerm[RolePermissionUseCase]
    SVC --> UserRole[UserRoleUseCase]
    SVC --> Mapping[RoleMappingUseCase]
    SVC --> RBAC[RBACUseCase]

    RBAC --> Adapter[Casbin Redis Adapter]
    Adapter --> Redis[(Redis)]
    RBAC --> Pub[Redis Pub/Sub casbin:reload]

    Perm --> PermR[(permissions)]
    Role --> RoleR[(roles)]
    RolePerm --> RPR[(role_permissions)]
    UserRole --> URR[(user_roles)]
    Mapping --> RMR[(role_mappings)]
```

### 3.2 Decomposition Description

| 套件 | 職責 |
|------|------|
| `config/` | CasbinConfig、CacheConfig、ReloadConfig |
| `domain/entity/` | Permission、Role、RolePermission、UserRole、RoleMapping |
| `domain/enum/` | Status、PermissionType、RoleSource |
| `domain/repository/` | 5 個 Mongo repo 介面 + Casbin adapter port |
| `domain/usecase/` | 7 個 usecase 介面 + DTO |
| `repository/` | Mongo + Redis Casbin adapter |
| `usecase/` | 7 個 atomic usecase 實作 + permission_tree |
| `seed/` | catalog.json + Apply CLI |

### 3.3 RBAC Model

Casbin 模型（`etc/rbac.conf`）：

```ini
[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)
```

**授權檢查（any-allow）：** 使用者所有 open role 逐一 EnforceEx，任一 allow 即通過。

```mermaid
sequenceDiagram
    participant MW as CasbinRBAC Middleware
    participant RBAC as RBACUseCase
    participant URR as UserRoleRepository
    participant Enf as casbin.Enforcer

    MW->>RBAC: Check{tenant, uid, path, method}
    RBAC->>URR: ListByUser
    loop each open role
        RBAC->>Enf: EnforceEx(tenant, role.key, path, method)
        alt allow
            RBAC-->>MW: Allow
        end
    end
    RBAC-->>MW: Deny → 403
```

### 3.4 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*
```

分類節點（無 `http_path`）不寫入 Casbin policy；Replace RolePermission 時自動補齊 parent closure。

### 3.5 Policy Reload（多 Pod）

```mermaid
sequenceDiagram
    participant PodA as Pod A
    participant Redis
    participant PodB as Pod B

    PodA->>PodA: RolePermission.Replace + LoadPolicy
    PodA->>Redis: PUBLISH casbin:reload {tenant_id}
    Redis-->>PodB: message
    PodB->>PodB: LoadPolicy(tenant)
```

---

## 4. Data Design

### 4.1 Data Dictionary

#### permissions（MongoDB — 平台全局）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| parent | String | 父節點 ObjectId hex | parent |
| name | String | dot notation 唯一名稱 | Unique |
| http_methods | String | GET 或 GET\|POST\|PATCH | — |
| http_path | String | keyMatch2 path pattern | — |
| status | String | open / close | status |
| type | String | backend_user / frontend_user | type |
| create_at | Int64 | 建立時間 ms | — |
| update_at | Int64 | 更新時間 ms | — |

#### roles（MongoDB — 租戶範圍）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| tenant_id | String | 租戶 ID | Unique(tenant_id, key) |
| key | String | 角色 key（不可改） | Unique(tenant_id, key) |
| display_name | String | 顯示名稱 | — |
| creator_uid | String | 建立者 UID | — |
| status | String | open / close | (tenant_id, is_system) |
| is_system | Bool | 平台預設角色 | (tenant_id, is_system) |
| create_at | Int64 | 建立時間 ms | — |
| update_at | Int64 | 更新時間 ms | — |

#### role_permissions（MongoDB）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| tenant_id | String | 租戶 ID | Unique(tenant_id, role_id, permission_id) |
| role_id | String | Role ObjectId hex | (tenant_id, role_id) |
| permission_id | String | Permission ObjectId hex | (tenant_id, permission_id) |
| create_at | Int64 | 建立時間 ms | — |

#### user_roles（MongoDB）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| tenant_id | String | 租戶 ID | Unique(tenant_id, uid, role_id) |
| uid | String | 會員 UID | (tenant_id, uid, source) |
| role_id | String | Role ObjectId hex | (tenant_id, role_id) |
| source | String | manual / zitadel / ldap / scim | (tenant_id, uid, source) |
| create_at | Int64 | 建立時間 ms | — |

#### role_mappings（MongoDB）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| tenant_id | String | 租戶 ID | Unique(tenant_id, external_source, external_key) |
| external_source | String | zitadel / ldap / scim | Unique(tenant_id, external_source, external_key) |
| external_key | String | 外部 group/role key | Unique(tenant_id, external_source, external_key) |
| internal_role_id | String | 內部 Role ID | (tenant_id, internal_role_id) |
| create_at | Int64 | 建立時間 ms | — |
| update_at | Int64 | 更新時間 ms | — |

#### Redis Keys

| Key Pattern | 內容 | TTL |
|-------------|------|-----|
| permission:casbin:rules:{tenant_id} | Set of JSON Casbin rules | 永久 |
| perm:user_roles:{tenant_id}:{uid} | role keys 快取（預留） | 300s |
| perm:role_perms:{tenant_id}:{role_id} | permission names 快取（預留） | 300s |
| (channel) casbin:reload | Pub/Sub reload payload | — |

**Casbin rule 格式：** `[tenant, role.key, http_path, http_methods, perm.name]`

---

## 5. API Design

**Base Path：** `/api/v1/permissions`

| Method | Path | 說明 |
|--------|------|------|
| GET | `/catalog` | 全局 Permission Catalog（tree/list） |
| GET | `/me` | 當前 user 的 roles + permissions |
| GET | `/roles` | 租戶角色清單 |
| POST | `/roles` | 建立角色 |
| PATCH | `/roles/:id` | 更新 display_name / status |
| DELETE | `/roles/:id` | 刪除角色 |
| GET | `/roles/:id/permissions` | 角色 permission 集合 |
| PUT | `/roles/:id/permissions` | 全量取代 + parent closure + reload |
| GET | `/users/:uid/roles` | 使用者 role 列表 |
| POST | `/users/:uid/roles` | 指派角色 |
| DELETE | `/users/:uid/roles/:role_id` | 撤銷角色 |
| GET | `/role-mappings` | 外部映射列表 |
| PUT | `/role-mappings` | Upsert 外部映射 |
| DELETE | `/role-mappings` | 刪除外部映射 |
| POST | `/policy/reload` | 強制重載 policy（單租戶或 `*`） |

完整 schema 見 `generate/api/permission.api`。

---

## 6. Resource

| 資源 | 路徑 |
|------|------|
| API 定義 | `generate/api/permission.api` |
| Logic | `internal/logic/permission/` |
| Middleware | `internal/middleware/casbin_rbac.go` |
| 模組原始碼 | `internal/model/permission/` |
| 開發 README | `internal/model/permission/README.md` |
| Casbin 模型 | `etc/rbac.conf` |
| Seed 資料 | `internal/model/permission/seed/catalog.json` |
| Seed CLI | `cmd/permission-seed` |
| 設定範例 | `etc/gateway.dev.example.yaml` → `Permission` 區塊 |
| 設計參考 | `docs/identity-member-design.md` §6 / §7.3 / §13 |
