fix: don't let foreign owned relations block database reconciliation - #385
Open
mahlunar wants to merge 1 commit into
Open
fix: don't let foreign owned relations block database reconciliation#385mahlunar wants to merge 1 commit into
mahlunar wants to merge 1 commit into
Conversation
Relations created by extensions are owned by the admin user as CREATE EXTENSION is executed on the admin connection. As extensions are installed into the service schema, the following reconciliation failed with 'permission denied for table hypopg_list_indexes' because GRANT ... ON ALL TABLES IN SCHEMA aborts if a single relation in the schema is owned by another role and the grantor holds no privileges on it. This blocked all further reconciliation of the database, ie. no password updates and no new read/readwrite grants. Grants are now applied per relation, filtered on pg_has_role(current_user, relowner, 'USAGE'), so foreign owned relations are skipped while relations owned by the service user, or by a role it is a member of, are still granted. The latter keeps shared databases working. REVOKE ALL ON ALL TABLES IN SCHEMA public in revokeAllOnPublic had the same latent issue and got the same treatment.
tmablunar
approved these changes
Aug 4, 2026
| // privileges on it. That happens for relations created by extensions, as | ||
| // extensions are installed by the admin user, and would block all further | ||
| // reconciliation of the database. | ||
| func forEachOwnedRelationAs(db *sql.DB, schema, actor, statement string) error { |
Contributor
There was a problem hiding this comment.
can we rename the actor argument to make it more clear who that actor is at this stage? Of course requires that it is not reused
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Reconciliation of the
deferredmessagesdatabase failed with:CREATE EXTENSIONis executed on the admin connection (pkg/postgres/extensions.go), and extensions are installed into the service schema. Sohypopg_list_indexesis a view owned by the admin user living in thedeferredmessagesschema. The next reconcile then runswhich aborts the entire statement when it reaches a relation the grantor does not own.
Postgres behaves differently depending on what the grantor holds on the foreign owned relation:
pg_stat_statements, which grants toPUBLIC)WARNING: no privileges were granted→ continuesERROR: permission denied for table …→ statement abortsThat is why the existing
pg_stat_statementstests were green while hypopg blocked reconciliation.Impact: a single foreign owned relation in the schema wedges the
PostgreSQLDatabasereconcile permanently, so password updates stop andPostgreSQLUseraccess requests for that database silently stop being applied.Fix
GRANT/REVOKE ... ON ALL TABLES IN SCHEMAis replaced by a per relation loop inforEachOwnedRelationAs, filtered onpg_has_role(current_user, c.relowner, 'USAGE'):revokeAllOnPublichad the identical latent failure forREVOKE ALL ON ALL TABLES IN SCHEMA publicand got the same treatment. That one triggers if an extension is installed manually without an explicit schema, aspublicis the default.Tests
Two new integration tests, one for the service schema and one for
public. Both reproduce the exact production error when the fix is reverted:Full
pkg/postgressuite passes againstpostgres:18.2.Follow up, not in this PR
Extensions are still installed into the service schema, so extension created relations no longer receive read/readwrite grants. They can be granted explicitly where a service needs them.
Note
Medium Risk
Changes how read/readwrite and PUBLIC revokes are applied on existing relations; foreign-owned extension objects are no longer granted via reconcile (may need explicit grants).
Overview
Fixes database reconciliation wedging when a schema contains relations owned by another role (e.g. extension objects created as the admin user). Bulk
GRANT/REVOKE ... ON ALL TABLES IN SCHEMAused to fail the whole statement withpermission denied for table …on the first such object.Privilege application on existing relations now runs through
forEachOwnedRelationAs, which loops tables/views in the schema and only runs grant/revoke wherepg_has_role(current_user, relowner, 'USAGE'). Service-owned (and member-role-owned) objects behave as before; foreign-owned relations are skipped instead of aborting reconcile. The same pattern replacesREVOKE ALL ON ALL TABLES IN SCHEMA publicinrevokeAllOnPublic. A smallexecAshelper runs the dynamicDOblock underSET ROLE.Integration tests cover admin-owned “extension” objects in the service schema and in
public, asserting reconcile succeeds and privileges on service-owned tables are still applied.Reviewed by Cursor Bugbot for commit 5e90c39. Configure here.