Subject: [PATCH v1 1/1] GRANTED BY

From: Nathan Bossart <[email-masked]>
Date: Thu, 13 Nov 2025 09:29:35 -0600

---
 src/backend/catalog/aclchk.c             | 89 +++++++++++++++++-------
 src/backend/utils/adt/acl.c              |  2 +-
 src/include/utils/acl.h                  |  2 +
 src/include/utils/aclchk_internal.h      |  1 +
 src/test/regress/expected/privileges.out |  2 +-
 5 files changed, 68 insertions(+), 28 deletions(-)

diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index cd139bd65a6..9ee3ff9e494 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -397,19 +397,15 @@ ExecuteGrantStmt(GrantStmt *stmt)
 
 	if (stmt->grantor)
 	{
-		Oid			grantor;
-
-		grantor = get_rolespec_oid(stmt->grantor, false);
-
-		/*
-		 * Currently, this clause is only for SQL compatibility, not very
-		 * interesting otherwise.
-		 */
-		if (grantor != GetUserId())
+		istmt.grantor = get_rolespec_oid(stmt->grantor, false);
+		if (!has_privs_of_role(GetUserId(), istmt.grantor))
 			ereport(ERROR,
-					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					 errmsg("grantor must be current user")));
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("must inherit privileges of role \"%s\"",
+							GetUserNameFromId(istmt.grantor, false))));
 	}
+	else
+		istmt.grantor = InvalidOid;
 
 	/*
 	 * Turn the regular GrantStmt into the InternalGrant form.
@@ -1538,6 +1534,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid)
 		istmt.all_privs = true;
 		istmt.privileges = ACL_NO_RIGHTS;
 		istmt.col_privs = NIL;
+		istmt.grantor = InvalidOid;
 		istmt.grantees = list_make1_oid(roleid);
 		istmt.grant_option = false;
 		istmt.behavior = DROP_CASCADE;
@@ -1694,9 +1691,17 @@ ExecGrant_Attribute(InternalGrant *istmt, Oid relOid, const char *relname,
 	merged_acl = aclconcat(old_rel_acl, old_acl);
 
 	/* Determine ID to do the grant as, and available grant options */
-	select_best_grantor(GetUserId(), col_privileges,
-						merged_acl, ownerId,
-						&grantorId, &avail_goptions);
+	if (OidIsValid(istmt->grantor))
+	{
+		grantorId = istmt->grantor;
+		avail_goptions = aclmask_direct(merged_acl, grantorId, ownerId,
+										ACL_GRANT_OPTION_FOR(col_privileges),
+										ACLMASK_ALL);
+	}
+	else
+		select_best_grantor(GetUserId(), col_privileges,
+							merged_acl, ownerId,
+							&grantorId, &avail_goptions);
 
 	pfree(merged_acl);
 
@@ -1967,9 +1972,17 @@ ExecGrant_Relation(InternalGrant *istmt)
 			ObjectType	objtype;
 
 			/* Determine ID to do the grant as, and available grant options */
-			select_best_grantor(GetUserId(), this_privileges,
-								old_acl, ownerId,
-								&grantorId, &avail_goptions);
+			if (OidIsValid(istmt->grantor))
+			{
+				grantorId = istmt->grantor;
+				avail_goptions = aclmask_direct(old_acl, grantorId, ownerId,
+												ACL_GRANT_OPTION_FOR(this_privileges),
+												ACLMASK_ALL);
+			}
+			else
+				select_best_grantor(GetUserId(), this_privileges,
+									old_acl, ownerId,
+									&grantorId, &avail_goptions);
 
 			switch (pg_class_tuple->relkind)
 			{
@@ -2182,9 +2195,17 @@ ExecGrant_common(InternalGrant *istmt, Oid classid, AclMode default_privs,
 		}
 
 		/* Determine ID to do the grant as, and available grant options */
-		select_best_grantor(GetUserId(), istmt->privileges,
-							old_acl, ownerId,
-							&grantorId, &avail_goptions);
+		if (OidIsValid(istmt->grantor))
+		{
+			grantorId = istmt->grantor;
+			avail_goptions = aclmask_direct(old_acl, grantorId, ownerId,
+											ACL_GRANT_OPTION_FOR(istmt->privileges),
+											ACLMASK_ALL);
+		}
+		else
+			select_best_grantor(GetUserId(), istmt->privileges,
+								old_acl, ownerId,
+								&grantorId, &avail_goptions);
 
 		nameDatum = SysCacheGetAttrNotNull(cacheid, tuple,
 										   get_object_attnum_name(classid));
@@ -2337,9 +2358,17 @@ ExecGrant_Largeobject(InternalGrant *istmt)
 		}
 
 		/* Determine ID to do the grant as, and available grant options */
-		select_best_grantor(GetUserId(), istmt->privileges,
-							old_acl, ownerId,
-							&grantorId, &avail_goptions);
+		if (OidIsValid(istmt->grantor))
+		{
+			grantorId = istmt->grantor;
+			avail_goptions = aclmask_direct(old_acl, grantorId, ownerId,
+											ACL_GRANT_OPTION_FOR(istmt->privileges),
+											ACLMASK_ALL);
+		}
+		else
+			select_best_grantor(GetUserId(), istmt->privileges,
+								old_acl, ownerId,
+								&grantorId, &avail_goptions);
 
 		/*
 		 * Restrict the privileges to what we can actually grant, and emit the
@@ -2483,9 +2512,17 @@ ExecGrant_Parameter(InternalGrant *istmt)
 		}
 
 		/* Determine ID to do the grant as, and available grant options */
-		select_best_grantor(GetUserId(), istmt->privileges,
-							old_acl, ownerId,
-							&grantorId, &avail_goptions);
+		if (OidIsValid(istmt->grantor))
+		{
+			grantorId = istmt->grantor;
+			avail_goptions = aclmask_direct(old_acl, grantorId, ownerId,
+											ACL_GRANT_OPTION_FOR(istmt->privileges),
+											ACLMASK_ALL);
+		}
+		else
+			select_best_grantor(GetUserId(), istmt->privileges,
+								old_acl, ownerId,
+								&grantorId, &avail_goptions);
 
 		/*
 		 * Restrict the privileges to what we can actually grant, and emit the
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index fbcd64a2609..bf078888b0f 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -1473,7 +1473,7 @@ aclmask(const Acl *acl, Oid roleid, Oid ownerId,
  * This is exactly like aclmask() except that we consider only privileges
  * held *directly* by roleid, not those inherited via role membership.
  */
-static AclMode
+AclMode
 aclmask_direct(const Acl *acl, Oid roleid, Oid ownerId,
 			   AclMode mask, AclMaskHow how)
 {
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 01ae5b719fd..50d3b42bfcc 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -207,6 +207,8 @@ extern bool aclequal(const Acl *left_acl, const Acl *right_acl);
 
 extern AclMode aclmask(const Acl *acl, Oid roleid, Oid ownerId,
 					   AclMode mask, AclMaskHow how);
+extern AclMode aclmask_direct(const Acl *acl, Oid roleid, Oid ownerId,
+							  AclMode mask, AclMaskHow how);
 extern int	aclmembers(const Acl *acl, Oid **roleids);
 
 extern bool has_privs_of_role(Oid member, Oid role);
diff --git a/src/include/utils/aclchk_internal.h b/src/include/utils/aclchk_internal.h
index 62af290779a..051a9630256 100644
--- a/src/include/utils/aclchk_internal.h
+++ b/src/include/utils/aclchk_internal.h
@@ -36,6 +36,7 @@ typedef struct
 	bool		all_privs;
 	AclMode		privileges;
 	List	   *col_privs;
+	Oid			grantor;
 	List	   *grantees;
 	bool		grant_option;
 	DropBehavior behavior;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index daafaa94fde..997c4b68f47 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -321,7 +321,7 @@ SELECT pg_get_acl(0, 0, 0); -- null
 (1 row)
 
 GRANT TRUNCATE ON atest2 TO regress_priv_user4 GRANTED BY regress_priv_user5;  -- error
-ERROR:  grantor must be current user
+ERROR:  must inherit privileges of role "regress_priv_user5"
 SET SESSION AUTHORIZATION regress_priv_user2;
 SELECT session_user, current_user;
     session_user    |    current_user    
-- 
2.39.5 (Apple Git-154)

--0180Z+uLTgRXM/m1--