Files
dsh_ai1net_server/交付物/插件建库权限-D档落地Runbook-20260922.md
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 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)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

7.4 KiB
Raw Permalink Blame History

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。


五 ⚠️ 必须同步落的两处(否则换机器重建时漏掉)

  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)。

六 平台侧代码接口(下一棒照此实现)

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