142 lines
7.4 KiB
Markdown
142 lines
7.4 KiB
Markdown
# D 档落地 Runbook · 插件建库权限(`SECURITY DEFINER` 函数 + `GRANT EXECUTE`)
|
||||
|
|
|
|||
|
|
> 🔴 **2026-09-23 状态:本件已作废,⛔ 不得执行。**
|
|||
|
|
> **原因**:2026-09-23 实测证伪 —— PG 显式禁止从函数内执行 `CREATE DATABASE`(`ERROR: CREATE DATABASE cannot be executed from a function`),`SECURITY DEFINER` 封装路径对建库无效。
|
|||
|
|
> **替代件**:`交付物/建库权限-D档被证伪与修正判定-20260923.md`(含实测证据 + B / C 两候选,待拍板)。
|
|||
|
|
> **正文以下保留作过程记录,⛔ 不再作为执行依据。**
|
|||
|
|
>
|
|||
|
|
> **决策**:2026-09-22 19:3x 用户拍板 **D**(原话「D」)。
|
|||
|
|
> **目标**:让平台能"点按钮建库",而 **⛔ 不**把 `CREATEDB` 给 `dshs` 角色。
|
|||
|
|
> **执行前置**:`dshs.service` 全局执行锁必须已释放(动服务器属 R9 管辖);本件可**幂等重跑**。
|
|||
|
|
> **机位**:仅 **47**(PG 只在 Manager 上;106 无 PG)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一 前置事实(2026-09-22 实测 · 全部现场取证)
|
|||
|
|
|
|||
|
|
| 项 | 读数 | 含义 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 超级用户通路 | `su postgres -c 'cd /tmp && psql -h /var/run/postgresql -p 15432 -d dshs …'` ⇒ `current_user=postgres` | ✅ **可用**(`local all all peer` + 本机 socket)|⚠️ 必须先 `cd /tmp`,否则 `su` 切目录被拒 |
|
|||
|
|
| `dshs` 角色 | `rolsuper=f` · `rolcreatedb=f` · `rolcreaterole=f` · **角色成员 = none** | 无任何间接提权路径 |
|
|||
|
|
| `pg_hba` | `host all all 127.0.0.1/32 scram-sha-256` | ✅ `dshs` 对**新建的** `dshs_pl_*` 库也能连 |
|
|||
|
|
| 数据目录 / socket | `/var/lib/dshs-pg` · `/var/run/postgresql/.s.PGSQL.15432` | 非容器;`docker exec` 写法一律无效 |
|
|||
|
|
| 既有 `dshs_%` 函数 | **0 条** | 无命名冲突 |
|
|||
|
|
| ⚠️ `public` schema ACL | `{postgres=UC/postgres,=UC/postgres}` | **PUBLIC 在 `public` 上有 CREATE**(PG13 默认)⇒ 🔴 函数**不得**放 `public` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二 执行(一次性 · 幂等)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 1) 私有 schema(放 public 有被同库其他角色抢先建同名函数的风险)
|
|||
|
|
CREATE SCHEMA IF NOT EXISTS dshs_int AUTHORIZATION postgres;
|
|||
|
|
REVOKE ALL ON SCHEMA dshs_int FROM PUBLIC;
|
|||
|
|
GRANT USAGE ON SCHEMA dshs_int TO dshs;
|
|||
|
|
|
|||
|
|
-- 2) 建库函数:库名白名单 + SECURITY DEFINER
|
|||
|
|
CREATE OR REPLACE FUNCTION dshs_int.create_plugin_database(p_name text)
|
|||
|
|
RETURNS text
|
|||
|
|
LANGUAGE plpgsql
|
|||
|
|
SECURITY DEFINER
|
|||
|
|
SET search_path = pg_catalog -- ⛔ 必须有:防 search_path 劫持
|
|||
|
|
AS $fn$
|
|||
|
|
DECLARE
|
|||
|
|
v_owner constant text := 'dshs';
|
|||
|
|
BEGIN
|
|||
|
|
IF p_name IS NULL OR p_name !~ '^dshs_pl_[a-z][a-z0-9_]{0,40}$' THEN
|
|||
|
|
RAISE EXCEPTION 'invalid plugin database name: %', p_name USING ERRCODE = '22023';
|
|||
|
|
END IF;
|
|||
|
|
IF EXISTS (SELECT 1 FROM pg_database WHERE datname = p_name) THEN
|
|||
|
|
RETURN 'exists';
|
|||
|
|
END IF;
|
|||
|
|
EXECUTE format('CREATE DATABASE %I OWNER %I ENCODING %L TEMPLATE template0',
|
|||
|
|
p_name, v_owner, 'UTF8');
|
|||
|
|
RAISE LOG 'dshs plugin db created: %', p_name;
|
|||
|
|
RETURN 'created';
|
|||
|
|
EXCEPTION
|
|||
|
|
WHEN duplicate_database THEN
|
|||
|
|
RETURN 'exists';
|
|||
|
|
END;
|
|||
|
|
$fn$;
|
|||
|
|
|
|||
|
|
REVOKE ALL ON FUNCTION dshs_int.create_plugin_database(text) FROM PUBLIC;
|
|||
|
|
GRANT EXECUTE ON FUNCTION dshs_int.create_plugin_database(text) TO dshs;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**平台侧调用**(`dshs` 连接、`dshs` 库):
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
select dshs_int.create_plugin_database('dshs_pl_trpg_kit'); -- → created | exists
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
要点:
|
|||
|
|
- 返回 `created` / `exists` 两态 ⇒ **幂等由函数保证**,平台不必先查 `pg_database`(避免 TOCTOU)。
|
|||
|
|
- `RAISE EXCEPTION … ERRCODE 22023` ⇒ 库名非法(含注入串)**直接抛**,平台按 400 处理。
|
|||
|
|
- `RAISE LOG` 写进 PG 日志(journald 可查);⚠️ 不进 `plugin_data_audit`(那是**内核表**,跨库不可写 —— 平台侧在**调用后**写台账 + 审计)。
|
|||
|
|
- `OWNER = dshs` ⇒ 新库的 owner 是平台角色,后续建表 / `pg_dump` / `DROP` 都不再需要超级用户。
|
|||
|
|
- `TEMPLATE template0` + `ENCODING UTF8` ⇒ 不受 `template1` 里被塞过的对象影响。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三 验收(7 条 · 逐条可复现)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
① 建库成功 [dshs] select dshs_int.create_plugin_database('dshs_pl_selftest') → created
|
|||
|
|
② 幂等 [dshs] 再跑同一条 → exists
|
|||
|
|
③ 库与属主 [postgres] select datname, pg_get_userbyid(datdba) from pg_database
|
|||
|
|
where datname='dshs_pl_selftest' → dshs_pl_selftest | dshs
|
|||
|
|
④ 白名单 [dshs] select dshs_int.create_plugin_database('evil') → ERROR 22023
|
|||
|
|
④' 注入串 [dshs] select dshs_int.create_plugin_database('dshs_pl_x; DROP DATABASE dshs') → ERROR 22023
|
|||
|
|
⑤ ⛔ 权限未扩大 [postgres] select rolsuper, rolcreatedb from pg_roles where rolname='dshs' → f | f
|
|||
|
|
⑥ 无旁路 [dshs] create database t → ERROR 42501(权限不足)
|
|||
|
|
⑦ 可连新库 [dshs] psql -h 127.0.0.1 -p 15432 -d dshs_pl_selftest -c 'create schema s' → CREATE SCHEMA
|
|||
|
|
清理 [postgres] DROP DATABASE dshs_pl_selftest (验收产物,⛔ 别留)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
⑤ 是本档的**核心断言**:`rolcreatedb` 必须**仍是 f** —— 若它变成 t,说明做成了 A 档,立即回滚。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四 回滚
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
REVOKE EXECUTE ON FUNCTION dshs_int.create_plugin_database(text) FROM dshs; -- 只停新库创建
|
|||
|
|
-- 或彻底移除:
|
|||
|
|
DROP FUNCTION dshs_int.create_plugin_database(text);
|
|||
|
|
DROP SCHEMA dshs_int; -- 空 schema 才可删
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
影响面:**已建好的 `dshs_pl_*` 库不受影响**(owner 是 `dshs`,平台照常建表读写);只是新插件建不了库。回滚点 = 平台建库口返回 `PLUGIN_DB_HELPER_MISSING`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五 ⚠️ 必须同步落的两处(否则换机器重建时漏掉)
|
|||
|
|
|
|||
|
|
1. **`dsh-server-docs/DEPLOY-本部署.md`** 的 DB 初始化步骤追加本份 DDL(标 🔴「`dshs` 角色**不**给 `CREATEDB`,建库走 `dshs_int.create_plugin_database`」)。
|
|||
|
|
2. **`交接单/插件投放与分库线-①…md §五 S4-2`** 改写:建库 = **admin 显式按钮** → 调本函数;`§十-D1` 同步改(原文写的是"上传钩子建库")。
|
|||
|
|
3. **`DB-03-插件数据面规范.md §三 / §四 / §六`** 回改(谁建库 / 何时建 / 台账表 `plugin_datastores`)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六 平台侧代码接口(下一棒照此实现)
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// src/db/plugin-data/datastore.ts
|
|||
|
|
export async function createPluginDatabase(client: PoolClient, dbName: string): Promise<'created' | 'exists'>
|
|||
|
|
// 错误映射(供路由层用):
|
|||
|
|
// ERRCODE 22023 ⇒ 400 invalid_plugin_db_name
|
|||
|
|
// ERRCODE 42501 ⇒ 503 PLUGIN_DB_HELPER_MISSING (函数缺 / 无 EXECUTE 权)
|
|||
|
|
// 42883 undefined_function ⇒ 503 PLUGIN_DB_HELPER_MISSING
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`.dsh` 侧声明 → 库名的换算(`DB-03 §一`,⛔ 不改):包名去 scope、小写、`-`→`_`,前缀 `dshs_pl_`。
|
|||
|
|
例:`dsh-plugin-mcn-suite` → `dshs_pl_dsh_plugin_mcn_suite`;`@dsh-local/storyforge` → `dshs_pl_storyforge`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七 未做(具名原因)
|
|||
|
|
|
|||
|
|
| 项 | 原因 |
|
|||
|
|
|---|---|
|
|||
|
|
| 本 Runbook 的**执行** | `EXEC_LOCK_HELD_BY_IM_LINE_BATON5`(19:33 起占用;动服务器受 R9 管辖)⇒ 本轮只出可照抄脚本,⛔ 未执行 |
|
|||
|
|
| 对称的 `drop_plugin_database`(彻底卸载用) | ⛔ **不在本次拍板范围** ⇒ 不顺手做;登记为后续(`DB-03 §六-4` 的 `DROP DATABASE` 流程将来一并走) |
|