fix(rectification): v3 opens cases as the invoker so it can call v2 (BUG-1502)
The staging gate's real-database tests refused open_agentic_rectification_case_v3: it was security definer, and 20260814010000 revokes EXECUTE on the 11-argument v2 even from its owner, leaving service_role alone. v3 is now security invoker, like the 12-argument v2; reading the person and labelling the chat session move into two service_role-only security-definer helpers, because service_role has no rights on chart_profiles or chat_sessions. The migration has never run, so it is corrected in place. Also adds the annual.full price rows to the whole-table pricing assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
This commit is contained in:
@@ -20539,3 +20539,19 @@
|
||||
- 相关记录:BUG-1095、BUG-1220(同一函数的地点 / 参考系参数)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/annual-153`。
|
||||
|
||||
## BUG-1502 | 给他人做生时校正的 v3 开案函数调不动 v2,staging 门禁真库测试失败
|
||||
|
||||
- 状态:resolved(迁移修正 + 回归测试;待 staging 门禁复核)
|
||||
- 首次发现 / 最近更新:2026-11-01 / 2026-11-01(staging backend-quality-gate run 6342,提交 `1a7aba02`)
|
||||
- 影响面:`frontend/supabase/migrations/20261101010000_rectification_chart_subject.sql` 新增的 `open_agentic_rectification_case_v3`(#160)。给档案里的人开校正、重开一个已有校正会话都走它;本人新开校正仍走 v2,不受影响。迁移从未在任何环境执行过(两次门禁都在迁移步骤之前失败),没有用户受影响。
|
||||
- 现象:门禁真库测试报 `permission denied for function open_agentic_rectification_case_v2`(`database-rectification-chart-subject-20261101.test.ts`),以及 `rectification_service_failed`(`database-segment-skill-history.test.ts` 的「post-bump historical exact Skill … session open」)。
|
||||
- 触发条件:任何对 v3 的调用(给他人开案,或按会话重开)。
|
||||
- 根因:v3 写成了 `security definer`,以属主身份调用 11 参数的 v2;而 `20260814010000_immutable_skill_registry.sql` 有意把这几个函数的 EXECUTE 从属主身上也收回,只留给 service_role,所以 definer 包装被拒。另一方面 service_role 对 `chart_profiles`、`chat_sessions` 没有权限(`20260715030000`),也不能简单改成 invoker 就完事。
|
||||
- 修复:v3 改为 `security invoker`(和 12 参数 v2 一致,以 service_role 身份调 v2、改校正案例);查人物、写会话人物标签拆到两个只授权 service_role 的 `security definer` 小函数 `agentic_rectification_case_subject_name_v1`、`agentic_rectification_bind_session_subject_v1`。迁移从未执行过,原文件就地修正。
|
||||
- 验证:`frontend/tests/rectification-chart-subject-20261101.test.ts` 锁住 v3 为 invoker、两个辅助函数为 definer 且只授权 service_role;本机没有 Postgres,真库测试由 staging 门禁跑。
|
||||
- 防复发:上述源文本断言;凡是包装 `20260814010000` 里那六个函数的新函数,都不能写成 definer。
|
||||
- 同一次门禁还暴露:`database-local-business.test.ts` 的定价整表断言没加 `annual.full`(#153 新定价),已补。前一次门禁(run 6338)是年运代码超出 `jyotish_api_server.py` 方法数、`scripts/` 根目录文件数和 `JyotishAPIHandler.__new__` 次数上限,已把年运代码移进 `scripts/annual_service/` 修正,没有放宽上限。
|
||||
- 相关记录:BUG-177(同一开案流程,进行中案例的约束)。
|
||||
- 复发自:无
|
||||
- 修复版本:staging 集成分支 `codex/chart-base-layer-label`。
|
||||
|
||||
@@ -400,12 +400,65 @@ revoke all on function public.agentic_rectification_chart_profiles_rebaseline()
|
||||
-- 5. Open a case for a person
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- The person is read and the chat session is labelled by two narrow
|
||||
-- security-definer helpers: service_role has no rights on chart_profiles or
|
||||
-- chat_sessions (20260715030000). Only service_role may execute them.
|
||||
create or replace function public.agentic_rectification_case_subject_name_v1(
|
||||
p_user_id uuid, p_chart_profile_id uuid
|
||||
)
|
||||
returns text
|
||||
language plpgsql
|
||||
stable
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_name text;
|
||||
begin
|
||||
select name into v_name
|
||||
from public.chart_profiles
|
||||
where id = p_chart_profile_id and user_id = p_user_id and role = 'other';
|
||||
if not found then
|
||||
raise exception 'agentic_rectification_subject_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
return v_name;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function public.agentic_rectification_bind_session_subject_v1(
|
||||
p_user_id uuid, p_session_id uuid, p_chart_profile_id uuid, p_name text
|
||||
)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
update public.chat_sessions
|
||||
set chart_profile_id = p_chart_profile_id::text,
|
||||
chart_profile_name = coalesce(nullif(pg_catalog.btrim(p_name), ''), chart_profile_name),
|
||||
chart_profile_role = 'other'
|
||||
where id = p_session_id and user_id = p_user_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.agentic_rectification_case_subject_name_v1(uuid, uuid)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.agentic_rectification_case_subject_name_v1(uuid, uuid) to service_role;
|
||||
revoke all on function public.agentic_rectification_bind_session_subject_v1(uuid, uuid, uuid, text)
|
||||
from public, anon, authenticated;
|
||||
grant execute on function public.agentic_rectification_bind_session_subject_v1(uuid, uuid, uuid, text)
|
||||
to service_role;
|
||||
|
||||
-- Wraps open_agentic_rectification_case_v2. p_product_domain null (or left
|
||||
-- out) = the 11-argument form: the session intent passes no domain, exactly
|
||||
-- as before. p_chart_profile_id null (or left out) = the owner. The person is
|
||||
-- checked here, against the same user, before anything is created; a fresh
|
||||
-- case and its conversation are then bound to that person. A replayed
|
||||
-- requestId or an exact session must already belong to the same person.
|
||||
-- Security invoker, like the 12-argument v2: the 11-argument v2 is executable
|
||||
-- by service_role only, not even by its owner (20260814010000), so a definer
|
||||
-- wrapper would be refused.
|
||||
create or replace function public.open_agentic_rectification_case_v3(
|
||||
p_user_id uuid, p_request_id uuid, p_intent text, p_session_id uuid,
|
||||
p_skill_name text, p_skill_version text, p_skill_sha256 text, p_skill_source_commit text,
|
||||
@@ -414,24 +467,19 @@ create or replace function public.open_agentic_rectification_case_v3(
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
security invoker
|
||||
set search_path = ''
|
||||
as $$
|
||||
declare
|
||||
v_result jsonb;
|
||||
v_case public.agentic_rectification_cases%rowtype;
|
||||
v_subject public.chart_profiles%rowtype;
|
||||
v_subject_name text;
|
||||
begin
|
||||
if p_user_id is null then
|
||||
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
|
||||
end if;
|
||||
if p_chart_profile_id is not null then
|
||||
select * into v_subject
|
||||
from public.chart_profiles
|
||||
where id = p_chart_profile_id and user_id = p_user_id and role = 'other';
|
||||
if not found then
|
||||
raise exception 'agentic_rectification_subject_not_found' using errcode = 'P0001';
|
||||
end if;
|
||||
v_subject_name := public.agentic_rectification_case_subject_name_v1(p_user_id, p_chart_profile_id);
|
||||
end if;
|
||||
|
||||
if p_product_domain is null then
|
||||
@@ -459,11 +507,7 @@ begin
|
||||
set chart_profile_id = p_chart_profile_id
|
||||
where id = v_case.id
|
||||
returning * into v_case;
|
||||
update public.chat_sessions
|
||||
set chart_profile_id = p_chart_profile_id::text,
|
||||
chart_profile_name = coalesce(nullif(pg_catalog.btrim(v_subject.name), ''), chart_profile_name),
|
||||
chart_profile_role = 'other'
|
||||
where id = v_case.session_id and user_id = p_user_id;
|
||||
perform public.agentic_rectification_bind_session_subject_v1(p_user_id, v_case.session_id, p_chart_profile_id, v_subject_name);
|
||||
end if;
|
||||
|
||||
if p_intent <> 'session' and v_case.chart_profile_id is distinct from p_chart_profile_id then
|
||||
|
||||
@@ -255,7 +255,13 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
||||
// 原值: "rectification:standard:3:draft"
|
||||
// 新值: 上面那行,再加上 synastry.full 的 standard / premium / internal 各 19 点、published
|
||||
// 原因: 20261020010000 发布了合盘定价。这条查询读的是整张表,不是只读刚插入的校正草稿。
|
||||
// 原值: 以 rectification 那行开头
|
||||
// 新值: 前面再加 annual.full 的 internal / premium / standard 各 20 点、published
|
||||
// 原因: 20261101120100 发布了年运报告定价(#153),按 feature_key 排序排在最前
|
||||
[
|
||||
"annual.full:internal:20:published",
|
||||
"annual.full:premium:20:published",
|
||||
"annual.full:standard:20:published",
|
||||
"rectification:standard:3:draft",
|
||||
"synastry.full:internal:19:published",
|
||||
"synastry.full:premium:19:published",
|
||||
|
||||
@@ -327,7 +327,24 @@ test("the migration only adds: old cases stay the owner's, and no data is rewrit
|
||||
assert.equal(sets.length, 1);
|
||||
const v3 = functionBody(migration, "open_agentic_rectification_case_v3");
|
||||
assert.ok(v3.includes("set chart_profile_id = p_chart_profile_id"));
|
||||
assert.match(v3, /where id = p_chart_profile_id and user_id = p_user_id and role = 'other'/);
|
||||
// 原值:v3 自己 select chart_profiles(where id = p_chart_profile_id and user_id = p_user_id and role = 'other')
|
||||
// 新值:v3 是 security invoker,人物由 security definer 的 agentic_rectification_case_subject_name_v1 查,
|
||||
// 会话标签由 agentic_rectification_bind_session_subject_v1 写,两者只授权 service_role
|
||||
// 原因:staging 门禁真库报「permission denied for function open_agentic_rectification_case_v2」:
|
||||
// 11 参数的 v2 连属主也没有 EXECUTE(20260814010000),definer 包装调用不了;
|
||||
// service_role 又没有 chart_profiles / chat_sessions 的权限(20260715030000)
|
||||
assert.match(v3, /security invoker/);
|
||||
assert.doesNotMatch(v3, /security definer/);
|
||||
assert.match(v3, /public\.agentic_rectification_case_subject_name_v1\(p_user_id, p_chart_profile_id\)/);
|
||||
assert.match(v3, /perform public\.agentic_rectification_bind_session_subject_v1\(/);
|
||||
const subject = functionBody(migration, "agentic_rectification_case_subject_name_v1");
|
||||
assert.match(subject, /security definer/);
|
||||
assert.match(subject, /where id = p_chart_profile_id and user_id = p_user_id and role = 'other'/);
|
||||
assert.match(functionBody(migration, "agentic_rectification_bind_session_subject_v1"), /security definer/);
|
||||
for (const helper of ["agentic_rectification_case_subject_name_v1", "agentic_rectification_bind_session_subject_v1"]) {
|
||||
assert.match(migration, new RegExp(`revoke all on function public\\.${helper}\\([^)]*\\)\\s+from public, anon, authenticated;`));
|
||||
assert.match(migration, new RegExp(`grant execute on function public\\.${helper}\\([^)]*\\)\\s*to service_role;`));
|
||||
}
|
||||
assert.match(v3, /agentic_rectification_subject_mismatch/);
|
||||
assert.match(migration, /grant execute on function public\.open_agentic_rectification_case_v3\(\s*uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb, text, uuid\s*\) to service_role;/);
|
||||
// Deletes exist only inside delete_chart_subject (2A).
|
||||
|
||||
Reference in New Issue
Block a user