fix(handlers): add handlers
This commit is contained in:
87
docs/AUTHZ_DESIGN.md
Normal file
87
docs/AUTHZ_DESIGN.md
Normal 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
|
||||
- 选择理由:
|
||||
- 租户隔离是安全边界,必须在最靠前的入口稳定执行,避免业务分支遗漏
|
||||
|
||||
### 授权(Authorization):Service 层为规则源,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 之前完成决策;决策审计由统一审计组件记录。
|
||||
|
||||
## 审计与可观测性
|
||||
|
||||
- 建议在认证/授权路径内记录审计事件:
|
||||
- actor:tenant_id、user_id
|
||||
- decision:allow/deny
|
||||
- policy:匹配的 RBAC 权限或 ABAC 策略 ID
|
||||
- cost:授权耗时
|
||||
- 当前库 `common-telemetry` 已提供统一错误返回;建议后续在错误响应中补齐 trace_id(从 tracing context 获取)。
|
||||
|
||||
## 数据隔离方案对比(建议)
|
||||
|
||||
### 现状:共享库共享表(tenant_id 过滤)
|
||||
|
||||
- 优点:实现简单、性能可控、易于水平扩容
|
||||
- 风险:任何遗漏 `tenant_id` 条件都可能造成越权
|
||||
- 现有防线:
|
||||
- 中间件注入 `TenantId`
|
||||
- Service 查询必须显式绑定 tenant_id
|
||||
|
||||
### 演进方案 A:独立 Schema(每租户一个 schema)
|
||||
|
||||
- 优点:逻辑隔离更强,可按租户迁移/清理
|
||||
- 缺点:管理成本高,连接池与迁移复杂
|
||||
|
||||
### 演进方案 B:Postgres Row-Level Security(RLS)
|
||||
|
||||
- 优点:隔离在数据库层兜底,减少“遗漏 where tenant_id”的风险
|
||||
- 缺点:策略配置复杂,需要设置 session 变量(如 `app.tenant_id`)并在连接池层保证一致
|
||||
|
||||
建议:当租户数量增长或合规要求上升时,引入 RLS 作为数据库兜底隔离层。
|
||||
|
||||
90
docs/DB_PROVISIONING.md
Normal file
90
docs/DB_PROVISIONING.md
Normal 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
|
||||
```
|
||||
|
||||
- macOS(Homebrew):
|
||||
|
||||
```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
155
docs/SCALAR_GUIDE.md
Normal 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/s,burst 10(同一 IP)
|
||||
- `/auth/register`:约 1 req/s,burst 5(同一 IP)
|
||||
- 触发后返回:HTTP 429 + `code=40000`
|
||||
|
||||
23
docs/TEMP.md
Normal file
23
docs/TEMP.md
Normal 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
104
docs/TENANT_API.md
Normal 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`
|
||||
|
||||
说明:当前实现为物理删除。生产建议改为软删除并触发异步数据清理流程。
|
||||
|
||||
Reference in New Issue
Block a user