# B 档落地 · 插件建库权限(角色级授权 `CREATEDB`) > **拍板**:2026-09-23 用户原话「**B**」⇒ 建库通路 = **B 档(给 `dshs` 角色加 `CREATEDB`)**。 > **状态**:✅ **已在 47 落地并通过 11 条验收**(2026-09-23 05:4x)。 > **机位**:仅 **47**(PG 只在 Manager 上;106 无 PG)。 > **前置件**:`交付物/建库权限-D档被证伪与修正判定-20260923.md`(本档为何取代 D 档)。 --- ## 一 拍板内容与落地动作 ```sql ALTER ROLE dshs CREATEDB; -- 唯一一句 DDL(幂等) ``` 平台侧建库 = 用**现有** `dshs` 连接、**单语句**执行: ```sql CREATE DATABASE dshs_pl_ OWNER dshs ENCODING 'UTF8' TEMPLATE template0; ``` 🔴 **必须单语句、不走事务** —— `CREATE DATABASE` 不能在事务块内(这是 D 档被证伪的同一条 PG 限制,B 档同样受约束,只是不再需要函数封装)。平台侧用 `pg` 的**非事务**执行路径(顶层 `client.query('CREATE DATABASE …')`,⛔ 不要包在 `BEGIN/COMMIT` 里)。 --- ## 二 验收(11 条 · 实测读数) | # | 断言 | 实测 | 判定 | |---|---|---|---| | ① | `dshs` 可建库 | 建库成功 | ✅ | | ② | 重名建库被拒(幂等靠错误码捕获) | `ERROR: database … already exists` | ✅ | | ③ | **新库属主 = `dshs`** | `dshs_pl_bselftest/dshs` | ✅ | | ④ | 可连新库并建表 | `create table` 成功 | ✅ | | ⑤ | 新库内对象归 `dshs` | 表数 = `1` | ✅ | | ⑥ | **权限精确**(须 `f\|t\|f`) | `false\|true\|false` | ✅ | | ⑦ | ⛔ 未拿到 `CREATEROLE` | `t`(= 无该权) | ✅ | | ⑧ | ⛔ 仍非超级用户 | `t`(= 非超管) | ✅ | | ⑨ | 库名白名单 | **PG 侧零拦截**(建 `evil_probe` 成功) | ⚠️ 见 §三 | | ⑩ | 内核表完好 | `13` | ✅ | | ⑪ | 现有库未损 | `2`(`dshs` + `postgres`) | ✅ | | — | 清理后残留 | `0` | 服务 `dshs = active` | ✅ | ⑥ 是本档的**核心断言**:只多出 `CREATEDB` 一位,`rolsuper` / `rolcreaterole` 必须保持 `false`。 --- ## 三 🔴 本档的已知代价(必须由平台代码兜住) ⑨ 揭示了 B 档的真实缺口:**PG 侧没有"只允许建 `dshs_pl_*` 前缀库"的原生机制** ⇒ 白名单**只能靠平台代码自觉**。这正是 09-22 否掉 A 档的同一条理由,B 档原样继承。 **因此平台侧必须强制三点**(缺一即为该档的漏洞面): 1. **库名单点生成** —— 库名只能来自 `.dsh` 声明的换算函数(包名去 scope、小写、`-`→`_`、前缀 `dshs_pl_`),⛔ 不接受外部传入的任意串。 2. **建库前正则复校** —— 照抄 D 档那条:`^dshs_pl_[a-z][a-z0-9_]{0,40}$`,不匹配直接 400,**⛔ 不把变量拼进 SQL 字符串**(用标识符引用或参数化)。 3. **每次建库落台账 + 审计** —— 写 `plugin_datastores`(库名 / 插件 ID / 建库时间 / 结果),使"平台到底建过哪些库"可对账;`DROP` 须 admin 二次确认(`DB-03 §六-4`)。 --- ## 四 回滚 ```sql ALTER ROLE dshs NOCREATEDB; -- 一句话回滚 ``` 影响面:**已建好的 `dshs_pl_*` 库不受影响**(属主是 `dshs`,平台照常建表读写)⇒ 只是新插件建不了库。回滚点 = 平台建库口返回 `PG_CREATEDB_MISSING`(503)。 --- ## 五 ⚠️ 必须同步落的文档(回滚/重建时不能漏) 1. **`dsh-server-docs/DEPLOY-本部署.md`** DB 初始化步骤追加:`ALTER ROLE dshs CREATEDB;`(标 🔴 用途 = 插件建库;⛔ 不得顺手给 `CREATEROLE` / `SUPERUSER`)。 2. **`交接单/插件投放与分库线-①…md`** —— `§五 S4-2` 建库 = **admin 显式按钮**;`§十-D1` 原文「上传钩子建库」须同步改。 3. **`DB-03-插件数据面规范.md`** —— `§三`(谁建库)/ `§四`(何时建)/ `§六`(台账表 `plugin_datastores`)。 --- ## 六 平台侧代码接口 ```ts // src/db/plugin-data/datastore.ts export function pluginDbNameOf(pkgName: string): string // 纯函数:包名 → dshs_pl_* export function assertPluginDbName(dbName: string): void // ^dshs_pl_[a-z][a-z0-9_]{0,40}$ export async function createPluginDatabase(client: PoolClient, dbName: string): Promise<'created' | 'exists'> // 错误映射(供路由层用): // PG 42P04 duplicate_database ⇒ 'exists'(幂等,非错误) // PG 42501 权限不足 ⇒ 503 PG_CREATEDB_MISSING(未授 CREATEDB / 已回滚) // 名称不合白名单 ⇒ 400 invalid_plugin_db_name(平台侧复校,⛔ 不下发到 PG) ``` > 错误码变更:D 档的 `PLUGIN_DB_HELPER_MISSING` 随档位**作废**,恢复为 `PG_CREATEDB_MISSING`。 --- ## 七 未做(具名原因) | 项 | 原因 | |---|---| | 平台侧代码实现(§六) | 属交接单 §五 S4 的**代码面**,按用户拍板的「A 档排期」= 等 IM 线整阶段收官后一次性跑完 | | §五 三份文档回写 | 本轮落点已定,但改交接单/DB-03 会与他线**共享文件**冲突;按 R9 拉长到同一批执行 | | `drop_plugin_database` | ⛔ 不在本次拍板范围(`DB-03 §六-4` 将来一并走) |