<para>
If <literal>WITH ADMIN OPTION</literal> is specified, the member can
in turn grant membership in the role to others, and revoke membership
- in the role as well. Without the admin option, ordinary users cannot do
- that. However,
- database superusers can grant or revoke membership in any role to anyone.
- Roles having <literal>CREATEROLE</> privilege can grant or revoke
- membership in any role that is not a superuser.
+ in the role as well. Without the admin option, ordinary users cannot
+ do that. A role is not considered to hold <literal>WITH ADMIN
+ OPTION</literal> on itself, but it may grant or revoke membership in
+ itself from a database session where the session user matches the
+ role. Database superusers can grant or revoke membership in any role
+ to anyone. Roles having <literal>CREATEROLE</> privilege can grant
+ or revoke membership in any role that is not a superuser.
</para>
<para>
rolename)));
}
- /* XXX not sure about this check */
+ /*
+ * The role membership grantor of record has little significance at
+ * present. Nonetheless, inasmuch as users might look to it for a crude
+ * audit trail, let only superusers impute the grant to a third party.
+ *
+ * Before lifting this restriction, give the member == role case of
+ * is_admin_of_role() a fresh look. Ensure that the current role cannot
+ * use an explicit grantor specification to take advantage of the session
+ * user's self-admin right.
+ */
if (grantorId != GetUserId() && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
{
if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
{
+ /*
+ * XXX For roleid == role_oid, is_admin_of_role() also examines the
+ * session and call stack. That suits two-argument pg_has_role(), but
+ * it gives the three-argument version a lamentable whimsy.
+ */
if (is_admin_of_role(roleid, role_oid))
return ACLCHECK_OK;
}
/*
- * Is member an admin of role (directly or indirectly)? That is, is it
- * a member WITH ADMIN OPTION?
- *
- * We could cache the result as for is_member_of_role, but currently this
- * is not used in any performance-critical paths, so we don't.
+ * 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?
*/
bool
is_admin_of_role(Oid member, Oid role)
List *roles_list;
ListCell *l;
- /* Fast path for simple case */
- if (member == role)
- return true;
-
- /* Superusers have every privilege, so are part of every role */
if (superuser_arg(member))
return true;
+ if (member == role)
+ /*
+ * A role can admin itself when it matches the session user and we're
+ * outside any security-restricted operation, SECURITY DEFINER or
+ * similar context. SQL-standard roles cannot self-admin. However,
+ * SQL-standard users are distinct from roles, and they are not
+ * grantable like roles: PostgreSQL's role-user duality extends the
+ * standard. Checking for a session user match has the effect of
+ * letting a role self-admin only when it's conspicuously behaving
+ * like a user. Note that allowing self-admin under a mere SET ROLE
+ * would make WITH ADMIN OPTION largely irrelevant; any member could
+ * SET ROLE to issue the otherwise-forbidden command.
+ *
+ * Withholding self-admin in a security-restricted operation prevents
+ * object owners from harnessing the session user identity during
+ * administrative maintenance. Suppose Alice owns a database, has
+ * issued "GRANT alice TO bob", and runs a daily ANALYZE. Bob creates
+ * an alice-owned SECURITY DEFINER function that issues "REVOKE alice
+ * FROM carol". If he creates an expression index calling that
+ * function, Alice will attempt the REVOKE during each ANALYZE.
+ * Checking InSecurityRestrictedOperation() thwarts that attack.
+ *
+ * Withholding self-admin in SECURITY DEFINER functions makes their
+ * behavior independent of the calling user. There's no security or
+ * SQL-standard-conformance need for that restriction, though.
+ *
+ * A role cannot have actual WITH ADMIN OPTION on itself, because that
+ * would imply a membership loop. Therefore, we're done either way.
+ */
+ return member == GetSessionUserId() &&
+ !InLocalUserIdChange() && !InSecurityRestrictedOperation();
+
/*
* Find all the roles that member is a member of, including multi-level
* recursion. We build a list in the same way that is_member_of_role does
ALTER GROUP regressgroup2 ADD USER regressuser2; -- duplicate
NOTICE: role "regressuser2" is already a member of role "regressgroup2"
ALTER GROUP regressgroup2 DROP USER regressuser2;
-ALTER GROUP regressgroup2 ADD USER regressuser4;
+GRANT regressgroup2 TO regressuser4 WITH ADMIN OPTION;
-- test owner privileges
SET SESSION AUTHORIZATION regressuser1;
SELECT session_user, current_user;
t
(1 row)
+-- Admin options
+SET SESSION AUTHORIZATION regressuser4;
+CREATE FUNCTION dogrant_ok() RETURNS void LANGUAGE sql SECURITY DEFINER AS
+ 'GRANT regressgroup2 TO regressuser5';
+GRANT regressgroup2 TO regressuser5; -- ok: had ADMIN OPTION
+SET ROLE regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- fails: SET ROLE suspended privilege
+ERROR: must have admin option on role "regressgroup2"
+SET SESSION AUTHORIZATION regressuser1;
+GRANT regressgroup2 TO regressuser5; -- fails: no ADMIN OPTION
+ERROR: must have admin option on role "regressgroup2"
+SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
+NOTICE: role "regressuser5" is already a member of role "regressgroup2"
+CONTEXT: SQL function "dogrant_ok" statement 1
+ dogrant_ok
+------------
+
+(1 row)
+
+SET ROLE regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- fails: SET ROLE did not help
+ERROR: must have admin option on role "regressgroup2"
+SET SESSION AUTHORIZATION regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- ok: a role can self-admin
+NOTICE: role "regressuser5" is already a member of role "regressgroup2"
+CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
+ 'GRANT regressgroup2 TO regressuser5';
+SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
+ERROR: must have admin option on role "regressgroup2"
+CONTEXT: SQL function "dogrant_fails" statement 1
+DROP FUNCTION dogrant_fails();
+SET SESSION AUTHORIZATION regressuser4;
+DROP FUNCTION dogrant_ok();
+REVOKE regressgroup2 FROM regressuser5;
-- test that dependent privileges are revoked (or not) properly
\c -
set session role regressuser1;
ALTER GROUP regressgroup2 ADD USER regressuser2; -- duplicate
ALTER GROUP regressgroup2 DROP USER regressuser2;
-ALTER GROUP regressgroup2 ADD USER regressuser4;
+GRANT regressgroup2 TO regressuser4 WITH ADMIN OPTION;
-- test owner privileges
SELECT has_table_privilege('regressuser1', 'atest4', 'SELECT WITH GRANT OPTION'); -- true
+-- Admin options
+
+SET SESSION AUTHORIZATION regressuser4;
+CREATE FUNCTION dogrant_ok() RETURNS void LANGUAGE sql SECURITY DEFINER AS
+ 'GRANT regressgroup2 TO regressuser5';
+GRANT regressgroup2 TO regressuser5; -- ok: had ADMIN OPTION
+SET ROLE regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- fails: SET ROLE suspended privilege
+
+SET SESSION AUTHORIZATION regressuser1;
+GRANT regressgroup2 TO regressuser5; -- fails: no ADMIN OPTION
+SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
+SET ROLE regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- fails: SET ROLE did not help
+
+SET SESSION AUTHORIZATION regressgroup2;
+GRANT regressgroup2 TO regressuser5; -- ok: a role can self-admin
+CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
+ 'GRANT regressgroup2 TO regressuser5';
+SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
+DROP FUNCTION dogrant_fails();
+
+SET SESSION AUTHORIZATION regressuser4;
+DROP FUNCTION dogrant_ok();
+REVOKE regressgroup2 FROM regressuser5;
+
+
-- test that dependent privileges are revoked (or not) properly
\c -