Repository navigation
Fire the event trigger for DROP SCHEMA and DROP OWNED - #55
jnasbyupgrade wants to merge 15 commits into
Conversation
`DROP SCHEMA ... CASCADE` and `DROP OWNED BY` can drop an extension, but the trigger only fired for `DROP EXTENSION`. The registered SQL never ran and the leftover row in `extension_drop__commands` made every later `DROP EXTENSION` raise `XD001`. The trigger's `WHEN tag` list now includes both tags. The update script recreates the trigger, since `ALTER EVENT TRIGGER` cannot change `WHEN`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
|
Claude finished @jnasbyupgrade's task in 30s —— View job Review of PR #55: fire the event trigger for DROP SCHEMA and DROP OWNEDI read the diff and the trigger and sanity-check code. I did not build the extension or run the test suite. The The change is small and consistent. The trigger Concern: the trigger now fires for ordinary users (not verified by running it)
Event triggers run with the privileges of the user who issued the command. So a non-superuser running There is a second, related effect. Any stale row in Suggestions:
Minor
|
Now that the trigger fires for every `DROP SCHEMA` and `DROP OWNED`, it must not read `extension_drop__commands` or run the sanity assert for drops that involve no extension: a non-superuser's ordinary drops failed with a permission error, and a stale row broke unrelated drops. The row variable is a `record` so the function compiles without access to the extension's schema. Adds tests for non-superuser and stale-row drops, and factors the shared post-drop assertions into a helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude here. Addressing the review's non-superuser concern (confirmed by running it: a plain role's |
|
Claude finished @jnasbyupgrade's task in 21s —— View job Review of PR #55: fire the event trigger for DROP SCHEMA and DROP OWNEDI read the diff, including the trigger SQL, the update script and I found no bugs. The earlier review's two concerns are both handled in the current head:
The fresh-install and 0.1.1→stable paths are consistent:
Minor notes (no action required)
|
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Claude here. Decisions needed from the maintainer:
|
|
Claude finished @jnasbyupgrade's task in 16s —— View job Review of PR #55: fire the event trigger for DROP SCHEMA and DROP OWNEDI read the full diff: the install and update SQL, README/HISTORY, and the three new tests plus the helper. I did not build the extension or run the tests. The I found no bugs.
Minor notes (no action required)
|
|
Claude finished @jnasbyupgrade's task in 18s —— View job Review of PR #55: fire the event trigger for DROP SCHEMA and DROP OWNEDI read the diff of the install SQL, the 0.1.1→stable update script, and I found no bugs.
Minor notes (no action required)
|
| , $body$ | ||
| DECLARE | ||
| r extension_drop__commands; | ||
| r record; -- Not extension_drop__commands: the type must not resolve before the early return |
There was a problem hiding this comment.
Need to actually explain why that matters.
There was a problem hiding this comment.
Claude here. Fixed in 0cae837: the comment now says why. A typed variable is resolved when plpgsql compiles the function on first call, before the early return, so a role that cannot see the extension's schema fails every unrelated DROP with type "extension_drop__commands" does not exist. Reverting to the typed variable makes the unrelated_drops non-superuser DROP SCHEMA CASCADE and DROP OWNED BY tests fail; with record they pass. Same wording applied to sql/extension_drop--0.1.1--stable.sql.
A typed variable is resolved when the function compiles, before the early return, so a role without access to the extension's schema fails unrelated drops.
|
Claude finished @jnasbyupgrade's task in 11s —— View job Review of PR #55The No new bugs found. jnasbyupgrade's review comment said the The early return, the |
…om unrelated_drops The non-superuser role now owns schemas created via CREATE SCHEMA ... AUTHORIZATION, so it needs no CREATE privilege on the database.
| SELECT plan( | ||
| 0 | ||
|
|
||
| + 1 -- Create non-superuser role |
There was a problem hiding this comment.
Lets consolidate these some... we definitely don't need a +1 for each line. Apply to the rest of the new test files as well.
There was a problem hiding this comment.
Claude here. Consolidated in 823be14: unrelated_drops.sql plan() is now 4 lines (was 8), and drop_owned.sql and drop_schema_cascade.sql are 5 lines each, grouped by purpose. The totals in those two are unchanged.
There was a problem hiding this comment.
Claude here. 50bf9be consolidates the plan() terms in drop_owned, drop_schema_cascade and unrelated_drops into one labelled line per group, and each helper's count now has its own line labelled with the helper's path; totals are unchanged.
| , 'Create non-superuser role' | ||
| ); | ||
| SELECT lives_ok( | ||
| format('GRANT CREATE ON DATABASE %I TO extension_drop_test_plain', current_database()) |
There was a problem hiding this comment.
Uh... can't you just do this when creating the role?
|
Claude finished @jnasbyupgrade's task in 14s —— View job Review of PR #55The I found no bugs.
Minor notes (no action required)
|
Group the per-file assertion counts into a single term and label the helper terms with the helper's path. Totals are unchanged.
|
Claude finished @jnasbyupgrade's task in 12s —— View job Review of PR #55The I found no bugs.
Minor notes (no action required)
|
|
Claude finished @jnasbyupgrade's task in 11s —— View job Review of PR #55The I found no bugs.
Minor notes (no action required)
|
…ommands A role that drops an extension (via DROP EXTENSION, DROP SCHEMA ... CASCADE or DROP OWNED) without access to extension_drop__commands got a bare "permission denied for table". The trigger now checks USAGE on the table's schema plus SELECT and DELETE on the table, after the early return for drops that involve no extension, and raises insufficient_privilege with a DETAIL naming the table and a HINT naming its owner and the grants that would fix it. The table is located through pg_extension and pg_class rather than by name, since name lookup skips a schema the caller can't use. The function stays SECURITY INVOKER: SECURITY DEFINER would run registered SQL as the owner.
|
Claude finished @jnasbyupgrade's task in 11s —— View job Review of PR #55The I found no bugs.
Minor notes (no action required)
|
The event trigger now reads and deletes the dropped extensions' registrations in one statement wrapped in an exception block for insufficient_privilege and undefined_table (a caller without USAGE on the schema gets "does not exist"). Registered SQL runs afterward, outside the block, so a privilege error from it is never mistaken for extension_drop's own. The handler raises the same actionable insufficient_privilege error, with the original message added to the DETAIL; the table owner and name for the HINT are looked up there, through pg_extension and pg_class, since the caller may lack USAGE. drop_denied now also covers SELECT without DELETE, a fully granted role dropping successfully, and a privilege error from registered SQL surfacing unchanged.
drop_denied now applies the grants the HINT names (directly, and through a group role) and checks the drop succeeds. HISTORY.asc and README.md no longer describe DROP EXTENSION as newly restricted, describe the trigger's current order (registration removed, then registered SQL run), and note that the registered SQL runs as the caller and may need further privileges.
|
Claude finished @jnasbyupgrade's task in 20s —— View job Review of PR #55The I found no bugs.
Minor notes (no action required)
|
…mmands Add a SECURITY DEFINER extension_drop__registered(name[]) that the event trigger uses to find which dropped extensions are registered. Only those go through the trapped DELETE as the caller.
|
Claude finished @jnasbyupgrade's task in 13s —— View job Review of PR #55The I found no bugs in the code I read.
Notes
|
…oked helper EXECUTE The primary message now reads: permission denied for table "s"."extension_drop__commands" (owned by role "r"), which extension_drop needs to drop extension(s) X. DETAIL carries the underlying error. Restate the unregistered-drop requirement as a single rule (USAGE on the extension's schema), and cover the trap's insufficient_privilege branch with a test.
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
extension_drop's event trigger now fires forDROP SCHEMAandDROP OWNEDas well asDROP EXTENSION, so an extension dropped byDROP SCHEMA ... CASCADEorDROP OWNED BYruns its registered SQL and has its registration removed instead of leaving a stale row inextension_drop__commands(which makes every laterDROP EXTENSIONraiseXD001).These three are the only commands that can drop an extension. An extension has ordinary
pg_dependrows only on its schema and on any extension it requires, so it goes away viaDROP EXTENSION(including cascading to dependents),DROP SCHEMA ... CASCADE, orDROP OWNED, which walks the owner dependencies inshdepend. This doesn't depend on the PostgreSQL version. Corroboration on PG12 and PG17 with an unfilteredsql_droptrigger: only those three tags dropped an extension, andDROP ROLE,DROP TYPE/DROP FUNCTION ... CASCADEon a member, andDROP SCHEMAwithoutCASCADEall error out, whileREASSIGN OWNEDdrops nothing.Since the trigger now runs for every
DROP SCHEMAandDROP OWNED,extension_drop__event_trigger()returns immediately unless an extension is among the dropped objects. Unrelated drops therefore don't readextension_drop__commandsor run the sanity assert, so they work for non-superusers and are unaffected by a stale row.Behavior change: dropping an unregistered extension (by any of the three commands) needs only USAGE on the extension's schema, to resolve a new
SECURITY DEFINERfunction,extension_drop__registered(name[]), executable byPUBLIC, which the trigger asks which of the dropped extensions are registered; it needs no privileges onextension_drop__commands. Dropping a registered extension additionally needs SELECT and DELETE on that table. A role without the access gets SQLSTATE 42501 with the primary messagepermission denied for table "<schema>"."extension_drop__commands" (owned by role "<owner>"), which extension_drop needs to drop extension(s) <name>, the underlying error as DETAIL, and a HINT giving the grants that fix it, instead ofpermission denied for table extension_drop__commands(ortype "extension_drop__commands" does not existwithout USAGE on the schema). The registered SQL runs as the caller and may need further privileges.Mechanism: after the early return, the trigger calls
extension_drop__registered(), which returns the registered subset of the dropped extensions, runs the sanity check (XD001) when it is empty, and writes nothing; itssearch_pathis the extension's schema followed bypg_temp, so a caller's temp table can't shadow the real one. If the result is empty the trigger returns. Otherwise (or if the helper can't be resolved) it deletes the registrations in a singleDELETE ... USING pg_event_trigger_dropped_objects() ... RETURNINGstatement as the caller, wrapped in an exception block forinsufficient_privilegeandundefined_table(a caller without USAGE on the schema gets "does not exist", since name lookup skips a schema it can't use). The registered SQL runs afterward, outside the block, so its own privilege errors surface unchanged. The handler looks up the table's owner and schema throughpg_extensionandpg_classfor the HINT, since name lookup can't be used there either. The trigger function stays SECURITY INVOKER, since SECURITY DEFINER would run registered SQL as the owner; only the read-only helper is a definer.ALTER EVENT TRIGGERcan't changeWHEN, soextension_drop--0.1.1--stable.sqldrops and recreates the trigger and replaces the function. Function properties (proacl,proconfig,prosecdef, source hash) andpg_event_trigger.evttagsare identical after a fresh install and after updating from 0.1.1, on PG12 and PG17.Tests:
drop_schema_cascadeanddrop_ownedfail without the widerWHENlist.unrelated_dropsguards the widening: it fails if the trigger fires for these commands without the early return.drop_deniedcovers the error (code, primary message, DETAIL, HINT) for a registered extension underDROP SCHEMA ... CASCADEwith and without USAGE on the schema,DROP OWNED BY, andDROP EXTENSIONby a demoted owner; SELECT without DELETE and the HINT's grants (directly and through a group role) letting the drop succeed; a privilege error from registered SQL surfacing unchanged; and, for an unregistered extension, drops with no grants, with USAGE only and with SELECT only, plus the no-USAGE error. It also covers a temp table namedextension_drop__commandsnot hiding the registration from the helper, a stale registration still raisingXD001on an unregistered drop, and the helper'sEXECUTErevoked fromPUBLIC(the trigger falls through to the caller's DELETE and raises the same error). Mutations that makedrop_deniedfail: neutering the handler's exception clause; removingundefined_tablefrom it; removinginsufficient_privilegefrom the helper-resolution trap (the revoked-EXECUTEtest);pg_tempfirst in the helper'ssearch_path(the temp-table test); dropping the helper's sanity assert (the stale-registration tests).