diff --git a/test/check_ledger.tsv b/test/check_ledger.tsv index a975ef2d2..d9db3d2ef 100644 --- a/test/check_ledger.tsv +++ b/test/check_ledger.tsv @@ -2080,6 +2080,7 @@ rollup rollup and after the refresh that ran beside an insert the rollup equals rollup rollup and after the small append the rollup equals a full recompute 15;16;17;18;19 2026-10-13 refresh_rollup combining min with greatest;refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group;refresh_rollup recording the folded groups from a fresh read after the fold;row_tid mapping 290 rows a block instead of the AM's rollup rollup and after two refreshes started together the rollup equals a full recompute 15;16;17;18;19 2026-10-13 refresh_rollup combining min with greatest;refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group;refresh_rollup recording the folded groups from a fresh read after the fold;row_tid mapping 290 rows a block instead of the AM's rollup rollup and cannot refresh it 15;16;17;18;19 2026-10-13 the rollup catalogs without row-level security;the rollup catalogs without their grants +rollup rollup and its unqualified UPDATE and DELETE reach none of the owner's catalog rows 15;16;17;18;19 2026-10-14 a permissive UPDATE policy on pgcolumnar.rollup and DELETE policy on rollup_group, USING (true);the rollup catalog policies given WITH CHECK (true) rollup rollup and leaves the rollup as it was 15;16;17;18;19 2026-10-13 refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group rollup rollup and leaves the rollup's rows alone 15;16;17;18;19 2026-10-13 rebase_rollup clearing the rollup's rows rollup rollup and no refused rollup left a table behind 15;16;17;18;19 2026-10-13 create_rollup allowing * with any function;create_rollup without the equal-length check;create_rollup without the months refusal @@ -2092,14 +2093,18 @@ rollup rollup and the refresh after the rewrite recomputes nothing 15;16;17;18;1 rollup rollup and the refresh rewrote no partial outside the changed groups' time range 15;16;17;18;19 2026-10-13 refresh_rollup recomputing every bucket once any group changed rollup rollup and the rollup no longer records the groups retention retired 15;16;17;18;19 2026-10-13 refresh_rollup keeping retired groups in its record rollup rollup and the rollup still equals a full recompute 15;16;17;18;19 2026-10-13 refresh_rollup combining min with greatest;refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group;row_tid mapping 290 rows a block instead of the AM's +rollup rollup another role cannot add a group row to the owner's rollup (42501) 15;16;17;18;19 2026-10-14 the rollup catalog policies given WITH CHECK (true) +rollup rollup another role cannot read the source's groups through rollup_source_groups (42501) 15;16;17;18;19 2026-10-14 rollup_source_groups without its require_caller_select guard rollup rollup another role sees none of its rollup catalog rows 15;16;17;18;19 2026-10-13 the rollup catalogs without row-level security;the rollup catalogs without their grants rollup rollup each row group's TID range holds exactly that group's rows 15;16;17;18;19 2026-10-13 row_tid mapping 290 rows a block instead of the AM's +rollup rollup nor its retired groups through rollup_source_retired (42501) 15;16;17;18;19 2026-10-14 rollup_source_retired without its require_caller_select guard rollup rollup premise: and the fixture has NULL-key and NULL-bucket groups 15;16;17;18;19 never - rollup rollup premise: compact_rewrite rewrote a row group 15;16;17;18;19 never - rollup rollup premise: expire retired the group in bucket D alone and kept the one that shares it 15;16;17;18;19 never - rollup rollup premise: expire retired the group the delete thinned 15;16;17;18;19 never - rollup rollup premise: expire retired the old row groups 15;16;17;18;19 never - rollup rollup premise: pgcolumnar.vacuum gave the source's row groups new identities 15;16;17;18;19 never - +rollup rollup premise: the owner's rollup has group rows another role could reach 15;16;17;18;19 never - rollup rollup premise: the rollup functions exist 15;16;17;18;19 never - rollup rollup premise: the rollup holds partials the delete below cannot reach 15;16;17;18;19 never - rollup rollup premise: the source holds every row 15;16;17;18;19 never - @@ -2109,6 +2114,7 @@ rollup rollup the first refresh folds every row 15;16;17;18;19 2026-10-13 refres rollup rollup the history rollup folded every row 15;16;17;18;19 2026-10-13 refresh_rollup folding only the first new group rollup rollup the history rollup holds old buckets 15;16;17;18;19 never - rollup rollup the next refresh folds the rows committed during the last one, once 15;16;17;18;19 2026-10-13 refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group;refresh_rollup recording the folded groups from a fresh read after the fold;row_tid mapping 290 rows a block instead of the AM's +rollup rollup the owner reads its source's groups through the definer helpers 15;16;17;18;19 never - rollup rollup the paused refresh folded what was there before it began 15;16;17;18;19 2026-10-13 refresh_rollup folding every group, ignoring the recorded snapshot;refresh_rollup folding only the first new group;row_tid mapping 290 rows a block instead of the AM's rollup rollup the refresh after a small append read the source at all 15;16;17;18;19 never - rollup rollup the rollup holds rows before the rewrite 15;16;17;18;19 never - diff --git a/test/check_ledger_budget.txt b/test/check_ledger_budget.txt index cf3fcaf7f..a0cc267ee 100644 --- a/test/check_ledger_budget.txt +++ b/test/check_ledger_budget.txt @@ -1832,4 +1832,19 @@ suites_not_covered 246 # never 1798 - 2 = 1796 # dated 545 + 2 = 547 # 1796 + 547 == 2343 -checks_never_observed_red 1796 +# +# #1341, the rollup catalogs' write side and the definer helpers (OffgridwithJD): +# 1796 -> 1798. Six new rollup checks. Four are dated by a mutation of the extension +# script that main's suite survives: +# - each helper's require_caller_select deleted (one arm each); +# - WITH CHECK (true) on the catalog policies (the INSERT arm, and the write arm, +# which counts the inserted row); +# - a permissive UPDATE and DELETE policy USING (true) (the write arm). +# The premise that the owner's rollup has group rows, and the owner's own call +# through the helpers, arrive never. Five-major runs: rollup 78/78 on 15-19. +# +# ledger rows 2343 + 6 = 2349 +# never 1796 + 2 = 1798 +# dated 547 + 4 = 551 +# 1798 + 551 == 2349 +checks_never_observed_red 1798 diff --git a/test/pytest/test_rollup.py b/test/pytest/test_rollup.py index 11e672f01..f1cac101e 100644 --- a/test/pytest/test_rollup.py +++ b/test/pytest/test_rollup.py @@ -459,6 +459,34 @@ def as_role(role, sql): _one(conn, f"SELECT string_agg(b || ':' || n || ':' || m, ' ' ORDER BY b) FROM" f" (SELECT date_bin('1 day', at, '{ORIGIN}') b, count(*) n, max(kwh) m FROM meter GROUP BY 1) x"), "and reads what a recompute gives") + # THE WRITE SIDE AND THE DEFINER HELPERS (#1341). The policies have USING only, + # so they also check an INSERT's new row; an UPDATE or DELETE with no WHERE reads + # nothing, so only the policy's USING keeps it off another owner's rows. Asked + # before the read-side arms, so a write that got through is what fails. + rg_sql = "SELECT count(*) FROM pgcolumnar.rollup_group WHERE rollup = 'meter_daily'::regclass" + rg_own = _one(conn, rg_sql) + expect.text("some" if rg_own > 0 else f"none ({rg_own})", "some", + "premise: the owner's rollup has group rows another role could reach") + expect.text(as_role("rp_other", "INSERT INTO pgcolumnar.rollup_group VALUES" + " ('public.meter_daily'::regclass, 999999, 1, 0, NULL, NULL, false)"), + "42501", "another role cannot add a group row to the owner's rollup (42501)") + as_role("rp_other", "UPDATE pgcolumnar.rollup SET history_has_null = true") + as_role("rp_other", "DELETE FROM pgcolumnar.rollup_group") + has_null = _one(conn, "SELECT history_has_null FROM pgcolumnar.rollup" + " WHERE rollup = 'meter_daily'::regclass") + expect.text(f"{has_null}|{_one(conn, rg_sql)}", f"False|{rg_own}", + "and its unqualified UPDATE and DELETE reach none of the owner's catalog rows") + # rollup_source_groups and rollup_source_retired run as their definer, so the + # catalogs they read refuse no one; the caller's SELECT on the source is checked + # by require_caller_select against the OUTER user. + own_groups = as_role("rp_owner", "SELECT (count(*) > 0)::text FROM pgcolumnar.rollup_source_groups('meter')") + own_retired = as_role("rp_owner", "SELECT count(*) >= 0 FROM pgcolumnar.rollup_source_retired('meter')") + expect.text(f"{own_groups}|{own_retired}", "true|True", + "the owner reads its source's groups through the definer helpers") + expect.text(as_role("rp_other", "SELECT count(*) FROM pgcolumnar.rollup_source_groups('public.meter')"), "42501", + "another role cannot read the source's groups through rollup_source_groups (42501)") + expect.text(as_role("rp_other", "SELECT count(*) FROM pgcolumnar.rollup_source_retired('public.meter')"), "42501", + "nor its retired groups through rollup_source_retired (42501)") expect.text(f"{as_role('rp_other', 'SELECT count(*) FROM pgcolumnar.rollup')} " f"{as_role('rp_other', 'SELECT count(*) FROM pgcolumnar.rollup_group')}", "0 0", "another role sees none of its rollup catalog rows") diff --git a/test/rollup.sh b/test/rollup.sh index 8662648ac..2fa7421a6 100755 --- a/test/rollup.sh +++ b/test/rollup.sh @@ -381,6 +381,30 @@ check_num "and refreshes it" "$(as_role ru_a "SELECT pgcolumnar.refresh_rollup(' check "and reads what a recompute gives" \ "$(as_role ru_a "SELECT string_agg(bucket || ':' || count || ':' || sum_v, ' ' ORDER BY bucket) FROM own_r")" \ "$(q "SELECT string_agg(b || ':' || n || ':' || s, ' ' ORDER BY b) FROM (SELECT date_bin('1 day', ts, '$ORIGIN') b, count(*) n, sum(v)::numeric s FROM own_src GROUP BY 1) x;")" +# THE WRITE SIDE AND THE DEFINER HELPERS (#1341). The policies have USING only, so +# they also check an INSERT's new row; and an UPDATE or DELETE with no WHERE reads +# nothing, so only the policy's USING keeps it off another owner's rows. Asked +# before the read-side arms below, so a write that got through is what fails. +_st() { sed -n 's/^ERROR: *\([0-9A-Z]\{5\}\).*/\1/p'; } +_rg_own="$(q "SELECT count(*) FROM pgcolumnar.rollup_group WHERE rollup = 'own_r'::regclass;")" +check "premise: the owner's rollup has group rows another role could reach" \ + "$([ "${_rg_own:-0}" -gt 0 ] 2>/dev/null && echo some || echo "none ($_rg_own)")" "some" +check "another role cannot add a group row to the owner's rollup (42501)" \ + "$(as_role ru_b "INSERT INTO pgcolumnar.rollup_group VALUES ('public.own_r'::regclass, 999999, 1, 0, NULL, NULL, false)" | _st)" "42501" +as_role ru_b "UPDATE pgcolumnar.rollup SET history_has_null = true" >/dev/null +as_role ru_b "DELETE FROM pgcolumnar.rollup_group" >/dev/null +check "and its unqualified UPDATE and DELETE reach none of the owner's catalog rows" \ + "$(q "SELECT history_has_null FROM pgcolumnar.rollup WHERE rollup = 'own_r'::regclass;")|$(q "SELECT count(*) FROM pgcolumnar.rollup_group WHERE rollup = 'own_r'::regclass;")" \ + "f|$_rg_own" +# rollup_source_groups and rollup_source_retired run as their definer, so the +# catalogs they read refuse no one; the caller's SELECT on the source is checked +# by require_caller_select against the OUTER user. +check "the owner reads its source's groups through the definer helpers" \ + "$(as_role ru_a "SELECT (count(*) > 0)::text FROM pgcolumnar.rollup_source_groups('own_src')")|$(as_role ru_a "SELECT count(*) >= 0 FROM pgcolumnar.rollup_source_retired('own_src')")" "true|t" +check "another role cannot read the source's groups through rollup_source_groups (42501)" \ + "$(as_role ru_b "SELECT count(*) FROM pgcolumnar.rollup_source_groups('public.own_src')" | _st)" "42501" +check "nor its retired groups through rollup_source_retired (42501)" \ + "$(as_role ru_b "SELECT count(*) FROM pgcolumnar.rollup_source_retired('public.own_src')" | _st)" "42501" check "another role sees none of its rollup catalog rows" \ "$(as_role ru_b "SELECT count(*) FROM pgcolumnar.rollup") $(as_role ru_b "SELECT count(*) FROM pgcolumnar.rollup_group")" "0 0" # Row-level security hides the catalog row, so to that role it is no rollup at