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
6 changes: 4 additions & 2 deletions test/check_ledger.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2100,10 +2100,12 @@ native_reclaim native_reclaim reused-block data matches heap mirror 15;16;17;18;
native_reclaim native_reclaim row count correct after cycle 15;16;17;18;19 never -
native_reclaim native_reclaim ten row groups, every one half deleted 15;16;17;18;19 2026-10-14 the write state ignores stripe_row_limit
objstore_credentials objstore_credentials a mapping with an access key and no secret is refused before any request (28000) 15;16;17;18;19 2026-10-14 the user-mapping credential pairing check removed (an access key alone crashes the backend)
objstore_credentials objstore_credentials a role with no mapping of its own reads through the PUBLIC mapping 15;16;17;18;19 2026-10-14 the user-mapping lookup has no PUBLIC fallback
objstore_credentials objstore_credentials a role's own mapping is used before the PUBLIC one (bob's wrong secret is still refused 28000) 15;16;17;18;19 2026-10-14 the user-mapping lookup tries PUBLIC before the role's own
objstore_credentials objstore_credentials ambient-is-privilege: carol (credentials_required=false, but no env exists) is 28000 15;16;17;18;19 never -
objstore_credentials objstore_credentials ambient: a superuser with no mapping reads via the environment 15;16;17;18;19 never -
objstore_credentials objstore_credentials ambient: carol (credentials_required=false) now reads via the environment 15;16;17;18;19 never -
objstore_credentials objstore_credentials ambient: without her mapping, carol is refused despite the environment 15;16;17;18;19 never -
objstore_credentials objstore_credentials ambient: without her mapping, carol is refused despite the environment 15;16;17;18;19 2026-10-14 every mapping may use the server's ambient credentials (allow_ambient = true)
objstore_credentials objstore_credentials and neither half-credential read reached the endpoint 15;16;17;18;19 never -
objstore_credentials objstore_credentials and one with a secret and no access key is refused the same way 15;16;17;18;19 2026-10-14 the user-mapping credential pairing check removed (an access key alone crashes the backend)
objstore_credentials objstore_credentials catalog: count(*) with catalog credentials only 15;16;17;18;19 never -
Expand All @@ -2115,7 +2117,7 @@ objstore_credentials objstore_credentials no-leak: srvoptions carry no secret (b
objstore_credentials objstore_credentials per-user: alice (good secret in her mapping) reads 15;16;17;18;19 never -
objstore_credentials objstore_credentials per-user: bob (wrong secret in his mapping) is refused 28000 15;16;17;18;19 never -
objstore_credentials objstore_credentials placement: a credential on a USER MAPPING is accepted 15;16;17;18;19 never -
objstore_credentials objstore_credentials placement: a non-superuser cannot set credentials_required false (42501) 15;16;17;18;19 never -
objstore_credentials objstore_credentials placement: a non-superuser cannot set credentials_required false (42501) 15;16;17;18;19 2026-10-14 the validator lets a non-superuser set credentials_required false
objstore_credentials objstore_credentials placement: a superuser can (for carol, used below) 15;16;17;18;19 never -
objstore_credentials objstore_credentials placement: endpoint and region on the SERVER are accepted 15;16;17;18;19 never -
objstore_credentials objstore_credentials placement: endpoint on a TABLE is HV00D 15;16;17;18;19 never -
Expand Down
25 changes: 24 additions & 1 deletion test/check_ledger_budget.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2150,4 +2150,27 @@ suites_not_covered 237
# never 2041 + 27 - 18 = 2050
# dated 615 + 18 = 633
# 2050 + 633 == 2683
checks_never_observed_red 2050
#
# #1341, objstore_credentials gets its pytest twin (OffgridwithJD): 2050 -> 2048. No new
# rows. Two arms are dated for the first time: every mapping allowed the ambient
# credentials (carol then read through the environment without her mapping), and the
# validator letting a non-superuser set credentials_required false. Two shell arms
# that expected an empty SQLSTATE now expect "ok", so an empty capture cannot pass.
# Five-major runs: 25/25 on 15-19, the twin passing with pyarrow required.
#
# ledger rows 2683 = 2683
# never 2050 - 2 = 2048
# dated 633 + 2 = 635
# 2048 + 635 == 2683
#
# #1341, a PUBLIC user mapping (OffgridwithJD): 2048 -> 2048. Two new objstore_credentials
# arms, each dated: the lookup without its PUBLIC fallback (a role with no mapping of
# its own then read nothing), and the lookup trying PUBLIC first (bob's own wrong
# secret was then bypassed by the PUBLIC mapping's good one). Five-major runs: 27/27
# on 15-19, the twin passing with pyarrow required.
#
# ledger rows 2683 + 2 = 2685
# never 2048 = 2048
# dated 635 + 2 = 637
# 2048 + 637 == 2685
checks_never_observed_red 2048
35 changes: 30 additions & 5 deletions test/objstore_credentials.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,21 @@ pg_restart_env() {
echo "FATAL: cluster did not come back after env restart"; exit 1
}

sqlstate_of() { # sqlstate_of <sql> [role]
env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U "${2:-postgres}" \
-d "$PGC_DB" -qtA 2>&1 <<SQLEOF | sed -n 's/^ERROR: \([0-9A-Z]\{5\}\).*/\1/p' | head -1
# sqlstate_of <sql> [role] -> the SQLSTATE the statement fails with; "ok" only when
# psql ran it and exited 0; "noconnect" when there was no session to run it in. Success
# is positive evidence (#1400 review): an empty capture, which a session that never
# connected also gives, is never read as success.
sqlstate_of() {
local out rc st
out="$(env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U "${2:-postgres}" \
-d "$PGC_DB" -qtA -v ON_ERROR_STOP=1 2>&1 <<SQLEOF
\\set VERBOSITY sqlstate
$1;
SQLEOF
)"
rc=$?
st="$(sed -n 's/^ERROR: \([0-9A-Z]\{5\}\).*/\1/p' <<<"$out" | head -1)"
if [ -n "$st" ]; then echo "$st"; elif [ "$rc" = 0 ]; then echo ok; else echo noconnect; fi
}
q_as() { # q_as <role> <sql>
env PATH="$PGC_BINDIR:$PATH" psql -h 127.0.0.1 -p "$PGC_PORT" -U "$1" \
Expand Down Expand Up @@ -112,7 +121,7 @@ check "placement: endpoint and region on the SERVER are accepted" \
"$(q "SELECT count(*) FROM pg_foreign_server WHERE srvname = 'csrv'")" "1"
check "placement: a credential on a USER MAPPING is accepted" \
"$(sqlstate_of "CREATE USER MAPPING FOR postgres SERVER csrv
OPTIONS (access_key_id '$AKID', secret_access_key '$SECRET')")" ""
OPTIONS (access_key_id '$AKID', secret_access_key '$SECRET')")" "ok"
check "placement: endpoint on a USER MAPPING is HV00D" \
"$(sqlstate_of "CREATE USER MAPPING FOR cred_alice SERVER csrv OPTIONS (endpoint 'x')")" "HV00D"

Expand All @@ -125,7 +134,7 @@ psql_run "CREATE USER MAPPING FOR cred_bob SERVER csrv
check "placement: a non-superuser cannot set credentials_required false (42501)" \
"$(sqlstate_of "ALTER USER MAPPING FOR cred_alice SERVER csrv OPTIONS (ADD credentials_required 'false')" cred_alice)" "42501"
check "placement: a superuser can (for carol, used below)" \
"$(sqlstate_of "CREATE USER MAPPING FOR cred_carol SERVER csrv OPTIONS (credentials_required 'false')")" ""
"$(sqlstate_of "CREATE USER MAPPING FOR cred_carol SERVER csrv OPTIONS (credentials_required 'false')")" "ok"

# ---- tables -----------------------------------------------------------------
psql_run "CREATE FOREIGN TABLE flocal ($COLDEFS) SERVER csrv OPTIONS (path '$PGC_WORKDIR/$BUCKET/fixture.parquet');"
Expand Down Expand Up @@ -209,6 +218,22 @@ check_num "no-leak: EXPLAIN VERBOSE carries no secret" \
check_num "no-leak: srvoptions carry no secret (by construction)" \
"$(q "SELECT count(*) FROM pg_foreign_server WHERE srvoptions::text LIKE '%$SECRET%'")" "0"

# ---- a PUBLIC mapping (#1341) ----------------------------------------------
# sql-reference.md: a read resolves the scanning user's own mapping first, then a
# PUBLIC one. Frank has no mapping of his own; bob's own carries the wrong secret, so
# a PUBLIC mapping holding the right one must not rescue him. Dropped again before
# phase 2, where a PUBLIC mapping would answer for the ambient arms.
psql_run "CREATE ROLE cred_frank LOGIN; GRANT pg_read_server_files TO cred_frank;
GRANT USAGE ON FOREIGN SERVER csrv TO cred_frank; GRANT SELECT ON fs3 TO cred_frank;"
psql_run "CREATE USER MAPPING FOR PUBLIC SERVER csrv
OPTIONS (access_key_id '$AKID', secret_access_key '$SECRET');"
# A refused read counts no rows, so a refusal is a measurement and not an empty capture.
check_num "a role with no mapping of its own reads through the PUBLIC mapping" \
"$(c="$(q_as cred_frank "SELECT count(*) FROM fs3")"; echo "${c:-0}")" "$((G * ROWS_PER_GROUP))"
check "a role's own mapping is used before the PUBLIC one (bob's wrong secret is still refused 28000)" \
"$(sqlstate_of "SELECT count(*) FROM fs3" cred_bob)" "28000"
psql_run "DROP USER MAPPING FOR PUBLIC SERVER csrv;"

# ---- phase 2: environment present; ambient is still a privilege -------------
pg_restart_env "AWS_ENDPOINT_URL='http://127.0.0.1:$S3_PORT'" \
"AWS_ACCESS_KEY_ID='$AKID'" "AWS_SECRET_ACCESS_KEY='$SECRET'" \
Expand Down
11 changes: 11 additions & 0 deletions test/pytest/TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ behaviour, the source of that number is named.
- [108. test_arrow_import.py: import_arrow round trips, reads a foreign file, and refuses what it cannot decode](#108-test_arrow_importpy-import_arrow-round-trips-reads-a-foreign-file-and-refuses-what-it-cannot-decode)
- [109. test_entry_point_privilege.py: every relation-taking entry point has a decided privilege bar](#109-test_entry_point_privilegepy-every-relation-taking-entry-point-has-a-decided-privilege-bar)
- [110. test_am_contracts.py: contracts the access method owes core DML and transactions](#110-test_am_contractspy-contracts-the-access-method-owes-core-dml-and-transactions)
- [111. test_objstore_credentials.py: object-store credentials live on a user mapping, and the environment is a privilege](#111-test_objstore_credentialspy-object-store-credentials-live-on-a-user-mapping-and-the-environment-is-a-privilege)

## 1. How to read a test in here

Expand Down Expand Up @@ -6906,3 +6907,13 @@ THE WITHDRAWAL IS REACHED ONLY BY A RACE. When one session holds a key uncommitt
| `test_an_aborted_inner_block_keeps_the_outer_statements_rows` | an outer `INSERT` and an outer `DELETE` whose expression calls a function with an aborted EXCEPTION block keep heap's answer. Before #1341's fix the INSERT kept 1 row of 5000. The same loss through a read inside the block is #1360 |
| `test_an_insert_racing_an_uncommitted_delete_of_its_key` | an insert of a key whose delete (or key-changing update) is uncommitted waits, then fails with 23505 if the delete rolls back and succeeds if it commits: never two rows with the key. Before #1341's fix the rollback left two |
| `test_an_index_built_after_a_delete_serves_an_older_snapshot` | a REPEATABLE READ snapshot from before a committed DELETE still finds the row through an index built after it (an Index Scan, asserted), and a new snapshot does not; a UNIQUE index still builds over a key whose other holder is deleted. Before #1341's fix the old snapshot's lookup found 0 |

## 111. test_objstore_credentials.py: object-store credentials live on a user mapping, and the environment is a privilege

The pytest twin of `test/objstore_credentials.sh` (#393 M4, ported for #1341), every check by the shell's name. Its fixture server is its own: an http.server in a thread that verifies every request's AWS Signature Version 4 from the spec with the standard library, so a green read proves the C signer and an independent implementation agree, and that records every request. A private cluster, because phase 2 restarts the postmaster with `AWS_*` in its environment; phase 1 starts it with every `AWS_*` of the test process removed. Secrets misplaced on a server, table or (for an endpoint) a mapping are refused `HV00D`; only a superuser may set `credentials_required 'false'`; each role's own mapping decides its read (a wrong secret is `28000` from the server); a mapping with half a credential is refused `28000` by its own message before any request; no secret reaches an error, `EXPLAIN VERBOSE` or `srvoptions`; and with the environment present, only a superuser or a `credentials_required 'false'` mapping may use it.

### Every test

| test | what it holds |
| --- | --- |
| `test_object_store_credentials` | placement, per-user resolution, the half-credential refusals and the request count behind them, no leak, and ambient credentials as a privilege across the restart |
4 changes: 3 additions & 1 deletion test/pytest/expected_tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -656,4 +656,6 @@ guard_tests 433
#
# cluster_tests 551 -> 558 for #1341's test_am_contracts.py, the twin of
# test/am_contracts.sh: seven tests, by collection over the cluster files on this tree.
cluster_tests 558
# cluster_tests 558 -> 559 for #1341's test_objstore_credentials.py, the twin of
# objstore_credentials.sh: one test, by collection over the cluster files on this tree.
cluster_tests 559
1 change: 1 addition & 0 deletions test/pytest/test_compare_to_bash.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"native_reclaim_cycles",
"native_reclaim_frag",
"native_vacuum_race",
"objstore_credentials",
"parallel_am_scan",
"parallel_scan_cost",
"partition_lifecycle",
Expand Down
Loading
Loading