From 3f3d7576e44d58ff08cd303f07d23852aff62e85 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Mon, 19 Oct 2026 20:21:12 +0000 Subject: [PATCH 1/2] test: iceberg_data_files's privilege arm requires the gate's own refusal (#1341) "an unprivileged role is refused (42501)" read only the SQLSTATE, and a schema denial is 42501 too. With the twins' GRANT USAGE removed, both stayed green on "permission denied for schema pgcolumnar" (found by OffgridwithJD, logged on #1341), so the arm reached the pg_read_server_files gate only while that grant stayed in the fixture. Both twins now pass only on a 42501 whose message names pg_read_server_files. Anything else is reported as it came: the shell's first line of output, the twin's "SQLSTATE: message". The check's name and passing value are unchanged, so its ledger row stands. The GRANT EXECUTE is gone; PUBLIC already holds EXECUTE on the function. Measured, PG17, both twins: - GRANT USAGE removed: green on main (19/19, 21/21); now red in each, as "42501: permission denied for schema pgcolumnar". - The gate disabled in ice_srf_begin: red in each (the shell reads a row count, the twin "ok"). - Clean: 19/19 and 21 passed on PG15-19. Co-Authored-By: Claude Opus 5.5 --- test/iceberg_data_files.sh | 24 ++++++++++-------- test/pytest/test_iceberg_data_files.py | 34 ++++++++++++++++++-------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/test/iceberg_data_files.sh b/test/iceberg_data_files.sh index f8fac4e37..10236e692 100755 --- a/test/iceberg_data_files.sh +++ b/test/iceberg_data_files.sh @@ -288,23 +288,27 @@ check "a table with no current snapshot yields no data files" \ "$(q "SELECT count(*) FROM pgcolumnar.iceberg_data_files('$NOSNAP')")" "0" # ---- privilege: needs pg_read_server_files --------------------------------- -# The role gets USAGE on the schema and EXECUTE on the function, so the refusal is -# the function's own gate. Without them the 42501 is "permission denied for schema -# pgcolumnar", the function never runs, and the arm cannot fail on the gate it names. +# The role gets USAGE on the schema, so the refusal is the function's own gate (EXECUTE +# it already has, through PUBLIC). Without USAGE the 42501 is "permission denied for +# schema pgcolumnar" and the function never runs, so the arm requires the gate's own +# message, naming pg_read_server_files, and not only the SQLSTATE they share. DROP_UNPRIV="DO \$\$BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'icedf_unpriv') THEN EXECUTE 'DROP OWNED BY icedf_unpriv'; END IF; END\$\$; DROP ROLE IF EXISTS icedf_unpriv;" psql_run "$DROP_UNPRIV CREATE ROLE icedf_unpriv LOGIN; -GRANT USAGE ON SCHEMA pgcolumnar TO icedf_unpriv; -GRANT EXECUTE ON FUNCTION pgcolumnar.iceberg_data_files TO icedf_unpriv;" -check "an unprivileged role is refused (42501)" \ - "$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U icedf_unpriv -d "$PGC_DB" \ - -qtA 2>&1 <&1 < None, or the failed reading.""" try: @@ -208,14 +222,14 @@ def _run(dsn, *stmts): return _failed(exc) -# The unprivileged role reaches the function: USAGE on the schema and EXECUTE on the -# function, or its 42501 is the schema's and the pg_read_server_files gate never runs. +# The unprivileged role reaches the function: USAGE on the schema (EXECUTE it has +# through PUBLIC), or its 42501 is the schema's and the pg_read_server_files gate never +# runs. The arm requires the gate's own message, not only the SQLSTATE they share. DROP_UNPRIV = ("DO $$BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'icedf_unpriv') " "THEN EXECUTE 'DROP OWNED BY icedf_unpriv'; END IF; END$$", "DROP ROLE IF EXISTS icedf_unpriv") MAKE_UNPRIV = DROP_UNPRIV + ("CREATE ROLE icedf_unpriv LOGIN", - "GRANT USAGE ON SCHEMA pgcolumnar TO icedf_unpriv", - "GRANT EXECUTE ON FUNCTION pgcolumnar.iceberg_data_files TO icedf_unpriv") + "GRANT USAGE ON SCHEMA pgcolumnar TO icedf_unpriv") def _alive(pgc_cluster): @@ -313,7 +327,7 @@ def ice(pgc_cluster, tmp_path_factory): got[f"entry {k}"] = _sqlstate(dsn, entries[k]) got["alive entries"] = _recovered(pgc_cluster, _alive(pgc_cluster)) got["no snapshot"] = _one(dsn, lst.format("count(*)"), str(nosnap)) - got["unprivileged"] = _run(dsn, *MAKE_UNPRIV) or _sqlstate(dsn, md, role="icedf_unpriv") + got["unprivileged"] = _run(dsn, *MAKE_UNPRIV) or _gate_refusal(dsn, md, "icedf_unpriv") try: yield got finally: From a4897d881ba0792dc14e3ebbb0ae6d26ce5a2dd9 Mon Sep 17 00:00:00 2001 From: "Joshua D. Drake" Date: Mon, 19 Oct 2026 20:30:56 +0000 Subject: [PATCH 2/2] test: iceberg_data_files's shell arm reads only the ERROR line, as the twin does (#1341) @OffgridwithJD's review of #1542: the shell matched pg_read_server_files anywhere in psql's output, HINT and DETAIL lines included, while the twin reads only message_primary. With the gate's wording moved into a hint (errmsg "must be superuser to read a server file", errhint "Grant the pg_read_server_files role."), the shell stayed green and the twin went red. The shell now takes the message after "ERROR: " up to the end of that line, with parameter expansions (no pipe, selftest part 080), and matches only that. Measured, PG17, both twins: the hint mutant was green in the shell before this and is red in both now; the gate-off and no-USAGE mutants stay red in both, by the same name; clean is 19/19 and 21 passed. Co-Authored-By: Claude Opus 5.5 --- test/iceberg_data_files.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/iceberg_data_files.sh b/test/iceberg_data_files.sh index 10236e692..fb53b1b79 100755 --- a/test/iceberg_data_files.sh +++ b/test/iceberg_data_files.sh @@ -304,8 +304,15 @@ icedf_out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U ic SELECT count(*) FROM pgcolumnar.iceberg_data_files('$MD'); SQL )" +# Only the ERROR line's message counts, as the twin reads message_primary: a role +# named in a HINT or DETAIL line is not the gate saying so. +icedf_err="${icedf_out#*ERROR: }"; icedf_err="${icedf_err%%$'\n'*}" case "$icedf_out" in - *"ERROR: 42501: "*pg_read_server_files*) icedf_got=42501 ;; + *"ERROR: "*) ;; + *) icedf_err="" ;; +esac +case "$icedf_err" in + "42501: "*pg_read_server_files*) icedf_got=42501 ;; *) icedf_got="${icedf_out%%$'\n'*}"; icedf_got="${icedf_got:-no output}" ;; esac check "an unprivileged role is refused (42501)" "$icedf_got" "42501"