Skip to content

Give the test role schema rights only, never a database-level grant - #64

Merged
jnasbyupgrade merged 6 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:grant-schema-only
Sep 8, 2026
Merged

Give the test role schema rights only, never a database-level grant#64
jnasbyupgrade merged 6 commits into
Postgres-Extensions:masterfrom
jnasbyupgrade:grant-schema-only

Conversation

@jnasbyupgrade

Copy link
Copy Markdown
Contributor

The docs claim any role with rights on the target schema can install count_nulls, but the suite never tested that: use_test_user.sql granted the test role CREATE on the whole database, and the test role then created and owned every schema it used, so a regression making the install depend on database-level CREATE would have passed unnoticed. The connecting role now creates every schema the harness needs — the extension's target schema and _null_count_test — and grants the test role USAGE and CREATE on that one schema, so the test role holds nothing on the database on any path.

Granting only CREATE turns out to be silently useless rather than an error: CREATE EXTENSION puts "<target>", pg_temp on search_path, search_path skips a schema the role cannot USAGE, and the extension is then built in pg_temp and lost at disconnect while CREATE EXTENSION still reports success — so count_nulls_install_extension() now checks the functions actually landed in the target schema, and the docs say USAGE and CREATE rather than CREATE alone.

The docs say any role with rights on the target schema can install
count_nulls, but the suite never tested that: use_test_user.sql granted
the test role CREATE on the whole database, and the test role then
created and owned every schema it used. A regression making the install
depend on database-level CREATE would have passed unnoticed.

The connecting role now creates every schema the harness needs and
grants the test role USAGE and CREATE on that one schema, so the test
role holds nothing on the database on any path.
count_nulls_install_extension() is split from the schema creation, which
moves to count_nulls_prepare_test_schema() on the connecting role's side
of the switch; test/deps.sql likewise creates _null_count_test, which
was the only remaining reason the test role needed database CREATE.

Granting the target schema is what a caller must now supply, so the
grant stays inside count_nulls_prepare_test_user(): that is where the
role is guaranteed to exist, and a grant issued before it would fail on
a cluster that has never run the suite.

Installing with CREATE but not USAGE is silently useless rather than an
error - CREATE EXTENSION puts "<target>, pg_temp" on search_path,
search_path drops a schema the role cannot USAGE, and the whole
extension is then built in pg_temp and lost at disconnect while CREATE
EXTENSION still reports success. count_nulls_install_extension() now
checks that the functions actually landed in the target schema, and the
docs say USAGE and CREATE rather than CREATE alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 75c74374-3762-42c4-ada2-d5052f0cfe7a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 5m 37s —— View job


Claude is reviewing this PR

Comment thread test/helpers/extension_installer.sql
Comment thread test/install/load.sql
Comment thread test/deps.sql
Comment thread HISTORY.md Outdated
`superuser = false`: any role with `CREATE` on the target schema can install
it. The test suite runs as an ordinary role throughout, so this can't
silently regress.
`superuser = false`: any role with `USAGE` and `CREATE` on the target schema

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this on the previous PR. This is waaay more detail than is warranted. Users don't care about the control file, and this being SQL functions has nothing to do with anything. This can literally just be - Extension no longer requires superuser to install

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here — replaced the entry with exactly that line.

Comment thread README.md Outdated

No superuser required: count_nulls is nothing but SQL functions, so any role
with `CREATE` on the target schema can add it.
with `USAGE` and `CREATE` on the target schema can add it, without any

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAY TOO MUCH DETAIL. In fact, I don't think it's even worth mentioning in the README that we don't require SU. Take the whole para out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here — removed the whole paragraph.

…ed literal

test/deps.sql declared count_nulls_grant_schema right after CREATE SCHEMA had
already hardcoded the same name, so the two could silently drift apart.
Setting the variable first and using :"count_nulls_grant_schema" in CREATE
SCHEMA (as use_test_user.sql already does for the run-as role) makes the
name single-sourced.
…tions

Only 'existing' was special-cased; any other value - including a typo or an
unpropagated GUC - fell through to creating a new schema unconditionally.
count_nulls_load already rejects anything outside 'fresh'/'update'/'existing'
with this same message; give the schema-naming function the identical check
so a bad mode fails loudly there too instead of silently misbehaving.
Users installing the extension don't need the control-file mechanics or the
search_path reasoning restated in two more places - drop the README
paragraph entirely and cut the HISTORY entry down to the one-line fact.
A markdown link isn't Claude Code's @path import syntax, so nothing actually
loaded ../ai/CLAUDE.md into context automatically - an agent had to notice
and follow the pointer by hand, which is easy to skip. Switch to a real
@../ai/CLAUDE.md import, keeping an explicit fallback note for the case
where that file isn't cloned locally yet, since it's unclear whether a
missing @-imported file fails loudly or is silently skipped.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude is reviewing this PR

View branch

…AISE

Quote the bad value with %L instead of a bare %, assign ERRCODE
invalid_parameter_value instead of leaving it generic, and phrase the
primary message as a fixed input-domain violation (no trailing period,
valid values moved to HINT) - matching real Postgres GUC-validation
messages like "invalid value for parameter \"%s\": \"%s\"".
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @jnasbyupgrade's task in 3m 1s —— View job


Claude finished the code review

Summary

Reviewed the full diff (git diff origin/master...HEAD, 11 files, +160/-56) against the latest commit (76a0c2f). This push already addresses everything from the earlier review rounds:

  • count_nulls_prepare_test_schema now validates p_mode and raises loudly instead of falling through to an unconditional CREATE SCHEMA.
  • test/deps.sql creates the schema via :"count_nulls_grant_schema" instead of a hardcoded literal.
  • HISTORY.md and README.md are trimmed to the requested one-liner / removed paragraph.
  • count_nulls_install_extension now verifies the extension's functions actually landed in the target schema, catching the case where search_path silently drops a schema the role can't USAGE and the install ends up in pg_temp while CREATE EXTENSION still reports success.
  • The mode-validation added to count_nulls_load in load.sql was confirmed as already covering p_mode upstream in that function — no dead code needed there.

One minor, non-blocking nit posted inline:

  • test/helpers/extension_installer.sql:60-66 — the new mode-validation RAISE EXCEPTION USING MESSAGE/HINT/ERRCODE (added per the "Postgres error-style guide" commit) validates the same count_nulls.test_load_mode value that test/install/load.sql:72-77's count_nulls_load also validates, but with a plain RAISE EXCEPTION '...got %' instead. Both are reachable from the same test/install/load.sql run, so the error shape a user sees for an invalid mode depends on which check fires first. Likely intentional scope-limiting on the "new" RAISE per the commit message, but flagging in case the old one should be brought in line too.

No correctness issues found beyond that — the privilege model (grant USAGE, CREATE on one schema, nothing on the database), the pg_depend-based check for extension-owned functions, and the schema-creation/ownership split between connecting role and test role all check out against the stated goals in the PR description.

Comment on lines +60 to +66
IF p_mode NOT IN ('fresh', 'update', 'existing') THEN
RAISE EXCEPTION USING
MESSAGE = format('invalid value for parameter "count_nulls.test_load_mode": %L', p_mode)
, HINT = 'Valid values are ''fresh'', ''update'', and ''existing''.'
, ERRCODE = 'invalid_parameter_value'
;
END IF;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor consistency nit: this validates the same value (count_nulls.test_load_mode, passed through as p_mode) that count_nulls_load in test/install/load.sql:72-77 also validates, but with a different error shape — RAISE EXCEPTION USING MESSAGE/HINT/ERRCODE here vs. a plain RAISE EXCEPTION '...got %' there. Both are reachable from the same test/install/load.sql run (this one via count_nulls_prepare_test_schema, called before count_nulls_load), so an invalid mode raises a differently-styled error depending on exactly where the validation happens to fire first. Worth either applying the same Postgres-error-style-guide shape to load.sql's check too, or a short comment noting the old one is intentionally left alone — not blocking, just flagging so it's a choice rather than an oversight.

Fix this →

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude here — correct, and already discussed on this PR: load.sql's check pre-dates this PR (see the reply on 3961370056) and is deliberately out of scope here per the maintainer - not brought in line with the new style in this PR.

@jnasbyupgrade
jnasbyupgrade merged commit e46e019 into Postgres-Extensions:master Sep 8, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant