# 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` 流程将来一并走) |