mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-23 16:02:25 +00:00
21 KiB
21 KiB
API Rule 引擎 — 完整架构文档
版本:v1(当前实现)+ PocketBase 对标分析 最后更新:2026-04-21
一、概述
API Rule 引擎为 CMS 动态路由提供表达式级别的行级访问控制。不同于简单的 public/member/admin 三档权限,Rule 引擎允许在 TOML schema 中声明过滤表达式,实现:
- 匿名用户只看到已发布文章(
filter) - 登录用户额外看到自己的草稿(
filter_auth) - 只能修改/删除自己的记录(运行时求值)
- 管理员专属操作(角色检查)
二、整体架构
┌─────────────────────────────────────────────────────────────────────┐
│ 启动时(一次性) │
│ │
│ TOML Schema ──serde──▶ ContentTypeSchema │
│ │ │ │
│ │ ├─ cache_select_columns() │
│ │ └─ cache_rules() │
│ │ │ │
│ │ ▼ │
│ │ Rule::parse(filter_str) │
│ │ Rule::parse(filter_auth_str) │
│ │ │ │
│ │ ▼ │
│ │ CachedRules(AST) │
│ │ 存入 Arc<ContentTypeSchema> │
└───────┼─────────────────────────────────────────────────────────────┘
│
┌───────┼─────────────────────────────────────────────────────────────┐
│ │ 请求时(每次请求) │
│ │ │
│ ▼ │
│ HTTP Request ──▶ Handler │
│ │ │ │
│ │ ├─ 1. check_api_access(access) ← 粗粒度权限 │
│ │ │ │
│ │ └─ 2. build_rule_sql() ← 细粒度过滤 │
│ │ │ │
│ │ ├─ 读取 ct.cached_rules.list │
│ │ ├─ compile_rule_sql(filter, auth) │
│ │ └─ compile_rule_sql(filter_auth, auth) │
│ │ │ │
│ │ ▼ │
│ │ (sql_fragment, params) │
│ │ │ │
│ │ ▼ │
│ │ ContentQuery { │
│ │ rule_where: Some("status = ?1"), │
│ │ rule_params: vec!["published"], │
│ │ } │
│ │ │ │
│ │ ▼ │
│ │ Repository::find() │
│ │ └─ WHERE ... AND (status = 'published') │
│ │ │
│ └─ 3. do_update/do_delete → evaluate(rule, record, ctx) │
│ └─ 运行时对记录求值 → 允许/拒绝 │
└────────────────────────────────────────────────────────────────────┘
三、TOML Schema 定义
3.1 结构
每个 content type 的 [api] 部分为 5 个端点独立配置:
[api.list] # GET /cms/{plural}
[api.get] # GET /cms/{plural}/{id}
[api.create] # POST /cms/{plural}
[api.update] # PUT /cms/{plural}/{id}
[api.delete] # DELETE /cms/{plural}/{id}
每个端点有 3 个字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
access |
"none" / "public" / "member" / "admin" |
是(默认 public) |
粗粒度访问级别 |
filter |
字符串(规则表达式) | 否 | 对所有请求生效的数据过滤 |
filter_auth |
字符串(规则表达式) | 否 | 已登录用户的额外过滤(与 filter 取 OR) |
3.2 示例
博客文章 — 匿名只看已发布,登录可创建
[api.list]
access = "public"
filter = 'status = "published"'
[api.get]
access = "public"
filter = 'status = "published"'
[api.create]
access = "member"
[api.update]
access = "member"
filter = 'author_id = @request.auth.id'
[api.delete]
access = "admin"
电商订单 — 用户只能看自己的订单
[api.list]
access = "member"
filter_auth = 'user_id = @request.auth.id'
[api.get]
access = "member"
filter_auth = 'user_id = @request.auth.id'
[api.create]
access = "member"
[api.update]
access = "member"
filter = 'user_id = @request.auth.id'
[api.delete]
access = "admin"
完全禁止外部访问(仅内部/插件使用)
[api.list]
access = "none"
[api.get]
access = "none"
[api.create]
access = "none"
[api.update]
access = "none"
[api.delete]
access = "none"
四、规则表达式语法
4.1 当前已实现(v1)
比较运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
= |
等于 | status = "published" |
!= |
不等于 | status != "draft" |
> |
大于 | price > 100 |
>= |
大于等于 | stock >= 1 |
< |
小于 | view_count < 1000 |
<= |
小于等于 | created_at <= "2026-01-01" |
~ |
LIKE 模糊匹配 | title ~ "%rust%" |
!~ |
NOT LIKE | title !~ "%spam%" |
逻辑组合
| 运算符 | 含义 | 示例 |
|---|---|---|
&& |
逻辑与 | status = "published" && author_id = @request.auth.id |
|| |
逻辑或 | status = "published" || status = "draft" |
() |
优先级分组 | (a = 1 || b = 2) && c = 3 |
特殊变量
| 变量 | 含义 | 示例 |
|---|---|---|
@request.auth.id |
当前登录用户 ID | author_id = @request.auth.id |
@request.auth.role |
当前登录用户角色 | @request.auth.role = "admin" |
字面量
| 类型 | 示例 |
|---|---|
| 字符串 | "published"、"admin" |
| 数字 | 100、3.14 |
| 布尔 | true、false |
| 空值 | null |
4.2 filter 与 filter_auth 的组合逻辑
请求到达
│
├─ 未登录 → 只用 filter
│ filter 存在 → 编译为 SQL WHERE
│ filter 不存在 → 不过滤
│
└─ 已登录 → filter OR filter_auth
两者都有 → WHERE (filter_sql OR filter_auth_sql)
只有 filter → WHERE filter_sql
只有 filter_auth → WHERE filter_auth_sql
都没有 → 不过滤
4.3 已实现(Phase 1)
| 功能 | 语法 | SQL 编译 | 运行时求值 | 状态 |
|---|---|---|---|---|
| 请求体引用 | @request.body.title |
占位符(仅运行时) | ✅ 从 RuleContext.body 取值 | ✅ |
| 查询参数引用 | @request.query.category |
占位符(仅运行时) | ✅ 从 RuleContext.query_params 取值 | ✅ |
| 当前时间 | @now |
datetime('now')(可配置) |
✅ ISO 8601 UTC | ✅ |
| 字段存在检查 | title:isset |
IS NOT NULL(可配置) |
✅ !is_null() |
✅ |
| 长度检查 | tags:length > 0 |
LENGTH(field)(可配置) |
✅ String/Array/Object | ✅ |
4.4 未实现(对标 PocketBase)
| 功能 | PocketBase 语法 | raisfast 语法 | 用途 | 优先级 |
|---|---|---|---|---|
| 字段变更检查 | title:changed |
title:changed |
update 时判断字段是否修改 | P2 |
| 数组遍历 | tags:each = "rust" |
tags:each = "rust" |
检查数组元素 | P2 |
| 多值 ANY 操作 | status ?= "published|draft" |
status ?= "published|draft" |
字段匹配任意一个值 | P2 |
| 多值 NONE 操作 | status ?!= "deleted" |
status ?!= "deleted" |
字段不匹配任何值 | P2 |
| 多值 LIKE | title ?~ "%rust%|%.js" |
title ?~ "%rust%|%.js" |
模糊匹配任意一个模式 | P2 |
| 跨表引用 | @collection.posts.author |
@table.posts.author |
关联查询 | P3 |
| 时间格式化 | strftime("%Y", created_at) |
strftime("%Y", created_at) |
提取时间部分 | P3 |
| 地理距离 | geoDistance(lat, lng, 40.7, -74.0) |
geoDistance(lat, lng, 40.7, -74.0) |
LBS 过滤 | P3 |
五、核心代码结构
5.1 文件组织
src/content_type/
├── rule_engine.rs # 表达式引擎核心(Lexer + Parser + AST + SQL/Evaluate)
├── schema.rs # ApiConfig + ApiEndpointConfig + CachedRules
├── handler.rs # build_rule_sql() + do_list/do_get/do_update/do_delete 集成
├── repository.rs # ContentQuery.rule_where/rule_params + find() 注入
└── ...
plugin-wit/plugin.wit # WIT 接口(含 filter/filter_auth 类型)
5.2 rule_engine.rs 内部结构
rule_engine.rs
│
├── RuleContext # 求值上下文(auth_user_id, auth_role, body, query_params)
│
├── Lexer # 词法分析:字符串 → Vec<Token>
│ ├── Token::Identifier # 字段名、@request.auth.*
│ ├── Token::StringLit # "字符串"
│ ├── Token::NumberLit # 数字
│ ├── Token::BoolLit # true/false
│ ├── Token::NullLit # null
│ ├── Token::Eq/Neq/... # 比较运算符
│ ├── Token::And/Or # 逻辑运算符
│ └── Token::LParen/RParen
│
├── Parser # 语法分析:Vec<Token> → Expr (AST)
│ ├── parse_or() # || 优先级最低
│ ├── parse_and() # && 优先级中间
│ ├── parse_comparison() # 比较运算符优先级最高
│ └── parse_atom() # 操作数(字段名/字面量/特殊变量)
│
├── Expr (AST) # 抽象语法树
│ ├── Compare { left, op, right } # 比较表达式
│ ├── And(lhs, rhs) # 逻辑与
│ └── Or(lhs, rhs) # 逻辑或
│
├── Operand # 操作数
│ ├── Field(String) # 数据库字段
│ ├── AuthId # @request.auth.id
│ ├── AuthRole # @request.auth.role
│ ├── StringLit/NumberLit/... # 字面量
│ └── Null # null
│
├── Rule # 解析后的规则
│ ├── parse(source) → Rule # 入口:字符串 → AST
│ ├── to_sql(offset) → (sql, params) # 编译为 SQL
│ └── evaluate(record, ctx) → bool # 运行时求值
│
└── compile_rule_sql() # 带 auth 替换的 SQL 编译(公共接口)
5.3 schema.rs 相关类型
// 访问级别
enum ApiAccess { None, Public, Member, Admin }
// 单个端点配置
struct ApiEndpointConfig {
access: ApiAccess,
filter: Option<String>, // 原始表达式字符串
filter_auth: Option<String>, // 原始表达式字符串
}
// 5 个端点
struct ApiConfig {
list: ApiEndpointConfig,
get: ApiEndpointConfig,
create: ApiEndpointConfig,
update: ApiEndpointConfig,
delete: ApiEndpointConfig,
}
// 预解析缓存(启动时填充,不序列化)
struct CachedEndpointRules {
filter: Option<Rule>, // 已解析的 AST
filter_auth: Option<Rule>, // 已解析的 AST
}
struct CachedRules {
list: CachedEndpointRules,
get: CachedEndpointRules,
create: CachedEndpointRules,
update: CachedEndpointRules,
delete: CachedEndpointRules,
}
struct ContentTypeSchema {
// ...
api: ApiConfig,
cached_rules: Option<CachedRules>, // 注册时 cache_rules() 填充
}
5.4 handler.rs 集成点
请求到达
│
├─ check_api_access(endpoint.access) ← 粗粒度:None/Public/Member/Admin
│
├─ build_rule_sql(&endpoint, auth) ← 细粒度:编译规则为 SQL
│ ├─ filter 编译为 SQL(参数化)
│ ├─ filter_auth 编译为 SQL(注入 auth.id/role)
│ └─ 组合:(filter_sql OR filter_auth_sql)
│
├─ [list/get] ContentQuery.rule_where 注入 → Repository::find()
│ → SQL WHERE ... AND (rule)
│
└─ [update/delete] 先 find_by_id → Rule::evaluate(record, ctx)
→ false → 403 Forbidden
5.5 repository.rs 查询注入
struct ContentQuery {
// ... 原有字段 ...
pub rule_where: Option<String>, // API Rule 编译的 SQL 片段
pub rule_params: Vec<String>, // 对应参数
}
// find() 中:
// 1. 构建 WHERE 子句(status、filters、tenant_id...)
// 2. 追加 rule_where:AND (rule_sql)
// 3. 追加 rule_params 到参数列表
// 4. 绑定参数执行查询
六、性能设计
| 环节 | 策略 | 说明 |
|---|---|---|
| 规则解析 | 启动时一次 | cache_rules() 在 schema 注册时解析,请求时零解析开销 |
| SQL 编译 | 每次请求 | compile_rule_sql() 生成 WHERE 片段 + 参数列表,开销 < 1μs |
| 运行时求值 | CUD 时 | Rule::evaluate() 遍历 AST 树,开销 < 1μs |
| 参数绑定 | 参数化 | 规则编译为 ?N 占位符 + 参数列表,防 SQL 注入 |
| LIKE 实现 | SQL LIKE | 使用 SQL 原生 LIKE,非 Regex 编译 |
七、安全设计
| 威胁 | 防护 |
|---|---|
| SQL 注入 | 规则编译为参数化查询,用户输入不拼入 SQL |
| 越权访问 | check_api_access + rule evaluate 双重检查 |
| auth 伪造 | auth.id/role 从 JWT 解析,非客户端传入 |
| 规则语法错误 | Rule::parse() 返回 Err,schema 加载时 log warn 并跳过该规则 |
| 未认证请求 | compile_rule_sql 对需要 auth 的规则返回 None → 拒绝请求 |
八、请求处理流程(完整示例)
场景:匿名用户 GET /cms/articles
1. Handler 收到请求
2. check_api_access(Public) → ✅ 通过
3. build_rule_sql(list, auth=None)
├─ filter = Rule('status = "published"')
├─ compile_rule_sql(rule, auth=None)
│ ├─ to_sql(0) → ("status = ?1", ["published"])
│ └─ 无 auth 替换 → 返回 ("status = ?1", ["published"])
└─ filter_auth 存在但 auth=None → 忽略
4. ContentQuery { rule_where: "status = ?1", rule_params: ["published"] }
5. Repository::find()
SQL: SELECT ... FROM articles WHERE ... AND (status = ?1)
Bind: ["published"]
6. 返回已发布文章列表
场景:作者 GET /cms/articles(已登录 user_id=abc)
1. Handler 收到请求
2. check_api_access(Public) → ✅ 通过
3. build_rule_sql(list, auth=Some(abc, member))
├─ filter = Rule('status = "published"')
│ → compile → ("status = ?1", ["published"])
├─ filter_auth = Rule('author_id = @request.auth.id')
│ → compile with auth → ("author_id = 'abc'", [])
└─ 组合 → ("(status = ?1 OR author_id = 'abc')", ["published"])
4. ContentQuery { rule_where: "(status = ?1 OR author_id = 'abc')", ... }
5. SQL: WHERE ... AND ((status = 'published' OR author_id = 'abc'))
6. 返回已发布文章 + 作者 abc 的草稿
场景:非作者 PUT /cms/articles/123(user_id=xyz, author_id=abc)
1. Handler 收到请求
2. check_api_access(Member) → ✅ 通过(已登录)
3. do_update 中 evaluate:
├─ 先 find_by_id → record = { author_id: "abc", ... }
├─ rule = Rule('author_id = @request.auth.id')
├─ evaluate(record, ctx={ auth_user_id: "xyz" })
│ → "abc" == "xyz" → false
└─ 返回 403 Forbidden
九、与 PocketBase 对比
| 维度 | PocketBase | raisfast(当前) | raisfast(计划) |
|---|---|---|---|
| 配置格式 | listRule/viewRule/... 字符串 | [api.list] access + filter + filter_auth |
同 |
| 表达式解析 | fexpr(自研) |
自研 Lexer + Parser | 同 |
| SQL 编译 | filter → WHERE | Rule::to_sql() + RuleEngineConfig | 同 |
| 运行时求值 | Go 原生 | Rust serde_json + RuleEngineConfig | 同 |
| 环境变量配置 | ❌ | ✅ RuleEngineConfig(14 项) | 同 |
@request.auth.* |
✅ | ✅ | — |
@request.body.* |
✅ | ✅ | — |
@request.query.* |
✅ | ✅ | — |
@now |
✅ | ✅ | — |
:isset |
✅ | ✅ | — |
:length |
✅ | ✅ | — |
:changed |
✅ | ❌ | Phase 2 |
:each |
✅ | ❌ | Phase 2 |
?= ?!= ?~ |
✅ | ❌ | Phase 2 |
@table.*(跨表) |
✅ | ❌ | Phase 3 |
geoDistance() |
✅ | ❌ | Phase 3 |
strftime() |
✅ | ❌ | Phase 3 |
| 覆盖率 | 100% | ~30% | ~85%(Phase 1-3 后) |
十、扩展路线图
Phase 1(1 天,覆盖 60% 场景)
新增 Operand 变体:
- RequestBody(String) ← @request.body.field_name
- RequestQuery(String) ← @request.query.param_name
- Now ← @now
新增 AST 节点:
- IsSet(Operand) ← field:isset
- Length(Operand) ← field:length
新增 SQL 编译:
- Now → datetime('now')
- IsSet → field IS NOT NULL
- Length → LENGTH(field)
Phase 2(1 天,覆盖 75% 场景)
新增 CmpOp:
- AnyEq / AnyNeq / AnyLike ← ?= ?!= ?~
新增后缀解析:
- :changed ← update 前后对比
新增 AST 节点:
- Each(Operand, Expr) ← field:each = "value"
修改 evaluate 签名:
- 改为 async(为 @collection 准备)
Phase 3(2 天,覆盖 85% 场景)
新增跨表查询:
- @table.posts.author ← async DB 查询(前缀可配置 RULE_PREFIX_CROSS_TABLE)
新增函数:
- strftime(fmt, field) ← 时间格式化
- geoDistance(lat, lng, lat2, lng2) ← 地理距离
十一、环境变量配置
所有规则引擎中的硬编码值均可通过环境变量覆盖,便于适配不同数据库后端(SQLite / PostgreSQL / MySQL)。
在 AppConfig.rule_engine: RuleEngineConfig 中定义,通过 .env 或环境变量设置:
表达式前缀
| 环境变量 | 默认值 | 说明 |
|---|---|---|
RULE_PREFIX_AUTH_ID |
@request.auth.id |
认证用户 ID 前缀 |
RULE_PREFIX_AUTH_ROLE |
@request.auth.role |
认证用户角色前缀 |
RULE_PREFIX_REQUEST_BODY |
@request.body. |
请求体字段前缀 |
RULE_PREFIX_REQUEST_QUERY |
@request.query. |
URL 查询参数前缀 |
RULE_PREFIX_NOW |
@now |
当前时间前缀 |
RULE_PREFIX_CROSS_TABLE |
@table. |
跨表引用前缀(Phase 3) |
SQL 编译
| 环境变量 | 默认值 | PostgreSQL 替代 | 说明 |
|---|---|---|---|
RULE_SQL_NOW_FN |
datetime('now') |
NOW() |
@now 编译为的 SQL 函数 |
RULE_SQL_ISSET_OP |
IS NOT NULL |
IS NOT NULL |
:isset 编译为的操作符 |
RULE_SQL_LENGTH_FN |
LENGTH |
CHAR_LENGTH |
:length 编译为的函数名 |
LIKE 通配符
| 环境变量 | 默认值 | 说明 |
|---|---|---|
RULE_SQL_LIKE_WILDCARD |
% |
SQL LIKE 通配符 |
RULE_SQL_LIKE_SINGLE_CHAR |
_ |
SQL LIKE 单字符通配符 |
RULE_REGEX_LIKE_WILDCARD |
.* |
LIKE 通配符对应的正则 |
RULE_REGEX_LIKE_SINGLE_CHAR |
. |
LIKE 单字符对应的正则 |
缓存
| 环境变量 | 默认值 | 说明 |
|---|---|---|
CMS_CACHE_TTL |
30 |
CMS 列表/详情缓存 TTL(秒) |
配置示例
PostgreSQL 适配:
RULE_SQL_NOW_FN=NOW()
RULE_SQL_LENGTH_FN=CHAR_LENGTH
CMS_CACHE_TTL=60