Skip to content

Commit 16dca2a

Browse files
committed
Fix role propagation error due to missing grantor dependencies (#8466)
DESCRIPTION: Add grantor role dependencies to fix node addition with interdependent roles Modified ExpandRolesToGroups in dependency.c to track grantor roles as dependencies alongside role memberships during node activation. This ensures grantors are propagated before roles that reference them in GRANT ... GRANTED BY statements. Added regression test covering the interdependent role scenario. Fixes #8425
1 parent 3dd6f41 commit 16dca2a

3 files changed

Lines changed: 194 additions & 14 deletions

File tree

src/backend/distributed/metadata/dependency.c

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,13 @@ static void ApplyAddCitusDependedObjectsToDependencyList(ObjectAddressCollector
187187
static List * GetViewRuleReferenceDependencyList(Oid relationId);
188188
static List * ExpandCitusSupportedTypes(ObjectAddressCollector *collector,
189189
ObjectAddress target);
190+
static List * ExpandCitusSupportedTypesForNodeActivation(ObjectAddressCollector *
191+
collector,
192+
ObjectAddress target);
190193
static List * ExpandForPgVanilla(ObjectAddressCollector *collector,
191194
ObjectAddress target);
192195
static List * GetDependentRoleIdsFDW(Oid FDWOid);
193-
static List * ExpandRolesToGroups(Oid roleid);
196+
static List * ExpandRolesToGroups(Oid roleid, bool includeGrantors);
194197
static ViewDependencyNode * BuildViewDependencyGraph(Oid relationId, HTAB *nodeMap);
195198
static bool IsObjectAddressOwnedByExtension(const ObjectAddress *target,
196199
ObjectAddress *extensionAddress);
@@ -344,7 +347,7 @@ OrderObjectAddressListInDependencyOrder(List *objectAddressList)
344347
}
345348

346349
RecurseObjectDependencies(*objectAddress,
347-
&ExpandCitusSupportedTypes,
350+
&ExpandCitusSupportedTypesForNodeActivation,
348351
&FollowAllSupportedDependencies,
349352
&ApplyAddToDependencyList,
350353
&collector);
@@ -1545,9 +1548,15 @@ ExpandCitusSupportedTypes(ObjectAddressCollector *collector, ObjectAddress targe
15451548
{
15461549
/*
15471550
* Roles are members of other roles. These relations are not recorded directly
1548-
* but can be deduced from pg_auth_members
1551+
* but can be deduced from pg_auth_members.
1552+
*
1553+
* Note: we intentionally do NOT include grantors here. Grantors are
1554+
* only relevant for ordering role creation during node activation
1555+
* (see ExpandCitusSupportedTypesForNodeActivation). Including them
1556+
* in the generic dependency graph would produce false-positive
1557+
* circular-dependency errors for legitimate mutual GRANTs.
15491558
*/
1550-
return ExpandRolesToGroups(target.objectId);
1559+
return ExpandRolesToGroups(target.objectId, false);
15511560
}
15521561

15531562
case ExtensionRelationId:
@@ -1737,6 +1746,32 @@ ExpandCitusSupportedTypes(ObjectAddressCollector *collector, ObjectAddress targe
17371746
}
17381747

17391748

1749+
/*
1750+
* ExpandCitusSupportedTypesForNodeActivation is a variant of
1751+
* ExpandCitusSupportedTypes used only when ordering distributed objects for
1752+
* propagation to a newly-activated node. For roles, it additionally treats
1753+
* grantors of membership tuples as dependencies so that a role used as a
1754+
* grantor is created on the new node before the grantee role. This is what
1755+
* fixes issue #8425.
1756+
*
1757+
* This expansion must NOT be used by the generic dependency-collection paths
1758+
* (creation, cycle-detection, DDL propagation) because mutually-granted roles
1759+
* would appear to form a cycle and be rejected by
1760+
* DeferErrorIfCircularDependencyExists.
1761+
*/
1762+
static List *
1763+
ExpandCitusSupportedTypesForNodeActivation(ObjectAddressCollector *collector,
1764+
ObjectAddress target)
1765+
{
1766+
if (target.classId == AuthIdRelationId)
1767+
{
1768+
return ExpandRolesToGroups(target.objectId, true);
1769+
}
1770+
1771+
return ExpandCitusSupportedTypes(collector, target);
1772+
}
1773+
1774+
17401775
/*
17411776
* ExpandForPgVanilla only expands only comosite types because other types
17421777
* will find their dependencies in pg_depend. The method should only be called by
@@ -1800,10 +1835,19 @@ GetDependentRoleIdsFDW(Oid FDWOid)
18001835

18011836
/*
18021837
* ExpandRolesToGroups returns a list of object addresses pointing to roles that roleid
1803-
* depends on.
1838+
* depends on. This always includes:
1839+
* 1. Roles that roleid is a member of (membership->roleid)
1840+
*
1841+
* When includeGrantors is true, it additionally includes:
1842+
* 2. Roles that are used as grantors for roleid's memberships (membership->grantor)
1843+
*
1844+
* The grantor dependency is only used for ordering role propagation during node
1845+
* activation (see ExpandCitusSupportedTypesForNodeActivation). It must NOT be used
1846+
* in the generic dependency graph because legitimate mutual GRANTs between roles
1847+
* would otherwise be reported as circular dependencies.
18041848
*/
18051849
static List *
1806-
ExpandRolesToGroups(Oid roleid)
1850+
ExpandRolesToGroups(Oid roleid, bool includeGrantors)
18071851
{
18081852
Relation pgAuthMembers = table_open(AuthMemRelationId, AccessShareLock);
18091853
HeapTuple tuple = NULL;
@@ -1819,15 +1863,47 @@ ExpandRolesToGroups(Oid roleid)
18191863
true, NULL, scanKeyCount, scanKey);
18201864

18211865
List *roles = NIL;
1866+
1867+
/*
1868+
* Track all role OIDs we have already emitted as dependencies so that
1869+
* parent roles and grantors are de-duplicated through a single set.
1870+
* A role can appear multiple times in pg_auth_members for the same
1871+
* member (different grantors), and the same OID may show up as both a
1872+
* parent role and a grantor; one DependencyDefinition per OID is enough.
1873+
*
1874+
* Note: For roles with many memberships this O(n) membership check could
1875+
* be replaced with a hash set, but in practice the number of memberships
1876+
* per role is small.
1877+
*/
1878+
List *seenRoleIds = NIL;
18221879
while ((tuple = systable_getnext(scanDescriptor)) != NULL)
18231880
{
18241881
Form_pg_auth_members membership = (Form_pg_auth_members) GETSTRUCT(tuple);
18251882

1826-
DependencyDefinition *definition = palloc0(sizeof(DependencyDefinition));
1827-
definition->mode = DependencyObjectAddress;
1828-
ObjectAddressSet(definition->data.address, AuthIdRelationId, membership->roleid);
1883+
Oid candidates[2] = { membership->roleid, membership->grantor };
1884+
int numCandidates = includeGrantors ? 2 : 1;
1885+
for (int i = 0; i < numCandidates; i++)
1886+
{
1887+
Oid candidateOid = candidates[i];
18291888

1830-
roles = lappend(roles, definition);
1889+
/*
1890+
* Skip self-references: a role cannot depend on itself (the
1891+
* parent-role case cannot hit this because pg_auth_members does
1892+
* not allow roleid == member, but the grantor case can).
1893+
*/
1894+
if (candidateOid == roleid ||
1895+
!OidIsValid(candidateOid) ||
1896+
list_member_oid(seenRoleIds, candidateOid))
1897+
{
1898+
continue;
1899+
}
1900+
1901+
DependencyDefinition *definition = palloc0(sizeof(DependencyDefinition));
1902+
definition->mode = DependencyObjectAddress;
1903+
ObjectAddressSet(definition->data.address, AuthIdRelationId, candidateOid);
1904+
roles = lappend(roles, definition);
1905+
seenRoleIds = lappend_oid(seenRoleIds, candidateOid);
1906+
}
18311907
}
18321908

18331909
systable_endscan(scanDescriptor);

src/test/regress/expected/create_role_propagation.out

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,11 @@ SELECT rolname FROM pg_authid WHERE rolname LIKE '%dist\_%' ORDER BY 1;
337337

338338
\c - - - :worker_2_port
339339
SELECT roleid::regrole::text AS role, member::regrole::text, grantor::regrole::text, admin_option FROM pg_auth_members WHERE roleid::regrole::text LIKE '%dist\_%' ORDER BY 1, 2;
340-
role | member | grantor | admin_option
340+
role | member | grantor | admin_option
341341
---------------------------------------------------------------------
342-
non_dist_role_4 | dist_role_4 | postgres | f
343-
(1 row)
342+
dist_role_1 | non_dist_role_1 | postgres | t
343+
non_dist_role_4 | dist_role_4 | postgres | f
344+
(2 rows)
344345

345346
SELECT rolname FROM pg_authid WHERE rolname LIKE '%dist\_%' ORDER BY 1;
346347
rolname
@@ -349,8 +350,9 @@ SELECT rolname FROM pg_authid WHERE rolname LIKE '%dist\_%' ORDER BY 1;
349350
dist_role_2
350351
dist_role_3
351352
dist_role_4
353+
non_dist_role_1
352354
non_dist_role_4
353-
(5 rows)
355+
(6 rows)
354356

355357
\c - - - :master_port
356358
DROP ROLE dist_role_3, non_dist_role_3, dist_role_4, non_dist_role_4;
@@ -750,4 +752,62 @@ SELECT rolname FROM pg_authid WHERE rolname LIKE '%existing%' ORDER BY 1;
750752
(0 rows)
751753

752754
\c - - - :master_port
755+
-- test interdependent roles with grantor dependencies
756+
-- This test recreates the issue where role1 is used as a grantor for role2's grants,
757+
-- but role1 hasn't been granted admin option on the parent role yet.
758+
SELECT master_remove_node('localhost', :worker_2_port);
759+
master_remove_node
760+
---------------------------------------------------------------------
761+
762+
(1 row)
763+
764+
CREATE ROLE read_only_role;
765+
CREATE ROLE interdep_role1;
766+
CREATE ROLE interdep_role2;
767+
-- Grant read_only_role to interdep_role1 WITH ADMIN OPTION
768+
-- This allows interdep_role1 to grant read_only_role to other roles
769+
GRANT read_only_role TO interdep_role1 WITH ADMIN OPTION;
770+
-- Grant read_only_role to interdep_role2, using interdep_role1 as the grantor
771+
-- Also make interdep_role1 a member of interdep_role2 with admin option
772+
GRANT read_only_role TO interdep_role2 GRANTED BY interdep_role1;
773+
GRANT interdep_role1 TO interdep_role2 WITH ADMIN OPTION;
774+
-- Verify the grant relationships on coordinator
775+
SELECT roleid::regrole::text AS role, member::regrole::text, grantor::regrole::text, admin_option
776+
FROM pg_auth_members
777+
WHERE roleid::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
778+
OR member::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
779+
ORDER BY role, member;
780+
role | member | grantor | admin_option
781+
---------------------------------------------------------------------
782+
interdep_role1 | interdep_role2 | postgres | t
783+
read_only_role | interdep_role1 | postgres | t
784+
read_only_role | interdep_role2 | interdep_role1 | f
785+
(3 rows)
786+
787+
-- Add worker_2 back - this should succeed with our fix
788+
-- Before the fix, this would fail because interdep_role2 would be propagated before
789+
-- interdep_role1's admin option on read_only_role was set up
790+
SELECT 1 FROM master_add_node('localhost', :worker_2_port);
791+
?column?
792+
---------------------------------------------------------------------
793+
1
794+
(1 row)
795+
796+
-- Verify the grants were properly propagated to worker_2
797+
\c - - - :worker_2_port
798+
SELECT roleid::regrole::text AS role, member::regrole::text, grantor::regrole::text, admin_option
799+
FROM pg_auth_members
800+
WHERE roleid::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
801+
OR member::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
802+
ORDER BY role, member;
803+
role | member | grantor | admin_option
804+
---------------------------------------------------------------------
805+
interdep_role1 | interdep_role2 | postgres | t
806+
read_only_role | interdep_role1 | postgres | t
807+
read_only_role | interdep_role2 | interdep_role1 | f
808+
(3 rows)
809+
810+
\c - - - :master_port
811+
-- Clean up interdependent roles
812+
DROP ROLE interdep_role2, interdep_role1, read_only_role;
753813
DROP ROLE nondist_cascade_1, nondist_cascade_2, nondist_cascade_3, dist_cascade;

src/test/regress/sql/create_role_propagation.sql

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,48 @@ SELECT rolname FROM pg_authid WHERE rolname LIKE '%existing%' ORDER BY 1;
320320
SELECT rolname FROM pg_authid WHERE rolname LIKE '%existing%' ORDER BY 1;
321321
\c - - - :master_port
322322

323+
-- test interdependent roles with grantor dependencies
324+
-- This test recreates the issue where role1 is used as a grantor for role2's grants,
325+
-- but role1 hasn't been granted admin option on the parent role yet.
326+
327+
SELECT master_remove_node('localhost', :worker_2_port);
328+
329+
CREATE ROLE read_only_role;
330+
CREATE ROLE interdep_role1;
331+
CREATE ROLE interdep_role2;
332+
333+
-- Grant read_only_role to interdep_role1 WITH ADMIN OPTION
334+
-- This allows interdep_role1 to grant read_only_role to other roles
335+
GRANT read_only_role TO interdep_role1 WITH ADMIN OPTION;
336+
337+
-- Grant read_only_role to interdep_role2, using interdep_role1 as the grantor
338+
-- Also make interdep_role1 a member of interdep_role2 with admin option
339+
GRANT read_only_role TO interdep_role2 GRANTED BY interdep_role1;
340+
GRANT interdep_role1 TO interdep_role2 WITH ADMIN OPTION;
341+
342+
-- Verify the grant relationships on coordinator
343+
SELECT roleid::regrole::text AS role, member::regrole::text, grantor::regrole::text, admin_option
344+
FROM pg_auth_members
345+
WHERE roleid::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
346+
OR member::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
347+
ORDER BY role, member;
348+
349+
-- Add worker_2 back - this should succeed with our fix
350+
-- Before the fix, this would fail because interdep_role2 would be propagated before
351+
-- interdep_role1's admin option on read_only_role was set up
352+
SELECT 1 FROM master_add_node('localhost', :worker_2_port);
353+
354+
-- Verify the grants were properly propagated to worker_2
355+
\c - - - :worker_2_port
356+
SELECT roleid::regrole::text AS role, member::regrole::text, grantor::regrole::text, admin_option
357+
FROM pg_auth_members
358+
WHERE roleid::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
359+
OR member::regrole::text IN ('read_only_role', 'interdep_role1', 'interdep_role2')
360+
ORDER BY role, member;
361+
362+
\c - - - :master_port
363+
364+
-- Clean up interdependent roles
365+
DROP ROLE interdep_role2, interdep_role1, read_only_role;
366+
323367
DROP ROLE nondist_cascade_1, nondist_cascade_2, nondist_cascade_3, dist_cascade;

0 commit comments

Comments
 (0)