Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions test/iceberg_data_files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -288,23 +288,34 @@ 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 <<SQL | sed -n 's/^ERROR: \([0-9A-Z]\{5\}\).*/\1/p' | head -1
\\set VERBOSITY sqlstate
GRANT USAGE ON SCHEMA pgcolumnar TO icedf_unpriv;"
icedf_out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U icedf_unpriv -d "$PGC_DB" \
-qtA 2>&1 <<SQL
\\set VERBOSITY verbose
SELECT count(*) FROM pgcolumnar.iceberg_data_files('$MD');
SQL
)" "42501"
)"
# 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: "*) ;;
*) 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"
psql_run "$DROP_UNPRIV"

pgc_summary
34 changes: 24 additions & 10 deletions test/pytest/test_iceberg_data_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,33 @@ def _one(dsn, sql, *params):
return _failed(exc)


def _sqlstate(dsn, path, role=None):
"""The SQLSTATE listing `path` fails with, or "ok", in a session of its own;
`role`, if given, is SET first. A failure with no SQLSTATE is a failed reading."""
def _sqlstate(dsn, path):
"""The SQLSTATE listing `path` fails with, or "ok", in a session of its own. A
failure with no SQLSTATE is a failed reading."""
try:
with psycopg.connect(dsn, autocommit=True) as conn:
if role:
conn.execute(f"SET ROLE {role}")
conn.execute("SELECT * FROM pgcolumnar.iceberg_data_files(%s)", (str(path),)).fetchall()
return "ok"
except psycopg.Error as exc:
return exc.sqlstate or _failed(exc)


def _gate_refusal(dsn, path, role):
""""42501" when `role` listing `path` is refused by the pg_read_server_files gate;
otherwise what happened instead, or the failed reading."""
try:
with psycopg.connect(dsn, autocommit=True) as conn:
conn.execute(f"SET ROLE {role}")
conn.execute("SELECT * FROM pgcolumnar.iceberg_data_files(%s)", (str(path),)).fetchall()
return "ok"
except psycopg.Error as exc:
if not exc.sqlstate:
return _failed(exc)
primary = exc.diag.message_primary or ""
return "42501" if exc.sqlstate == "42501" and "pg_read_server_files" in primary \
else f"{exc.sqlstate}: {primary}"


def _run(dsn, *stmts):
"""Statements in a session of their own -> None, or the failed reading."""
try:
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
Loading