Propagate GRANTED BY when deparsing table grants - #8767
Merged
ibrahim halatci (ihalatci) merged 1 commit intoAug 22, 2026
Merged
ibrahim halatci (ihalatci) merged 1 commit into
ibrahim halatci (ihalatci) merged 1 commit into
Conversation
PreprocessGrantStmt() builds the DDL it sends to the workers by hand and never emitted GrantStmt.grantor, so the GRANTED BY clause was silently dropped on the way to the shards. This was unreachable before PostgreSQL 19: until then GRANTED BY only accepted the current user, so the grantor the coordinator recorded was always the role we connect to the workers as, and each worker independently arrived at the same answer. PostgreSQL 19 (commit dd1398f1) relaxed the clause to accept any role whose privileges the current user inherits. Once the named grantor can differ from the connecting role, the workers no longer have enough information to reproduce the coordinator's ACL: they run select_best_grantor() on their own and may legitimately pick a different eligible role. The result is a coordinator/shard ACL divergence that survives a revoke. Granting the same privilege twice under two different grantors and then revoking only one of them leaves the coordinator reporting has_table_privilege() = true while the shards report false, so the grantee's queries fail with a permission error even though the coordinator says the privilege is held. The mirror construction also leaks privileges on the shards after a coordinator-side revoke. Emit GRANTED BY in the deparsed statement so the workers record the same grantor the coordinator did. The clause goes after WITH GRANT OPTION for GRANT, and after the grantee list but before CASCADE/RESTRICT for REVOKE. GrantStmt.grantor has existed since PG14, so no version gate is needed; the field is simply never set on PG16-18. The regression test uses a third-party table owner and two eligible grantors, both inherited by the acting role. The extra owner matters: if the named grantor were also the role select_best_grantor() would have chosen anyway, the test would pass even with the bug present. It asserts the grantor recorded in the shard ACLs via aclexplode(), not just that some privilege exists, and checks has_table_privilege() parity between the coordinator and the shards after a selective revoke. A grantor whose name requires quoting covers RoleSpecString(), and a grant carrying WITH GRANT OPTION plus a matching REVOKE GRANT OPTION FOR exercise both slots the grantor clause shares with the grant option. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## pg19-support #8767 +/- ##
================================================
- Coverage 88.74% 88.71% -0.04%
================================================
Files 289 289
Lines 65065 65068 +3
Branches 8200 8202 +2
================================================
- Hits 57743 57726 -17
- Misses 4957 4978 +21
+ Partials 2365 2364 -1 🚀 New features to boost your workflow:
|
This was referenced Aug 12, 2026
Onur Tirtir (onurctirtir)
approved these changes
Aug 21, 2026
ibrahim halatci (ihalatci)
merged commit Aug 22, 2026
7d65689
into
pg19-support
188 of 194 checks passed
ibrahim halatci (ihalatci)
deleted the
ihalatci-fix-table-granted-by-grantor
branch
August 22, 2026 07:59
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.
Summary
Preserve a table
GRANT/REVOKEstatement's explicitGRANTED BYrole when Citus deparses and propagates the command to physical shards.This PR is intentionally limited to table-grantor serialization in
commands/grant.c. It is separate from #8766, which fixes shared-objectREVOKEclause ordering in the deparser. The two are complementary halves of the same report and must not be conflated.Tracks #8759
Umbrella tracking: #8597
Root cause
PreprocessGrantStmt()hand-builds the worker DDL string rather than routing through the deparser, and it never emittedGrantStmt.grantor. The explicit grantor was therefore silently dropped on the way to every worker.PostgreSQL 19 (commit
dd1398f1) widenedGRANTED BYto accept any role the acting role inherits, not justcurrent_user. Once the grantor is dropped from the worker command, each worker independently runsselect_best_grantor()and can pick a different role than the coordinator did. The result is a coordinator/shard ACL divergence.On PG16-18 the same statement requires
grantor = current_user, so the omission is unobservable there. This is why the defect is only reachable, and only testable, on PG19.Impact
Once the ACLs diverge, a later
REVOKEthat names the coordinator's grantor matches nothing on the shards. Concretely, the coordinator reports the privilege as revoked while the physical shards still grant it — a silent privilege leak thathas_table_privilege()on the coordinator will not reveal.Fix
commands/grant.conly, +16/-4. Builds agrantedByfragment whengrantStmt->grantoris set and appends it to both format strings. InREVOKEit lands after the grantee list and beforeCASCADE/RESTRICT, which is the same ordering #8766 establishes for shared objects, so the two scopes agree by construction.GrantStmt.grantorhas existed since PG14, so no version gate is needed; the code is inert on PG16-18 where the field cannot be set to anything but the current role.Test design
The scenario is deliberately discriminating. A third-party role owns the table and is never named as grantor, so the non-discriminating
owner == grantorcase cannot mask a failure. Two eligible grantors are inherited by the acting role, and the statement explicitly names the one the workers would not choose on their own.Coverage:
aclexplode(relacl).grantorparity, read throughrun_command_on_shards()has_table_privilege()on coordinator and shards"Grant Owner"), exercisingRoleSpecString(..., true)WITH GRANT OPTIONand the matchingREVOKE GRANT OPTION FORTests are gated on
server_version_ge_19and are pure additions: zero removed lines in eitherpg19.sqlorpg19.out.Negative control
Reverting
grant.cto stock, rebuilding, reinstalling and re-running turnspg19red. Verified twice in independent cycles, the second time with a binary-level gate confirming the builtcitus.soactually matched the intended source in each phase (strings citus.so | grep -c 'GRANT %s ON %s TO %s%s%s'-> stock 0, fixed 1).Four assertions flip, and every one of them is shard-side. All coordinator rows are byte-identical between the two runs and appear only as unchanged context:
pg19_grantor_apg19_grantor_bf(coordinator readst)tGRANT ... WITH GRANT OPTIONpg19_grantor_a, grantable=true"Grant Owner", grantable=trueREVOKE GRANT OPTION FORpg19_grantor_a, grantable=false"Grant Owner", grantable=falseThe sharpest single line is row 2:
coordinator_privilege = tsits two lines aboveshard_privilege = fas unchanged context in the same diff hunk.Disclosed limitation, stated plainly: rows 3 and 4 discriminate on grantor identity only, not on the
grantableflag. Unfixed, the shard ACL entry and the un-attributed revoke both resolve topg19_grantor_aand therefore coincide, so the grant option is still correctly dropped. Rows 1 and 2 carry the proof; rows 3 and 4 are corroborating.Validation
origin/pg19-support@db67cd7e2with zero intervening delta, so the validated tree is the pushed treecitus_indent --checkclean (exit 0, noFAILlines)pg19regression on a 3-node PostgreSQL 19beta2 harness:okwith the fix, zero diffexpected/pg19.outis harness-generated, never hand-written, and re-verified byte-identical to a freshly generatedresults/pg19.out