fix(store-accounts): 放宽 passwordConfigured 密文判定正则并同步契约文档
20260908120000 追加的 passwordConfigured 在生产 958 行全部为 false, 与 DBA 基线 957 true / 1 false 不符。 视图的 FROM 子查询本身就取自 public."StoreAccount",EXISTS 又按 credential.id = original.id 自连接同一行,id 必然命中;唯一失配点是密文正则。 该正则只接受 URL-safe base64([A-Za-z0-9_-])且长度固定,标准 base64 的 + / = 填充会让整体失配,而 Node 的 base64url 解码器对填充宽容, 所以"可解密"与"正则失配"可以同时成立。 本迁移只重写视图定义,不读写任何数据: - 用 replace 换字符集片段(避开 pg_get_viewdef 的反斜杠转义差异) - 幂等,未接入该视图或缺少该列则跳过 - 列名/列序/列类型/行数/ACL 均不变,空值、空串与明文仍判定为 false 已在本地 PostgreSQL 17 复现并验证:带填充密文由 false 变 true, base64url 保持 true,空值/空串/明文垃圾值仍为 false,ACL 与数据无变化。 同步更新 finance-store-account-password-reveal.md 中已过时的 "生产 passwordConfigured 尚未落库" 结论与验收记录。
This commit is contained in:
parent
df9c0ad67f
commit
8d8419a124
@ -7,6 +7,7 @@
|
||||
- 密码真相源是 `public."StoreAccount"."passwordCiphertext"`,使用现有 `src/lib/store-account-crypto.ts` 的 AES-256-GCM(随机 12 字节 IV、认证标签、`v1:` 封装)。不是明文或不可逆 Hash。
|
||||
- 生产密钥来自 `STORE_ACCOUNT_ENCRYPTION_KEY`,不从数据库读取,不复用财务服务认证凭据。已有银行账号加密也使用此密钥,轮换时必须同时处理。
|
||||
- 2026-09-08 对 `furniture3d_prod` 的只读核验:`accounts_v1` COUNT=958、DISTINCT id=958;957 条非空密文均可在生产主机内存中解密,空值 1、解密失败 0。过程只输出聚合,不导出密码或密文。
|
||||
- 2026-09-09 外部只读账号复核:`passwordConfigured` 聚合为 **false=958**,与上述 957/1/0 基线不符。已定位为 `20260908120000` 的判定正则只接受 URL-safe base64,标准 base64 的 `+`、`/`、`=` 填充会让正则整体失配(Node 的 base64url 解码器对填充宽容,因此"可解密"与"正则失配"并不矛盾)。修正见 `20260909120000_fix_store_account_password_configured_regex`,纯视图 DDL、不读写任何数据,生产执行仍需 DBA 审批。
|
||||
- 普通 `store_account_reader` 可读视图、不能读取基表或密码列。此次迁移不增加其基表权限。
|
||||
- 本接口只信任专用服务身份 `finance-store-accounts`。财务系统必须在自身后端校验用户登录态、查看/复制权限及店铺范围,然后从可信会话中取 `requesterId`。不得把客户端传入的任意人员 ID 当成操作者。服务凭据仅保存在财务后端,禁止交给浏览器。
|
||||
- 不提供列表、批量密码导出或自动预取。列表只读取 `passwordConfigured`;每次明确的查看/复制操作单独调用接口。上游审计记录的用户 ID 是专用财务服务的身份断言,未授权请求中的 ID 不可信,`serviceId=null`。
|
||||
@ -95,6 +96,7 @@ Content-Type: application/json
|
||||
- 新增 `StoreAccountPasswordAccessAudit`,固定字段为请求追踪 ID、人员标识、服务身份、账号 ID、用途、HTTP状态、数据库时间;不关联用户/店铺外键,以保留未授权、不存在和删除账号的失败审计。状态 200 表示成功,其他状态表示失败。
|
||||
- 只在原视图存在时,以原视图查询作为子查询,在末尾追加 `passwordConfigured boolean`,保留原字段、筛选、每行内容、ID、所有者与 ACL;`EXISTS` 不会产生重复行。未配置外部视图的环境不擅自创建外部读入口。
|
||||
- `passwordConfigured` 表示存在格式完整的、非空 AES-GCM 密码封装。SQL 不接触密钥,所以不在每次列表查询时验证 GCM 标签/密钥健康;生产预检必须做主机内存中的解密聚合验证。密钥丢失或密文损坏属于 503 运维故障,不能把它伪装成未设置密码。
|
||||
- 判定正则必须同时接受 base64url 与带 `=` 填充的标准 base64。只认 URL-safe 字符集会把全部存量密文误判成"未配置"(`20260909120000` 修正的就是这一点);同时仍保留 `v1:` 前缀、三段结构与 IV 长度约束,空值、空串与明文垃圾值依旧判定为 false。
|
||||
- 无历史明文迁移,无现有密码或银行账号数据变更,无额外只读权限。审计数据增量写入,上线后按安全审计要求保留至少 365 天;此 PR 不创建自动删除任务。
|
||||
|
||||
迁移前由 DBA 只读确认目标库、视图定义/所有者/ACL、字段顺序、行数与唯一性、密码聚合、普通读取账号和其成员角色权限。若生产仍以本次基线验收,COUNT 与 DISTINCT id 必须均为 958;若业务新增/删除则暂停并重新确认基线,不改数据凑数。
|
||||
@ -124,7 +126,7 @@ Content-Type: application/json
|
||||
- 隔离本地 PostgreSQL 集成测试:10 项通过,覆盖完整加解密与 HTTP handler、已提交审计、403/404/429/no-store、并发/跨实例限流、分钟/24小时配额、审计失败拒绝泄露、原字段及筛选保留、958/958 与 boolean 957/1/0、普通只读角色拒绝基表/密码/审计读取。
|
||||
- 开启本地测试连接的 PostgreSQL statement 日志,只在内存扫描 `docker logs`;应用日志、审计行、SQL请求与真实数据库日志均未检出随机测试密码。测试无需打印测试密码,断言只输出布尔结果。
|
||||
- Type-check 通过;范围内 ESLint 通过。全仓库主数据边界巡检有 6 项已有违规,均位于本次未修改模块;不得据此声称全仓库通过。
|
||||
- 生产仅完成只读基线核查;生产 `passwordConfigured` 尚未落库,生产接口尚未发布,因此没有生产新增字段/新接口的验收结论。
|
||||
- 生产已完成只读基线核查;`passwordConfigured` 已随 `20260908120000` 落库,但当前 958 行全部判定为 false,修正迁移 `20260909120000` 已交付、待 DBA 审批执行。修正后应复跑 `scripts/sql/check-store-account-password-reveal.sql`,预期回到 957/1/0;若仍不符,说明生产密文存在第三种形态,需暂停并重新确认。生产接口尚未发布,因此没有生产新接口的验收结论。
|
||||
|
||||
运行:`npx jest --runInBand --runTestsByPath src/server/services/store-account-password-reveal.service.test.ts src/proxy-store-account-reveal.test.ts "src/app/internal/store-accounts/[id]/password/reveal/route.test.ts" tests/integration/store-account-password-reveal.test.ts`。未配置本地测试库变量时,集成套件明确 skip,不能算真实数据库验收通过。
|
||||
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
-- 修正 external_store_accounts.accounts_v1 的 passwordConfigured 误判为 false。
|
||||
--
|
||||
-- 背景:
|
||||
-- 20260908120000 把 passwordConfigured 追加为
|
||||
-- EXISTS(public."StoreAccount" 中 id 相同且 passwordCiphertext ~ '^v1:[A-Za-z0-9_-]{16}\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{2,}$')
|
||||
-- 该视图的 FROM 子查询本身就取自 public."StoreAccount",EXISTS 再用 credential.id = original.id
|
||||
-- 自连接同一行,因此 id 必然命中;唯一可能失败的判定就是密文正则。
|
||||
-- 生产实测 958 行全部 false,而 DBA 基线核验为 957 条非空且可解密密文,说明密文真实格式不匹配该正则。
|
||||
--
|
||||
-- 根因:
|
||||
-- 正则只接受 URL-safe base64 字符集(A-Za-z0-9_-)且长度固定。若密文以标准 base64 保存,
|
||||
-- 会出现 +、/、以及 = 填充(16 字节 tag 的填充形态为 22 字符 + '=='),正则整体失配。
|
||||
-- 本迁移放宽字符集并允许尾部填充,不改变 v1: 前缀、三段结构与 IV 长度约束。
|
||||
--
|
||||
-- 数据影响:**无**。本迁移只重写视图定义(DDL),不读写、不改写、不删除任何业务数据,
|
||||
-- 不触碰 passwordCiphertext,不重建索引,不修改 owner/ACL(CREATE OR REPLACE VIEW 保留既有授权)。
|
||||
-- 列集合、列顺序、列类型完全不变,仅替换布尔表达式中的正则字面量。
|
||||
|
||||
BEGIN;
|
||||
SET LOCAL lock_timeout = '5s';
|
||||
SET LOCAL statement_timeout = '30s';
|
||||
|
||||
DO $fix$
|
||||
DECLARE
|
||||
d TEXT;
|
||||
BEGIN
|
||||
-- 未接入该外部视图的环境直接跳过,不擅自创建外部读入口。
|
||||
IF to_regclass('external_store_accounts.accounts_v1') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- 只有已经追加过 passwordConfigured 的库才需要修正;否则保持原状。
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'external_store_accounts'
|
||||
AND table_name = 'accounts_v1'
|
||||
AND column_name = 'passwordConfigured'
|
||||
) THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
d := rtrim(pg_get_viewdef('external_store_accounts.accounts_v1'::regclass, true), E';\n\r ');
|
||||
|
||||
-- 幂等:已修正过则直接跳过。
|
||||
IF position('[A-Za-z0-9+/_=-]{22}={0,2}' IN d) > 0 THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- 只替换字符集片段,避开 pg_get_viewdef 对反斜杠的转义差异。
|
||||
-- IV 段 {16} 不动:12 字节 IV 在两种编码下都是 16 字符且永不填充。
|
||||
IF position('[A-Za-z0-9_-]{22}' IN d) = 0
|
||||
OR position('[A-Za-z0-9_-]{2,}' IN d) = 0 THEN
|
||||
RAISE EXCEPTION '未找到待修正的旧正则片段,视图可能已被其它迁移改写,需人工复核';
|
||||
END IF;
|
||||
|
||||
d := replace(d, '[A-Za-z0-9_-]{22}', '[A-Za-z0-9+/_=-]{22}={0,2}');
|
||||
d := replace(d, '[A-Za-z0-9_-]{2,}', '[A-Za-z0-9+/_=-]{2,}={0,2}');
|
||||
|
||||
-- 列集合与类型不变,CREATE OR REPLACE VIEW 合法;既有 ACL 与 owner 保持。
|
||||
EXECUTE format(
|
||||
'CREATE OR REPLACE VIEW external_store_accounts.accounts_v1 AS %s',
|
||||
d
|
||||
);
|
||||
END $fix$;
|
||||
|
||||
COMMIT;
|
||||
Loading…
x
Reference in New Issue
Block a user