回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复: - 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录) - tmp/(32.4 M,按接续棒命名的过程临时区) - .workbuddy/tmp/(39.5 M) - 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物) - tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留 入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与 接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、 .workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。 排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、 打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
7.4 KiB
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 |
二 执行(一次性 · 幂等)
-- 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 库):
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 档,立即回滚。
四 回滚
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。
五 ⚠️ 必须同步落的两处(否则换机器重建时漏掉)
dsh-server-docs/DEPLOY-本部署.md的 DB 初始化步骤追加本份 DDL(标 🔴「dshs角色不给CREATEDB,建库走dshs_int.create_plugin_database」)。交接单/插件投放与分库线-①…md §五 S4-2改写:建库 = admin 显式按钮 → 调本函数;§十-D1同步改(原文写的是"上传钩子建库")。DB-03-插件数据面规范.md §三 / §四 / §六回改(谁建库 / 何时建 / 台账表plugin_datastores)。
六 平台侧代码接口(下一棒照此实现)
// 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 流程将来一并走) |