Files
raisfast/docs/db-execute-design.md
T
2026-04-16 18:56:58 +08:00

313 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# db_execute 插件扩展设计
> 2026-04-16 · 为插件系统增加数据库写操作能力,解锁电商/工作流等业务场景。
---
## 1. 目标
在现有 `Host.dbQuery(sql)` (只读) 基础上,新增 `Host.dbExecute(sql)` (写入),让插件可以执行 INSERT/UPDATE/DELETE。
**不做的事:**
- 不支持 DDLCREATE/DROP/ALTER/TRUNCATE
- 不支持事务(BEGIN/COMMIT/ROLLBACK
- 不支持批量语句(只执行第一条)
---
## 2. API 设计
### 插件端调用
```javascript
// JS 插件
const result = Host.dbExecute("UPDATE products SET stock = stock - 1 WHERE id = 'abc'");
// result: '{"rows_affected":1}' 或 '{"error":"no write permission for table: products"}'
```
```lua
-- Lua 插件
local result = Host.dbExecute("INSERT INTO orders (id, status) VALUES ('ord-1', 'pending')")
-- result: '{"rows_affected":1}' 或 '{"error":"..."}'
```
```rust
// WASM 插件(预期签名)
fn host_db_execute(ptr: i32, len: i32) -> i32;
```
### 返回格式
```json
// 成功
{"rows_affected": 3}
// 失败
{"error": "only INSERT/UPDATE/DELETE are allowed"}
{"error": "no write permission for table: orders"}
{"error": "DDL operations are not allowed"}
```
---
## 3. 权限模型
### manifest.toml 声明
```toml
[permissions]
database = [
"read:products", # 只读 products 表
"write:orders", # 只写 orders 表
"products", # 读写 products 表(等同于 read + write
"read:order_items",
"write:order_items",
]
```
### 权限规则(复用已有实现)
| 声明 | `db_query` (读) | `db_execute` (写) |
|---|---|---|
| `"*"` | ✅ 所有表 | ✅ 所有表 |
| `"products"` (裸表名) | ✅ | ✅ |
| `"read:products"` | ✅ | ❌ |
| `"write:products"` | ❌ | ✅ |
已有的 `is_table_writable()` (`src/plugins/permissions.rs:84-93`) 已覆盖以上逻辑。
---
## 4. 安全防护(4 层)
### Layer 1: SQL 类型白名单
```rust
fn is_write_query(sql: &str) -> bool {
let trimmed = sql.trim().to_uppercase();
trimmed.starts_with("INSERT")
|| trimmed.starts_with("UPDATE")
|| trimmed.starts_with("DELETE")
}
```
只允许 INSERT / UPDATE / DELETE,拒绝 SELECT / DDL / 其他。
### Layer 2: DDL 黑名单
```rust
fn is_ddl_query(sql: &str) -> bool {
let trimmed = sql.trim().to_uppercase();
trimmed.starts_with("CREATE")
|| trimmed.starts_with("DROP")
|| trimmed.starts_with("ALTER")
|| trimmed.starts_with("TRUNCATE")
|| trimmed.starts_with("ATTACH")
|| trimmed.starts_with("DETACH")
|| trimmed.starts_with("PRAGMA")
|| trimmed.starts_with("REINDEX")
|| trimmed.starts_with("ANALYZE")
|| trimmed.starts_with("VACUUM")
}
```
即使绕过 Layer 1,DDL 也会被拦截。
### Layer 3: 表级权限 RBAC
```rust
let table = extract_write_table_name(sql);
if !PermissionChecker::is_table_writable(&self.permissions, &table) {
return r#"{"error":"no write permission for table: ..."}"#.to_string();
}
```
### Layer 4: 系统表保护
```rust
const PROTECTED_TABLES: &[&str] = &[
"users", "roles", "permissions",
"extensions", "audit_log",
"plugin_storage", "options",
"rbac_roles", "rbac_permissions", "rbac_role_permissions",
"tenants",
];
fn is_protected_table(table: &str) -> bool {
PROTECTED_TABLES.contains(&table.to_lowercase().as_str())
}
```
即使声明 `"write:users"``"*"`,系统核心表也拒绝写入。
---
## 5. 写 SQL 表名提取
现有 `extract_table_name` 只解析 `SELECT ... FROM table`。新增 `extract_write_table_name`
```rust
fn extract_write_table_name(sql: &str) -> Option<String> {
let upper = sql.trim().to_uppercase();
if upper.starts_with("INSERT") {
// INSERT INTO table_name ...
extract_after_keyword(&upper, "INTO")
} else if upper.starts_with("UPDATE") {
// UPDATE table_name SET ...
extract_first_identifier_after(&upper, "UPDATE")
} else if upper.starts_with("DELETE") {
// DELETE FROM table_name ...
extract_after_keyword(&upper, "FROM")
} else {
None
}
}
```
---
## 6. 实现变更清单
### `src/plugins/permissions.rs`
| 操作 | 说明 |
|---|---|
| 新增 `is_write_query(sql)` | 检查是否 INSERT/UPDATE/DELETE |
| 新增 `is_ddl_query(sql)` | DDL 黑名单检查 |
| 新增 `extract_write_table_name(sql)` | 提取写操作的目标表名 |
| 新增 `is_protected_table(table)` | 系统表保护 |
| 已有 `is_table_writable(permissions, table)` | 无需修改 |
### `src/plugins/host_common.rs`
| 操作 | 说明 |
|---|---|
| 新增 `HostContext::db_execute(&self, sql: &str) -> String` | 核心实现:4 层安全检查 + 执行 + 返回 JSON |
### `src/plugins/js_host.rs`
| 操作 | 说明 |
|---|---|
| 新增 `dbExecute` 函数注册 | `Host.dbExecute(sql: string) -> string` |
### `src/plugins/lua_host.rs`
| 操作 | 说明 |
|---|---|
| 新增 `dbExecute` 函数注册 | `Host.dbExecute(sql) -> string` |
### `src/plugins/host.rs`
| 操作 | 说明 |
|---|---|
| 新增 `host_db_execute` 函数 | WASM ABI`(ptr, len) -> ptr` |
### `src/plugins/mod.rs`
| 操作 | 说明 |
|---|---|
| 无修改 | `rows_to_json` 不适用于写操作,新增 `execute_result_to_json` |
---
## 7. db_execute 核心实现
```rust
// host_common.rs
impl HostContext {
pub fn db_execute(&self, sql: &str) -> String {
// Layer 1: 只允许 INSERT/UPDATE/DELETE
if !PermissionChecker::is_write_query(sql) {
return r#"{"error":"only INSERT/UPDATE/DELETE are allowed"}"#.to_string();
}
// Layer 2: 拒绝 DDL(双重保险)
if PermissionChecker::is_ddl_query(sql) {
return r#"{"error":"DDL operations are not allowed"}"#.to_string();
}
// Layer 3: 表级权限
let table = match PermissionChecker::extract_write_table_name(sql) {
Some(t) => t,
None => return r#"{"error":"cannot parse table name from SQL"}"#.to_string(),
};
if PermissionChecker::is_protected_table(&table) {
return format!(r#"{{"error":"table '{table}' is protected and cannot be modified by plugins"}}"#);
}
if !PermissionChecker::is_table_writable(&self.permissions, &table) {
return format!(r#"{{"error":"no write permission for table: {table}"}}"#);
}
// 执行
let Some(pool) = &self.pool else {
return r#"{"error":"no database access"}"#.to_string();
};
let handle = tokio::runtime::Handle::current();
let sql = crate::db::dialect::translate(sql).into_owned();
tokio::task::block_in_place(|| {
match handle.block_on(async {
let result = sqlx::query(&sql).execute(pool).await?;
Ok::<_, sqlx::Error>(result)
}) {
Ok(r) => format!(r#"{{"rows_affected":{}}}"#, r.rows_affected()),
Err(e) => format!(r#"{{"error":"{e}"}}"#),
}
})
}
}
```
---
## 8. 测试计划
### 单元测试 (`permissions.rs`)
| 测试 | 输入 | 预期 |
|---|---|---|
| `is_write_query("INSERT INTO ...")` | INSERT | true |
| `is_write_query("UPDATE ...")` | UPDATE | true |
| `is_write_query("DELETE FROM ...")` | DELETE | true |
| `is_write_query("SELECT ...")` | SELECT | false |
| `is_write_query("DROP TABLE ...")` | DDL | false |
| `is_ddl_query("CREATE TABLE ...")` | CREATE | true |
| `is_ddl_query("ALTER TABLE ...")` | ALTER | true |
| `is_ddl_query("INSERT INTO ...")` | INSERT | false |
| `extract_write_table_name("INSERT INTO orders ...")` | INSERT | Some("orders") |
| `extract_write_table_name("UPDATE products SET ...")` | UPDATE | Some("products") |
| `extract_write_table_name("DELETE FROM cart_items ...")` | DELETE | Some("cart_items") |
| `extract_write_table_name("SELECT * FROM orders")` | SELECT | None |
| `is_protected_table("users")` | users | true |
| `is_protected_table("orders")` | orders | false |
| `is_table_writable(["write:orders"], "orders")` | — | true |
| `is_table_writable(["read:orders"], "orders")` | — | false |
| `is_table_writable(["orders"], "orders")` | — | true |
### 集成测试
| 测试 | 说明 |
|---|---|
| 插件写入有权限的表 | 成功,返回 rows_affected |
| 插件写入无权限的表 | 返回 error |
| 插件写入系统表 | 返回 protected error |
| 插件执行 SELECT | 返回 "only INSERT/UPDATE/DELETE allowed" |
| 插件执行 DDL | 返回 "DDL not allowed" |
---
## 9. 工作量
| 文件 | 操作 | 时间 |
|---|---|---|
| `permissions.rs` | 新增 4 个函数 + 15 个单元测试 | 2h |
| `host_common.rs` | 新增 `db_execute` 方法 | 1h |
| `js_host.rs` | 注册 `dbExecute` | 30min |
| `lua_host.rs` | 注册 `dbExecute` | 30min |
| `host.rs` | 注册 `host_db_execute` | 30min |
| 集成测试 | API 端到端测试 | 1h |
| **合计** | | **~5-6h** |