Skip to content

fix: don't let foreign owned relations block database reconciliation - #385

Open
mahlunar wants to merge 1 commit into
masterfrom
fix/foreign-owned-relations-block-reconcile
Open

fix: don't let foreign owned relations block database reconciliation#385
mahlunar wants to merge 1 commit into
masterfrom
fix/foreign-owned-relations-block-reconcile

Conversation

@mahlunar

@mahlunar mahlunar commented Aug 3, 2026

Copy link
Copy Markdown
Member

Problem

Reconciliation of the deferredmessages database failed with:

set default read privileges for role deferredmessages_read: grant SELECT privileges on existing tables:
pq: permission denied for table hypopg_list_indexes

CREATE EXTENSION is executed on the admin connection (pkg/postgres/extensions.go), and extensions are installed into the service schema. So hypopg_list_indexes is a view owned by the admin user living in the deferredmessages schema. The next reconcile then runs

SET ROLE deferredmessages;
GRANT SELECT ON ALL TABLES IN SCHEMA deferredmessages TO deferredmessages_read;

which 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:

Grantor state Result
Holds the privilege but no grant option (e.g. pg_stat_statements, which grants to PUBLIC) WARNING: no privileges were granted → continues
Holds no privileges at all (hypopg) ERROR: permission denied for table … → statement aborts

That is why the existing pg_stat_statements tests were green while hypopg blocked reconciliation.

Impact: a single foreign owned relation in the schema wedges the PostgreSQLDatabase reconcile permanently, so password updates stop and PostgreSQLUser access requests for that database silently stop being applied.

Fix

GRANT/REVOKE ... ON ALL TABLES IN SCHEMA is replaced by a per relation loop in forEachOwnedRelationAs, filtered on pg_has_role(current_user, c.relowner, 'USAGE'):

  • relations owned by the service user are granted as before,
  • relations owned by a role the service user is a member of are still granted, so shared databases keep working,
  • foreign owned relations are skipped instead of aborting the reconcile.

revokeAllOnPublic had the identical latent failure for REVOKE ALL ON ALL TABLES IN SCHEMA public and got the same treatment. That one triggers if an extension is installed manually without an explicit schema, as public is 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:

--- FAIL: TestDatabase_foreignOwnedRelationInServiceSchema
    pq: permission denied for table extension_owned, as test_…

Full pkg/postgres suite passes against postgres: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 SCHEMA used to fail the whole statement with permission 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 where pg_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 replaces REVOKE ALL ON ALL TABLES IN SCHEMA public in revokeAllOnPublic. A small execAs helper runs the dynamic DO block under SET 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.

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.
@mahlunar
mahlunar requested a review from tmablunar August 3, 2026 12:20
@mahlunar
mahlunar marked this pull request as ready for review August 4, 2026 08:35
@mahlunar
mahlunar requested a review from a team as a code owner August 4, 2026 08:35
Comment thread pkg/postgres/database.go
// 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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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.

2 participants