fix(handlers): add handlers

This commit is contained in:
2026-01-30 16:31:53 +08:00
parent bb82c75834
commit ce12b997f4
38 changed files with 3746 additions and 317 deletions

87
docs/AUTHZ_DESIGN.md Normal file
View File

@@ -0,0 +1,87 @@
# 权限控制架构设计Tenant-User-Role
## 目标
- 保证租户隔离:任何用户只能访问其 Token 绑定的租户数据
- 保证授权一致:同一类请求在所有入口都遵循同样的鉴权与授权规则
- 保证可演进RBAC 可以稳定运行ABAC 可逐步引入策略引擎与审计闭环
## 分层职责与落点
### 认证Authentication中间件层
- 位置:`src/middleware/auth.rs`
- 输入:`Authorization: Bearer <access_token>`
- 输出:解析/验签 Token 并注入 `AuthContext { tenant_id, user_id, roles, permissions }`
- 选择理由:
- 认证属于横切关注点,放在中间件可避免每个 Handler 重复实现
- 401/403 与错误码可统一,便于网关与客户端处理
- 便于埋点在统一入口记录审计字段IP/UA/结果/耗时)
### 租户上下文Tenant Context中间件层
- 位置:`src/middleware/mod.rs`
- 行为:
- 优先使用 `AuthContext.tenant_id` 注入 `TenantId`
- 兼容 `X-Tenant-ID`,若 Header 与 Token 同时存在则强制一致,否则返回 403
- 选择理由:
- 租户隔离是安全边界,必须在最靠前的入口稳定执行,避免业务分支遗漏
### 授权AuthorizationService 层为规则源Handler/中间件为拦截点
当前实现采用“规则在 Service、拦截在 Handler”的折中
- 规则源Service`src/services/authorization.rs`
- 负责 RBAC 查库:用户→角色→权限
- 产出统一决策(允许/拒绝)并返回 `AppError::PermissionDenied`
- 拦截点Handler在具体接口中调用 `AuthorizationService::require_permission(...)`
- 选择理由:
- Handler 更接近“路由-动作”语义,决定某个接口需要什么权限更直观
- Service 只负责“如何判断”,不关心“哪个路由需要什么”
当路由数量增长后,建议演进为“拦截点上移到中间件/Layer”
- 给不同 Router 分组挂载不同的授权 Layer如 tenant 管理、用户管理、角色管理)
- 或引入宏/属性(例如 `#[require("tenant:write")]`)生成统一的 Guard
### ABAC建议落点后续实现
ABAC 需要资源属性、环境属性、请求属性,通常依赖:
- 资源属性DB 查询或缓存读取(属于业务领域)
- 环境属性IP、时间、设备风险、地理位置属于安全/风控)
- 请求属性:请求参数与上下文字段
因此 ABAC 的“策略评估”建议由独立模块/服务承担(例如 `PolicyEngine`),并由 Handler 在调用业务 Service 之前完成决策;决策审计由统一审计组件记录。
## 审计与可观测性
- 建议在认证/授权路径内记录审计事件:
- actortenant_id、user_id
- decisionallow/deny
- policy匹配的 RBAC 权限或 ABAC 策略 ID
- cost授权耗时
- 当前库 `common-telemetry` 已提供统一错误返回;建议后续在错误响应中补齐 trace_id从 tracing context 获取)。
## 数据隔离方案对比(建议)
### 现状共享库共享表tenant_id 过滤)
- 优点:实现简单、性能可控、易于水平扩容
- 风险:任何遗漏 `tenant_id` 条件都可能造成越权
- 现有防线:
- 中间件注入 `TenantId`
- Service 查询必须显式绑定 tenant_id
### 演进方案 A独立 Schema每租户一个 schema
- 优点:逻辑隔离更强,可按租户迁移/清理
- 缺点:管理成本高,连接池与迁移复杂
### 演进方案 BPostgres Row-Level SecurityRLS
- 优点:隔离在数据库层兜底,减少“遗漏 where tenant_id”的风险
- 缺点:策略配置复杂,需要设置 session 变量(如 `app.tenant_id`)并在连接池层保证一致
建议:当租户数量增长或合规要求上升时,引入 RLS 作为数据库兜底隔离层。

90
docs/DB_PROVISIONING.md Normal file
View File

@@ -0,0 +1,90 @@
# 数据库初始化与权限(归档)
本项目的 schema 初始化已从历史的 `init.sql` 拆分为“基础设施初始化DB/User”与“schema 初始化DDL/DML”两部分
- **基础设施初始化DB/User**:通常由 DBA/平台完成,适用于首次部署或新环境准备。
- **schema 初始化DDL/DML**:适用于开发/测试环境的可重复重建,见 `sql/schema_post_init.sql`,并由 `scripts/db/rebuild_iam_db.sh` 一键执行。
## 1) 创建用户与数据库(首次环境准备)
在具有足够权限的数据库账号下执行:
```sql
CREATE USER iam_service_user WITH PASSWORD 'iam_service_password';
CREATE DATABASE iam_service_db OWNER iam_service_user;
GRANT ALL PRIVILEGES ON DATABASE iam_service_db TO iam_service_user;
```
注意事项:
- 生产环境不要在仓库内硬编码密码;应由密钥管理系统注入并轮换。
- 如果需要启用扩展(如 `uuid-ossp`),请确认应用用户是否有权限,或由 DBA 预先安装。
## 2) schema 重建(开发/CI
推荐使用一键脚本(会 DROP 并重建表结构):
```bash
export DATABASE_URL='postgres://iam_service_user:***@host:5432/iam_service_db'
BACKUP=1 ./scripts/db/rebuild_iam_db.sh
```
脚本会按顺序执行:
- `sql/drop_iam_schema.sql`
- `sql/schema_post_init.sql`
- `sql/verify_iam_schema.sql`
## 3) 常见问题
### 3.1 `.env` 配了 DATABASE_URL 但脚本报 “DATABASE_URL is required”
`.env` 只是“文件”,不会自动变成进程环境变量。`cargo run` 会通过 `dotenvy` 加载 `.env`,但 bash 脚本默认不会。
解决方式:
- 直接 `export DATABASE_URL=...` 后再执行脚本;或
- 保持 `.env` 存在于项目根目录,脚本会自动读取其中的 `DATABASE_URL`
### 3.2 执行脚本报 “psql: 未找到命令”
原因:系统未安装 PostgreSQL 客户端工具(`psql`/`pg_dump`),或不在 `PATH` 中。
安装方式:
- Ubuntu/Debian
```bash
sudo apt-get update && sudo apt-get install -y postgresql-client
```
- RHEL/CentOS/Fedora
```bash
sudo dnf install -y postgresql
```
- Alpine
```bash
sudo apk add postgresql-client
```
- macOSHomebrew
```bash
brew install libpq
brew link --force libpq
```
验证方式:
```bash
psql --version
```
安装完成后重新执行:
```bash
BACKUP=1 ./scripts/db/rebuild_iam_db.sh
```

155
docs/SCALAR_GUIDE.md Normal file
View File

@@ -0,0 +1,155 @@
# IAM Service — Scalar 调用顺序指南
## Authentication认证方式
本服务使用 **JWT Bearer Token**
- 登录成功后拿到 `access_token`
- 后续请求在 Header 中带:
- `Authorization: Bearer <access_token>`
- 租户上下文:
- 保护接口默认从 Token claim 的 `tenant_id` 推导租户
- 可选兼容 `X-Tenant-ID: <uuid>`,若同时提供 Header 与 Token则必须一致否则返回 403
## 通用响应结构
成功响应:
```json
{ "code": 0, "message": "Success|Created|Accepted", "data": {}, "trace_id": null }
```
错误响应(示例):
```json
{ "code": 20006, "message": "Missing authorization header", "details": null, "trace_id": null }
```
常见错误码(节选):
- 20006缺少必要 Header如 Authorization
- 20003无权限403
- 20005账号或密码错误
- 30000请求参数错误400
- 30002资源不存在404
- 30003资源冲突409
- 40000请求过于频繁429
- 10001数据库错误500
## Step-by-step可复制流程
### Step 0创建租户可选
**POST** `/tenants/register`
- Header
- Body
```json
{ "name": "Tenant A", "config": { "theme": { "primary": "#1d4ed8" } } }
```
成功201`data.id` 取出租户 ID
```json
{ "code": 0, "message": "Created", "data": { "id": "<tenant_id>", "name": "Tenant A", "status": "active", "config": {} }, "trace_id": null }
```
下一步依赖:`tenant_id`(用于注册/登录时的 `X-Tenant-ID`)。
### Step 1注册用户
**POST** `/auth/register`
- 必需 Header`X-Tenant-ID: <tenant_id>`
- Body
```json
{ "email": "user@example.com", "password": "securePassword123" }
```
成功201`data.id` 取出 `user_id`(后续可用于用户管理接口):
```json
{ "code": 0, "message": "Created", "data": { "id": "<user_id>", "email": "user@example.com" }, "trace_id": null }
```
### Step 2登录获取访问令牌Authentication 入口)
**POST** `/auth/login`
- 必需 Header`X-Tenant-ID: <tenant_id>`
- Body
```json
{ "email": "user@example.com", "password": "securePassword123" }
```
成功200`data.access_token` 取出访问令牌:
```json
{ "code": 0, "message": "Success", "data": { "access_token": "<jwt>", "refresh_token": "<opaque>", "token_type": "Bearer", "expires_in": 900 }, "trace_id": null }
```
下一步依赖:`access_token`
### Step 3获取当前租户信息Tenant
**GET** `/tenants/me`
- 必需 Header`Authorization: Bearer <access_token>`
- 可选 Header`X-Tenant-ID: <tenant_id>`(如提供必须与 token tenant_id 一致)
成功200
```json
{ "code": 0, "message": "Success", "data": { "id": "<tenant_id>", "name": "Tenant A", "status": "active", "config": {} }, "trace_id": null }
```
### Step 4查看当前用户权限Me
**GET** `/me/permissions`
- 必需 Header`Authorization: Bearer <access_token>`
成功200
```json
{ "code": 0, "message": "Success", "data": ["tenant:read","tenant:write"], "trace_id": null }
```
下一步依赖:确认具备目标权限(例如 `user:read` / `role:read`)。
### Step 5列出用户User
**GET** `/users?page=1&page_size=20`
- 必需 Header`Authorization: Bearer <access_token>`
- 分页规则:
- `page` 默认 1必须 >= 1
- `page_size` 默认 20范围 1..=200
成功200
```json
{ "code": 0, "message": "Success", "data": [{ "id": "<user_id>", "email": "user@example.com" }], "trace_id": null }
```
### Step 6列出角色Role
**GET** `/roles`
- 必需 Header`Authorization: Bearer <access_token>`
成功200
```json
{ "code": 0, "message": "Success", "data": [{ "id": "<role_id>", "name": "Admin", "description": "..." }], "trace_id": null }
```
## 限流说明Auth
- `/auth/login`:约 2 req/sburst 10同一 IP
- `/auth/register`:约 1 req/sburst 5同一 IP
- 触发后返回HTTP 429 + `code=40000`

23
docs/TEMP.md Normal file
View File

@@ -0,0 +1,23 @@
```json
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3Njk3NTc3MTAsImlhdCI6MTc2OTc1NjgxMCwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiJdLCJwZXJtaXNzaW9ucyI6WyJyb2xlOnJlYWQiLCJyb2xlOndyaXRlIiwidGVuYW50OnJlYWQiLCJ0ZW5hbnQ6d3JpdGUiLCJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIl19.VGsoZdMwodRWKW4NQuQwezh3xZivFbRUzSw_-RnD-EJIv7qPHmcNbdIcxNSKXCHGKdK_b1B3404m7ji2wdEOweKz0GEcwPWswc9fannP5_6l9k83jn0ZKQ1pS3l27V5mr9feym_83ZIqEtFfKcCKGIM684Ze7CMM6i-gfYisn0poG1XW3K4ptsVnuNZux0TWNFl5TO6kgiw0_399tZnSH5qc4CckHOuoF3Jz1Q2aIgnvyfxbxEFTNZm-ykjhlbK5zWBpYfJdYOALQg-FQ3eGuVnSF4U_If1MNQKQ0p6DqDKMCO0IfdCr2WMBvfCYA1SxmPbETr2Tm7RguhJBEiVQ4Q",
"refresh_token": "e1649e730ef3583cd80087f7fa63774330deb88e81aec2edae41322764e441eb",
"token_type": "Bearer",
"expires_in": 900
}
}
{
"code": 0,
"message": "Success",
"data": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJmOTg5MWI2Yy0xNTQ1LTQ0NGItODIxOC0yNTQ5MDA1NDczMGYiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3Njk3NTg4NDMsImlhdCI6MTc2OTc1Nzk0MywiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6W10sInBlcm1pc3Npb25zIjpbXX0.Is-JjQ9l1BuZoYVr6QAt-4ZSfRQro3COPSVYHVl1NI0CAz7T2x9Hz2QiJPFamjsX7cFrCIJxNMn9ioqK2FzEnSTu2oVATqlhE5OMCcK1M7Mq_FvZ7WqGgPl8CE06s7yvneC97mknk5y1-nm5claYYGeHAjLrRPbjO2t3zUQO5boNPdjzEGx4kTFvgmJbwWMrsBtkeaW1nacxhFiSj-RFCSzHOOaSRoKLDsx9nUsuDJL1NCaHDuKDacphkwpjP5AWLd41hlrs6PC8XLUPey2EXHqJ5SmOaDdQ60LfItvohgHBTY6CO8IUIJgtZobrFsKUlnHqA9eZwm2dvAW560g4VA",
"refresh_token": "71e3ba6285b503891294ca7dad81cdc6bd5b3f72b09b1e2b796979a433d687f3",
"token_type": "Bearer",
"expires_in": 900
}
}
```

104
docs/TENANT_API.md Normal file
View File

@@ -0,0 +1,104 @@
# 租户管理 API 使用说明
## 通用约定
### 成功响应
所有成功响应使用 `common-telemetry` 的统一包装:
```json
{
"code": 0,
"message": "Success|Created|Accepted",
"data": {},
"trace_id": null
}
```
### 错误响应
错误响应由 `AppError` 统一转换为 JSON
```json
{
"code": 30000,
"message": "Invalid request parameters: ...",
"details": "...",
"trace_id": null
}
```
常见错误码(节选,来自 `common-telemetry`
- 20006缺少认证 Header`Authorization`
- 20003无权限403
- 30000请求参数错误400
- 30002资源不存在404
- 10001数据库错误500
## 认证与租户上下文
- 受保护接口需要:
- `Authorization: Bearer <access_token>`
- 租户上下文优先来自 Token 的 `tenant_id` claim
- 兼容 `X-Tenant-ID`
- 如果同时传 `X-Tenant-ID` 与 Token则必须一致否则返回 403
## 接口列表
### 1) 租户注册(公共)
`POST /tenants/register`
请求体:
```json
{
"name": "Tenant A",
"config": {
"theme": { "primary": "#1d4ed8" },
"password_policy": { "min_len": 12 }
}
}
```
响应:`TenantResponse`
### 2) 获取当前租户信息(需要 tenant:read
`GET /tenants/me`
请求头:
- `Authorization: Bearer <access_token>`
- `X-Tenant-ID: <uuid>`(可选)
### 3) 更新当前租户信息(需要 tenant:write
`PATCH /tenants/me`
请求体:
```json
{
"name": "New Tenant Name",
"config": { "theme": { "primary": "#0ea5e9" } }
}
```
### 4) 变更当前租户状态(需要 tenant:write
`POST /tenants/me/status`
请求体:
```json
{ "status": "active|disabled|suspended" }
```
### 5) 删除当前租户(需要 tenant:write
`DELETE /tenants/me`
说明:当前实现为物理删除。生产建议改为软删除并触发异步数据清理流程。