# 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 │ └───────┼─────────────────────────────────────────────────────────────┘ │ ┌───────┼─────────────────────────────────────────────────────────────┐ │ │ 请求时(每次请求) │ │ │ │ │ ▼ │ │ 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 个端点独立配置: ```toml [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 示例 **博客文章 — 匿名只看已发布,登录可创建** ```toml [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" ``` **电商订单 — 用户只能看自己的订单** ```toml [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" ``` **完全禁止外部访问(仅内部/插件使用)** ```toml [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::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 → 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 相关类型 ```rust // 访问级别 enum ApiAccess { None, Public, Member, Admin } // 单个端点配置 struct ApiEndpointConfig { access: ApiAccess, filter: Option, // 原始表达式字符串 filter_auth: Option, // 原始表达式字符串 } // 5 个端点 struct ApiConfig { list: ApiEndpointConfig, get: ApiEndpointConfig, create: ApiEndpointConfig, update: ApiEndpointConfig, delete: ApiEndpointConfig, } // 预解析缓存(启动时填充,不序列化) struct CachedEndpointRules { filter: Option, // 已解析的 AST filter_auth: Option, // 已解析的 AST } struct CachedRules { list: CachedEndpointRules, get: CachedEndpointRules, create: CachedEndpointRules, update: CachedEndpointRules, delete: CachedEndpointRules, } struct ContentTypeSchema { // ... api: ApiConfig, cached_rules: Option, // 注册时 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 查询注入 ```rust struct ContentQuery { // ... 原有字段 ... pub rule_where: Option, // API Rule 编译的 SQL 片段 pub rule_params: Vec, // 对应参数 } // 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 适配:** ```env RULE_SQL_NOW_FN=NOW() RULE_SQL_LENGTH_FN=CHAR_LENGTH CMS_CACHE_TTL=60 ```