postgres.git / summary / log / commit / refs

commit    be0076b71b68f2e7cb2553af48aa8f8ecc8bda40
Author:   Nathan Bossart <nathan@postgresql.org>
Date:     Thu Aug 13 21:35:07 2026 +0000

    Fix authorization check for role membership changes.
    
    Presently, check_role_membership_authorization() decides whether
    the current user may grant or revoke membership in a role by
    calling is_admin_of_role(), which recurses through all grants,
    while it chooses the grantor to record for the resulting entry by
    calling select_best_admin(), which recurses only through inherited
    grants.  When the two disagree, the permission check passes and the
    grantor lookup then comes up empty, so the user sees an internal
    "no possible grantors" error.  ALTER GROUP ... ADD USER reaches the
    same error through the separate check in AlterRole().
    
    To fix, teach both checks to search the same way
    select_best_admin() does via a new has_admin_privs_of_role().  The
    new check passes exactly when the grantor lookup was going to
    succeed, so nothing that works today starts failing; the internal
    error simply becomes a proper permission error.  Note that this
    leaves the other callers of is_admin_of_role() alone, so a role
    reachable only through a non-inherited grant can still be dropped,
    renamed, or altered.  Whether that ought to change as well is left
    as a future exercise.
    
    Oversight in commit ce6b672e44.
    
    Reported-by: ChangAo Chen <cca5507@qq.com>
    Author: ChangAo Chen <cca5507@qq.com>
    Reviewed-by: Chao Li <li.evan.chao@gmail.com>
    Reviewed-by: Pretham <prezza672@gmail.com>
    Reviewed-by: Robert Haas <robertmhaas@gmail.com>
    Reviewed-by: Jacob Champion <jacob.champion@enterprisedb.com>
    Discussion: https://postgr.es/m/tencent_ADCE2B34B230A9B631854806104FEF40C105%40qq.com
    Discussion: https://postgr.es/m/CAJUn_kN%2BMhbb8fYP5xxQCq1KEziOinM6HgYx4ts_pPDnQ2y1nQ%40mail.gmail.com
    Backpatch-through: 16


src/backend/commands/user.c | 4 ++-- src/backend/utils/adt/acl.c | 27 +++++++++++++++++++++++++++ src/include/utils/acl.h | 1 + src/test/regress/expected/privileges.out | 9 +++++++++ src/test/regress/sql/privileges.sql | 5 +++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index aed01cf4cae..486718133a2 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -818,7 +818,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) } /* To add or drop members, you need ADMIN OPTION. */ - if (drolemembers && !is_admin_of_role(currentUserId, roleid)) + if (drolemembers && !has_admin_privs_of_role(currentUserId, roleid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to alter role"), @@ -2150,7 +2150,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid, /* * Otherwise, must have admin option on the role to be changed. */ - if (!is_admin_of_role(currentUserId, roleid)) + if (!has_admin_privs_of_role(currentUserId, roleid)) { if (is_grant) ereport(ERROR, diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index cd18f31bb97..93e6dd8d2da 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -5276,6 +5276,8 @@ is_member_of_role_nosuper(Oid member, Oid role) * Is member an admin of role? That is, is member the role itself (subject to * restrictions below), a member (directly or indirectly) WITH ADMIN OPTION, * or a superuser? + * + * See also has_admin_privs_of_role() below. */ bool is_admin_of_role(Oid member, Oid role) @@ -5293,6 +5295,31 @@ is_admin_of_role(Oid member, Oid role) return OidIsValid(admin_role); } +/* + * Does member hold ADMIN OPTION on role, either directly or through a role + * whose privileges member inherits? + * + * Unlike is_admin_of_role(), this does not recurse through grants that are not + * inherited. Callers that must go on to record a grantor for the operation + * should use this rather than is_admin_of_role(), since select_best_admin() + * searches the same way. + */ +bool +has_admin_privs_of_role(Oid member, Oid role) +{ + Oid admin_role; + + if (superuser_arg(member)) + return true; + + /* By policy, a role cannot have WITH ADMIN OPTION on itself. */ + if (member == role) + return false; + + (void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role); + return OidIsValid(admin_role); +} + /* * Find a role whose privileges "member" inherits which has ADMIN OPTION * on "role", ignoring super-userness. diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 96c906bab20..49ce189055a 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -215,6 +215,7 @@ extern void check_can_set_role(Oid member, Oid role); extern bool is_member_of_role(Oid member, Oid role); extern bool is_member_of_role_nosuper(Oid member, Oid role); extern bool is_admin_of_role(Oid member, Oid role); +extern bool has_admin_privs_of_role(Oid member, Oid role); extern Oid select_best_admin(Oid member, Oid role); extern Oid get_role_oid(const char *rolname, bool missing_ok); extern Oid get_role_oid_or_public(const char *rolname); diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index 4fbcfb1baf1..aeddeeb59fa 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -78,6 +78,15 @@ SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1' regress_priv_user2 (1 row) +RESET ROLE; +REVOKE INHERIT OPTION FOR regress_priv_user2 FROM regress_priv_user3; +SET ROLE regress_priv_user3; +GRANT regress_priv_user1 TO regress_priv_user5; -- fail +ERROR: permission denied to grant role "regress_priv_user1" +DETAIL: Only roles with the ADMIN option on role "regress_priv_user1" may grant this role. +ALTER GROUP regress_priv_user1 ADD USER regress_priv_user5; -- fail +ERROR: permission denied to alter role +DETAIL: Only roles with the ADMIN option on role "regress_priv_user1" may add or drop members. RESET ROLE; REVOKE regress_priv_user2 FROM regress_priv_user3; REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE; diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index 618e583f50b..9745b572eb9 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -60,6 +60,11 @@ SET ROLE regress_priv_user3; GRANT regress_priv_user1 TO regress_priv_user4; SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole; RESET ROLE; +REVOKE INHERIT OPTION FOR regress_priv_user2 FROM regress_priv_user3; +SET ROLE regress_priv_user3; +GRANT regress_priv_user1 TO regress_priv_user5; -- fail +ALTER GROUP regress_priv_user1 ADD USER regress_priv_user5; -- fail +RESET ROLE; REVOKE regress_priv_user2 FROM regress_priv_user3; REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE; [parent: b42d0c310bec]