fixing CREATEROLE
The CREATEROLE permission is in a very bad spot right now. The biggest
problem that I know about is that it allows you to trivially access
the OS user account under which PostgreSQL is running, which is
expected behavior for a superuser but simply wrong behavior for any
other user. This is because CREATEROLE conveys powerful capabilities
not only to create roles but also to manipulate them in various ways,
including granting any non-superuser role in the system to any new or
existing user, including themselves. Since v11, the roles that can be
granted include pg_execute_server_program and pg_write_server_files
which are trivially exploitable. Perhaps this should have been treated
as an urgent security issue and a fix back-patched, although it is not
clear to me exactly what such a fix would look like. Since we haven't
done that, I went looking for a way to improve things in a principled
way going forward, taking advantage also of recent master-only work to
improve various aspects of the role grant system.
Here, I feel it important to point out that I think the current system
would be broken even if we didn't have predefined roles that are
trivially exploitable to obtain OS user access. We would still lack
any way to restrict the scope of the CREATEROLE privilege. Sure, the
privilege doesn't extend to superusers, but that's not really good
enough. Consider:
rhaas=# create role alice createrole;
CREATE ROLE
rhaas=# create role bob password 'known_only_to_bob';
CREATE ROLE
rhaas=# set session authorization alice;
SET
rhaas=> alter role bob password 'known_to_alice';
ALTER ROLE
Assuming that some form of password authentication is supported, alice
is basically empowered to break into any non-superuser account on the
system and assume all of its privileges. That's really not cool: it's
OK, I think, to give a non-superuser the right to change somebody
else's passwords, but it should be possible to limit it in some way,
e.g. to the users that alice creates. Also, while the ability to make
this sort of change seems to be the clear intention of the code, it's
not documented on the CREATE ROLE page. The problems with
pg_execute_server_program et. al. are not documented either; all it
says is that you should "regard roles that have the CREATEROLE
privilege as almost-superuser-roles," which seems to me to be
understating the extent of the problem.
I have drafted a few patches to try to improve the situation. It seems
to me that the root of any fix in this area must be to change the rule
that CREATEROLE can administer any role whatsoever. Instead, I propose
to change things so that you can only administer roles for which you
have ADMIN OPTION. Administering a role here includes changing the
password for a role, renaming a role, dropping a role, setting the
comment or security label on a role, or granting membership in that
role to another role, whether at role creation time or later. All of
these options are treated in essentially two places in the code, so it
makes sense to handle them all in a symmetric way. One problem with
this proposal is that, if we did exactly this much, then a CREATEROLE
user wouldn't be able to administer the roles which they themselves
had just created. That seems like it would be restricting the
privileges of CREATEROLE users too much.
To fix that, I propose when a non-superuser creates a role, the role
be implicitly granted back to the creator WITH ADMIN OPTION. This
arguably doesn't add any fundamentally new capability because the
CREATEROLE user could do something like "CREATE ROLE some_new_role
ADMIN myself" anyway, but that's awkward to remember and doing it
automatically seems a lot more convenient. However, there's a little
bit of trickiness here, too. Granting the new role back to the creator
might, depending on whether the INHERIT or SET flags are true or false
for the new grant, allow the CREATEROLE user to inherit the privileges
of, or set role to, the target role, which under current rules would
not be allowed. We can minimize behavior changes from the status quo
by setting up the new, implicit grant with SET FALSE, INHERIT FALSE.
However, that might not be what everyone wants. It's definitely not
what *I* want. I want a way for non-superusers to create new roles and
automatically inherit the privileges of those roles just as a
superuser automatically inherits everyone's privileges. I just don't
want the users who can do this to also be able to break out to the OS
as if they were superusers when they're not actually supposed to be.
However, it's clear from previous discussion that other people do NOT
want that, so I propose to make it configurable. Thus, this patch
series also proposes to add INHERITCREATEDROLES and SETCREATEDROLES
properties to roles. These have no meaning if the role is not marked
CREATEROLE. If it is, then they control the properties of the implicit
grant that happens when a CREATEROLE user who is not a superuser
creates a role. If INHERITCREATEDROLES is set, then the implicit grant
back to the creator is WITH INHERIT TRUE, else it's WITH INHERIT
FALSE; likewise, SETCREATEDROLES affects whether the implicit grant is
WITH SET TRUE or WITH SET FALSE.
I'm curious to hear what other people think of these proposals, but
let me first say what I think about them. First, I think it's clear
that we need to do something, because things right now are pretty
badly broken and in a way that affects security. Although these
patches are not back-patchable, they at least promise to improve
things as older versions go out of use. Second, it's possible that we
should look for back-patchable fixes here, but I can't really see that
we're going to come up with anything much better than just telling
people not to use this feature against older releases, because
back-patching catalog changes or dramatic behavior changes seems like
a non-starter. In other words, I think this is going to be a
master-only fix. Third, someone could well have a better or just
different idea how to fix the problems in this area than what I'm
proposing here. This is the best that I've been able to come up with
so far, but that's not to say it's free of problems or that no
improvements are possible.
Finally, I think that whatever we do about the code, the documentation
needs quite a bit of work, because the code is doing a lot of stuff
that is security-critical and entirely non-obvious from the
documentation. I have not in this version of these patches included
any documentation changes and the regression test changes that I have
included are quite minimal. That all needs to be fixed up before there
could be any thought of moving forward with these patches. However, I
thought it best to get rough patches and an outline of the proposed
direction on the table first, before doing a lot of work refining
things.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v1-0003-Restrict-the-privileges-of-CREATEROLE-users.patchapplication/octet-stream; name=v1-0003-Restrict-the-privileges-of-CREATEROLE-users.patchDownload
From f6b3e35013ed76ba92a1bdedd5658895cce0c440 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Nov 2022 15:29:00 -0500
Subject: [PATCH v1 3/4] Restrict the privileges of CREATEROLE users.
Previously, CREATEROLE users were permitted to make nearly arbitrary
changes to roles that they didn't create, with certain exceptions,
such as superuser roles. Instead, allow CREATEROLE users to make such
changes only to roles for which they possess ADMIN OPTION, and to
grant membership only in roles for which they possess ADMIN OPTION.
When a CREATEROLE user who is not a superuser creates a role, grant
ADMIN OPTION on the newly-created role to the creator, so that they
can administer roles they create or for which they have been given
privileges.
With these changes, CREATEROLE users still have very significant
powers that unprivileged users do not receive: they can alter, rename,
drop, comment on, change the password for, and change security labels
on roles. However, they can now do these things only for roles for
which they possess some privileges, rather than all non-superuser
roles; moreover, they cannot grant a role such as
pg_execute_server_program unless they themselves possess it.
FIXME: Add more regression tests.
FIXME: Add documentation.
---
src/backend/catalog/objectaddress.c | 9 +-
src/backend/commands/user.c | 100 ++++++++++++++++------
src/test/regress/expected/create_role.out | 37 ++++----
src/test/regress/sql/create_role.sql | 23 ++---
4 files changed, 106 insertions(+), 63 deletions(-)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index fe97fbf79d..c574e43c2a 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2543,7 +2543,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
/*
* We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
+ * provided that they also have admin option on the role.
+ *
+ * However, superusers are only owned by superusers.
*/
if (superuser_arg(address.objectId))
{
@@ -2558,6 +2560,11 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege")));
+ if (!is_admin_of_role(address.objectId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(roleid, true))));
}
break;
case OBJECT_TSPARSER:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 37fc4f9627..1bfc42a157 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -519,6 +519,42 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
}
+ /*
+ * If the current user isn't a superuser, make them an admin of the new
+ * role so that they can administer the new object they just created.
+ * Superusers will be able to do that anyway.
+ *
+ * The grantor of record for this implicit grant is the bootstrap
+ * superuser, which means that the CREATEROLE user cannot revoke the
+ * grant. They can however grant the created role back to themselves
+ * with different options, since they enjoy ADMIN OPTION on it.
+ */
+ if (!superuser())
+ {
+ RoleSpec *current_role = makeNode(RoleSpec);
+ GrantRoleOptions poptself;
+
+ current_role->roletype = ROLESPEC_CURRENT_ROLE;
+ current_role->location = -1;
+
+ poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ poptself.admin = true;
+ poptself.inherit = false;
+ poptself.set = false;
+
+ AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
+ list_make1(current_role), list_make1_oid(GetUserId()),
+ BOOTSTRAP_SUPERUSERID, &poptself);
+
+ /*
+ * We must make the implicit grant visible to the code below, else
+ * the additional grants will fail.
+ */
+ CommandCounterIncrement();
+ }
+
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
@@ -694,9 +730,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/*
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
- * property. Otherwise, if you don't have createrole, you're only allowed
- * to (1) change your own password or (2) add members to a role for which
- * you have ADMIN OPTION.
+ * property.
*/
if (authform->rolsuper || dissuper)
{
@@ -719,29 +753,35 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+
+ /*
+ * Most changes to a role require that you both have CREATEROLE privileges
+ * and also ADMIN OPTION on the role.
+ */
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
{
- /* things you certainly can't do without CREATEROLE */
+ /* things an unprivileged user certainly can't do */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
- /* without CREATEROLE, can only change your own password */
+ /* an unprivileged user can change their own password */
if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
-
- /* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\" to add members",
- rolename)));
}
+ /* To add members to a role, you need ADMIN OPTION. */
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
+
/* Convert validuntil to internal form */
if (dvalidUntil)
{
@@ -935,8 +975,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
shdepLockAndCheckObject(AuthIdRelationId, roleid);
/*
- * To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * To mess with a superuser you gotta be superuser; otherwise you
+ * need CREATEROLE plus admin option on the target role; unless you're
+ * just trying to change your own settings
*/
if (roleform->rolsuper)
{
@@ -947,7 +988,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if ((!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
+ && roleid != GetUserId())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1067,13 +1110,18 @@ DropRole(DropRoleStmt *stmt)
/*
* For safety's sake, we allow createrole holders to drop ordinary
- * roles but not superuser roles. This is mainly to avoid the
- * scenario where you accidentally drop the last superuser.
+ * roles but not superuser roles, and only if they also have ADMIN
+ * OPTION.
*/
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ role)));
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1312,7 +1360,8 @@ RenameRole(const char *oldname, const char *newname)
errmsg("role \"%s\" already exists", newname)));
/*
- * createrole is enough privilege unless you want to mess with a superuser
+ * Only superusers can mess with superusers. Otherwise, a user with
+ * CREATEROLE can rename a role for which they have ADMIN OPTION.
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1323,7 +1372,8 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -2022,11 +2072,9 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
else
{
/*
- * Otherwise, must have createrole or admin option on the role to be
- * changed.
+ * Otherwise, must have admin option on the role to be changed.
*/
- if (!has_createrole_privilege(currentUserId) &&
- !is_admin_of_role(currentUserId, roleid))
+ if (!is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -2048,7 +2096,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
* be passed as InvalidOid, and this function will infer the user to be
* recorded as the grantor. In many cases, this will be the current user, but
* things get more complicated when the current user doesn't possess ADMIN
- * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * OPTION on the role but rather relies on having SUPERUSER privileges, or
* on inheriting the privileges of a role which does have ADMIN OPTION. See
* below for details.
*
@@ -2074,7 +2122,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
* not depend on any other existing grants, so always default to this
* interpretation when possible.
*/
- if (has_createrole_privilege(currentUserId))
+ if (superuser_arg(currentUserId))
return BOOTSTRAP_SUPERUSERID;
/*
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..6e0cce4579 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -13,7 +13,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -70,20 +70,35 @@ ALTER VIEW tenant_view OWNER TO regress_role_admin;
ERROR: must be owner of view tenant_view
DROP VIEW tenant_view;
ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
ERROR: permission denied to reassign objects
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_monitor IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
+DETAIL: owner of table tenant_table
+owner of view tenant_view
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -114,22 +129,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
-DETAIL: owner of table tenant_table
-owner of view tenant_view
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..d491684db3 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -11,7 +11,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -71,10 +71,10 @@ DROP TABLE tenant_table;
ALTER VIEW tenant_view OWNER TO regress_role_admin;
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
CREATE ROLE regress_monitor IN ROLE pg_monitor;
@@ -86,6 +86,9 @@ CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -109,20 +112,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
-
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.24.3 (Apple Git-128)
v1-0004-Add-role-attributes-INHERITCREATEDROLES-and-SETCR.patchapplication/octet-stream; name=v1-0004-Add-role-attributes-INHERITCREATEDROLES-and-SETCR.patchDownload
From 8e12d14ee01ef00ebb1b044449809ff593bfd580 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 18 Nov 2022 16:03:06 -0500
Subject: [PATCH v1 4/4] Add role attributes INHERITCREATEDROLES and
SETCREATEDROLES.
These control whether the implicit grants created when a
CREATEROLE user creates a new role are marked with the
inherit and set options respectively.
FIXME: Add some regression tests.
FIXME: Add documentation.
FIXME: REMEMBER TO BUMP THE CATALOG VERSION
---
src/backend/commands/user.c | 66 +++++++++++++++++++++++++++++--
src/backend/parser/gram.y | 8 ++++
src/include/catalog/pg_authid.dat | 39 ++++++++++++------
src/include/catalog/pg_authid.h | 2 +
4 files changed, 99 insertions(+), 16 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 1bfc42a157..599d545363 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -141,6 +141,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
bool issuper = false; /* Make the user a superuser? */
bool inherit = true; /* Auto inherit privileges? */
bool createrole = false; /* Can this user create roles? */
+ bool inheritcreatedroles = false; /* inherit any created roles? */
+ bool setcreatedroles = false; /* set role to any created roles? */
bool createdb = false; /* Can the user create databases? */
bool canlogin = false; /* Can this user login? */
bool isreplication = false; /* Is this a replication role? */
@@ -156,6 +158,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dissuper = NULL;
DefElem *dinherit = NULL;
DefElem *dcreaterole = NULL;
+ DefElem *dinheritcreatedroles = NULL;
+ DefElem *dsetcreatedroles = NULL;
DefElem *dcreatedb = NULL;
DefElem *dcanlogin = NULL;
DefElem *disreplication = NULL;
@@ -214,6 +218,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errorConflictingDefElem(defel, pstate);
dcreaterole = defel;
}
+ else if (strcmp(defel->defname, "inheritcreatedroles") == 0)
+ {
+ if (dinheritcreatedroles)
+ errorConflictingDefElem(defel, pstate);
+ dinheritcreatedroles = defel;
+ }
+ else if (strcmp(defel->defname, "setcreatedroles") == 0)
+ {
+ if (dsetcreatedroles)
+ errorConflictingDefElem(defel, pstate);
+ dsetcreatedroles = defel;
+ }
else if (strcmp(defel->defname, "createdb") == 0)
{
if (dcreatedb)
@@ -281,6 +297,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
inherit = boolVal(dinherit->arg);
if (dcreaterole)
createrole = boolVal(dcreaterole->arg);
+ if (dinheritcreatedroles)
+ inheritcreatedroles = boolVal(dinheritcreatedroles->arg);
+ if (dsetcreatedroles)
+ setcreatedroles = boolVal(dsetcreatedroles->arg);
if (dcreatedb)
createdb = boolVal(dcreatedb->arg);
if (dcanlogin)
@@ -402,6 +422,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
+ new_record[Anum_pg_authid_rolinheritcreatedroles - 1] =
+ BoolGetDatum(inheritcreatedroles);
+ new_record[Anum_pg_authid_rolsetcreatedroles - 1] =
+ BoolGetDatum(setcreatedroles);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
@@ -531,18 +555,28 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
if (!superuser())
{
- RoleSpec *current_role = makeNode(RoleSpec);
+ RoleSpec *current_role;
+ HeapTuple utup;
+ Form_pg_authid uform;
GrantRoleOptions poptself;
+ current_role = makeNode(RoleSpec);
current_role->roletype = ROLESPEC_CURRENT_ROLE;
current_role->location = -1;
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(GetUserId()));
+ if (!HeapTupleIsValid(utup))
+ elog(LOG, "cache lookup failed for role %u", GetUserId());
+ uform = (Form_pg_authid) GETSTRUCT(utup);
+
poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
| GRANT_ROLE_SPECIFIED_INHERIT
| GRANT_ROLE_SPECIFIED_SET;
poptself.admin = true;
- poptself.inherit = false;
- poptself.set = false;
+ poptself.inherit = uform->rolinheritcreatedroles;
+ poptself.set = uform->rolsetcreatedroles;
+
+ ReleaseSysCache(utup);
AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
list_make1(current_role), list_make1_oid(GetUserId()),
@@ -612,6 +646,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *dissuper = NULL;
DefElem *dinherit = NULL;
DefElem *dcreaterole = NULL;
+ DefElem *dinheritcreatedroles = NULL;
+ DefElem *dsetcreatedroles = NULL;
DefElem *dcreatedb = NULL;
DefElem *dcanlogin = NULL;
DefElem *disreplication = NULL;
@@ -655,6 +691,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
errorConflictingDefElem(defel, pstate);
dcreaterole = defel;
}
+ else if (strcmp(defel->defname, "inheritcreatedroles") == 0)
+ {
+ if (dinheritcreatedroles)
+ errorConflictingDefElem(defel, pstate);
+ dinheritcreatedroles = defel;
+ }
+ else if (strcmp(defel->defname, "setcreatedroles") == 0)
+ {
+ if (dsetcreatedroles)
+ errorConflictingDefElem(defel, pstate);
+ dsetcreatedroles = defel;
+ }
else if (strcmp(defel->defname, "createdb") == 0)
{
if (dcreatedb)
@@ -841,6 +889,18 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
new_record_repl[Anum_pg_authid_rolcreaterole - 1] = true;
}
+ if (dinheritcreatedroles)
+ {
+ new_record[Anum_pg_authid_rolinheritcreatedroles - 1] = BoolGetDatum(boolVal(dinheritcreatedroles->arg));
+ new_record_repl[Anum_pg_authid_rolinheritcreatedroles - 1] = true;
+ }
+
+ if (dsetcreatedroles)
+ {
+ new_record[Anum_pg_authid_rolsetcreatedroles - 1] = BoolGetDatum(boolVal(dsetcreatedroles->arg));
+ new_record_repl[Anum_pg_authid_rolsetcreatedroles - 1] = true;
+ }
+
if (dcreatedb)
{
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(boolVal(dcreatedb->arg));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9384214942..0d6bdf1531 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1206,6 +1206,14 @@ AlterOptRoleElem:
$$ = makeDefElem("createrole", (Node *) makeBoolean(true), @1);
else if (strcmp($1, "nocreaterole") == 0)
$$ = makeDefElem("createrole", (Node *) makeBoolean(false), @1);
+ else if (strcmp($1, "inheritcreatedroles") == 0)
+ $$ = makeDefElem("inheritcreatedroles", (Node *) makeBoolean(true), @1);
+ else if (strcmp($1, "noinheritcreatedroles") == 0)
+ $$ = makeDefElem("inheritcreatedroles", (Node *) makeBoolean(false), @1);
+ else if (strcmp($1, "setcreatedroles") == 0)
+ $$ = makeDefElem("setcreatedroles", (Node *) makeBoolean(true), @1);
+ else if (strcmp($1, "nosetcreatedroles") == 0)
+ $$ = makeDefElem("setcreatedroles", (Node *) makeBoolean(false), @1);
else if (strcmp($1, "replication") == 0)
$$ = makeDefElem("isreplication", (Node *) makeBoolean(true), @1);
else if (strcmp($1, "noreplication") == 0)
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 3343a69ddb..79390240ca 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -21,67 +21,80 @@
{ oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID',
rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
- rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
+ rolcreaterole => 't', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 't', rolcanlogin => 't',
rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '6171', oid_symbol => 'ROLE_PG_DATABASE_OWNER',
rolname => 'pg_database_owner', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '6181', oid_symbol => 'ROLE_PG_READ_ALL_DATA',
rolname => 'pg_read_all_data', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '6182', oid_symbol => 'ROLE_PG_WRITE_ALL_DATA',
rolname => 'pg_write_all_data', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '3373', oid_symbol => 'ROLE_PG_MONITOR',
rolname => 'pg_monitor', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '3374', oid_symbol => 'ROLE_PG_READ_ALL_SETTINGS',
rolname => 'pg_read_all_settings', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '3375', oid_symbol => 'ROLE_PG_READ_ALL_STATS',
rolname => 'pg_read_all_stats', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '3377', oid_symbol => 'ROLE_PG_STAT_SCAN_TABLES',
rolname => 'pg_stat_scan_tables', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4569', oid_symbol => 'ROLE_PG_READ_SERVER_FILES',
rolname => 'pg_read_server_files', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4570', oid_symbol => 'ROLE_PG_WRITE_SERVER_FILES',
rolname => 'pg_write_server_files', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4571', oid_symbol => 'ROLE_PG_EXECUTE_SERVER_PROGRAM',
rolname => 'pg_execute_server_program', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4200', oid_symbol => 'ROLE_PG_SIGNAL_BACKEND',
rolname => 'pg_signal_backend', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINT',
rolname => 'pg_checkpoint', rolsuper => 'f', rolinherit => 't',
- rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+ rolcreaterole => 'f', rolinheritcreatedroles => 't',
+ rolsetcreatedroles => 't', rolcreatedb => 'f', rolcanlogin => 'f',
rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 3512601c80..83fca311ed 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -35,6 +35,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
+ bool rolinheritcreatedroles; /* if creates role, inherit it? */
+ bool rolsetcreatedroles; /* if creates role, set role to it? */
bool rolcreatedb; /* allowed to create databases? */
bool rolcanlogin; /* allowed to log in as session user? */
bool rolreplication; /* role used for streaming replication */
--
2.24.3 (Apple Git-128)
v1-0001-Refactor-permissions-checking-for-role-grants.patchapplication/octet-stream; name=v1-0001-Refactor-permissions-checking-for-role-grants.patchDownload
From 007002e1d204f95afb44e8e3b440cccf30b1de68 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Aug 2022 14:55:01 -0400
Subject: [PATCH v1 1/4] Refactor permissions-checking for role grants.
Instead of having checks in AddRoleMems() and DelRoleMems(), have
the callers perform checks where it's required. In some cases it
isn't, either because the caller has already performed a check for
the same condition, or because the check couldn't possibly fail.
The "Skip permission check if nothing to do" check in each of
AddRoleMems() and DelRoleMems() is pointless. Most call sites
can't pass an empty list, and in the one case where an empty
list could be passed, the presence of this check couldn't possibly
avoid an error.
---
src/backend/commands/user.c | 116 +++++++++++++++++-------------------
1 file changed, 54 insertions(+), 62 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 8b6543edee..08e3fea135 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -94,6 +94,8 @@ static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
+static void check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant);
static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
bool is_grant);
static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
@@ -505,6 +507,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Oid oldroleid = oldroleform->oid;
char *oldrolename = NameStr(oldroleform->rolname);
+ /* can only add this role to roles for which you have rights */
+ check_role_membership_authorization(GetUserId(), oldroleid, true);
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
@@ -517,6 +521,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
+ *
+ * NB: No permissions check is required here. If you have enough rights
+ * to create a role, you can add any members you like.
*/
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
@@ -1442,6 +1449,8 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
+ check_role_membership_authorization(GetUserId(), roleid,
+ stmt->is_grant);
if (stmt->is_grant)
AddRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
@@ -1566,43 +1575,6 @@ AddRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
- /*
- * The charter of pg_database_owner is to have exactly one, implicit,
- * situation-dependent member. There's no technical need for this
- * restriction. (One could lift it and take the further step of making
- * object_ownercheck(DatabaseRelationId, ...) equivalent to has_privs_of_role(roleid,
- * ROLE_PG_DATABASE_OWNER), in which case explicit, situation-independent
- * members could act as the owner of any database.)
- */
- if (roleid == ROLE_PG_DATABASE_OWNER)
- ereport(ERROR,
- errmsg("role \"%s\" cannot have explicit members", rolename));
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
@@ -1901,31 +1873,6 @@ DelRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
@@ -2039,6 +1986,51 @@ DelRoleMems(const char *rolename, Oid roleid,
table_close(pg_authmem_rel, NoLock);
}
+/*
+ * Check that currentUserId has permission to modify the membership list for
+ * roleid. Throw an error if not.
+ */
+static void
+check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant)
+{
+ /*
+ * The charter of pg_database_owner is to have exactly one, implicit,
+ * situation-dependent member. There's no technical need for this
+ * restriction. (One could lift it and take the further step of making
+ * object_ownercheck(DatabaseRelationId, ...) equivalent to
+ * has_privs_of_role(roleid, ROLE_PG_DATABASE_OWNER), in which case
+ * explicit, situation-independent members could act as the owner of any
+ * database.)
+ */
+ if (is_grant && roleid == ROLE_PG_DATABASE_OWNER)
+ ereport(ERROR,
+ errmsg("role \"%s\" cannot have explicit members",
+ GetUserNameFromId(roleid, false)));
+
+ /* To mess with a superuser role, you gotta be superuser. */
+ if (superuser_arg(roleid))
+ {
+ if (!superuser_arg(currentUserId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to alter superusers")));
+ }
+ else
+ {
+ /*
+ * Otherwise, must have createrole or admin option on the role to be
+ * changed.
+ */
+ if (!has_createrole_privilege(currentUserId) &&
+ !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+}
+
/*
* Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
* targeting a role.
--
2.24.3 (Apple Git-128)
v1-0002-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchapplication/octet-stream; name=v1-0002-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchDownload
From 49cbf1ba379a5496332324d0f00a69def38aac96 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Nov 2022 14:46:03 -0500
Subject: [PATCH v1 2/4] Pass down current user ID to AddRoleMems and
DelRoleMems.
This is just refactoring; there should be no functonal change.
---
src/backend/commands/user.c | 41 ++++++++++++++++++++-----------------
1 file changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 08e3fea135..37fc4f9627 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -87,10 +87,10 @@ int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
-static void AddRoleMems(const char *rolename, Oid roleid,
+static void AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt);
-static void DelRoleMems(const char *rolename, Oid roleid,
+static void DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
@@ -133,6 +133,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
HeapTuple tuple;
Datum new_record[Natts_pg_authid] = {0};
bool new_record_nulls[Natts_pg_authid] = {0};
+ Oid currentUserId = GetUserId();
Oid roleid;
ListCell *item;
ListCell *option;
@@ -508,8 +509,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
char *oldrolename = NameStr(oldroleform->rolname);
/* can only add this role to roles for which you have rights */
- check_role_membership_authorization(GetUserId(), oldroleid, true);
- AddRoleMems(oldrolename, oldroleid,
+ check_role_membership_authorization(currentUserId, oldroleid, true);
+ AddRoleMems(currentUserId, oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
InvalidOid, &popt);
@@ -525,12 +526,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* NB: No permissions check is required here. If you have enough rights
* to create a role, you can add any members you like.
*/
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
popt.admin = true;
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
InvalidOid, &popt);
@@ -583,6 +584,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
Oid roleid;
+ Oid currentUserId = GetUserId();
GrantRoleOptions popt;
check_rolespec_name(stmt->role,
@@ -727,13 +729,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
errmsg("permission denied")));
/* without CREATEROLE, can only change your own password */
- if (dpassword && roleid != GetUserId())
+ if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
/* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\" to add members",
@@ -888,11 +890,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
CommandCounterIncrement();
if (stmt->action == +1) /* add members to role */
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
else if (stmt->action == -1) /* drop members from role */
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt, DROP_RESTRICT);
}
@@ -1378,6 +1380,7 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
List *grantee_ids;
ListCell *item;
GrantRoleOptions popt;
+ Oid currentUserId = GetUserId();
/* Parse options list. */
InitGrantRoleOptions(&popt);
@@ -1449,14 +1452,14 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
- check_role_membership_authorization(GetUserId(), roleid,
- stmt->is_grant);
+ check_role_membership_authorization(currentUserId,
+ roleid, stmt->is_grant);
if (stmt->is_grant)
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt);
else
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt, stmt->behavior);
}
@@ -1555,15 +1558,17 @@ roleSpecsToIds(List *memberNames)
/*
* AddRoleMems -- Add given members to the specified role
*
+ * currentUserId: OID of role performing the operation
* rolename: name of role to add to (used only for error messages)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership (InvalidOid if not set explicitly)
+ * grantorId: OID that should be recorded as having granted the membership
+ * (InvalidOid if not set explicitly)
* popt: information about grant options
*/
static void
-AddRoleMems(const char *rolename, Oid roleid,
+AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt)
{
@@ -1571,7 +1576,6 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1858,7 +1862,7 @@ AddRoleMems(const char *rolename, Oid roleid,
* behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
-DelRoleMems(const char *rolename, Oid roleid,
+DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
{
@@ -1866,7 +1870,6 @@ DelRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
CatCList *memlist;
RevokeRoleGrantAction *actions;
int i;
--
2.24.3 (Apple Git-128)
Robert Haas:
It seems
to me that the root of any fix in this area must be to change the rule
that CREATEROLE can administer any role whatsoever.
Agreed.
Instead, I propose
to change things so that you can only administer roles for which you
have ADMIN OPTION. [...] > I'm curious to hear what other people think of these proposals, [...]
Third, someone could well have a better or just
different idea how to fix the problems in this area than what I'm
proposing here.
Once you can restrict CREATEROLE to only manage "your own" (no matter
how that is defined, e.g. via ADMIN or through some "ownership" concept)
roles, the possibility to "namespace" those roles somehow will become a
lot more important. For example in a multi-tenant setup in the same
cluster, where each tenant has their own database and admin user with a
restricted CREATEROLE privilege, it will very quickly be at least quite
annoying to have conflicts with other tenants' role names. I'm not sure
whether it could even be a serious problem, because I should still be
able to GRANT my own roles to other roles from other tenants - and that
could affect matching of +group records in pg_hba.conf?
My suggestion to $subject and the namespace problem would be to
introduce database-specific roles, i.e. add a database column to
pg_authid. Having this column set to 0 will make the role a cluster-wide
role ("cluster role") just as currently the case. But having a database
oid set will make the role exist in the context of that database only
("database role"). Then, the following principles should be enforced:
- database roles can not share the same name with a cluster role.
- database roles can have the same name as database roles in other
databases.
- database roles can not be members of database roles in **other**
databases.
- database roles with CREATEROLE can only create or alter database roles
in their own database, but not roles in other databases and also not
cluster roles.
- database roles with CREATEROLE can GRANT all database roles in the
same database, but only those cluster roles they have ADMIN privilege on.
- database roles with CREATEROLE can not set SUPERUSER.
To be able to create database roles with a cluster role, there needs to
be some syntax, e.g. something like
CREATE ROLE name IN DATABASE dbname ...
A database role with CREATEROLE should not need to use that syntax,
though - every CREATE ROLE should be IN DATABASE anyway.
With database roles, it would be possible to hand out CREATEROLE without
the ability to grant SUPERUSER or any of the built-in roles. It would be
much more useful on top of that, too. Not only is the namespace problem
mentioned above solved, but it would also be possible to let pg_dump
dump a whole database, including the database roles and their
memberships. This would allow dumping (and restoring) a single
tenant/application including the relevant roles and privileges - without
dumping all roles in the cluster. Plus, it's backwards compatible
because without creating database roles, everything stays exactly the same.
Best,
Wolfgang
On Tue, Nov 22, 2022 at 3:02 AM <walther@technowledgy.de> wrote:
My suggestion to $subject and the namespace problem would be to
introduce database-specific roles, i.e. add a database column to
pg_authid. Having this column set to 0 will make the role a cluster-wide
role ("cluster role") just as currently the case. But having a database
oid set will make the role exist in the context of that database only
("database role"). Then, the following principles should be enforced:- database roles can not share the same name with a cluster role.
- database roles can have the same name as database roles in other
databases.
- database roles can not be members of database roles in **other**
databases.
- database roles with CREATEROLE can only create or alter database roles
in their own database, but not roles in other databases and also not
cluster roles.
- database roles with CREATEROLE can GRANT all database roles in the
same database, but only those cluster roles they have ADMIN privilege on.
- database roles with CREATEROLE can not set SUPERUSER.To be able to create database roles with a cluster role, there needs to
be some syntax, e.g. something likeCREATE ROLE name IN DATABASE dbname ...
I have three comments on this:
1. It's a good idea and might make for some interesting followup work.
2. There are some serious implementation challenges because the
constraints on duplicate object names must be something which can be
enforced by unique constraints on the relevant catalogs. Off-hand, I
don't see how to do that. It would be easy to make the cluster roles
all have unique names, and it would be easy to make the database roles
have unique names within each database, but I have no idea how you
would keep a database role from having the same name as a cluster
role. For anyone to try to implement this, we'd need to have a
solution to that problem.
3. I don't want to sidetrack this thread into talking about possible
future features or followup work. There is enough to do just getting
consensus on the design ideas that I proposed without addressing the
question of what else we might do later. I do not think there is any
reasonable argument that we can't clean up the CREATEROLE mess without
also implementing database-specific roles, and I have no intention of
including that in this patch series. Whether I or someone else might
work on it in the future is a question we can leave for another day.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas:
2. There are some serious implementation challenges because the
constraints on duplicate object names must be something which can be
enforced by unique constraints on the relevant catalogs. Off-hand, I
don't see how to do that. It would be easy to make the cluster roles
all have unique names, and it would be easy to make the database roles
have unique names within each database, but I have no idea how you
would keep a database role from having the same name as a cluster
role. For anyone to try to implement this, we'd need to have a
solution to that problem.
For each database created, create a partial unique index:
CREATE UNIQUE INDEX ... ON pg_authid (rolname) WHERE roldatabase IN (0,
<database_oid>);
Is that possible on catalogs?
Best,
Wolfgang
walther@technowledgy.de writes:
Robert Haas:
2. There are some serious implementation challenges because the
constraints on duplicate object names must be something which can be
enforced by unique constraints on the relevant catalogs. Off-hand, I
don't see how to do that.
For each database created, create a partial unique index:
CREATE UNIQUE INDEX ... ON pg_authid (rolname) WHERE roldatabase IN (0,
<database_oid>);
Is that possible on catalogs?
No, we don't support partial indexes on catalogs, and I don't think
we want to change that. Partial indexes would require expression
evaluations occurring at very inopportune times.
Also, we don't support creating shared indexes post-initdb.
The code has hard-wired lists of which relations are shared,
besides which there's no way to update other databases' pg_class.
Even without that, the idea of a shared catalog ending up with 10000
indexes after you create 10000 databases (requiring 10^8 pg_class
entries across the whole cluster) seems ... unattractive.
regards, tom lane
Tom Lane:
No, we don't support partial indexes on catalogs, and I don't think
we want to change that. Partial indexes would require expression
evaluations occurring at very inopportune times.
I see. Is that the same for indexes *on* an expression? Or would those
be ok?
With a custom operator, an EXCLUDE constraint on the ROW(reldatabase,
relname) expression could work. The operator would compare:
- (0, name1) and (0, name2) as name1 == name2
- (db1, name1) and (0, name2) as name1 == name2
- (0, name1) and (db2, name2) as name1 == name2
- (db1, name1) and (db2, name2) as db1 == db2 && name1 == name2
or just (db1 == 0 || db2 == 0 || db1 == db2) && name1 == name2.
Now, you are going to tell me that EXCLUDE constraints are not supported
on catalogs either, right? ;)
Best,
Wolfgang
Wolfgang Walther:
Tom Lane:
No, we don't support partial indexes on catalogs, and I don't think
we want to change that. Partial indexes would require expression
evaluations occurring at very inopportune times.I see. Is that the same for indexes *on* an expression? Or would those
be ok?With a custom operator, an EXCLUDE constraint on the ROW(reldatabase,
relname) expression could work. The operator would compare:
- (0, name1) and (0, name2) as name1 == name2
- (db1, name1) and (0, name2) as name1 == name2
- (0, name1) and (db2, name2) as name1 == name2
- (db1, name1) and (db2, name2) as db1 == db2 && name1 == name2or just (db1 == 0 || db2 == 0 || db1 == db2) && name1 == name2.
Does it even need to be on the expression? I don't think so. It would be
enough to just make it compare relname WITH = and reldatabase WITH the
custom operator (db1 == 0 || db2 == 0 || db1 == db2), right?
Best,
Wolfgang
walther@technowledgy.de writes:
No, we don't support partial indexes on catalogs, and I don't think
we want to change that. Partial indexes would require expression
evaluations occurring at very inopportune times.
I see. Is that the same for indexes *on* an expression? Or would those
be ok?
Right, we don't support those on catalogs either.
Now, you are going to tell me that EXCLUDE constraints are not supported
on catalogs either, right? ;)
Nor those.
regards, tom lane
On 11/21/22 15:39, Robert Haas wrote:
I'm curious to hear what other people think of these proposals, but
let me first say what I think about them. First, I think it's clear
that we need to do something, because things right now are pretty
badly broken and in a way that affects security. Although these
patches are not back-patchable, they at least promise to improve
things as older versions go out of use.
+1
Second, it's possible that we should look for back-patchable fixes
here, but I can't really see that we're going to come up with
anything much better than just telling people not to use this feature
against older releases, because back-patching catalog changes or
dramatic behavior changes seems like a non-starter. In other words, I
think this is going to be a master-only fix.
Yep, seems highly likely
Third, someone could well have a better or just different idea how to
fix the problems in this area than what I'm proposing here. This is
the best that I've been able to come up with so far, but that's not
to say it's free of problems or that no improvements are possible.
On quick inspection I like what you have proposed and no significantly
"better" ideas jump to mind. I will try to think on it though.
Finally, I think that whatever we do about the code, the documentation
needs quite a bit of work, because the code is doing a lot of stuff
that is security-critical and entirely non-obvious from the
documentation. I have not in this version of these patches included
any documentation changes and the regression test changes that I have
included are quite minimal. That all needs to be fixed up before there
could be any thought of moving forward with these patches. However, I
thought it best to get rough patches and an outline of the proposed
direction on the table first, before doing a lot of work refining
things.
I have looked at, and even done some doc improvements in this area in
the past, and concluded that it is simply hard to describe it in a
clear, straightforward way.
There are multiple competing concepts (privs on objects, attributes of
roles, membership, when things are inherited versus not, settings bound
to roles, etc). I don't know what to do about it, but yeah, fixing the
documentation would be a noble goal.
--
Joe Conway
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
On Nov 21, 2022, at 12:39 PM, Robert Haas <robertmhaas@gmail.com> wrote:
I have drafted a few patches to try to improve the situation.
The 0001 and 0002 patches appear to be uncontroversial refactorings. Patch 0003 looks on-point and a move in the right direction. The commit message in that patch is well written. Patch 0004 feels like something that won't get committed. The INHERITCREATEDROLES and SETCREATEDROLES in 0004 seems clunky.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Tue, Nov 22, 2022 at 3:01 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
On Nov 21, 2022, at 12:39 PM, Robert Haas <robertmhaas@gmail.com> wrote:
I have drafted a few patches to try to improve the situation.The 0001 and 0002 patches appear to be uncontroversial refactorings. Patch 0003 looks on-point and a move in the right direction. The commit message in that patch is well written.
Thanks.
Patch 0004 feels like something that won't get committed. The INHERITCREATEDROLES and SETCREATEDROLES in 0004 seems clunky.
I think role properties are kind of clunky in general, the way we've
implemented them in PostgreSQL, but I don't really see why these are
worse than anything else. I think we need some way to control the
behavior, and I don't really see a reasonable place to put it other
than a per-role property. And if we're going to do that then they
might as well look like the other properties that we've already got.
Do you have a better idea?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Nov 22, 2022, at 2:02 PM, Robert Haas <robertmhaas@gmail.com> wrote:
Patch 0004 feels like something that won't get committed. The INHERITCREATEDROLES and SETCREATEDROLES in 0004 seems clunky.
I think role properties are kind of clunky in general, the way we've
implemented them in PostgreSQL, but I don't really see why these are
worse than anything else. I think we need some way to control the
behavior, and I don't really see a reasonable place to put it other
than a per-role property. And if we're going to do that then they
might as well look like the other properties that we've already got.Do you have a better idea?
Whatever behavior is to happen in the CREATE ROLE statement should be spelled out in that statement. "CREATE ROLE bob WITH INHERIT false WITH SET false" doesn't seem too unwieldy, and has the merit that it can be read and understood without reference to hidden parameters. Forcing this to be explicit should be safer if these statements ultimately make their way into dump/restore scripts, or into logical replication.
That's not to say that I wouldn't rather that it always work one way or always the other. It's just to say that I don't want it to work differently based on some poorly advertised property of the role executing the command.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Tue, Nov 22, 2022 at 5:48 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
Whatever behavior is to happen in the CREATE ROLE statement should be spelled out in that statement. "CREATE ROLE bob WITH INHERIT false WITH SET false" doesn't seem too unwieldy, and has the merit that it can be read and understood without reference to hidden parameters. Forcing this to be explicit should be safer if these statements ultimately make their way into dump/restore scripts, or into logical replication.
That's not to say that I wouldn't rather that it always work one way or always the other. It's just to say that I don't want it to work differently based on some poorly advertised property of the role executing the command.
That seems rather pejorative. If we stipulate from the outset that the
property is poorly advertised, then obviously anything at all
depending on it is not going to seem like a very good idea. But why
should we assume that it will be poorly-advertised? I clearly said
that the documentation needs a bunch of work, and that I planned to
work on it. As an initial matter, the documentation is where we
advertise new features, so I think you ought to take it on faith that
this will be well-advertised, unless you think that I'm completely
hopeless at writing documentation or something.
On the actual issue, I think that one key question is who should
control what happens when a role is created. Is that the superuser's
decision, or the CREATEROLE user's decision? I think it's better for
it to be the superuser's decision. Consider first the use case where
you want to set up a user who "feels like a superuser" i.e. inherits
the privileges of users they create. You don't want them to have to
specify anything when they create a role for that to happen. You just
want it to happen. So you want to set up their account so that it will
happen automatically, not throw the complexity back on them. In the
reverse scenario where you don't want the privileges inherited, I
think it's a little less clear, possibly just because I haven't
thought about that scenario as much, but I think it's very reasonable
here too to want the superuser to set up a configuration for the
CREATEROLE user that does what the superuser wants, rather than what
the CREATEROLE user wants.
Even aside from the question of who controls what, I think it is far
better from a usability perspective to have ways of setting up good
defaults. That is why we have the default_tablespace GUC, for example.
We could have made the CREATE TABLE command always use the database's
default tablespace, or we could have made it always use the main
tablespace. Then it would not be dependent on (poorly advertised?)
settings elsewhere. But it would also be really inconvenient, because
if someone is creating a lot of tables and wants them all to end up in
the same place, they don't want to have to specify the name of that
tablespace each time. They want to set a default and have that get
used by each command.
There's another, subtler consideration here, too. Since
ce6b672e4455820a0348214be0da1a024c3f619f, there are constraints on who
can validly be recorded as the grantor of a particular role grant,
just as we have always done for other types of grants. The grants have
to form a tree, with each grant having a grantor that was themselves
granted ADMIN OPTION by someone else, until eventually you get back to
the bootstrap superuser who is the source of all privileges. Thus,
today, when a CREATEROLE user grants membership in a role, the grantor
is recorded as the bootstrap superuser, because they might not
actually possess ADMIN OPTION on the role at all, and so we can't
necessarily record them as the grantor. But this patch changes that,
which I think is a significant improvement. The implicit grant that is
created when CREATE ROLE is issued has the bootstrap superuser as
grantor, because there is no other legal option, but then any further
grants performed by the CREATE ROLE user rely on that user having that
grant, and thus record the OID of the CREATEROLE user as the grantor,
not the bootstrap superuser.
That, in turn, has a number of rather nice consequences. It means in
particular that the CREATEROLE user can't remove the implicit grant,
nor can they alter it. They are, after all, not the grantor, who is
the bootstrap superuser, nor do they any longer have the authority to
act as the bootstrap superuser. Thus, if you have two or more
CREATEROLE users running around doing stuff, you can tell who did
what. Every role that those users created is linked back to the
creating role in a way that the creator can't alter. A CREATEROLE user
is unable to contrive a situation where they no longer control a role
that they created. That also means that the superuser, if desired, can
revoke all membership grants performed by any particular CREATEROLE
user by revoking the implicit grants with CASCADE.
But since this implicit grant has, and must have, the bootstrap
superuser as grantor, it is also only reasonable that superusers get
to determine what options are used when creating that grant, rather
than leaving that up to the CREATEROLE user.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Nov 23, 2022, at 9:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:
That's not to say that I wouldn't rather that it always work one way or always the other. It's just to say that I don't want it to work differently based on some poorly advertised property of the role executing the command.
That seems rather pejorative. If we stipulate from the outset that the
property is poorly advertised, then obviously anything at all
depending on it is not going to seem like a very good idea. But why
should we assume that it will be poorly-advertised? I clearly said
that the documentation needs a bunch of work, and that I planned to
work on it. As an initial matter, the documentation is where we
advertise new features, so I think you ought to take it on faith that
this will be well-advertised, unless you think that I'm completely
hopeless at writing documentation or something.
Oh, I don't mean that it will be poorly documented. I mean that the way the command is written won't advertise what it is going to do. That's concerning if you fat-finger a CREATE ROLE command, then realize you need to drop and recreate the role, only to discover that a property you weren't thinking about, and which you are accustomed to being set the opposite way, is set such that you can't drop the role you just created. I think if you're going to create-and-disown something, you should have to say so, to make sure you mean it.
This consideration differs from the default schema or default tablespace settings. If I fat-finger the creation of a table, regardless of where it gets placed, I'm still the owner of the table, and I can still drop and recreate the table to fix my mistake.
Why not make this be a permissions issue, rather than a default behavior issue? Instead of a single CREATEROLE privilege, grant roles privileges to CREATE-WITH-INHERIT, CREATE-WITH-ADMIN, CREATE-SANS-INHERIT, CREATE-SANS-ADMIN, and thereby limit which forms of the command they may execute. That way, the semantics of the command do not depend on some property external to the command. Users (and older scripts) will expect the traditional syntax to behave consistent with how CREATE ROLE has worked in the past. The behaviors can be specified explicitly.
On the actual issue, I think that one key question is who should
control what happens when a role is created. Is that the superuser's
decision, or the CREATEROLE user's decision? I think it's better for
it to be the superuser's decision. Consider first the use case where
you want to set up a user who "feels like a superuser" i.e. inherits
the privileges of users they create. You don't want them to have to
specify anything when they create a role for that to happen. You just
want it to happen. So you want to set up their account so that it will
happen automatically, not throw the complexity back on them. In the
reverse scenario where you don't want the privileges inherited, I
think it's a little less clear, possibly just because I haven't
thought about that scenario as much, but I think it's very reasonable
here too to want the superuser to set up a configuration for the
CREATEROLE user that does what the superuser wants, rather than what
the CREATEROLE user wants.Even aside from the question of who controls what, I think it is far
better from a usability perspective to have ways of setting up good
defaults. That is why we have the default_tablespace GUC, for example.
We could have made the CREATE TABLE command always use the database's
default tablespace, or we could have made it always use the main
tablespace. Then it would not be dependent on (poorly advertised?)
settings elsewhere. But it would also be really inconvenient, because
if someone is creating a lot of tables and wants them all to end up in
the same place, they don't want to have to specify the name of that
tablespace each time. They want to set a default and have that get
used by each command.There's another, subtler consideration here, too. Since
ce6b672e4455820a0348214be0da1a024c3f619f, there are constraints on who
can validly be recorded as the grantor of a particular role grant,
just as we have always done for other types of grants. The grants have
to form a tree, with each grant having a grantor that was themselves
granted ADMIN OPTION by someone else, until eventually you get back to
the bootstrap superuser who is the source of all privileges. Thus,
today, when a CREATEROLE user grants membership in a role, the grantor
is recorded as the bootstrap superuser, because they might not
actually possess ADMIN OPTION on the role at all, and so we can't
necessarily record them as the grantor. But this patch changes that,
which I think is a significant improvement. The implicit grant that is
created when CREATE ROLE is issued has the bootstrap superuser as
grantor, because there is no other legal option, but then any further
grants performed by the CREATE ROLE user rely on that user having that
grant, and thus record the OID of the CREATEROLE user as the grantor,
not the bootstrap superuser.That, in turn, has a number of rather nice consequences. It means in
particular that the CREATEROLE user can't remove the implicit grant,
nor can they alter it. They are, after all, not the grantor, who is
the bootstrap superuser, nor do they any longer have the authority to
act as the bootstrap superuser. Thus, if you have two or more
CREATEROLE users running around doing stuff, you can tell who did
what. Every role that those users created is linked back to the
creating role in a way that the creator can't alter. A CREATEROLE user
is unable to contrive a situation where they no longer control a role
that they created. That also means that the superuser, if desired, can
revoke all membership grants performed by any particular CREATEROLE
user by revoking the implicit grants with CASCADE.But since this implicit grant has, and must have, the bootstrap
superuser as grantor, it is also only reasonable that superusers get
to determine what options are used when creating that grant, rather
than leaving that up to the CREATEROLE user.
Yes, this all makes sense, but does it entail that the CREATE ROLE command must behave differently on the basis of a setting?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Wed, Nov 23, 2022 at 12:36 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
Oh, I don't mean that it will be poorly documented. I mean that the way the command is written won't advertise what it is going to do. That's concerning if you fat-finger a CREATE ROLE command, then realize you need to drop and recreate the role, only to discover that a property you weren't thinking about, and which you are accustomed to being set the opposite way, is set such that you can't drop the role you just created.
That doesn't ever happen. No matter how the properties are set, you
end up with ADMIN OPTION on the newly-created role and can drop it.
The flags control things like whether you can select from the newly
created role's tables even if you otherwise lack permissions on them
(INHERIT), and whether you can SET ROLE to it (SET). You can always
administer it, i.e. grant rights on it to others, change its password,
drop it.
I think if you're going to create-and-disown something, you should have to say so, to make sure you mean it.
Reasonable, but not relevant, since that isn't what's happening.
Why not make this be a permissions issue, rather than a default behavior issue? Instead of a single CREATEROLE privilege, grant roles privileges to CREATE-WITH-INHERIT, CREATE-WITH-ADMIN, CREATE-SANS-INHERIT, CREATE-SANS-ADMIN, and thereby limit which forms of the command they may execute. That way, the semantics of the command do not depend on some property external to the command. Users (and older scripts) will expect the traditional syntax to behave consistent with how CREATE ROLE has worked in the past. The behaviors can be specified explicitly.
Perhaps if we get the confusion above cleared up you won't be as
concerned about this, but let me just say that this patch is
absolutely breaking backward compatibility. I don't feel bad about
that, either. I think it's a good thing in this case, because the
current behavior is abjectly broken and horrible. What we've been
doing for the last several years is shipping a product that has a
built-in exploit that a clever 10-year-old could use to escalate
privileges from CREATEROLE to SUPERUSER. We should not be OK with
that, and we should be OK with changing the behavior however much is
required to fix it. I'm personally of the opinion that this patch set
does a rather clever job minimizing that blast radius, but that might
be my own bias as the patch author. Regardless, I don't think there's
any reasonable argument for maintaining the current behavior. I don't
entirely follow exactly what you have in mind in the sentence above,
but if it involves keeping the current CREATEROLE behavior around in
any form, -1 from me.
But since this implicit grant has, and must have, the bootstrap
superuser as grantor, it is also only reasonable that superusers get
to determine what options are used when creating that grant, rather
than leaving that up to the CREATEROLE user.Yes, this all makes sense, but does it entail that the CREATE ROLE command must behave differently on the basis of a setting?
Well, we certainly don't HAVE to add those new role-level properties;
that's why they are in a separate patch. But I think they add a lot of
useful functionality for a pretty minimal amount of extra code
complexity.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Nov 23, 2022 at 12:36 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:Yes, this all makes sense, but does it entail that the CREATE ROLE command must behave differently on the basis of a setting?
Well, we certainly don't HAVE to add those new role-level properties;
that's why they are in a separate patch. But I think they add a lot of
useful functionality for a pretty minimal amount of extra code
complexity.
I haven't thought about these issues hard enough to form an overall
opinion (though I agree that making CREATEROLE less tantamount
to superuser would be an improvement). However, I share Mark's
discomfort about making these commands act differently depending on
context. We learned long ago that allowing GUCs to affect query
semantics was a bad idea. Basing query semantics on properties
of the issuing role (beyond success-or-permissions-failure) doesn't
seem a whole lot different from that. It still means that
applications can't just issue command X and expect Y to happen;
they have to inquire about context in order to find out that Z might
happen instead. That's bad in any case, but it seems especially bad
for security-critical behaviors.
regards, tom lane
On Wed, Nov 23, 2022 at 1:11 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I haven't thought about these issues hard enough to form an overall
opinion (though I agree that making CREATEROLE less tantamount
to superuser would be an improvement). However, I share Mark's
discomfort about making these commands act differently depending on
context. We learned long ago that allowing GUCs to affect query
semantics was a bad idea. Basing query semantics on properties
of the issuing role (beyond success-or-permissions-failure) doesn't
seem a whole lot different from that. It still means that
applications can't just issue command X and expect Y to happen;
they have to inquire about context in order to find out that Z might
happen instead. That's bad in any case, but it seems especially bad
for security-critical behaviors.
I'm not sure that this behavior qualifies as security-critical. If
INHERITCREATEDROLES and SETCREATEDROLES are both true, then the grant
has INHERIT TRUE and SET TRUE and there are no more rights to be
gained. If not, the createrole user can do something like GRANT
new_role TO my_own_account WITH INHERIT TRUE, SET TRUE. Even if we
somehow disallowed that, they could gain access to the privilege of
the created role in a bunch of other ways, such as granting the rights
to someone else, or just changing the password and using the new
password to log into the account.
When I started working in this area, I thought non-inherited grants
were pretty useless, because you can so easily work around it.
However, other people did not agree. From what I can gather, I think
the reason why people like non-inherited grants is that they prevent
mistakes. A user who has ADMIN OPTION on another role but does not
inherit its privileges can break into that account and do whatever
they want, but they cannot ACCIDENTALLY perform an operation that
makes use of that user's privileges. They will have to SET ROLE, or
GRANT themselves something, and those actions can be logged and
audited if desired. Because of the potential for that sort of logging
and auditing, you can certainly make an argument that this is a
security-critical behavior, but it's not that clear cut, because it's
making assumptions about the behavior of other software, and of human
beings. Strictly speaking, looking just at PostgreSQL, these options
don't affect security.
On the more general question of configurability, I tend to agree that
it's not great to have the behavior of commands depend too much on
context, especially for security-critical things. A particularly toxic
example IMHO is search_path, which I think is an absolute disaster in
terms of security that I believe we will never be able to fully fix.
Yet there are plenty of examples of configurability that no one finds
problematic. No one agitates against the idea that a database can have
a default tablespace, or that you can ALTER USER or ALTER DATABASE to
configure an setting on a user-specific or database-specific setting,
even a security-critical one like search_path, or one that affects
query behavior like work_mem. No one is outraged that a data type has
a default btree operator class that is used for indexes unless you
specify another one explicitly. What people mostly complain about IME
is stuff like standard_conforming_strings, or bytea_output, or
datestyle. Often, when proposal come up on pgsql-hackers and get shot
down on these grounds, the issue is that they would essentially make
it impossible to write SQL that will run portably on PostgreSQL.
Instead, you'd need to write your application to issue different SQL
depending on the value of settings on the local system. That's un-fun
at best, and impossible at worst, as in the case of extension scripts,
whose content has to be static when you ship the thing.
But it's not exactly clear to me what the broader organizing principle
is here, or ought to be. I think it would be ridiculous to propose --
and I assume that you are not proposing -- that no command should have
behavior that in any way depends on what SQL commands have been
executed previously. Taken to a silly extreme, that would imply that
CREATE TABLE ought to be removed, because the behavior of SELECT *
FROM something will otherwise depend on whether someone has previously
issued CREATE TABLE something. Obviously that is a stupid argument.
But on the other hand, it would also be ridiculous to propose the
reverse, that it's fine to add arbitrary settings that affect the
behavior of any command whatsoever in arbitrary ways. Simon's proposal
to add a GUC that would make vacuum request a background vacuum rather
than performing one in the foreground is a good example of a proposal
that did not sit well with either of us.
But I don't know on what basis exactly we put a proposal like this in
one category rather than the other. I'm not sure I can really
articulate the general principle in a sensible way. For me, this
clearly falls into the "good" category: it's configuration that you
put into the database that makes things happen the way you want, not a
behavior-changing setting that comes along and ruins somebody's day.
But if someone else feels otherwise, I'm not sure I can defend that
view in a really rigorous way, because I'm not really sure what the
litmus test is, or should be. I think the best that I can do is to say
that if we *don't* add those options but *do* adopt the rest of the
patch set, we will have to make a decision about what behavior
everyone is going to get, and no matter what we decide, some people
are not going to be really unhappy with the result. I would like to
find a way to avoid that.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Nov 23, 2022, at 11:02 AM, Robert Haas <robertmhaas@gmail.com> wrote:
For me, this
clearly falls into the "good" category: it's configuration that you
put into the database that makes things happen the way you want, not a
behavior-changing setting that comes along and ruins somebody's day.
I had incorrectly imagined that if the bootstrap superuser granted CREATEROLE to Alice with particular settings, those settings would limit the things that Alice could do when creating role Bob, specifically limiting how much she could administer/inherit/set role Bob thereafter. Apparently, your proposal only configures what happens by default, and Alice can work around that if she wants to. But if that's the case, did I misunderstand upthread that these are properties the superuser specifies about Alice? Can Alice just set these properties about herself, so she gets the behavior she wants? I'm confused now about who controls these settings.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Wed, Nov 23, 2022 at 2:28 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
On Nov 23, 2022, at 11:02 AM, Robert Haas <robertmhaas@gmail.com> wrote:
For me, this
clearly falls into the "good" category: it's configuration that you
put into the database that makes things happen the way you want, not a
behavior-changing setting that comes along and ruins somebody's day.I had incorrectly imagined that if the bootstrap superuser granted CREATEROLE to Alice with particular settings, those settings would limit the things that Alice could do when creating role Bob, specifically limiting how much she could administer/inherit/set role Bob thereafter. Apparently, your proposal only configures what happens by default, and Alice can work around that if she wants to.
Right.
But if that's the case, did I misunderstand upthread that these are properties the superuser specifies about Alice? Can Alice just set these properties about herself, so she gets the behavior she wants? I'm confused now about who controls these settings.
Because they are role-level properties, they can be set by whoever has
ADMIN OPTION on the role. That always includes every superuser, and it
never includes Alice herself (except if she's a superuser). It could
include other users depending on the system configuration. For
example, under this proposal, the superuser might create alice and set
her account to CREATEROLE, configuring the INHERITCREATEDROLES and
SETCREATEDROLES properties on Alice's account according to preference.
Then, alice might create another user, say bob, and make him
CREATEROLE as well. In such a case, either the superuser or alice
could set these properties for role bob, because alice enjoys ADMIN
OPTION on role bob.
Somewhat to one side of the question you were asking, but related to
the above, I believe there is an opportunity, and perhaps a need, to
modify the scope of CREATEROLE in terms of what role-level options a
CREATEROLE user can set. For instance, if a CREATEROLE user doesn't
have CREATEDB, they can still create users and give them that
privilege, even with these patches, and likewise these two new
properties. This patch is only concerned about which roles you can
manipulate, not what role-level properties you can set. Somebody might
feel that's a serious problem, and they might even feel that this
patch set ought to something about it. In my view, the issues are
somewhat severable. I don't think you can do anything as evil by
setting role-level properties (except for SUPERUSER, of course) as
what you can do by granting predefined roles, so I don't find
restricting those capabilities to be as urgent as doing something to
restrict role grants.
Also, and this definitely plays into it too, I think there's some
debate about what the model ought to look like there. For instance,
you could simply stipulate that you can't give what you don't have,
but that would mean that every CREATEROLE user can create additional
CREATEROLE users, and I suspect some people might like to restrict
that. We could add a new CREATECREATEROLE property to decide whether a
user can make CREATEROLE users, but by that argument we'd also need a
new CREATECREATECREATEROLE property to decide whether a role can make
CREATECREATEROLE users, and then it just recurses indefinitely from
there. Similarly for CREATEDB. Also, what if anything should you
restrict about how the new INHERITCREATEDROLES and SETCREATEDROLES
properties should be set? You could argue that they ought to be
superuser-only (because the implicit grant is performed by the
bootstrap superuser) or that it's fine for them to be set by a
CREATEROLE user with ADMIN OPTION (because it's not all that
security-critical how they get set) or maybe even that a user ought to
be able to set those properties on his or her own role.
I'm not very certain about any of that stuff; I don't have a clear
mental model of how it should work, or even what exact problem we're
trying to solve. To me, the patches that I posted make sense as far as
they go, but I'm not under the illusion that they solve all the
problems in this area, or even that I understand what all of the
problems are.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Nov 23, 2022, at 12:04 PM, Robert Haas <robertmhaas@gmail.com> wrote:
But if that's the case, did I misunderstand upthread that these are properties the superuser specifies about Alice? Can Alice just set these properties about herself, so she gets the behavior she wants? I'm confused now about who controls these settings.
Because they are role-level properties, they can be set by whoever has
ADMIN OPTION on the role. That always includes every superuser, and it
never includes Alice herself (except if she's a superuser). It could
include other users depending on the system configuration. For
example, under this proposal, the superuser might create alice and set
her account to CREATEROLE, configuring the INHERITCREATEDROLES and
SETCREATEDROLES properties on Alice's account according to preference.
Then, alice might create another user, say bob, and make him
CREATEROLE as well. In such a case, either the superuser or alice
could set these properties for role bob, because alice enjoys ADMIN
OPTION on role bob.
Ok, so the critical part of this proposal is that auditing tools can tell when Alice circumvents these settings. Without that bit, the whole thing is inane. Why make Alice jump through hoops that you are explicitly allowing her to jump through? Apparently the answer is that you can point a high speed camera at the hoops.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Robert Haas <robertmhaas@gmail.com> writes:
On Wed, Nov 23, 2022 at 2:28 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:I had incorrectly imagined that if the bootstrap superuser granted
CREATEROLE to Alice with particular settings, those settings would
limit the things that Alice could do when creating role Bob,
specifically limiting how much she could administer/inherit/set role
Bob thereafter. Apparently, your proposal only configures what happens
by default, and Alice can work around that if she wants to.
Right.
Okay ...
But if that's the case, did I misunderstand upthread that these are
properties the superuser specifies about Alice? Can Alice just set
these properties about herself, so she gets the behavior she wants?
I'm confused now about who controls these settings.
Because they are role-level properties, they can be set by whoever has
ADMIN OPTION on the role. That always includes every superuser, and it
never includes Alice herself (except if she's a superuser).
That is just bizarre. Alice can do X, and she can do Y, but she
can't control a flag that says which of those happens by default?
How is that sane (disregarding the question of whether the existence
of the flag is a good idea, which I'm now even less sold on)?
regards, tom lane
On Wed, Nov 23, 2022 at 3:11 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
Ok, so the critical part of this proposal is that auditing tools can tell when Alice circumvents these settings. Without that bit, the whole thing is inane. Why make Alice jump through hoops that you are explicitly allowing her to jump through? Apparently the answer is that you can point a high speed camera at the hoops.
Well put.
Also, it's a bit like 'su', right? Typically you don't just log in as
root and do everything a root, even if you have access to root
privileges. You log in as 'mdilger' or whatever and then when you want
to exercise elevated privileges you use 'su' or 'sudo' or something.
Similarly here you can make an argument that it's a lot cleaner to
give Alice the potential to access all of these privileges than to
make her have them all the time.
But on the flip side, one big advantage of having 'alice' have the
privileges all the time is that, for example, she can probably restore
a database dump that might otherwise be restorable only with superuser
privileges. As long as she has been granted all the relevant roles
with INHERIT TRUE, SET TRUE, the kinds of locutions that pg_dump spits
out should pretty much work fine, whereas if Alice is firewalled from
the privileges of the roles she manages, that is not going to work
well at all. To me, that is a pretty huge advantage, and it's a major
reason why I initially thought that alice should just categorically,
always inherit the privileges of the roles she creates.
But having been burned^Wenlightened by previous community discussion,
I can now see both sides of the argument, which is why I am now
proposing to let people pick the behavior they happen to want.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, Nov 23, 2022 at 1:04 PM Robert Haas <robertmhaas@gmail.com> wrote:
I'm not very certain about any of that stuff; I don't have a clear
mental model of how it should work, or even what exact problem we're
trying to solve. To me, the patches that I posted make sense as far as
they go, but I'm not under the illusion that they solve all the
problems in this area, or even that I understand what all of the
problems are.
I haven't yet formed a complete thought here but is there any reason we
cannot convert the permission-like attributes to predefined roles?
pg_login
pg_replication
pg_bypassrls
pg_createdb
pg_createrole
pg_haspassword (password and valid until)
pg_hasconnlimit
Presently, attributes are never inherited, but having that be controlled
via the INHERIT property of the grant seems desirable.
WITH ADMIN controls passing on of membership to other roles.
Example:
I have pg_createrole (set, noinherit, no with admin), pg_password (no set,
inherit, no with admin), and pg_createdb (set, inherit, with admin),
pg_login (no set, inherit, with admin)
Roles I create cannot be members of pg_createrole or pg_password but can be
given pg_createdb and pg_login (this would be a way to enforce external
authentication for roles created by me)
I can execute CREATE DATABASE due to inheriting pg_createdb
I must set role to pg_createrole in order to execute CREATE ROLE
Since I don't have admin on pg_createrole I cannot change my own
set/inherit, but I could do that for pg_createdb
David J.
On Wed, Nov 23, 2022 at 3:33 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Because they are role-level properties, they can be set by whoever has
ADMIN OPTION on the role. That always includes every superuser, and it
never includes Alice herself (except if she's a superuser).That is just bizarre. Alice can do X, and she can do Y, but she
can't control a flag that says which of those happens by default?
How is that sane (disregarding the question of whether the existence
of the flag is a good idea, which I'm now even less sold on)?
Look, I admitted later in that same email that I don't really know
what the rules for setting role-level properties ought to be. If you
have an idea, I'd love to hear it, but I'd rather if you didn't just
label things into which I have put quite a bit of work as insane
without giving any constructive feedback, especially if you haven't
yet fully understood the proposal.
Your description of the behavior here is not quite accurate.
Regardless of how the flags are set, alice, as a CREATEROLE user, can
gain access to all the privileges of the target role, and she can
arrange to have a grant of permissions on that role with INHERIT TRUE
and SET TRUE. However, there's a difference between the case where (a)
INHERITCREATEDROLE and SETCREATEDROLE are set, and alice gets the
permissions of the role by default and the one where (b)
NOINHERITCREATEDROLE and NOSETCREATEDROLE are set, and therefore alice
gets the permissions only if she does GRANT created_role TO ALICE WITH
INHERIT TRUE, SET TRUE. In the former case, there is only one grant,
and it has grantor=bootstrap_superuser/admin_option=true/inherit_option=true/set_option=true.
In the latter case there are two, one with
grantor=bootstrap_supeuser/admin_option=true/set_option=false/inherit_option=false
and a second with
grantor=alice/admin_option=false/set_option=true/inherit_option=true.
That is pretty nearly equivalent, but it is not the same, and it will
not, for example, be dumped in the same way. Furthermore, it's not
equivalent in the other direction at all. If the superuser gives alice
INHERITCREATEDROLES and SETCREATEDROLES, she can't renounce those
permissions in the patch as written. All of which is to say that I
don't think your characterization of this as "Alice can do X, and she
can do Y, but she can't control a flag that says which of those
happens by default?" is really correct. It's subtler than that.
But having said that, I could certainly change the patches so that any
user, or maybe just a createrole user since it's otherwise irrelevant,
can flip the INHERITCREATEDROLE and SETCREATEDROLE bits on their own
account. There would be no harm in that from a security or auditing
perspective AFAICS. It would, however, make the handling of those
flags different from the handling of most other role-level properties.
That is in fact why things ended up the way that they did: I just made
the new role-level properties which I added work like most of the
existing ones. I don't think that's insane at all. I even think it
might be the right decision. But it's certainly arguable. If you think
it should work differently, make an argument for that. What I would
particularly like to hear in such an argument, though, is a theory
that goes beyond those two particular properties and addresses what
ought to be done with all the other ones, especially CREATEDB and
CREATEROLE. If we can't come up with such a grand unifying theory but
are confident we know what to do about this case, so be it, but we
shouldn't make an idiosyncratic rule for this case without at least
thinking about the overall picture.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, Nov 23, 2022 at 3:59 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I haven't yet formed a complete thought here but is there any reason we cannot convert the permission-like attributes to predefined roles?
pg_login
pg_replication
pg_bypassrls
pg_createdb
pg_createrole
pg_haspassword (password and valid until)
pg_hasconnlimitPresently, attributes are never inherited, but having that be controlled via the INHERIT property of the grant seems desirable.
I think that something like this might be possible, but I'm not
convinced that it's a good idea. I've always felt that the role-level
properties seemed kind of like warts, but in studying these issues
recently, I've come to the conclusion that in some ways that's just a
visual impression. The syntax LOOKS outdated and clunky, whereas
granting someone a predefined role feels clean and modern. But the
reality is that the predefined roles system is full of really
unpleasant warts. For example, in talking through the now-committed
patch to allow control over SET ROLE, we had some fairly extensive
discussion of the fact that there was previously no way to avoid
having a user who has been granted the pg_read_all_stats predefined
role to create objects owned by pg_read_all_stats, or to alter
existing objects. That's really pretty grotty. We now have a way to
prevent that, but perhaps we should have something even better. I'm
also not really sure that's the only problem here, but maybe it is.
Either way, I'm not quite sure what the benefit of converting these
things to predefined roles is. I think actually the strongest argument
would be to do this for the superuser property! Make the bootstrap
superuser the only real superuser, and anyone else who wants to be a
superuser has to inherit that from that role. It's really unclear to
me why inheriting a lot of permissions is allowable, but inheriting
all of them is not allowable. Doing it for something like
pg_hasconnlimit seems pretty unappealing by contrast, because that's
an integer-valued property, not a Boolean, and it's not at all clear
to me why that should be inherited or what the semantics ought to be.
Really, I'm not that tempted to try to rejigger this kind of stuff
around because it seems like a lot of work for not a whole lot of
benefit. I think there's a perfectly reasonable case for setting some
things on a per-role basis that are actually per-role and not
inherited. A password is a fine example of that. You should never
inherit someone else's password. Whether we've chosen the right set of
things to treat as per-role properties rather than predefined roles is
very much debatable, though, as are a number of other aspects of the
role system.
For instance, I'm pretty well unconvinced that merging users and
groups into a uniformed thing called roles was a good idea. I think it
makes all of this machinery a LOT harder to understand, which may be
part of the reason why this area doesn't seem to have had much TLC in
quite a long time. But I think it's too late to revisit that decision,
and I also think it's too late to revisit the question of having
predefined roles at all. For better or for worse, that's what we did,
and what remains now is to find a way to make the best of it in light
of those decisions.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
But having said that, I could certainly change the patches so that any
user, or maybe just a createrole user since it's otherwise irrelevant,
can flip the INHERITCREATEDROLE and SETCREATEDROLE bits on their own
account. There would be no harm in that from a security or auditing
perspective AFAICS. It would, however, make the handling of those
flags different from the handling of most other role-level properties.
That is in fact why things ended up the way that they did: I just made
the new role-level properties which I added work like most of the
existing ones.
To be clear, I'm not saying that I know a better answer. But the fact
that these end up so different from other role-property bits seems to
me to suggest that making them role-property bits is the wrong thing.
They aren't privileges in any usual sense of the word --- if they
were, allowing Alice to flip her own bits would obviously be silly.
But all the existing role-property bits, with the exception of
rolinherit, certainly are in the nature of privileges.
What I would
particularly like to hear in such an argument, though, is a theory
that goes beyond those two particular properties and addresses what
ought to be done with all the other ones, especially CREATEDB and
CREATEROLE.
CREATEDB and CREATEROLE don't particularly bother me. We've talked before
about replacing them with memberships in predefined roles, and that would
be fine. But the reason nobody's got around to that (IMNSHO) is that it
won't really add much. The thing that I think is a big wart is
rolinherit. I don't know quite what to do about it. But these two new
proposed bits seem to be much the same kind of wart, so I'd rather not
invent them, at least not in the form of role properties.
regards, tom lane
On Wed, Nov 23, 2022 at 2:01 PM Robert Haas <robertmhaas@gmail.com> wrote:
In the latter case there are two, one with
grantor=bootstrap_supeuser/admin_option=true/set_option=false/inherit_option=false
and a second with
grantor=alice/admin_option=false/set_option=true/inherit_option=true.
This, IMO, is preferable. And I'd probably typically want to hide the
first grant from the user in typical cases - it is an implementation detail.
We have to grant the creating role membership in the created role, with
admin option, as a form of bookkeeping.
If the creating role really wants to be a member of the created role for
other reasons that should be done explicitly and granted by the creating
role.
This patch series need not be concerned about how easy or difficult it is
to get the additional grant entry into the database. The ability to refine
the permissions in the data model is there so there should be no complaints
that "it is impossible to set up this combination of permissions". We've
provided a detailed model and commands to alter it - the users can build
their scripts to glue those things together.
David J.
On Wed, Nov 23, 2022 at 2:18 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Wed, Nov 23, 2022 at 3:59 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:I haven't yet formed a complete thought here but is there any reason we
cannot convert the permission-like attributes to predefined roles?
pg_login
pg_replication
pg_bypassrls
pg_createdb
pg_createrole
pg_haspassword (password and valid until)
pg_hasconnlimitPresently, attributes are never inherited, but having that be controlled
via the INHERIT property of the grant seems desirable.
I think that something like this might be possible, but I'm not
convinced that it's a good idea.
Either way, I'm not quite sure what the benefit of converting these
things to predefined roles is.
Specifically, you gain inheritance/set and "admin option" for free. So
whether I have an ability and whether I can grant it are separate concerns.
A password is a fine example of that. You should never
inherit someone else's password. Whether we've chosen the right set of
things to treat as per-role properties rather than predefined roles is
very much debatable, though, as are a number of other aspects of the
role system.
You aren't inheriting a specific password, you are inheriting the right to
have a password stored in the database, with an optional expiration date.
For instance, I'm pretty well unconvinced that merging users and
groups into a uniformed thing called roles was a good idea.
I agree. No one was interested in the, admittedly complex, psql queries I
wrote the other month but I decided to undo some of that decision there.
David J.
"David G. Johnston" <david.g.johnston@gmail.com> writes:
On Wed, Nov 23, 2022 at 2:18 PM Robert Haas <robertmhaas@gmail.com> wrote:
Either way, I'm not quite sure what the benefit of converting these
things to predefined roles is.
Specifically, you gain inheritance/set and "admin option" for free.
Right: the practical issue with CREATEROLE/CREATEDB is that you need
some mechanism for managing who can grant those privileges. The
current answer isn't very flexible, which has been complained of
repeatedly. If they become predefined roles then we get a lot of
already-built-out infrastructure to solve that, instead of having to
write even more single-purpose logic. I think it's a sensible future
path, but said lack of flexibility hasn't yet spurred anyone to do it.
regards, tom lane
On Wed, Nov 23, 2022 at 4:18 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
To be clear, I'm not saying that I know a better answer. But the fact
that these end up so different from other role-property bits seems to
me to suggest that making them role-property bits is the wrong thing.
They aren't privileges in any usual sense of the word --- if they
were, allowing Alice to flip her own bits would obviously be silly.
But all the existing role-property bits, with the exception of
rolinherit, certainly are in the nature of privileges.
I think that's somewhat true, but I don't completely agree. I don't
think that INHERIT, LOGIN, CONNECTION LIMIT, PASSWORD, or VALID UNTIL
are privileges either. I think they're just properties. I would put
these in the same category: properties, not privileges. I think that
SUPERUSER, CREATEDB, CREATEROLE, REPLICATION, and BYPASSRLS are
privileges.
CREATEDB and CREATEROLE don't particularly bother me. We've talked before
about replacing them with memberships in predefined roles, and that would
be fine. But the reason nobody's got around to that (IMNSHO) is that it
won't really add much.
I agree, although I'm not sure that means that we don't need to do
anything about them as we evolve the system.
The thing that I think is a big wart is
rolinherit. I don't know quite what to do about it.
One option is to nuke it from orbit. Now that you can set that
property on a per-grant basis, the per-role basis serves only to set
the default. I think that's of dubious value, and arguably backwards,
because ISTM that in a lot of cases whether you want a role grant to
be inherited will depend on the nature of the role being granted
rather than the role to which it is being granted. The setting we have
works the other way around, and I can never keep in my head what the
use case for that is. I think there must be one, though, because Peter
Eisentraut seemed to like having it around. I don't understand why,
but I respect Peter. :-)
But these two new
proposed bits seem to be much the same kind of wart, so I'd rather not
invent them, at least not in the form of role properties.
I have to admit that when I realized that was the natural place to put
them to make the patch work, my first reaction internally was "well,
that can't possibly be right, role properties suck!". But I didn't and
still don't see where else to put them that makes any sense at all, so
I eventually decided that my initial reaction was misguided. So I
can't really blame you for not liking it either, and would be happy if
we could come up with something else that feels better. I just don't
know what it is: at least as of this moment in time, I believe these
naturally ARE properties of the role, and therefore I'm inclined to
view my initial reluctance to implement it that way as a reflex rather
than a well-considered opinion. That is, the CREATE ROLE syntax is
clunky, and it controls some things that are properties and others
that are permissions, but they're not inherited like regular
permissions, so it stinks, and thus adding things to it also feels
stinky. But if the existing command weren't such a mess I'm not sure
adding this stuff to it would feel bad either.
That might be the wrong view. As I say, I'm open to other ideas, and
it's possible there's some nicer way to do it that I just don't see
right now.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas:
I have to admit that when I realized that was the natural place to put
them to make the patch work, my first reaction internally was "well,
that can't possibly be right, role properties suck!". But I didn't and
still don't see where else to put them that makes any sense at all, so
I eventually decided that my initial reaction was misguided. So I
can't really blame you for not liking it either, and would be happy if
we could come up with something else that feels better. I just don't
know what it is: at least as of this moment in time, I believe these
naturally ARE properties of the role [...]That might be the wrong view. As I say, I'm open to other ideas, and
it's possible there's some nicer way to do it that I just don't see
right now.
INHERITCREATEDROLES and SETCREATEDROLES behave much like DEFAULT
PRIVILEGES. What about something like:
ALTER DEFAULT PRIVILEGES FOR alice
GRANT TO alice WITH INHERIT FALSE, SET TRUE, ADMIN TRUE
The "abbreviated grant" is very much abbreviated, because the original
syntax GRANT a TO b is already quite short to begin with, i.e. there is
no ON ROLE or something like that in it.
The initial DEFAULT privilege would be INHERIT FALSE, SET FALSE, ADMIN
TRUE, I guess?
Best,
Wolfgang
On Thu, Nov 24, 2022 at 2:41 AM <walther@technowledgy.de> wrote:
INHERITCREATEDROLES and SETCREATEDROLES behave much like DEFAULT
PRIVILEGES. What about something like:ALTER DEFAULT PRIVILEGES FOR alice
GRANT TO alice WITH INHERIT FALSE, SET TRUE, ADMIN TRUEThe "abbreviated grant" is very much abbreviated, because the original
syntax GRANT a TO b is already quite short to begin with, i.e. there is
no ON ROLE or something like that in it.The initial DEFAULT privilege would be INHERIT FALSE, SET FALSE, ADMIN
TRUE, I guess?
I don't know if changing the syntax from A to B is really getting us
anywhere. I generally agree that the ALTER DEFAULT PRIVILEGES syntax
looks nicer than the CREATE/ALTER ROLE syntax, but I'm not sure that's
a sufficient reason to move the control over this behavior to ALTER
DEFAULT PRIVILEGES. One thing to consider is that, as I've designed
this, whether or not ADMIN is included in the grant is non-negotiable.
I am, at least at present, inclined to think that was the right call,
partly because Mark Dilger expressed a lot of concern about the
CREATEROLE user losing control over the role they'd just created, and
allowing ADMIN to be turned off would have exactly that effect. Plus a
grant with INHERIT FALSE, SET FALSE, ADMIN FALSE would end up being
almost identical to no great at all, which seems pointless. Basically,
without ADMIN, the implicit GRANT fails to accomplish its intended
purpose, so I don't like having that as a possibility.
The other thing that's a little weird about the syntax which you
propose is that it's not obviously related to CREATE ROLE. The intent
of the patch as implemented is to allow control over only the implicit
GRANT that is created when a new role is created, not all grants that
might be created by or to a particular user. Setting defaults for all
grants doesn't seem like a particularly good idea to me, but it's
definitely a different idea than what the patch proposes to do.
I did spend some time thinking about trying to tie this to the
CREATEROLE syntax itself. For example, instead of CREATE ROLE alice
CREATEROLE INHERITCREATEDROLES SETCREATEDROLES you could write CREATE
ROLE alice CREATEROLE WITH (INHERIT TRUE, SET TRUE) or something like
this. That would avoid introducing new, lengthy keywords that are just
concatenations of other English words, a kind of syntax that doesn't
look particularly nice to me and probably is less friendly to
non-English speakers as well. I didn't do it that way because the
parser support would be more complex, but I could. CREATEROLE would
have to become a keyword again, but that's not a catastrophe.
Another idea would be to break the CREATEROLE stuff off from CREATE
ROLE entirely and put it all into GRANT. You could imagine syntax like
GRANT CREATEROLE (or CREATE ROLE?) TO role_specification WITH (INHERIT
TRUE/FALSE, SET TRUE/FALSE). There are a few potential issues with
this direction. One, if we did this, then CREATEROLE probably ought to
become inheritable, because that's the way grants work in general, and
this likely shouldn't be an exception, but this would be a behavior
change. However, if it is the consensus that such a behavior change
would be an improvement, that might be OK. Two, I wonder what we'd do
about the GRANTED BY role_specification clause. We could leave it out,
but that would be asymmetric with other GRANT commands. We could also
support it and record that information and make this work more like
other cases, including, I suppose, the possibility of dependent
grants. We'd have to think about what that means exactly. If you
revoke CREATEROLE from someone who has granted CREATEROLE to others, I
suppose that's a clear dependent grant and needs to be recursively
revoked. But what about the implicit grants that were created because
the person had CREATEROLE? Are those also dependent grants? And what
about the roles themselves? Should revoking CREATEROLE drop the roles
that the user in question created? That gets complicated, because
those roles might own objects. That's scary, because you might not
expect revoking a role permission to result in tables getting dropped.
It's also problematic, because those tables might be in some other
database where they are inaccessible to the current session. All in
all I'm inclined to think that recursing to the roles themselves is a
bad plan, but it's debatable.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas:
I don't know if changing the syntax from A to B is really getting us
anywhere. I generally agree that the ALTER DEFAULT PRIVILEGES syntax
looks nicer than the CREATE/ALTER ROLE syntax, but I'm not sure that's
a sufficient reason to move the control over this behavior to ALTER
DEFAULT PRIVILEGES.
The list of role attributes can currently be roughly divided into the
following categories:
- Settings with role-specific values: CONNECTION LIMIT, PASSWORD, VALID
UNTIL. It's hard to imagine storing them anywhere else, because they
need to have a different value for each role. Those are not just "flags"
like all the other attributes.
- Two special attributes in INHERIT and BYPASSRLS regarding
security/privileges. Those were invented because there was no other
syntax to do the same thing. Those could be interpreted as privileges to
do something, too - but lacking the ability to do that explicit. There
is no SET BYPASSRLS ON/OFF or SET INHERIT ON/OFF. Of course the INHERIT
case is now a bit different, because there is the inherit grant option
you introduced.
- Cluster-wide privileges: SUPERUSER, CREATEDB, CREATEROLE, LOGIN,
REPLICATION. Those can't be granted on some kind of object, because
there is no such global object. You could imagine inventing some kind of
global CLUSTER object and do something like GRANT SUPERUSER ON CLUSTER
TO alice; instead. Turning those into role attributes was the choice
made instead. Most likely it would have been only a syntactic difference
anyway: Even if there was something like GRANT .. ON CLUSTER, you'd most
likely implement that as... storing those grants as role attributes.
Your patch is introducing a new category of role attributes - those that
are affecting default behavior. But there is already a way to express
this right now, and that's ALTER DEFAULT PRIVILEGES in this case. Imho,
the question asked should not be "why change from syntax A to B?" but
rather: Why introduce a new category of role attributes, when there is a
way to express the same concept already? I can't see any compelling
reason for that, yet.
Or not "yet", but rather "anymore". When I understood and remember
correctly, you implemented it in a way that a user could not change
those new attributes on their own role. This is in fact different to how
ALTER DEFAULT PRIVILEGES works, so you could have made an argument that
this was better expressed as role attributes. But I think this was asked
and agreed on to act differently, so that the user can change this
default behavior of what happens when they create a role for themselves.
And now this reason is gone - there is no reason NOT to implement it as
DEFAULT PRIVILEGES.
One thing to consider is that, as I've designed
this, whether or not ADMIN is included in the grant is non-negotiable.
I am, at least at present, inclined to think that was the right call,
partly because Mark Dilger expressed a lot of concern about the
CREATEROLE user losing control over the role they'd just created, and
allowing ADMIN to be turned off would have exactly that effect. Plus a
grant with INHERIT FALSE, SET FALSE, ADMIN FALSE would end up being
almost identical to no great at all, which seems pointless. Basically,
without ADMIN, the implicit GRANT fails to accomplish its intended
purpose, so I don't like having that as a possibility.
With how you implemented it right now, is it possible to do the following?
CREATE ROLE alice;
REVOKE ADMIN OPTION FOR alice FROM CURRENT_USER;
If the answer is yes, then there is no reason to allow a user to set a
shortcut for SET and INHERIT, but not for ADMIN.
If the answer is no, then you could just not allow specifying the ADMIN
option in the ALTER DEFAULT PRIVILEGES statement and always force it to
be TRUE.
The other thing that's a little weird about the syntax which you
propose is that it's not obviously related to CREATE ROLE. The intent
of the patch as implemented is to allow control over only the implicit
GRANT that is created when a new role is created, not all grants that
might be created by or to a particular user. Setting defaults for all
grants doesn't seem like a particularly good idea to me, but it's
definitely a different idea than what the patch proposes to do.
Before I proposed that I was confused for a moment about this, too - but
it turns out to be wrong. ALTER DEFAULT PRIVILEGES in general works as:
When object A is created, issue a GRANT ON A automatically.
In my proposal, the "object" is not the GRANT of that role. It's the
role itself. So the default privileges express what should happen when
the role is created. The default privileges would NOT affect a regular
GRANT role TO role_spec command. They only run that command when a role
is created.
I did spend some time thinking about trying to tie this to the
CREATEROLE syntax itself. For example, instead of CREATE ROLE alice
CREATEROLE INHERITCREATEDROLES SETCREATEDROLES you could write CREATE
ROLE alice CREATEROLE WITH (INHERIT TRUE, SET TRUE) or something like
this. That would avoid introducing new, lengthy keywords that are just
concatenations of other English words, a kind of syntax that doesn't
look particularly nice to me and probably is less friendly to
non-English speakers as well. I didn't do it that way because the
parser support would be more complex, but I could. CREATEROLE would
have to become a keyword again, but that's not a catastrophe.
I agree, this would not have been any better.
Another idea would be to break the CREATEROLE stuff off from CREATE
ROLE entirely and put it all into GRANT. You could imagine syntax like
GRANT CREATEROLE (or CREATE ROLE?) TO role_specification WITH (INHERIT
TRUE/FALSE, SET TRUE/FALSE). There are a few potential issues with
this direction. One, if we did this, then CREATEROLE probably ought to
become inheritable, because that's the way grants work in general, and
this likely shouldn't be an exception, but this would be a behavior
change. However, if it is the consensus that such a behavior change
would be an improvement, that might be OK. Two, I wonder what we'd do
about the GRANTED BY role_specification clause. We could leave it out,
but that would be asymmetric with other GRANT commands. We could also
support it and record that information and make this work more like
other cases, including, I suppose, the possibility of dependent
grants. We'd have to think about what that means exactly. If you
revoke CREATEROLE from someone who has granted CREATEROLE to others, I
suppose that's a clear dependent grant and needs to be recursively
revoked. But what about the implicit grants that were created because
the person had CREATEROLE? Are those also dependent grants? And what
about the roles themselves? Should revoking CREATEROLE drop the roles
that the user in question created? That gets complicated, because
those roles might own objects. That's scary, because you might not
expect revoking a role permission to result in tables getting dropped.
It's also problematic, because those tables might be in some other
database where they are inaccessible to the current session. All in
all I'm inclined to think that recursing to the roles themselves is a
bad plan, but it's debatable.
I'm not sure how that relates to the role attributes vs. default
privileges discussion. Those seem to be orthogonal to the question of
how to treat the CREATEROLE privilege itself. Right now, it's a role
attribute. I proposed "database roles" and making CREATEROLE a privilege
on the database level. David Johnston proposed to use a pg_createrole
built-in role instead. Your proposal here is to invent a CREATEROLE
privilege that can be granted, which is very similar to what I wrote
above about "GRANT CREATEROLE ON CLUSTER". Side note: Without the ON
CLUSTER, there'd be no target object in your GRANT statement and as such
CREATEROLE should be treated as a role name - so I'm not sure your
proposal actually works. In any case: All those proposals change the
semantics of how this whole CREATEROLE "privilege" works in terms of
inheritance etc. However, those proposals all don't really change the
way you'll want to treat the ADMIN option on the role, I think, and can
all be made to create that implicit GRANT WITH ADMIN, when you create
the role. And once you do that, the question of how that GRANT looks by
default comes up - so in all those scenarios, we could talk about role
attributes vs. default privileges. Or we could just decide not to,
because is it really that hard to just issue a GRANT statement
immediately after CREATE ROLE, when you want to have SET or INHERIT
options on that role?
The answer to that question was "yes it is too hard" a while back and as
such DEFAULT PRIVILEGES were introduced.
Best,
Wolfgang
On Mon, Nov 28, 2022 at 11:57 AM <walther@technowledgy.de> wrote:
Robert Haas:
I don't know if changing the syntax from A to B is really getting us
anywhere. I generally agree that the ALTER DEFAULT PRIVILEGES syntax
looks nicer than the CREATE/ALTER ROLE syntax, but I'm not sure that's
a sufficient reason to move the control over this behavior to ALTER
DEFAULT PRIVILEGES.Your patch is introducing a new category of role attributes - those that
are affecting default behavior. But there is already a way to express
this right now, and that's ALTER DEFAULT PRIVILEGES in this case.
I do not like ALTER DEFAULT PRIVILEGES (ADP) for this. I don't really like
defaults, period, for this.
The role doing the creation and the role being created are both in scope
when the command is executed and if anything it is the role doing to the
creation that is receiving the privileges not the role being created. For
ADP, the role being created gets the privileges and it is objects not in
the scope of the executed command that are being affected.
One thing to consider is that, as I've designed
this, whether or not ADMIN is included in the grant is non-negotiable.
I am, at least at present, inclined to think that was the right call,
partly because Mark Dilger expressed a lot of concern about the
CREATEROLE user losing control over the role they'd just created, and
allowing ADMIN to be turned off would have exactly that effect. Plus a
grant with INHERIT FALSE, SET FALSE, ADMIN FALSE would end up being
almost identical to no great at all, which seems pointless. Basically,
without ADMIN, the implicit GRANT fails to accomplish its intended
purpose, so I don't like having that as a possibility.With how you implemented it right now, is it possible to do the following?
CREATE ROLE alice;
REVOKE ADMIN OPTION FOR alice FROM CURRENT_USER;If the answer is yes, then there is no reason to allow a user to set a
shortcut for SET and INHERIT, but not for ADMIN.If the answer is no, then you could just not allow specifying the ADMIN
option in the ALTER DEFAULT PRIVILEGES statement and always force it to
be TRUE.
A prior email described that the creation of a role by a CREATEROLE role
results in the necessary creation of an ADMIN grant from the creator to the
new role granted by the bootstrap superuser (or, possibly, whichever
superuser granted CREATEROLE). That REVOKE will not work as there would be
no existing "grant by current_user over alice granted by current_user"
immediately after current_user creates alice.
Or we could just decide not to,
because is it really that hard to just issue a GRANT statement
immediately after CREATE ROLE, when you want to have SET or INHERIT
options on that role?The answer to that question was "yes it is too hard" a while back and as
such DEFAULT PRIVILEGES were introduced.
A quick tally of the thread so far:
No Defaults needed: David J., Mark?, Tom?
Defaults needed - attached to role directly: Robert
Defaults needed - defined within Default Privileges: Walther?
The capability itself seems orthogonal to the rest of the patch to track
these details better. I think we can "Fix CREATEROLE" without any feature
regarding optional default behaviors and would suggest this patch be so
limited and that another thread be started for discussion of (assuming a
default specifying mechanism is wanted overall) how it should look. Let's
not let a usability debate distract us from fixing a real problem.
David J.
On Mon, Nov 28, 2022 at 1:56 PM <walther@technowledgy.de> wrote:
And now this reason is gone - there is no reason NOT to implement it as
DEFAULT PRIVILEGES.
I think there is, and it's this, which you wrote further down:
In my proposal, the "object" is not the GRANT of that role. It's the
role itself. So the default privileges express what should happen when
the role is created. The default privileges would NOT affect a regular
GRANT role TO role_spec command. They only run that command when a role
is created.
I agree that this is what you are proposing, but it is not what your
proposed syntax says. Your proposed syntax simply says ALTER DEFAULT
PRIVILEGES .. GRANT. Users who read that are going to think it
controls the default behavior for all grants, because that's what the
syntax says. If the proposed syntax mentioned CREATE ROLE someplace,
maybe that would have some potential. A proposal to make a command
that controls CREATE ROLE and only CREATE ROLE and mentions neither
CREATE nor ROLE anywhere in the syntax is never going to be
acceptable.
With how you implemented it right now, is it possible to do the following?
CREATE ROLE alice;
REVOKE ADMIN OPTION FOR alice FROM CURRENT_USER;If the answer is yes, then there is no reason to allow a user to set a
shortcut for SET and INHERIT, but not for ADMIN.If the answer is no, then you could just not allow specifying the ADMIN
option in the ALTER DEFAULT PRIVILEGES statement and always force it to
be TRUE.
It's no. Well, OK, you can do it, but it doesn't revoke anything,
because you can only revoke your own grant, not the bootstrap
superuser's grant.
attributes vs. default privileges. Or we could just decide not to,
because is it really that hard to just issue a GRANT statement
immediately after CREATE ROLE, when you want to have SET or INHERIT
options on that role?
It's not difficult in the sense that climbing Mount Everest is
difficult, but it makes the user experience as a CREATEROLE
non-superuser quite noticeably different from being a superuser.
Having a way to paper over such differences is, in my opinion, an
important usability feature.
--
Robert Haas
EDB: http://www.enterprisedb.com
David G. Johnston:
A quick tally of the thread so far:
No Defaults needed: David J., Mark?, Tom?
Defaults needed - attached to role directly: Robert
Defaults needed - defined within Default Privileges: Walther?
s/Walther/Wolfgang
The capability itself seems orthogonal to the rest of the patch to track
these details better. I think we can "Fix CREATEROLE" without any
feature regarding optional default behaviors and would suggest this
patch be so limited and that another thread be started for discussion of
(assuming a default specifying mechanism is wanted overall) how it
should look. Let's not let a usability debate distract us from fixing a
real problem.
+1
I didn't argue for whether defaults are needed in this case or not. I
just said that ADP is better for defaults than role attributes are. Or
the other way around: I think role attributes are not a good way to
express those.
Personally, I'm in the No Defaults needed camp, too.
Best,
Wolfgang
On Mon, Nov 28, 2022 at 12:42 PM <walther@technowledgy.de> wrote:
David G. Johnston:
A quick tally of the thread so far:
No Defaults needed: David J., Mark?, Tom?
Defaults needed - attached to role directly: Robert
Defaults needed - defined within Default Privileges: Walther?s/Walther/Wolfgang
Sorry 'bout that, I was just reading the To: line in my email reply.
Personally, I'm in the No Defaults needed camp, too.
I kinda thought so from your final comments, thanks for clarifying.
David J.
Robert Haas:
In my proposal, the "object" is not the GRANT of that role. It's the
role itself. So the default privileges express what should happen when
the role is created. The default privileges would NOT affect a regular
GRANT role TO role_spec command. They only run that command when a role
is created.I agree that this is what you are proposing, but it is not what your
proposed syntax says. Your proposed syntax simply says ALTER DEFAULT
PRIVILEGES .. GRANT. Users who read that are going to think it
controls the default behavior for all grants, because that's what the
syntax says. If the proposed syntax mentioned CREATE ROLE someplace,
maybe that would have some potential. A proposal to make a command
that controls CREATE ROLE and only CREATE ROLE and mentions neither
CREATE nor ROLE anywhere in the syntax is never going to be
acceptable.
Yes, I agree - the abbreviated GRANT syntax is confusing/misleading in
that case. Consistent with the other syntaxes, but easily confused
nonetheless.
It's no. Well, OK, you can do it, but it doesn't revoke anything,
because you can only revoke your own grant, not the bootstrap
superuser's grant.
Ah, I see. I didn't get that difference regarding the bootstrap
superuser, so far.
So in that sense, the ADP GRANT would be an additional GRANT issued by
the user that created the role in addition to the bootstrap superuser's
grant. You can't revoke the bootstrap superuser's grant - but you can't
modify it either. And there is no need to add SET or INHERIT to the
boostrap superuser's grant, because you can grant the role yourself
again, with those options.
I think it would be very strange to have a default for that bootstrap
superuser's grant. Or rather: A different default than the minimum
required - and that's just ADMIN, not SET, not INHERIT. When you have
the minimum, you can always choose to grant SET and INHERIT later on
yourself - and revoke it, too! But when the SET and INHERIT are on the
boostrap superuser's grant - then there is no way for you to revoke SET
or INHERIT on that grant anymore later.
Why should the superuser, who gave you CREATEROLE, insist on you having
SET or INHERIT forever and disallow to revoke it from yourself?
Best,
Wolfgang
On Nov 28, 2022, at 11:34 AM, David G. Johnston <david.g.johnston@gmail.com> wrote:
No Defaults needed: David J., Mark?, Tom?
As Robert has the patch organized, I think defaults are needed, but I see that as a strike against the patch.
Defaults needed - attached to role directly: Robert
Defaults needed - defined within Default Privileges: Walther?
The capability itself seems orthogonal to the rest of the patch to track these details better. I think we can "Fix CREATEROLE" without any feature regarding optional default behaviors and would suggest this patch be so limited and that another thread be started for discussion of (assuming a default specifying mechanism is wanted overall) how it should look. Let's not let a usability debate distract us from fixing a real problem.
In Robert's initial email, he wrote, "It seems to me that the root of any fix in this area must be to change the rule that CREATEROLE can administer any role whatsoever."
The obvious way to fix that is to revoke that rule and instead automatically grant ADMIN OPTION to a creator over any role they create. That's problematic, though, because as things stand, ADMIN OPTION is granted to somebody by granting them membership in the administered role WITH ADMIN OPTION, so membership in the role and administration of the role are conflated.
Robert's patch tries to deal with the (possibly unwanted) role membership by setting up defaults to mitigate the effects, but that is more confusing to me than just de-conflating role membership from role administration, and giving role creators administration over roles they create, without in so doing giving them role membership. I don't recall enough details about how hard it is to de-conflate role membership from role administration, and maybe that's a non-starter for reasons I don't recall at the moment. I expect Robert has already contemplated that idea and instead proposed this patch for good reasons. Robert?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Mark Dilger:
Robert's patch tries to deal with the (possibly unwanted) role membership by setting up defaults to mitigate the effects, but that is more confusing to me than just de-conflating role membership from role administration, and giving role creators administration over roles they create, without in so doing giving them role membership. I don't recall enough details about how hard it is to de-conflate role membership from role administration, and maybe that's a non-starter for reasons I don't recall at the moment.
Isn't this just GRANT .. WITH SET FALSE, INHERIT FALSE, ADMIN TRUE? That
should allow role administration, without actually granting membership
in that role, yet, right?
Best,
Wolfgang
On Mon, Nov 28, 2022 at 3:02 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
Robert's patch tries to deal with the (possibly unwanted) role membership by setting up defaults to mitigate the effects, but that is more confusing to me than just de-conflating role membership from role administration, and giving role creators administration over roles they create, without in so doing giving them role membership. I don't recall enough details about how hard it is to de-conflate role membership from role administration, and maybe that's a non-starter for reasons I don't recall at the moment. I expect Robert has already contemplated that idea and instead proposed this patch for good reasons. Robert?
"De-conflating role membership from role administration" isn't really
a specific proposal that someone can go out and implement. You have to
make some decision about *how* you are going to separate those
concepts. And that's what I did: I made INHERIT and SET into
grant-level options. That allows you to give someone access to the
privileges of a role without the ability to administer it (at least
one of INHERIT and SET true, and ADMIN false) or the ability to
administer a role without having any direct access to its privileges
(INHERIT FALSE, SET FALSE, ADMIN TRUE). I don't see that we can, or
need to, separate things any more than that.
You can argue that a grant with INHERIT FALSE, SET FALSE, ADMIN TRUE
still grants membership, and I think formally that's true, but I also
think it's just picking something to bicker about. The need isn't to
separate membership per se from administration. It's to separate
privilege inheritance and the ability to SET ROLE from role
administration. And I've done that.
I strongly disagree with the idea that the ability for users to
control defaults here isn't needed. You can set a default tablespace
for your database, and a default tablespace for your session, and a
default tablespace for new partitions of an existing partition table.
You can set default privileges for every type of object you can
create, and a default search path to find objects in the database. You
can set defaults for all of your connection parameters to the database
using environment variables, and the default data directory for
commands that need one. You can set defaults for all of your psql
settings in ~/.psqlrc. You can set defaults for the character sets,
locales and collations of new databases. You can set the default
version of an extension in the control file, so that the user doesn't
have to specify a version. And so on and so on. There's absolutely
scads of things for which it is useful to be able to set defaults and
for which we give people the ability to set defaults, and I don't
think anyone is making a real argument for why that isn't also true
here. The argument that has been made is essentially that you could
get by without it, but that's true of *every* default. Yet we keep
adding the ability to set defaults for new things, and to set the
defaults for existing things in new ways, and there's a very good
reason for that: it's extremely convenient. And that's true here, too.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Nov 28, 2022, at 12:08 PM, walther@technowledgy.de wrote:
Isn't this just GRANT .. WITH SET FALSE, INHERIT FALSE, ADMIN TRUE? That should allow role administration, without actually granting membership in that role, yet, right?
Can you clarify what you mean here? Are you inventing a new syntax?
+GRANT bob TO alice WITH SET FALSE, INHERIT FALSE, ADMIN TRUE;
+ERROR: syntax error at or near "SET"
+LINE 1: GRANT bob TO alice WITH SET FALSE, INHERIT FALSE, ADMIN TRUE...
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Nov 28, 2022, at 12:33 PM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:
Isn't this just GRANT .. WITH SET FALSE, INHERIT FALSE, ADMIN TRUE? That should allow role administration, without actually granting membership in that role, yet, right?
Can you clarify what you mean here? Are you inventing a new syntax?
Nevermind. After reading Robert's email, it's clear enough what you mean here.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Mon, Nov 28, 2022 at 1:28 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Nov 28, 2022 at 3:02 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
You can argue that a grant with INHERIT FALSE, SET FALSE, ADMIN TRUE
still grants membership, and I think formally that's true, but I also
think it's just picking something to bicker about. The need isn't to
separate membership per se from administration. It's to separate
privilege inheritance and the ability to SET ROLE from role
administration. And I've done that.
We seem to now be in agreement on this design choice, and the related bit
about bootstrap superuser granting admin on newly created roles by the
createrole user.
This seems like a patch in its own right.
It still leaves open the default membership behavior as well as whether we
want to rework the attributes into predefined roles.
I strongly disagree with the idea that the ability for users to
control defaults here isn't needed.
That's fine, but are you saying this patch is incapable (or simply
undesirable) of having the parts about handling defaults separated out from
the parts that define how the system works with a given set of permissions;
and the one implementation detail of having the bootstrap superuser
automatically grant admin to any roles a createuser role creates? If you
and others feel strongly about defaults I'm sure that the suggested other
thread focused on that will get attention and be committed in a timely
manner. But the system will work, and not be broken, if that got stalled,
and it could be added in later.
David J.
On Mon, Nov 28, 2022 at 4:19 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
That's fine, but are you saying this patch is incapable (or simply undesirable) of having the parts about handling defaults separated out from the parts that define how the system works with a given set of permissions; and the one implementation detail of having the bootstrap superuser automatically grant admin to any roles a createuser role creates? If you and others feel strongly about defaults I'm sure that the suggested other thread focused on that will get attention and be committed in a timely manner. But the system will work, and not be broken, if that got stalled, and it could be added in later.
The topics are so closely intertwined that I don't believe that trying
to have separate discussions will be useful or productive. There's no
hope of anybody understanding 0004 or having an educated opinion about
it without first understanding the earlier patches, and there's no
requirement that someone has to review 0004, or like it, just because
they review or like 0001-0003.
But so far nobody has actually reviewed anything, and all that's
happened is people have complained about 0004 for reasons which in my
opinion are pretty nebulous and largely ignore the factors that caused
it to exist in the first place. We had about 400 emails during the
last release cycle arguing about a whole bunch of topics related to
user management, and it became absolutely crystal clear in that
discussion that Stephen Frost and David Steele wanted to have roles
that could create other roles but not immediately be able to access
their privileges. Mark and I, on the other hand, wanted to have roles
that could create other roles WITH immediate access to their
privileges. That argument was probably the main thing that derailed
that entire patch set, which represented months of work by Mark. Now,
I have come up with a competing patch set that for the price of 100
lines of code and a couple of slightly ugly option names can do either
thing. So Stephen and David and any like-minded users can have what
they want, and Mark and I and any like-minded users can have what we
want. And the result is that I've got like five people, some of whom
particulated in those discussions, showing up to say "hey, we don't
need the ability to set defaults." Well, if that's the case, then why
did we have hundreds and hundreds of emails within the last 12 months
arguing about which way it should work?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Nov 28, 2022 at 2:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Nov 28, 2022 at 4:19 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:That's fine, but are you saying this patch is incapable (or simply
undesirable) of having the parts about handling defaults separated out from
the parts that define how the system works with a given set of permissions;
and the one implementation detail of having the bootstrap superuser
automatically grant admin to any roles a createuser role creates? If you
and others feel strongly about defaults I'm sure that the suggested other
thread focused on that will get attention and be committed in a timely
manner. But the system will work, and not be broken, if that got stalled,
and it could be added in later.The topics are so closely intertwined that I don't believe that trying
to have separate discussions will be useful or productive. There's no
hope of anybody understanding 0004 or having an educated opinion about
it without first understanding the earlier patches, and there's no
requirement that someone has to review 0004, or like it, just because
they review or like 0001-0003.But so far nobody has actually reviewed anything
Well, if that's the case, then why
did we have hundreds and hundreds of emails within the last 12 months
arguing about which way it should work?
When ya'll come to some final conclusion on how you want the defaults to
look, come tell the rest of us. You already have 4 people debating the
matter, I don't really see the point of adding more voices to that
cachopany. As you noted - voicing an opinion about 0004 is optional.
I'll reiterate my review from before, with a bit more confidence this time.
0001-0003 implements a desirable behavior change. In order for someone to
make some other role a member in some third role that someone must have
admin privileges on both other roles. CREATEROLE is not exempt from this
rule. A user with CREATEROLE will, upon creating a new role, be granted
admin privilege on that role by the bootstrap superuser.
The consequence of 0001-0003 in the current environment is that since the
newly created CREATEROLE user will not have admin rights on any existing
roles in the cluster, while they can create new roles in the system they
are unable to grant those new roles membership in any other roles not also
created by them. The ability to assign attributes to newly created roles
is unaffected.
As a unit of work, those are "ready-to-commit" for me. I'll leave it to
you and others to judge the technical quality of the patch and finishing up
the FIXMEs that have been noted.
Desirable follow-on patches include:
1) Automatically install an additional membership grant, with the
CREATEROLE user as the grantor, specifying INHERIT OR SET as TRUE (I
personally favor attaching these to ALTER ROLE, modifiable only by oneself)
2) Convert Attributes into default roles
David J.
On Mon, Nov 28, 2022 at 4:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
But so far nobody has actually reviewed anything, ...
Actually this isn't true. Mark did review. Thanks, Mark.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Nov 28, 2022 at 6:32 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
Desirable follow-on patches include:
1) Automatically install an additional membership grant, with the CREATEROLE user as the grantor, specifying INHERIT OR SET as TRUE (I personally favor attaching these to ALTER ROLE, modifiable only by oneself)
Hmm, that's an interesting alternative to what I actually implemented.
Some people might like it better, because it puts the behavior fully
under the control of the CREATEROLE user, which a number of you seem
to favor.
I suppose if we did it that way, it could even be a GUC, like
create_role_automatic_grant_options.
--
Robert Haas
EDB: http://www.enterprisedb.com
Mark Dilger:
Isn't this just GRANT .. WITH SET FALSE, INHERIT FALSE, ADMIN TRUE? That should allow role administration, without actually granting membership in that role, yet, right?
Can you clarify what you mean here? Are you inventing a new syntax?
+GRANT bob TO alice WITH SET FALSE, INHERIT FALSE, ADMIN TRUE; +ERROR: syntax error at or near "SET" +LINE 1: GRANT bob TO alice WITH SET FALSE, INHERIT FALSE, ADMIN TRUE...
This is valid syntax on latest master.
Best,
Wolfgang
Robert Haas:
And the result is that I've got like five people, some of whom
particulated in those discussions, showing up to say "hey, we don't
need the ability to set defaults." Well, if that's the case, then why
did we have hundreds and hundreds of emails within the last 12 months
arguing about which way it should work?
For me: "Needed" as in "required". I don't think we *require* defaults
to make this useful, just as David said as well. Personally, I don't
need defaults either, at least I didn't have a use-case for it, yet. I'm
not objecting to introduce defaults, but I do object to *how* they were
introduced in your patch set, so far. It just wasn't consistent with the
other stuff that already exists.
Best,
Wolfgang
Robert Haas:
1) Automatically install an additional membership grant, with the CREATEROLE user as the grantor, specifying INHERIT OR SET as TRUE (I personally favor attaching these to ALTER ROLE, modifiable only by oneself)
Hmm, that's an interesting alternative to what I actually implemented.
Some people might like it better, because it puts the behavior fully
under the control of the CREATEROLE user, which a number of you seem
to favor.
+1
I suppose if we did it that way, it could even be a GUC, like
create_role_automatic_grant_options.
I don't think using GUCs for that is any better. ALTER DEFAULT
PRIVILEGES is the correct way to do it. The only argument against it
was, so far, that it's easy to confuse with default options for newly
created role grants, due to the abbreviated grant syntax.
I propose a slightly different syntax instead:
ALTER DEFAULT PRIVILEGES GRANT CREATED ROLE TO role_specification WITH ...;
This, together with the proposal above regarding the grantor, should be
consistent.
Is there any other argument to be made against ADP?
Note, that ADP allows much more than just creating a grant for the
CREATEROLE user, which would be the case if the default GRANT was made
TO the_create_role_user. But it could be made towards *other* users as
well, so you could do something like this:
CREATE ROLE alice CREATEROLE;
CREATE ROLE bob;
ALTER DEFAULT PRIVILEGES FOR alice GRANT CREATED ROLE TO bob WITH SET
TRUE, INHERIT FALSE;
This is much more flexible than role attributes or GUCs.
Best,
Wolfgang
On Tue, Nov 29, 2022 at 12:32 AM <walther@technowledgy.de> wrote:
Is there any other argument to be made against ADP?
These aren't privileges, they are memberships. The pg_default_acl catalog
is also per-data while these settings should be present in a catalog which,
like pg_authid, is catalog-wide. This latter point, for me, disqualifies
the command itself from being used for this purpose. If we'd like to
create ALTER DEFAULT MEMBERSHIP (and a corresponding cluster-wide catalog)
then maybe the rest of the design would work within that.
Note, that ADP allows much more than just creating a grant for the
CREATEROLE user, which would be the case if the default GRANT was made
TO the_create_role_user. But it could be made towards *other* users as
well, so you could do something like this:CREATE ROLE alice CREATEROLE;
CREATE ROLE bob;ALTER DEFAULT PRIVILEGES FOR alice GRANT CREATED ROLE TO bob WITH SET
TRUE, INHERIT FALSE;
What does that accomplish? bob cannot create roles to actually exercise
his privilege.
This is much more flexible than role attributes or GUCs.
The main advantage of GUC over a role attribute is that you can institute
layers of defaults according to a given cluster's specific needs. ALTER
ROLE SET (pg_db_role_setting - also cluster-wide) also comes into play;
maybe alice wants auto-inherit while in db-a but not db-b (this would/will
be more convincing if we end up having per-database roles).
If we accept that some external configuration knowledge is going to
influence the result of executing this command (Tom?) then it seems that
all the features a GUC provides are desirable in determining how the final
execution context is configured. Which makes sense as this kind of thing is
precisely what the GUC subsystem was designed to handle - session context
environments related to the user and database presently connected.
David J.
On Tue, Nov 29, 2022 at 2:32 AM <walther@technowledgy.de> wrote:
I propose a slightly different syntax instead:
ALTER DEFAULT PRIVILEGES GRANT CREATED ROLE TO role_specification WITH ...;
This, together with the proposal above regarding the grantor, should be
consistent.
I think that is more powerful than what I proposed but less fit for
purpose. If alice is a CREATEROLE user and issues CREATE ROLE bob, my
proposal allows alice to automatically obtain access to bob's
privileges. Your proposal would allow that, but it would also allow
alice to automatically confer bob's privileges on some third user, say
charlie. Maybe that's useful to somebody, I don't know.
But one significant disadvantage of this is that every CREATEROLE user
must have their own configuration. If we have CREATE ROLE users alice,
dave, and ellen, then allice needs to execute ALTER DEFAULT PRIVILEGES
GRANT CREATED ROLE TO alice WITH ...; dave needs to do the same thing
with dave instead of alice; and ellen needs to do the same thing with
ellen instead of alice. There's no way to apply a system-wide
configuration that applies nicely to all CREATEROLE users.
A GUC would of course allow that, because it could be set in
postgresql.conf and then overridden for particular databases, users,
or sessions.
David claims that "these aren't privileges, they are memberships." I
don't entirely agree with that, because I think that we're basically
using memberships as a pseudonym for privileges where roles are
concerned. However, it is true that there's no precedent for referring
to role grants using the keyword PRIVILEGES at the SQL level, and the
fact that the underlying works in somewhat similar ways doesn't
necessarily mean that it's OK to conflate the two concepts at the SQL
level.
So I'm still not very sold on this idea.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Nov 28, 2022 at 8:33 PM Robert Haas <robertmhaas@gmail.com> wrote:
Hmm, that's an interesting alternative to what I actually implemented.
Some people might like it better, because it puts the behavior fully
under the control of the CREATEROLE user, which a number of you seem
to favor.
Here's an updated patch set.
0001 adds more precise and extensive documentation for the current
(broken) state of affairs. I propose to back-patch this to all
supported branches. It also removes a <tip> suggesting that you should
use a CREATEDB & CREATEROLE role instead of a superuser, because that
is pretty pointless as things stand, and is too simplistic for the new
system that I'm proposing to put in place, too.
0002 and 0003 are refactoring, unchanged from v1.
0004 is the core fix to CREATEROLE. It has been updated from the
previous version with documentation and some bug fixes.
0005 adopts David's suggestion: instead of giving the superuser a way
to control the options on the implicit grant, give CREATEROLE users a
way to grant newly-created roles to themselves automatically. I made
this a GUC, which means that the person setting up the system could
configure a default in postgresql.conf, but a user who doesn't prefer
that default can also override it using ALTER ROLE .. SET or ~/.psqlrc
or whatever. This is simpler than what I had before, doesn't involve a
catalog change, makes it clear that the behavior is not
security-critical, and puts the decision fully in the hands of the
CREATEROLE user rather than being partly controlled by that user and
partly by the superuser. Hopefully that's an improvement.
Comments?
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v2-0001-Improve-documentation-of-the-CREATEROLE-attibute.patchapplication/octet-stream; name=v2-0001-Improve-documentation-of-the-CREATEROLE-attibute.patchDownload
From 66954f3084e4608b92ca708a99810d58a222cbbb Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 30 Nov 2022 15:13:41 -0500
Subject: [PATCH v2 1/5] Improve documentation of the CREATEROLE attibute.
In user-manag.sgml, document precisely what privileges are conveyed
by CREATEROLE. Make particular note of the fact that it allows
changing passwords and granting access to high-privilege roles.
Also remove the suggestion of using a user with CREATEROLE and
CREATEDB instead of a superuser, as there is no real security
advantage to this approach.
Elsewhere in the documentation, adjust text that suggests that
<literal>CREATEROLE</literal> only allows for role creation, and
refer to the documentation in user-manag.sgml as appropriate.
(Proposed for back-patching to all supported branches.)
---
doc/src/sgml/ref/alter_role.sgml | 2 +-
doc/src/sgml/ref/create_role.sgml | 10 +++----
doc/src/sgml/ref/createuser.sgml | 18 ++++++++----
doc/src/sgml/user-manag.sgml | 47 ++++++++++++++++++++++---------
4 files changed, 52 insertions(+), 25 deletions(-)
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..922686e5ce 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -307,7 +307,7 @@ ALTER ROLE fred VALID UNTIL 'infinity';
</para>
<para>
- Give a role the ability to create other roles and new databases:
+ Give a role the ability to manage other roles and create new databases:
<programlisting>
ALTER ROLE miriam CREATEROLE CREATEDB;
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 029a193361..1ccc832558 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -119,11 +119,11 @@ in sync when changing the above synopsis!
<listitem>
<para>
These clauses determine whether a role will be permitted to
- create new roles (that is, execute <command>CREATE ROLE</command>).
- A role with <literal>CREATEROLE</literal> privilege can also alter
- and drop other roles.
- If not specified,
- <literal>NOCREATEROLE</literal> is the default.
+ create, alter, drop, comment on, change the security label for,
+ and grant or revoke membership in other roles.
+ See <xref linkend='role-creation' /> for more details about what
+ capabilities are conferred by this privilege.
+ If not specified, <literal>NOCREATEROLE</literal> is the default.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index c6a7c603f7..a41a2b24e6 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -41,10 +41,14 @@ PostgreSQL documentation
</para>
<para>
- If you wish to create a new superuser, you must connect as a
- superuser, not merely with <literal>CREATEROLE</literal> privilege.
+ If you wish to create a role with the <literal>SUPERUSER</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> privilege,
+ you must connect as a superuser, not merely with
+ <literal>CREATEROLE</literal> privilege.
Being a superuser implies the ability to bypass all access permission
- checks within the database, so superuser access should not be granted lightly.
+ checks within the database, so superuser access should not be granted
+ lightly. <literal>CREATEROLE</literal> also conveys
+ <link linkend='role-creation'>very extensive privileges</link>.
</para>
<para>
@@ -247,8 +251,12 @@ PostgreSQL documentation
<term><option>--createrole</option></term>
<listitem>
<para>
- The new user will be allowed to create new roles (that is,
- this user will have <literal>CREATEROLE</literal> privilege).
+ The new user will be allowed to create, alter, drop, comment on,
+ change the security label for, and grant or revoke membership in
+ other roles; that is,
+ this user will have <literal>CREATEROLE</literal> privilege.
+ See <xref linkend='role-creation' /> for more details about what
+ capabilities are conferred by this privilege.
</para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 2bff4e47d0..0839daecca 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -191,7 +191,7 @@ CREATE USER <replaceable>name</replaceable>;
</varlistentry>
<varlistentry>
- <term>role creation<indexterm><primary>role</primary><secondary>privilege to create</secondary></indexterm></term>
+ <term id='role-creation'>role creation<indexterm><primary>role</primary><secondary>privilege to create</secondary></indexterm></term>
<listitem>
<para>
A role must be explicitly given permission to create more roles
@@ -200,9 +200,38 @@ CREATE USER <replaceable>name</replaceable>;
<replaceable>name</replaceable> CREATEROLE</literal>.
A role with <literal>CREATEROLE</literal> privilege can alter and drop
other roles, too, as well as grant or revoke membership in them.
- However, to create, alter, drop, or change membership of a
- superuser role, superuser status is required;
- <literal>CREATEROLE</literal> is insufficient for that.
+ Altering a role includes most changes that can be made using
+ <literal>ALTER ROLE</literal>, including, for example, changing
+ passwords. It also includes modifications to a role that can
+ be made using the <literal>COMMENT</literal> and
+ <literal>SECURITY LABEL</literal> commands.
+ </para>
+ <para>
+ However, <literal>CREATEROLE</literal> does not convey the ability to
+ create <literal>SUPERUSER</literal> roles, nor does it convey any
+ power over <literal>SUPERUSER</literal> roles that already exist.
+ Furthermore, <literal>CREATEROLE</literal> does not convey the power
+ to create <literal>REPLICATION</literal> users, nor the ability to
+ grant or revoke the <literal>REPLICATION</literal> privilege, nor the
+ ability to the role properties of such users. However, it does allow
+ <literal>ALTER ROLE .. SET</literal> and
+ <literal>ALTER ROLE .. RENAME</literal> to be used on
+ <literal>REPLICATION</literal> roles, as well as the use of
+ <literal>COMMENT ON ROLE</literal>,
+ <literal>SECURITY LABEL ON ROLE</literal>,
+ and <literal>DROP ROLE</literal>.
+ Finally, <literal>CREATEROLE</literal> does not
+ confer the ability to grant or revoke the <literal>BYPASSRLS</literal>
+ privilege.
+ </para>
+ <para>
+ Because the <literal>CREATEROLE</literal> privilege allows a user
+ to grant or revoke membership even in roles to which it does not (yet)
+ have any access, a <literal>CREATEROLE</literal> user can obtain access
+ to the capabilities of every predefined role in the system, including
+ highly privileged roles such as
+ <literal>pg_execute_server_program</literal> and
+ <literal>pg_write_server_files</literal>.
</para>
</listitem>
</varlistentry>
@@ -280,16 +309,6 @@ CREATE USER <replaceable>name</replaceable>;
and <xref linkend="sql-alterrole"/> commands for details.
</para>
- <tip>
- <para>
- It is good practice to create a role that has the <literal>CREATEDB</literal>
- and <literal>CREATEROLE</literal> privileges, but is not a superuser, and then
- use this role for all routine management of databases and roles. This
- approach avoids the dangers of operating as a superuser for tasks that
- do not really require it.
- </para>
- </tip>
-
<para>
A role can also have role-specific defaults for many of the run-time
configuration settings described in <xref
--
2.24.3 (Apple Git-128)
v2-0002-Refactor-permissions-checking-for-role-grants.patchapplication/octet-stream; name=v2-0002-Refactor-permissions-checking-for-role-grants.patchDownload
From 62f1e04467b8fd3d6b6025f4afc9572c2f77606b Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Aug 2022 14:55:01 -0400
Subject: [PATCH v2 2/5] Refactor permissions-checking for role grants.
Instead of having checks in AddRoleMems() and DelRoleMems(), have
the callers perform checks where it's required. In some cases it
isn't, either because the caller has already performed a check for
the same condition, or because the check couldn't possibly fail.
The "Skip permission check if nothing to do" check in each of
AddRoleMems() and DelRoleMems() is pointless. Most call sites
can't pass an empty list, and in the one case where an empty
list could be passed, the presence of this check couldn't possibly
avoid an error.
---
src/backend/commands/user.c | 116 +++++++++++++++++-------------------
1 file changed, 54 insertions(+), 62 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 8b6543edee..08e3fea135 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -94,6 +94,8 @@ static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
+static void check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant);
static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
bool is_grant);
static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
@@ -505,6 +507,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Oid oldroleid = oldroleform->oid;
char *oldrolename = NameStr(oldroleform->rolname);
+ /* can only add this role to roles for which you have rights */
+ check_role_membership_authorization(GetUserId(), oldroleid, true);
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
@@ -517,6 +521,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
+ *
+ * NB: No permissions check is required here. If you have enough rights
+ * to create a role, you can add any members you like.
*/
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
@@ -1442,6 +1449,8 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
+ check_role_membership_authorization(GetUserId(), roleid,
+ stmt->is_grant);
if (stmt->is_grant)
AddRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
@@ -1566,43 +1575,6 @@ AddRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
- /*
- * The charter of pg_database_owner is to have exactly one, implicit,
- * situation-dependent member. There's no technical need for this
- * restriction. (One could lift it and take the further step of making
- * object_ownercheck(DatabaseRelationId, ...) equivalent to has_privs_of_role(roleid,
- * ROLE_PG_DATABASE_OWNER), in which case explicit, situation-independent
- * members could act as the owner of any database.)
- */
- if (roleid == ROLE_PG_DATABASE_OWNER)
- ereport(ERROR,
- errmsg("role \"%s\" cannot have explicit members", rolename));
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
@@ -1901,31 +1873,6 @@ DelRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
@@ -2039,6 +1986,51 @@ DelRoleMems(const char *rolename, Oid roleid,
table_close(pg_authmem_rel, NoLock);
}
+/*
+ * Check that currentUserId has permission to modify the membership list for
+ * roleid. Throw an error if not.
+ */
+static void
+check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant)
+{
+ /*
+ * The charter of pg_database_owner is to have exactly one, implicit,
+ * situation-dependent member. There's no technical need for this
+ * restriction. (One could lift it and take the further step of making
+ * object_ownercheck(DatabaseRelationId, ...) equivalent to
+ * has_privs_of_role(roleid, ROLE_PG_DATABASE_OWNER), in which case
+ * explicit, situation-independent members could act as the owner of any
+ * database.)
+ */
+ if (is_grant && roleid == ROLE_PG_DATABASE_OWNER)
+ ereport(ERROR,
+ errmsg("role \"%s\" cannot have explicit members",
+ GetUserNameFromId(roleid, false)));
+
+ /* To mess with a superuser role, you gotta be superuser. */
+ if (superuser_arg(roleid))
+ {
+ if (!superuser_arg(currentUserId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to alter superusers")));
+ }
+ else
+ {
+ /*
+ * Otherwise, must have createrole or admin option on the role to be
+ * changed.
+ */
+ if (!has_createrole_privilege(currentUserId) &&
+ !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+}
+
/*
* Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
* targeting a role.
--
2.24.3 (Apple Git-128)
v2-0003-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchapplication/octet-stream; name=v2-0003-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchDownload
From 2febc2fd27592f05a010fe0a3e17fe2d57e8a94d Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Nov 2022 14:46:03 -0500
Subject: [PATCH v2 3/5] Pass down current user ID to AddRoleMems and
DelRoleMems.
This is just refactoring; there should be no functonal change.
---
src/backend/commands/user.c | 41 ++++++++++++++++++++-----------------
1 file changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 08e3fea135..37fc4f9627 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -87,10 +87,10 @@ int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
-static void AddRoleMems(const char *rolename, Oid roleid,
+static void AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt);
-static void DelRoleMems(const char *rolename, Oid roleid,
+static void DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
@@ -133,6 +133,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
HeapTuple tuple;
Datum new_record[Natts_pg_authid] = {0};
bool new_record_nulls[Natts_pg_authid] = {0};
+ Oid currentUserId = GetUserId();
Oid roleid;
ListCell *item;
ListCell *option;
@@ -508,8 +509,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
char *oldrolename = NameStr(oldroleform->rolname);
/* can only add this role to roles for which you have rights */
- check_role_membership_authorization(GetUserId(), oldroleid, true);
- AddRoleMems(oldrolename, oldroleid,
+ check_role_membership_authorization(currentUserId, oldroleid, true);
+ AddRoleMems(currentUserId, oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
InvalidOid, &popt);
@@ -525,12 +526,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* NB: No permissions check is required here. If you have enough rights
* to create a role, you can add any members you like.
*/
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
popt.admin = true;
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
InvalidOid, &popt);
@@ -583,6 +584,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
Oid roleid;
+ Oid currentUserId = GetUserId();
GrantRoleOptions popt;
check_rolespec_name(stmt->role,
@@ -727,13 +729,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
errmsg("permission denied")));
/* without CREATEROLE, can only change your own password */
- if (dpassword && roleid != GetUserId())
+ if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
/* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\" to add members",
@@ -888,11 +890,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
CommandCounterIncrement();
if (stmt->action == +1) /* add members to role */
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
else if (stmt->action == -1) /* drop members from role */
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt, DROP_RESTRICT);
}
@@ -1378,6 +1380,7 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
List *grantee_ids;
ListCell *item;
GrantRoleOptions popt;
+ Oid currentUserId = GetUserId();
/* Parse options list. */
InitGrantRoleOptions(&popt);
@@ -1449,14 +1452,14 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
- check_role_membership_authorization(GetUserId(), roleid,
- stmt->is_grant);
+ check_role_membership_authorization(currentUserId,
+ roleid, stmt->is_grant);
if (stmt->is_grant)
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt);
else
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt, stmt->behavior);
}
@@ -1555,15 +1558,17 @@ roleSpecsToIds(List *memberNames)
/*
* AddRoleMems -- Add given members to the specified role
*
+ * currentUserId: OID of role performing the operation
* rolename: name of role to add to (used only for error messages)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership (InvalidOid if not set explicitly)
+ * grantorId: OID that should be recorded as having granted the membership
+ * (InvalidOid if not set explicitly)
* popt: information about grant options
*/
static void
-AddRoleMems(const char *rolename, Oid roleid,
+AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt)
{
@@ -1571,7 +1576,6 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1858,7 +1862,7 @@ AddRoleMems(const char *rolename, Oid roleid,
* behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
-DelRoleMems(const char *rolename, Oid roleid,
+DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
{
@@ -1866,7 +1870,6 @@ DelRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
CatCList *memlist;
RevokeRoleGrantAction *actions;
int i;
--
2.24.3 (Apple Git-128)
v2-0004-Restrict-the-privileges-of-CREATEROLE-users.patchapplication/octet-stream; name=v2-0004-Restrict-the-privileges-of-CREATEROLE-users.patchDownload
From e626c60a77843d2639012dd16891f20c8e68188c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 1 Dec 2022 15:07:02 -0500
Subject: [PATCH v2 4/5] Restrict the privileges of CREATEROLE users.
Previously, CREATEROLE users were permitted to make nearly arbitrary
changes to roles that they didn't create, with certain exceptions,
particularly superuser roles. Instead, allow CREATEROLE users to make such
changes to roles for which they possess ADMIN OPTION, and to
grant membership only in roles for which they possess ADMIN OPTION.
When a CREATEROLE user who is not a superuser creates a role, grant
ADMIN OPTION on the newly-created role to the creator, so that they
can administer roles they create or for which they have been given
privileges.
With these changes, CREATEROLE users still have very significant
powers that unprivileged users do not receive: they can alter, rename,
drop, comment on, change the password for, and change security labels
on roles. However, they can now do these things only for roles for
which they possess appropriate privileges, rather than all
non-superuser roles; moreover, they cannot grant a role such as
pg_execute_server_program unless they themselves possess it.
FIXME: Add more regression tests.
---
doc/src/sgml/ddl.sgml | 10 +-
doc/src/sgml/ref/alter_role.sgml | 8 +-
doc/src/sgml/ref/comment.sgml | 3 +-
doc/src/sgml/ref/create_role.sgml | 4 +-
doc/src/sgml/ref/createuser.sgml | 3 +-
doc/src/sgml/ref/drop_role.sgml | 2 +-
doc/src/sgml/ref/dropuser.sgml | 7 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++--
src/backend/catalog/objectaddress.c | 10 +-
src/backend/commands/user.c | 100 +++++++++++++-----
.../expected/dummy_seclabel.out | 17 +--
.../dummy_seclabel/sql/dummy_seclabel.sql | 13 ++-
src/test/regress/expected/create_role.out | 37 ++++---
src/test/regress/sql/create_role.sql | 23 ++--
15 files changed, 181 insertions(+), 104 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 38618de01c..52eabe78a6 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3228,13 +3228,11 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
name. Therefore, if each user has a separate schema, they access
their own schemas by default.) This pattern is a secure schema
usage pattern unless an untrusted user is the database owner or
- holds the <literal>CREATEROLE</literal> privilege, in which case no
- secure schema usage pattern exists.
+ has been granted <literal>ADMIN OPTION</literal> on a relevant role,
+ in which case no secure schema usage pattern exists.
</para>
<!-- A database owner can attack the database's users via "CREATE SCHEMA
- trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". A
- CREATEROLE user can issue "GRANT $dbowner TO $me" and then use the
- database owner attack. -->
+ trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". -->
<para>
In <productname>PostgreSQL</productname> 15 and later, the default
@@ -3262,7 +3260,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
unreliable</link>. If you create functions or extensions in the public
schema, use the first pattern instead. Otherwise, like the first
pattern, this is secure unless an untrusted user is the database owner
- or holds the <literal>CREATEROLE</literal> privilege.
+ or has been granted <literal>ADMIN OPTION</literal> on a relevant role.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 922686e5ce..d7dff8ba21 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -73,7 +73,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
Roles having <literal>CREATEROLE</literal> privilege can change any of these
settings except <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>,
and <literal>BYPASSRLS</literal>; but only for non-superuser and
- non-replication roles.
+ non-replication roles for which they have been
+ granted <literal>ADMIN OPTION</literal>.
Ordinary roles can only change their own password.
</para>
@@ -81,7 +82,7 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
The second variant changes the name of the role.
Database superusers can rename any role.
Roles having <literal>CREATEROLE</literal> privilege can rename non-superuser
- roles.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
The current session user cannot be renamed.
(Connect as a different user if you need to do that.)
Because <literal>MD5</literal>-encrypted passwords use the role name as
@@ -116,7 +117,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<para>
Superusers can change anyone's session defaults. Roles having
<literal>CREATEROLE</literal> privilege can change defaults for non-superuser
- roles. Ordinary roles can only set defaults for themselves.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
+ Ordinary roles can only set defaults for themselves.
Certain configuration variables cannot be set this way, or can only be
set if a superuser issues the command. Only superusers can change a setting
for all roles in all databases.
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 23d9029af9..7499da1d62 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -99,7 +99,8 @@ COMMENT ON
For most kinds of object, only the object's owner can set the comment.
Roles don't have owners, so the rule for <literal>COMMENT ON ROLE</literal> is
that you must be superuser to comment on a superuser role, or have the
- <literal>CREATEROLE</literal> privilege to comment on non-superuser roles.
+ <literal>CREATEROLE</literal> privilege and have been granted
+ <literal>ADMIN OPTION</literal> on the target role.
Likewise, access methods don't have owners either; you must be superuser
to comment on an access method.
Of course, a superuser can comment on anything.
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 1ccc832558..0863acbcac 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -119,8 +119,8 @@ in sync when changing the above synopsis!
<listitem>
<para>
These clauses determine whether a role will be permitted to
- create, alter, drop, comment on, change the security label for,
- and grant or revoke membership in other roles.
+ create, alter, drop, comment on, and change the security label for
+ other roles.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
If not specified, <literal>NOCREATEROLE</literal> is the default.
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index a41a2b24e6..f91dc500a4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -252,8 +252,7 @@ PostgreSQL documentation
<listitem>
<para>
The new user will be allowed to create, alter, drop, comment on,
- change the security label for, and grant or revoke membership in
- other roles; that is,
+ change the security label for other roles; that is,
this user will have <literal>CREATEROLE</literal> privilege.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..cbcb3cd3d3 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -32,7 +32,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<command>DROP ROLE</command> removes the specified role(s).
To drop a superuser role, you must be a superuser yourself;
to drop non-superuser roles, you must have <literal>CREATEROLE</literal>
- privilege.
+ privilege and have been granted <literal>ADMIN OPTION</literal> on the role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..b6be26d5b0 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,10 @@ PostgreSQL documentation
<para>
<application>dropuser</application> removes an existing
<productname>PostgreSQL</productname> user.
- Only superusers and users with the <literal>CREATEROLE</literal> privilege can
- remove <productname>PostgreSQL</productname> users. (To remove a
- superuser, you must yourself be a superuser.)
+ Superusers can use this command to remove any role; otherwise, only
+ non-superuser roles can be removed, and only by a user who possesses
+ the <literal>CREATEROLE</literal> privilege and has been granted
+ <literal>ADMIN OPTION</literal> on the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index c3c585be7e..53b68ffb8a 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -272,9 +272,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
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. Database superusers can grant or revoke
- membership in any role to anyone. Roles having
- <literal>CREATEROLE</literal> privilege can grant or revoke membership
- in any role that is not a superuser. This option defaults to
+ membership in any role to anyone. This option defaults to
<literal>FALSE</literal>.
</para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 0839daecca..84a29338ce 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -199,7 +199,12 @@ CREATE USER <replaceable>name</replaceable>;
checks). To create such a role, use <literal>CREATE ROLE
<replaceable>name</replaceable> CREATEROLE</literal>.
A role with <literal>CREATEROLE</literal> privilege can alter and drop
- other roles, too, as well as grant or revoke membership in them.
+ roles which have been granted to the <literal>CREATEROLE</literal>
+ user with the <literal>ADMIN</literal> option. Such a grant occurs
+ automatically when a <literal>CREATEROLE</literal> user that is not
+ a superuser creates a new role, so that by default, a
+ <literal>CREATEROLE</literal> user can alter and drop the roles
+ which they have created.
Altering a role includes most changes that can be made using
<literal>ALTER ROLE</literal>, including, for example, changing
passwords. It also includes modifications to a role that can
@@ -224,15 +229,6 @@ CREATE USER <replaceable>name</replaceable>;
confer the ability to grant or revoke the <literal>BYPASSRLS</literal>
privilege.
</para>
- <para>
- Because the <literal>CREATEROLE</literal> privilege allows a user
- to grant or revoke membership even in roles to which it does not (yet)
- have any access, a <literal>CREATEROLE</literal> user can obtain access
- to the capabilities of every predefined role in the system, including
- highly privileged roles such as
- <literal>pg_execute_server_program</literal> and
- <literal>pg_write_server_files</literal>.
- </para>
</listitem>
</varlistentry>
@@ -329,6 +325,34 @@ ALTER ROLE myname SET enable_indexscan TO off;
<literal>LOGIN</literal> privilege are fairly useless, since they will never
be invoked.
</para>
+
+ <para>
+ When a non-superuser creates a role using the <literal>CREATEROLE</literal>
+ privilege, the created role is automatically granted back to the creating
+ user, just as if the bootstrap superuser had executed the command
+ <literal>GRANT created_user TO creating_user WITH ADMIN TRUE, SET FALSE,
+ INHERIT FALSE</literal>. Since a <literal>CREATEROLE</literal> user can
+ only exercise special privileges with regard to an existing role if they
+ have <literal>ADMIN OPTION</literal> on it, this grant is just sufficient
+ to allow a <literal>CREATEROLE</literal> user to administer the roles they
+ created. However, because it is created with <literal>INHERIT FALSE, SET
+ FALSE</literal>, the <literal>CREATEROLE</literal> user doesn't inherit the
+ privileges of the created role, nor can it access the privileges of that
+ role using <literal>SET ROLE</literal>. However, since any user who has
+ <literal>ADMIN OPTION</literal> on a role can grant membership in that
+ role to any other user, the <literal>CREATEROLE</literal> user can gain
+ access to the created role by simplying granting that role back to
+ themselves with the <literal>INHERIT</literal> and/or <literal>SET</literal>
+ options. Thus, the fact that privileges are not inherited by default nor
+ is <literal>SET ROLE</literal> granted by default is a safeguard against
+ accidents, not a security feature. Also note that, because this automatic
+ grant is granted by the bootstrap user, it cannot be removed or changed by
+ the <literal>CREATEROLE</literal> user; however, any superuser could
+ revoke it, modify it, and/or issue additional such grants to other
+ <literal>CREATEROLE</literal> users. Whichever <literal>CREATEROLE</literal>
+ users have <literal>ADMIN OPTION</literal> on a role at any given time
+ can administer it.
+ </para>
</sect1>
<sect1 id="role-membership">
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index fe97fbf79d..bb5307dd2f 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2543,7 +2543,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
/*
* We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
+ * provided that they also have admin option on the role.
+ *
+ * However, superusers are only owned by superusers.
*/
if (superuser_arg(address.objectId))
{
@@ -2558,6 +2560,12 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege")));
+ if (!is_admin_of_role(roleid, address.objectId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(address.objectId,
+ true))));
}
break;
case OBJECT_TSPARSER:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 37fc4f9627..1bfc42a157 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -519,6 +519,42 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
}
+ /*
+ * If the current user isn't a superuser, make them an admin of the new
+ * role so that they can administer the new object they just created.
+ * Superusers will be able to do that anyway.
+ *
+ * The grantor of record for this implicit grant is the bootstrap
+ * superuser, which means that the CREATEROLE user cannot revoke the
+ * grant. They can however grant the created role back to themselves
+ * with different options, since they enjoy ADMIN OPTION on it.
+ */
+ if (!superuser())
+ {
+ RoleSpec *current_role = makeNode(RoleSpec);
+ GrantRoleOptions poptself;
+
+ current_role->roletype = ROLESPEC_CURRENT_ROLE;
+ current_role->location = -1;
+
+ poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ poptself.admin = true;
+ poptself.inherit = false;
+ poptself.set = false;
+
+ AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
+ list_make1(current_role), list_make1_oid(GetUserId()),
+ BOOTSTRAP_SUPERUSERID, &poptself);
+
+ /*
+ * We must make the implicit grant visible to the code below, else
+ * the additional grants will fail.
+ */
+ CommandCounterIncrement();
+ }
+
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
@@ -694,9 +730,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/*
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
- * property. Otherwise, if you don't have createrole, you're only allowed
- * to (1) change your own password or (2) add members to a role for which
- * you have ADMIN OPTION.
+ * property.
*/
if (authform->rolsuper || dissuper)
{
@@ -719,29 +753,35 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+
+ /*
+ * Most changes to a role require that you both have CREATEROLE privileges
+ * and also ADMIN OPTION on the role.
+ */
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
{
- /* things you certainly can't do without CREATEROLE */
+ /* things an unprivileged user certainly can't do */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
- /* without CREATEROLE, can only change your own password */
+ /* an unprivileged user can change their own password */
if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
-
- /* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\" to add members",
- rolename)));
}
+ /* To add members to a role, you need ADMIN OPTION. */
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
+
/* Convert validuntil to internal form */
if (dvalidUntil)
{
@@ -935,8 +975,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
shdepLockAndCheckObject(AuthIdRelationId, roleid);
/*
- * To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * To mess with a superuser you gotta be superuser; otherwise you
+ * need CREATEROLE plus admin option on the target role; unless you're
+ * just trying to change your own settings
*/
if (roleform->rolsuper)
{
@@ -947,7 +988,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if ((!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
+ && roleid != GetUserId())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1067,13 +1110,18 @@ DropRole(DropRoleStmt *stmt)
/*
* For safety's sake, we allow createrole holders to drop ordinary
- * roles but not superuser roles. This is mainly to avoid the
- * scenario where you accidentally drop the last superuser.
+ * roles but not superuser roles, and only if they also have ADMIN
+ * OPTION.
*/
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ role)));
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1312,7 +1360,8 @@ RenameRole(const char *oldname, const char *newname)
errmsg("role \"%s\" already exists", newname)));
/*
- * createrole is enough privilege unless you want to mess with a superuser
+ * Only superusers can mess with superusers. Otherwise, a user with
+ * CREATEROLE can rename a role for which they have ADMIN OPTION.
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1323,7 +1372,8 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -2022,11 +2072,9 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
else
{
/*
- * Otherwise, must have createrole or admin option on the role to be
- * changed.
+ * Otherwise, must have admin option on the role to be changed.
*/
- if (!has_createrole_privilege(currentUserId) &&
- !is_admin_of_role(currentUserId, roleid))
+ if (!is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -2048,7 +2096,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
* be passed as InvalidOid, and this function will infer the user to be
* recorded as the grantor. In many cases, this will be the current user, but
* things get more complicated when the current user doesn't possess ADMIN
- * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * OPTION on the role but rather relies on having SUPERUSER privileges, or
* on inheriting the privileges of a role which does have ADMIN OPTION. See
* below for details.
*
@@ -2074,7 +2122,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
* not depend on any other existing grants, so always default to this
* interpretation when possible.
*/
- if (has_createrole_privilege(currentUserId))
+ if (superuser_arg(currentUserId))
return BOOTSTRAP_SUPERUSERID;
/*
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..c57d4fd2df 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -6,9 +6,11 @@ CREATE EXTENSION dummy_seclabel;
SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2;
@@ -16,6 +18,8 @@ CREATE FUNCTION dummy_seclabel_four() RETURNS integer AS $$SELECT 4$$ language s
CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
--
@@ -43,16 +47,16 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
-- Test for shared database object
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
ERROR: only superuser can set 'secret' label
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
-ERROR: role "regress_dummy_seclabel_user3" does not exist
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
+ERROR: role "regress_dummy_seclabel_user4" does not exist
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
ERROR: must have CREATEROLE privilege
@@ -81,8 +85,8 @@ SELECT objtype, objname, provider, label FROM pg_seclabels
domain | dummy_seclabel_domain | dummy | classified
function | dummy_seclabel_four() | dummy | classified
publication | dummy_pub | dummy | classified
- role | regress_dummy_seclabel_user1 | dummy | classified
role | regress_dummy_seclabel_user2 | dummy | unclassified
+ role | regress_dummy_seclabel_user3 | dummy | classified
schema | dummy_seclabel_test | dummy | unclassified
subscription | dummy_sub | dummy | classified
table | dummy_seclabel_tbl1 | dummy | top secret
@@ -115,3 +119,4 @@ DROP SUBSCRIPTION dummy_sub;
DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..649409757e 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -8,11 +8,13 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -22,6 +24,8 @@ CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
@@ -47,12 +51,12 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
@@ -113,3 +117,4 @@ DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..6e0cce4579 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -13,7 +13,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -70,20 +70,35 @@ ALTER VIEW tenant_view OWNER TO regress_role_admin;
ERROR: must be owner of view tenant_view
DROP VIEW tenant_view;
ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
ERROR: permission denied to reassign objects
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_monitor IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
+DETAIL: owner of table tenant_table
+owner of view tenant_view
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -114,22 +129,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
-DETAIL: owner of table tenant_table
-owner of view tenant_view
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..d491684db3 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -11,7 +11,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -71,10 +71,10 @@ DROP TABLE tenant_table;
ALTER VIEW tenant_view OWNER TO regress_role_admin;
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
CREATE ROLE regress_monitor IN ROLE pg_monitor;
@@ -86,6 +86,9 @@ CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -109,20 +112,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
-
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.24.3 (Apple Git-128)
v2-0005-Add-new-GUC-createrole_self_grant.patchapplication/octet-stream; name=v2-0005-Add-new-GUC-createrole_self_grant.patchDownload
From 615e4bb8ce69049eb83f5ca80e47952b13438ba5 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 2 Dec 2022 08:54:18 -0500
Subject: [PATCH v2 5/5] Add new GUC createrole_self_grant.
Can be set to the empty string, or to either or both of "set" or
"inherit". If set to a non-empty value, a non-superuser who creates
a role (necessarily by relying up the CREATEROLE privilege) will
grant that role back to themselves with the specified options.
This isn't a security feature, because the grant that this feature
triggers can also be performed explicitly. Instead, it's a user experience
feature. A superuser would necessarily inherit the privileges of any
created role and be able to access all such roles via SET ROLE;
with this patch, you can configure createrole_self_grant = 'set, inherit'
to provide a similar experience for a user who has CREATEROLE but not
SUPERUSER.
FIXME: Add some regression tests.
---
doc/src/sgml/config.sgml | 33 +++++++
doc/src/sgml/ref/create_role.sgml | 1 +
doc/src/sgml/ref/createuser.sgml | 1 +
src/backend/commands/user.c | 97 ++++++++++++++++++-
src/backend/utils/misc/guc_tables.c | 12 +++
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/commands/user.h | 10 +-
7 files changed, 150 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 39d1c89e33..503a92248e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9365,6 +9365,39 @@ SET XML OPTION { DOCUMENT | CONTENT };
</listitem>
</varlistentry>
+ <varlistentry id="guc-createrole-self-grant" xreflabel="createrole_self_grant">
+ <term><varname>createrole_self_grant</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>createrole_self_grant</varname></primary>
+ <secondary>configuration parameter</secondary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ If a user who has <literal>CREATEROLE</literal> but not
+ <literal>SUPERUSER</literal> creates a role, and if this
+ is set to a non-empty value, the newly-created role will be granted
+ to the creating user with the options specified. The value must be
+ <literal>set</literal>, <literal>inherit</literal>, or a
+ comma-separated list of these.
+ </para>
+ <para>
+ The purpose of this option is to allow a <literal>CREATEROLE</literal>
+ user who is not a superuser to automatically inherit, or automatically
+ gain the ability to <literal>SET ROLE</literal> to, any created users.
+ Since a <literal>CREATEROLE</literal> user is always implicitly granted
+ <literal>ADMIN OPTION</literal> on created roles, that user could
+ always execute a <literal>GRANT</literal> statement that would achieve
+ the same effect as this setting. However, it can be convenient for
+ usability reasons if the grant happens automatically. A superuser
+ automatically inherits the privileges of every role and can always
+ <literal>SET ROLE</literal> to any role, and this setting can be used
+ to produce a similar behavior for <literal>CREATEROLE</literal> users
+ for users which they create.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</sect2>
<sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 0863acbcac..7ce4e38b45 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -506,6 +506,7 @@ CREATE ROLE <replaceable class="parameter">name</replaceable> [ WITH ADMIN <repl
<member><xref linkend="sql-grant"/></member>
<member><xref linkend="sql-revoke"/></member>
<member><xref linkend="app-createuser"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index f91dc500a4..9a1c3d01f4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -555,6 +555,7 @@ PostgreSQL documentation
<simplelist type="inline">
<member><xref linkend="app-dropuser"/></member>
<member><xref linkend="sql-createrole"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 1bfc42a157..8ba218e792 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -39,6 +39,7 @@
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+#include "utils/varlena.h"
/*
* Removing a role grant - or the admin option on it - might recurse to
@@ -81,8 +82,11 @@ typedef struct
#define GRANT_ROLE_SPECIFIED_INHERIT 0x0002
#define GRANT_ROLE_SPECIFIED_SET 0x0004
-/* GUC parameter */
+/* GUC parameters */
int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+char *createrole_self_grant = "";
+bool createrole_self_grant_enabled = false;
+GrantRoleOptions createrole_self_grant_options;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
@@ -532,10 +536,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (!superuser())
{
RoleSpec *current_role = makeNode(RoleSpec);
- GrantRoleOptions poptself;
+ GrantRoleOptions poptself;
+ List *memberSpecs;
+ List *memberIds = list_make1_oid(currentUserId);
current_role->roletype = ROLESPEC_CURRENT_ROLE;
current_role->location = -1;
+ memberSpecs = list_make1(current_role);
poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
| GRANT_ROLE_SPECIFIED_INHERIT
@@ -545,7 +552,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
poptself.set = false;
AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
- list_make1(current_role), list_make1_oid(GetUserId()),
+ memberSpecs, memberIds,
BOOTSTRAP_SUPERUSERID, &poptself);
/*
@@ -553,6 +560,20 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* the additional grants will fail.
*/
CommandCounterIncrement();
+
+ /*
+ * Because of the implicit grant above, a CREATEROLE user who creates
+ * a role has the ability to grant that role back to themselves with
+ * the INHERIT or SET options, if they wish to inherit the role's
+ * privileges or be able to SET ROLE to it. The createrole_self_grant
+ * GUC can be used to make this happen automatically. This has no
+ * security implications since the same user is able to make the same
+ * grant using an explicit GRANT statement; it's just convenient.
+ */
+ if (createrole_self_grant_enabled)
+ AddRoleMems(currentUserId, stmt->role, roleid,
+ memberSpecs, memberIds,
+ currentUserId, &createrole_self_grant_options);
}
/*
@@ -2413,3 +2434,73 @@ InitGrantRoleOptions(GrantRoleOptions *popt)
popt->inherit = false;
popt->set = true;
}
+
+/*
+ * GUC check_hook for createrole_self_grant
+ */
+bool
+check_createrole_self_grant(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ unsigned options = 0;
+ unsigned *result;
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+
+ if (pg_strcasecmp(tok, "SET") == 0)
+ options |= GRANT_ROLE_SPECIFIED_SET;
+ else if (pg_strcasecmp(tok, "INHERIT") == 0)
+ options |= GRANT_ROLE_SPECIFIED_INHERIT;
+ else
+ {
+ GUC_check_errdetail("Unrecognized key word: \"%s\".", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+
+ result = (unsigned *) guc_malloc(LOG, sizeof(unsigned));
+ *result = options;
+ *extra = result;
+
+ return true;
+}
+
+/*
+ * GUC assign_hook for createrole_self_grant
+ */
+void
+assign_createrole_self_grant(const char *newval, void *extra)
+{
+ unsigned options = * (unsigned *) extra;
+
+ createrole_self_grant_enabled = (options != 0);
+ createrole_self_grant_options.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ createrole_self_grant_options.admin = false;
+ createrole_self_grant_options.inherit =
+ (options & GRANT_ROLE_SPECIFIED_INHERIT) != 0;
+ createrole_self_grant_options.set =
+ (options & GRANT_ROLE_SPECIFIED_SET) != 0;
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1bf14eec66..3773be9ce4 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3916,6 +3916,18 @@ struct config_string ConfigureNamesString[] =
check_temp_tablespaces, assign_temp_tablespaces, NULL
},
+ {
+ {"createrole_self_grant", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets whether a CREATEROLE user automatically grants "
+ "the role to themselves, and with which options."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &createrole_self_grant,
+ "",
+ check_createrole_self_grant, assign_createrole_self_grant, NULL
+ },
+
{
{"dynamic_library_path", PGC_SUSET, CLIENT_CONN_OTHER,
gettext_noop("Sets the path for dynamically loadable modules."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 043864597f..efa005b38d 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -701,6 +701,7 @@
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_pending_list_limit = 4MB
+#createrole_self_grant = '' # set and/or inherit
# - Locale and Formatting -
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 54c720d880..97dcb93791 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -15,9 +15,11 @@
#include "libpq/crypt.h"
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
+#include "utils/guc.h"
-/* GUC. Is actually of type PasswordType. */
-extern PGDLLIMPORT int Password_encryption;
+/* GUCs */
+extern PGDLLIMPORT int Password_encryption; /* values from enum PasswordType */
+extern PGDLLIMPORT char *createrole_self_grant;
/* Hook to check passwords in CreateRole() and AlterRole() */
typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
@@ -34,4 +36,8 @@ extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern bool check_createrole_self_grant(char **newval, void **extra,
+ GucSource source);
+extern void assign_createrole_self_grant(const char *newval, void *extra);
+
#endif /* USER_H */
--
2.24.3 (Apple Git-128)
Reading 0001:
+ However, <literal>CREATEROLE</literal> does not convey the ability to
+ create <literal>SUPERUSER</literal> roles, nor does it convey any
+ power over <literal>SUPERUSER</literal> roles that already exist.
+ Furthermore, <literal>CREATEROLE</literal> does not convey the power
+ to create <literal>REPLICATION</literal> users, nor the ability to
+ grant or revoke the <literal>REPLICATION</literal> privilege, nor the
+ ability to the role properties of such users.
"... nor the ability to the role properties ..."
I think a verb is missing here.
The contents looks good to me other than that problem, and I agree to
backpatch it.
Why did you choose to use two dots for ellipses in some command
<literal>s rather than three? I know I've made that choice too on
occassion, but there aren't many such cases and maybe we should put a
stop to it (or a period) before it spreads too much.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On Thu, Dec 22, 2022 at 9:14 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
The contents looks good to me other than that problem, and I agree to
backpatch it.
Cool. Thanks for the review.
Why did you choose to use two dots for ellipses in some command
<literal>s rather than three? I know I've made that choice too on
occassion, but there aren't many such cases and maybe we should put a
stop to it (or a period) before it spreads too much.
Honestly, I wasn't aware that we had some other convention for it.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Dec 23, 2022 at 4:55 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Dec 22, 2022 at 9:14 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
The contents looks good to me other than that problem, and I agree to
backpatch it.Cool. Thanks for the review.
Why did you choose to use two dots for ellipses in some command
<literal>s rather than three? I know I've made that choice too on
occassion, but there aren't many such cases and maybe we should put a
stop to it (or a period) before it spreads too much.Honestly, I wasn't aware that we had some other convention for it.
Committed and back-patched 0001 with fixes for the issues that you pointed out.
Here's a trivial rebase of the rest of the patch set.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v3-0001-Refactor-permissions-checking-for-role-grants.patchapplication/octet-stream; name=v3-0001-Refactor-permissions-checking-for-role-grants.patchDownload
From 512a18f4d98728844fc4ce030cc71ef074691cce Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 24 Aug 2022 14:55:01 -0400
Subject: [PATCH v3 1/4] Refactor permissions-checking for role grants.
Instead of having checks in AddRoleMems() and DelRoleMems(), have
the callers perform checks where it's required. In some cases it
isn't, either because the caller has already performed a check for
the same condition, or because the check couldn't possibly fail.
The "Skip permission check if nothing to do" check in each of
AddRoleMems() and DelRoleMems() is pointless. Most call sites
can't pass an empty list, and in the one case where an empty
list could be passed, the presence of this check couldn't possibly
avoid an error.
---
src/backend/commands/user.c | 116 +++++++++++++++++-------------------
1 file changed, 54 insertions(+), 62 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index aee26f1789..280031e6eb 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -94,6 +94,8 @@ static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
+static void check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant);
static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
bool is_grant);
static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
@@ -505,6 +507,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Oid oldroleid = oldroleform->oid;
char *oldrolename = NameStr(oldroleform->rolname);
+ /* can only add this role to roles for which you have rights */
+ check_role_membership_authorization(GetUserId(), oldroleid, true);
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
@@ -517,6 +521,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
+ *
+ * NB: No permissions check is required here. If you have enough rights
+ * to create a role, you can add any members you like.
*/
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
@@ -1442,6 +1449,8 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
+ check_role_membership_authorization(GetUserId(), roleid,
+ stmt->is_grant);
if (stmt->is_grant)
AddRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
@@ -1566,43 +1575,6 @@ AddRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
- /*
- * The charter of pg_database_owner is to have exactly one, implicit,
- * situation-dependent member. There's no technical need for this
- * restriction. (One could lift it and take the further step of making
- * object_ownercheck(DatabaseRelationId, ...) equivalent to has_privs_of_role(roleid,
- * ROLE_PG_DATABASE_OWNER), in which case explicit, situation-independent
- * members could act as the owner of any database.)
- */
- if (roleid == ROLE_PG_DATABASE_OWNER)
- ereport(ERROR,
- errmsg("role \"%s\" cannot have explicit members", rolename));
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
@@ -1902,31 +1874,6 @@ DelRoleMems(const char *rolename, Oid roleid,
Assert(list_length(memberSpecs) == list_length(memberIds));
- /* Skip permission check if nothing to do */
- if (!memberIds)
- return;
-
- /*
- * Check permissions: must have createrole or admin option on the role to
- * be changed. To mess with a superuser role, you gotta be superuser.
- */
- if (superuser_arg(roleid))
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter superusers")));
- }
- else
- {
- if (!have_createrole_privilege() &&
- !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\"",
- rolename)));
- }
-
/* Validate grantor (and resolve implicit grantor if not specified). */
grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
@@ -2040,6 +1987,51 @@ DelRoleMems(const char *rolename, Oid roleid,
table_close(pg_authmem_rel, NoLock);
}
+/*
+ * Check that currentUserId has permission to modify the membership list for
+ * roleid. Throw an error if not.
+ */
+static void
+check_role_membership_authorization(Oid currentUserId, Oid roleid,
+ bool is_grant)
+{
+ /*
+ * The charter of pg_database_owner is to have exactly one, implicit,
+ * situation-dependent member. There's no technical need for this
+ * restriction. (One could lift it and take the further step of making
+ * object_ownercheck(DatabaseRelationId, ...) equivalent to
+ * has_privs_of_role(roleid, ROLE_PG_DATABASE_OWNER), in which case
+ * explicit, situation-independent members could act as the owner of any
+ * database.)
+ */
+ if (is_grant && roleid == ROLE_PG_DATABASE_OWNER)
+ ereport(ERROR,
+ errmsg("role \"%s\" cannot have explicit members",
+ GetUserNameFromId(roleid, false)));
+
+ /* To mess with a superuser role, you gotta be superuser. */
+ if (superuser_arg(roleid))
+ {
+ if (!superuser_arg(currentUserId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to alter superusers")));
+ }
+ else
+ {
+ /*
+ * Otherwise, must have createrole or admin option on the role to be
+ * changed.
+ */
+ if (!has_createrole_privilege(currentUserId) &&
+ !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+}
+
/*
* Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
* targeting a role.
--
2.37.1 (Apple Git-137.1)
v3-0004-Add-new-GUC-createrole_self_grant.patchapplication/octet-stream; name=v3-0004-Add-new-GUC-createrole_self_grant.patchDownload
From eb7cd95567407d61e11bd357d2e0e42cc01cc324 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 2 Dec 2022 08:54:18 -0500
Subject: [PATCH v3 4/4] Add new GUC createrole_self_grant.
Can be set to the empty string, or to either or both of "set" or
"inherit". If set to a non-empty value, a non-superuser who creates
a role (necessarily by relying up the CREATEROLE privilege) will
grant that role back to themselves with the specified options.
This isn't a security feature, because the grant that this feature
triggers can also be performed explicitly. Instead, it's a user experience
feature. A superuser would necessarily inherit the privileges of any
created role and be able to access all such roles via SET ROLE;
with this patch, you can configure createrole_self_grant = 'set, inherit'
to provide a similar experience for a user who has CREATEROLE but not
SUPERUSER.
FIXME: Add some regression tests.
---
doc/src/sgml/config.sgml | 33 +++++++
doc/src/sgml/ref/create_role.sgml | 1 +
doc/src/sgml/ref/createuser.sgml | 1 +
src/backend/commands/user.c | 97 ++++++++++++++++++-
src/backend/utils/misc/guc_tables.c | 12 +++
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/commands/user.h | 10 +-
7 files changed, 150 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 05b3862d09..d86385d308 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9421,6 +9421,39 @@ SET XML OPTION { DOCUMENT | CONTENT };
</listitem>
</varlistentry>
+ <varlistentry id="guc-createrole-self-grant" xreflabel="createrole_self_grant">
+ <term><varname>createrole_self_grant</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>createrole_self_grant</varname></primary>
+ <secondary>configuration parameter</secondary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ If a user who has <literal>CREATEROLE</literal> but not
+ <literal>SUPERUSER</literal> creates a role, and if this
+ is set to a non-empty value, the newly-created role will be granted
+ to the creating user with the options specified. The value must be
+ <literal>set</literal>, <literal>inherit</literal>, or a
+ comma-separated list of these.
+ </para>
+ <para>
+ The purpose of this option is to allow a <literal>CREATEROLE</literal>
+ user who is not a superuser to automatically inherit, or automatically
+ gain the ability to <literal>SET ROLE</literal> to, any created users.
+ Since a <literal>CREATEROLE</literal> user is always implicitly granted
+ <literal>ADMIN OPTION</literal> on created roles, that user could
+ always execute a <literal>GRANT</literal> statement that would achieve
+ the same effect as this setting. However, it can be convenient for
+ usability reasons if the grant happens automatically. A superuser
+ automatically inherits the privileges of every role and can always
+ <literal>SET ROLE</literal> to any role, and this setting can be used
+ to produce a similar behavior for <literal>CREATEROLE</literal> users
+ for users which they create.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</sect2>
<sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 0863acbcac..7ce4e38b45 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -506,6 +506,7 @@ CREATE ROLE <replaceable class="parameter">name</replaceable> [ WITH ADMIN <repl
<member><xref linkend="sql-grant"/></member>
<member><xref linkend="sql-revoke"/></member>
<member><xref linkend="app-createuser"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index f91dc500a4..9a1c3d01f4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -555,6 +555,7 @@ PostgreSQL documentation
<simplelist type="inline">
<member><xref linkend="app-dropuser"/></member>
<member><xref linkend="sql-createrole"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 1ae2d0a66f..4d193a6f9a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -39,6 +39,7 @@
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+#include "utils/varlena.h"
/*
* Removing a role grant - or the admin option on it - might recurse to
@@ -81,8 +82,11 @@ typedef struct
#define GRANT_ROLE_SPECIFIED_INHERIT 0x0002
#define GRANT_ROLE_SPECIFIED_SET 0x0004
-/* GUC parameter */
+/* GUC parameters */
int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+char *createrole_self_grant = "";
+bool createrole_self_grant_enabled = false;
+GrantRoleOptions createrole_self_grant_options;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
@@ -532,10 +536,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (!superuser())
{
RoleSpec *current_role = makeNode(RoleSpec);
- GrantRoleOptions poptself;
+ GrantRoleOptions poptself;
+ List *memberSpecs;
+ List *memberIds = list_make1_oid(currentUserId);
current_role->roletype = ROLESPEC_CURRENT_ROLE;
current_role->location = -1;
+ memberSpecs = list_make1(current_role);
poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
| GRANT_ROLE_SPECIFIED_INHERIT
@@ -545,7 +552,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
poptself.set = false;
AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
- list_make1(current_role), list_make1_oid(GetUserId()),
+ memberSpecs, memberIds,
BOOTSTRAP_SUPERUSERID, &poptself);
/*
@@ -553,6 +560,20 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* the additional grants will fail.
*/
CommandCounterIncrement();
+
+ /*
+ * Because of the implicit grant above, a CREATEROLE user who creates
+ * a role has the ability to grant that role back to themselves with
+ * the INHERIT or SET options, if they wish to inherit the role's
+ * privileges or be able to SET ROLE to it. The createrole_self_grant
+ * GUC can be used to make this happen automatically. This has no
+ * security implications since the same user is able to make the same
+ * grant using an explicit GRANT statement; it's just convenient.
+ */
+ if (createrole_self_grant_enabled)
+ AddRoleMems(currentUserId, stmt->role, roleid,
+ memberSpecs, memberIds,
+ currentUserId, &createrole_self_grant_options);
}
/*
@@ -2414,3 +2435,73 @@ InitGrantRoleOptions(GrantRoleOptions *popt)
popt->inherit = false;
popt->set = true;
}
+
+/*
+ * GUC check_hook for createrole_self_grant
+ */
+bool
+check_createrole_self_grant(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ unsigned options = 0;
+ unsigned *result;
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+
+ if (pg_strcasecmp(tok, "SET") == 0)
+ options |= GRANT_ROLE_SPECIFIED_SET;
+ else if (pg_strcasecmp(tok, "INHERIT") == 0)
+ options |= GRANT_ROLE_SPECIFIED_INHERIT;
+ else
+ {
+ GUC_check_errdetail("Unrecognized key word: \"%s\".", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+
+ result = (unsigned *) guc_malloc(LOG, sizeof(unsigned));
+ *result = options;
+ *extra = result;
+
+ return true;
+}
+
+/*
+ * GUC assign_hook for createrole_self_grant
+ */
+void
+assign_createrole_self_grant(const char *newval, void *extra)
+{
+ unsigned options = * (unsigned *) extra;
+
+ createrole_self_grant_enabled = (options != 0);
+ createrole_self_grant_options.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ createrole_self_grant_options.admin = false;
+ createrole_self_grant_options.inherit =
+ (options & GRANT_ROLE_SPECIFIED_INHERIT) != 0;
+ createrole_self_grant_options.set =
+ (options & GRANT_ROLE_SPECIFIED_SET) != 0;
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 68328b1402..e222b087eb 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3937,6 +3937,18 @@ struct config_string ConfigureNamesString[] =
check_temp_tablespaces, assign_temp_tablespaces, NULL
},
+ {
+ {"createrole_self_grant", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets whether a CREATEROLE user automatically grants "
+ "the role to themselves, and with which options."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &createrole_self_grant,
+ "",
+ check_createrole_self_grant, assign_createrole_self_grant, NULL
+ },
+
{
{"dynamic_library_path", PGC_SUSET, CLIENT_CONN_OTHER,
gettext_noop("Sets the path for dynamically loadable modules."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5afdeb04de..cf8cc12164 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -702,6 +702,7 @@
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_pending_list_limit = 4MB
+#createrole_self_grant = '' # set and/or inherit
# - Locale and Formatting -
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 54c720d880..97dcb93791 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -15,9 +15,11 @@
#include "libpq/crypt.h"
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
+#include "utils/guc.h"
-/* GUC. Is actually of type PasswordType. */
-extern PGDLLIMPORT int Password_encryption;
+/* GUCs */
+extern PGDLLIMPORT int Password_encryption; /* values from enum PasswordType */
+extern PGDLLIMPORT char *createrole_self_grant;
/* Hook to check passwords in CreateRole() and AlterRole() */
typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
@@ -34,4 +36,8 @@ extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern bool check_createrole_self_grant(char **newval, void **extra,
+ GucSource source);
+extern void assign_createrole_self_grant(const char *newval, void *extra);
+
#endif /* USER_H */
--
2.37.1 (Apple Git-137.1)
v3-0002-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchapplication/octet-stream; name=v3-0002-Pass-down-current-user-ID-to-AddRoleMems-and-DelR.patchDownload
From 7984e5e1d61f2eca36516e20d8575ee678a5b28d Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 21 Nov 2022 14:46:03 -0500
Subject: [PATCH v3 2/4] Pass down current user ID to AddRoleMems and
DelRoleMems.
This is just refactoring; there should be no functonal change.
---
src/backend/commands/user.c | 41 ++++++++++++++++++++-----------------
1 file changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 280031e6eb..bc2b95ac81 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -87,10 +87,10 @@ int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
-static void AddRoleMems(const char *rolename, Oid roleid,
+static void AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt);
-static void DelRoleMems(const char *rolename, Oid roleid,
+static void DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt,
DropBehavior behavior);
@@ -133,6 +133,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
HeapTuple tuple;
Datum new_record[Natts_pg_authid] = {0};
bool new_record_nulls[Natts_pg_authid] = {0};
+ Oid currentUserId = GetUserId();
Oid roleid;
ListCell *item;
ListCell *option;
@@ -508,8 +509,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
char *oldrolename = NameStr(oldroleform->rolname);
/* can only add this role to roles for which you have rights */
- check_role_membership_authorization(GetUserId(), oldroleid, true);
- AddRoleMems(oldrolename, oldroleid,
+ check_role_membership_authorization(currentUserId, oldroleid, true);
+ AddRoleMems(currentUserId, oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
InvalidOid, &popt);
@@ -525,12 +526,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* NB: No permissions check is required here. If you have enough rights
* to create a role, you can add any members you like.
*/
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
popt.admin = true;
- AddRoleMems(stmt->role, roleid,
+ AddRoleMems(currentUserId, stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
InvalidOid, &popt);
@@ -583,6 +584,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
Oid roleid;
+ Oid currentUserId = GetUserId();
GrantRoleOptions popt;
check_rolespec_name(stmt->role,
@@ -727,13 +729,13 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
errmsg("permission denied")));
/* without CREATEROLE, can only change your own password */
- if (dpassword && roleid != GetUserId())
+ if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
/* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\" to add members",
@@ -888,11 +890,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
CommandCounterIncrement();
if (stmt->action == +1) /* add members to role */
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt);
else if (stmt->action == -1) /* drop members from role */
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
InvalidOid, &popt, DROP_RESTRICT);
}
@@ -1378,6 +1380,7 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
List *grantee_ids;
ListCell *item;
GrantRoleOptions popt;
+ Oid currentUserId = GetUserId();
/* Parse options list. */
InitGrantRoleOptions(&popt);
@@ -1449,14 +1452,14 @@ GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
roleid = get_role_oid(rolename, false);
- check_role_membership_authorization(GetUserId(), roleid,
- stmt->is_grant);
+ check_role_membership_authorization(currentUserId,
+ roleid, stmt->is_grant);
if (stmt->is_grant)
- AddRoleMems(rolename, roleid,
+ AddRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt);
else
- DelRoleMems(rolename, roleid,
+ DelRoleMems(currentUserId, rolename, roleid,
stmt->grantee_roles, grantee_ids,
grantor, &popt, stmt->behavior);
}
@@ -1555,15 +1558,17 @@ roleSpecsToIds(List *memberNames)
/*
* AddRoleMems -- Add given members to the specified role
*
+ * currentUserId: OID of role performing the operation
* rolename: name of role to add to (used only for error messages)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership (InvalidOid if not set explicitly)
+ * grantorId: OID that should be recorded as having granted the membership
+ * (InvalidOid if not set explicitly)
* popt: information about grant options
*/
static void
-AddRoleMems(const char *rolename, Oid roleid,
+AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt)
{
@@ -1571,7 +1576,6 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1859,7 +1863,7 @@ AddRoleMems(const char *rolename, Oid roleid,
* behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
-DelRoleMems(const char *rolename, Oid roleid,
+DelRoleMems(Oid currentUserId, const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
{
@@ -1867,7 +1871,6 @@ DelRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
- Oid currentUserId = GetUserId();
CatCList *memlist;
RevokeRoleGrantAction *actions;
int i;
--
2.37.1 (Apple Git-137.1)
v3-0003-Restrict-the-privileges-of-CREATEROLE-users.patchapplication/octet-stream; name=v3-0003-Restrict-the-privileges-of-CREATEROLE-users.patchDownload
From 04510f4c721c369d0f4efc8b7b16bb651afa1dcc Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 1 Dec 2022 15:07:02 -0500
Subject: [PATCH v3 3/4] Restrict the privileges of CREATEROLE users.
Previously, CREATEROLE users were permitted to make nearly arbitrary
changes to roles that they didn't create, with certain exceptions,
particularly superuser roles. Instead, allow CREATEROLE users to make such
changes to roles for which they possess ADMIN OPTION, and to
grant membership only in roles for which they possess ADMIN OPTION.
When a CREATEROLE user who is not a superuser creates a role, grant
ADMIN OPTION on the newly-created role to the creator, so that they
can administer roles they create or for which they have been given
privileges.
With these changes, CREATEROLE users still have very significant
powers that unprivileged users do not receive: they can alter, rename,
drop, comment on, change the password for, and change security labels
on roles. However, they can now do these things only for roles for
which they possess appropriate privileges, rather than all
non-superuser roles; moreover, they cannot grant a role such as
pg_execute_server_program unless they themselves possess it.
FIXME: Add more regression tests.
---
doc/src/sgml/ddl.sgml | 10 +-
doc/src/sgml/ref/alter_role.sgml | 8 +-
doc/src/sgml/ref/comment.sgml | 3 +-
doc/src/sgml/ref/create_role.sgml | 4 +-
doc/src/sgml/ref/createuser.sgml | 3 +-
doc/src/sgml/ref/drop_role.sgml | 2 +-
doc/src/sgml/ref/dropuser.sgml | 7 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++--
src/backend/catalog/objectaddress.c | 10 +-
src/backend/commands/user.c | 100 +++++++++++++-----
.../expected/dummy_seclabel.out | 17 +--
.../dummy_seclabel/sql/dummy_seclabel.sql | 13 ++-
src/test/regress/expected/create_role.out | 37 ++++---
src/test/regress/sql/create_role.sql | 23 ++--
15 files changed, 181 insertions(+), 104 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 6e92bbddd2..ae88266469 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3216,13 +3216,11 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
name. Therefore, if each user has a separate schema, they access
their own schemas by default.) This pattern is a secure schema
usage pattern unless an untrusted user is the database owner or
- holds the <literal>CREATEROLE</literal> privilege, in which case no
- secure schema usage pattern exists.
+ has been granted <literal>ADMIN OPTION</literal> on a relevant role,
+ in which case no secure schema usage pattern exists.
</para>
<!-- A database owner can attack the database's users via "CREATE SCHEMA
- trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". A
- CREATEROLE user can issue "GRANT $dbowner TO $me" and then use the
- database owner attack. -->
+ trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". -->
<para>
In <productname>PostgreSQL</productname> 15 and later, the default
@@ -3250,7 +3248,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
unreliable</link>. If you create functions or extensions in the public
schema, use the first pattern instead. Otherwise, like the first
pattern, this is secure unless an untrusted user is the database owner
- or holds the <literal>CREATEROLE</literal> privilege.
+ or has been granted <literal>ADMIN OPTION</literal> on a relevant role.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 8a8f828137..6a865f21fd 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -73,7 +73,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
Roles having <literal>CREATEROLE</literal> privilege can change any of these
settings except <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>,
and <literal>BYPASSRLS</literal>; but only for non-superuser and
- non-replication roles.
+ non-replication roles for which they have been
+ granted <literal>ADMIN OPTION</literal>.
Ordinary roles can only change their own password.
</para>
@@ -81,7 +82,7 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
The second variant changes the name of the role.
Database superusers can rename any role.
Roles having <literal>CREATEROLE</literal> privilege can rename non-superuser
- roles.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
The current session user cannot be renamed.
(Connect as a different user if you need to do that.)
Because <literal>MD5</literal>-encrypted passwords use the role name as
@@ -116,7 +117,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<para>
Superusers can change anyone's session defaults. Roles having
<literal>CREATEROLE</literal> privilege can change defaults for non-superuser
- roles. Ordinary roles can only set defaults for themselves.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
+ Ordinary roles can only set defaults for themselves.
Certain configuration variables cannot be set this way, or can only be
set if a superuser issues the command. Only superusers can change a setting
for all roles in all databases.
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 23d9029af9..7499da1d62 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -99,7 +99,8 @@ COMMENT ON
For most kinds of object, only the object's owner can set the comment.
Roles don't have owners, so the rule for <literal>COMMENT ON ROLE</literal> is
that you must be superuser to comment on a superuser role, or have the
- <literal>CREATEROLE</literal> privilege to comment on non-superuser roles.
+ <literal>CREATEROLE</literal> privilege and have been granted
+ <literal>ADMIN OPTION</literal> on the target role.
Likewise, access methods don't have owners either; you must be superuser
to comment on an access method.
Of course, a superuser can comment on anything.
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 1ccc832558..0863acbcac 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -119,8 +119,8 @@ in sync when changing the above synopsis!
<listitem>
<para>
These clauses determine whether a role will be permitted to
- create, alter, drop, comment on, change the security label for,
- and grant or revoke membership in other roles.
+ create, alter, drop, comment on, and change the security label for
+ other roles.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
If not specified, <literal>NOCREATEROLE</literal> is the default.
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index a41a2b24e6..f91dc500a4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -252,8 +252,7 @@ PostgreSQL documentation
<listitem>
<para>
The new user will be allowed to create, alter, drop, comment on,
- change the security label for, and grant or revoke membership in
- other roles; that is,
+ change the security label for other roles; that is,
this user will have <literal>CREATEROLE</literal> privilege.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..cbcb3cd3d3 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -32,7 +32,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<command>DROP ROLE</command> removes the specified role(s).
To drop a superuser role, you must be a superuser yourself;
to drop non-superuser roles, you must have <literal>CREATEROLE</literal>
- privilege.
+ privilege and have been granted <literal>ADMIN OPTION</literal> on the role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..b6be26d5b0 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,10 @@ PostgreSQL documentation
<para>
<application>dropuser</application> removes an existing
<productname>PostgreSQL</productname> user.
- Only superusers and users with the <literal>CREATEROLE</literal> privilege can
- remove <productname>PostgreSQL</productname> users. (To remove a
- superuser, you must yourself be a superuser.)
+ Superusers can use this command to remove any role; otherwise, only
+ non-superuser roles can be removed, and only by a user who possesses
+ the <literal>CREATEROLE</literal> privilege and has been granted
+ <literal>ADMIN OPTION</literal> on the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 518bdb32d8..85f5f42ea6 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -271,9 +271,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
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. Database superusers can grant or revoke
- membership in any role to anyone. Roles having
- <literal>CREATEROLE</literal> privilege can grant or revoke membership
- in any role that is not a superuser. This option defaults to
+ membership in any role to anyone. This option defaults to
<literal>FALSE</literal>.
</para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 7aa6bdac16..71a2d8f298 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -199,7 +199,12 @@ CREATE USER <replaceable>name</replaceable>;
checks). To create such a role, use <literal>CREATE ROLE
<replaceable>name</replaceable> CREATEROLE</literal>.
A role with <literal>CREATEROLE</literal> privilege can alter and drop
- other roles, too, as well as grant or revoke membership in them.
+ roles which have been granted to the <literal>CREATEROLE</literal>
+ user with the <literal>ADMIN</literal> option. Such a grant occurs
+ automatically when a <literal>CREATEROLE</literal> user that is not
+ a superuser creates a new role, so that by default, a
+ <literal>CREATEROLE</literal> user can alter and drop the roles
+ which they have created.
Altering a role includes most changes that can be made using
<literal>ALTER ROLE</literal>, including, for example, changing
passwords. It also includes modifications to a role that can
@@ -224,15 +229,6 @@ CREATE USER <replaceable>name</replaceable>;
confer the ability to grant or revoke the <literal>BYPASSRLS</literal>
privilege.
</para>
- <para>
- Because the <literal>CREATEROLE</literal> privilege allows a user
- to grant or revoke membership even in roles to which it does not (yet)
- have any access, a <literal>CREATEROLE</literal> user can obtain access
- to the capabilities of every predefined role in the system, including
- highly privileged roles such as
- <literal>pg_execute_server_program</literal> and
- <literal>pg_write_server_files</literal>.
- </para>
</listitem>
</varlistentry>
@@ -329,6 +325,34 @@ ALTER ROLE myname SET enable_indexscan TO off;
<literal>LOGIN</literal> privilege are fairly useless, since they will never
be invoked.
</para>
+
+ <para>
+ When a non-superuser creates a role using the <literal>CREATEROLE</literal>
+ privilege, the created role is automatically granted back to the creating
+ user, just as if the bootstrap superuser had executed the command
+ <literal>GRANT created_user TO creating_user WITH ADMIN TRUE, SET FALSE,
+ INHERIT FALSE</literal>. Since a <literal>CREATEROLE</literal> user can
+ only exercise special privileges with regard to an existing role if they
+ have <literal>ADMIN OPTION</literal> on it, this grant is just sufficient
+ to allow a <literal>CREATEROLE</literal> user to administer the roles they
+ created. However, because it is created with <literal>INHERIT FALSE, SET
+ FALSE</literal>, the <literal>CREATEROLE</literal> user doesn't inherit the
+ privileges of the created role, nor can it access the privileges of that
+ role using <literal>SET ROLE</literal>. However, since any user who has
+ <literal>ADMIN OPTION</literal> on a role can grant membership in that
+ role to any other user, the <literal>CREATEROLE</literal> user can gain
+ access to the created role by simplying granting that role back to
+ themselves with the <literal>INHERIT</literal> and/or <literal>SET</literal>
+ options. Thus, the fact that privileges are not inherited by default nor
+ is <literal>SET ROLE</literal> granted by default is a safeguard against
+ accidents, not a security feature. Also note that, because this automatic
+ grant is granted by the bootstrap user, it cannot be removed or changed by
+ the <literal>CREATEROLE</literal> user; however, any superuser could
+ revoke it, modify it, and/or issue additional such grants to other
+ <literal>CREATEROLE</literal> users. Whichever <literal>CREATEROLE</literal>
+ users have <literal>ADMIN OPTION</literal> on a role at any given time
+ can administer it.
+ </para>
</sect1>
<sect1 id="role-membership">
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 1367b5e7c5..25c50d66fd 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2538,7 +2538,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
/*
* We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
+ * provided that they also have admin option on the role.
+ *
+ * However, superusers are only owned by superusers.
*/
if (superuser_arg(address.objectId))
{
@@ -2553,6 +2555,12 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege")));
+ if (!is_admin_of_role(roleid, address.objectId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(address.objectId,
+ true))));
}
break;
case OBJECT_TSPARSER:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index bc2b95ac81..1ae2d0a66f 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -519,6 +519,42 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
}
+ /*
+ * If the current user isn't a superuser, make them an admin of the new
+ * role so that they can administer the new object they just created.
+ * Superusers will be able to do that anyway.
+ *
+ * The grantor of record for this implicit grant is the bootstrap
+ * superuser, which means that the CREATEROLE user cannot revoke the
+ * grant. They can however grant the created role back to themselves
+ * with different options, since they enjoy ADMIN OPTION on it.
+ */
+ if (!superuser())
+ {
+ RoleSpec *current_role = makeNode(RoleSpec);
+ GrantRoleOptions poptself;
+
+ current_role->roletype = ROLESPEC_CURRENT_ROLE;
+ current_role->location = -1;
+
+ poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ poptself.admin = true;
+ poptself.inherit = false;
+ poptself.set = false;
+
+ AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
+ list_make1(current_role), list_make1_oid(GetUserId()),
+ BOOTSTRAP_SUPERUSERID, &poptself);
+
+ /*
+ * We must make the implicit grant visible to the code below, else
+ * the additional grants will fail.
+ */
+ CommandCounterIncrement();
+ }
+
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
@@ -694,9 +730,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/*
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
- * property. Otherwise, if you don't have createrole, you're only allowed
- * to (1) change your own password or (2) add members to a role for which
- * you have ADMIN OPTION.
+ * property.
*/
if (authform->rolsuper || dissuper)
{
@@ -719,29 +753,35 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+
+ /*
+ * Most changes to a role require that you both have CREATEROLE privileges
+ * and also ADMIN OPTION on the role.
+ */
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
{
- /* things you certainly can't do without CREATEROLE */
+ /* things an unprivileged user certainly can't do */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
- /* without CREATEROLE, can only change your own password */
+ /* an unprivileged user can change their own password */
if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
-
- /* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\" to add members",
- rolename)));
}
+ /* To add members to a role, you need ADMIN OPTION. */
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
+
/* Convert validuntil to internal form */
if (dvalidUntil)
{
@@ -935,8 +975,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
shdepLockAndCheckObject(AuthIdRelationId, roleid);
/*
- * To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * To mess with a superuser you gotta be superuser; otherwise you
+ * need CREATEROLE plus admin option on the target role; unless you're
+ * just trying to change your own settings
*/
if (roleform->rolsuper)
{
@@ -947,7 +988,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if ((!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
+ && roleid != GetUserId())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1067,13 +1110,18 @@ DropRole(DropRoleStmt *stmt)
/*
* For safety's sake, we allow createrole holders to drop ordinary
- * roles but not superuser roles. This is mainly to avoid the
- * scenario where you accidentally drop the last superuser.
+ * roles but not superuser roles, and only if they also have ADMIN
+ * OPTION.
*/
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ role)));
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1312,7 +1360,8 @@ RenameRole(const char *oldname, const char *newname)
errmsg("role \"%s\" already exists", newname)));
/*
- * createrole is enough privilege unless you want to mess with a superuser
+ * Only superusers can mess with superusers. Otherwise, a user with
+ * CREATEROLE can rename a role for which they have ADMIN OPTION.
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1323,7 +1372,8 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -2023,11 +2073,9 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
else
{
/*
- * Otherwise, must have createrole or admin option on the role to be
- * changed.
+ * Otherwise, must have admin option on the role to be changed.
*/
- if (!has_createrole_privilege(currentUserId) &&
- !is_admin_of_role(currentUserId, roleid))
+ if (!is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -2049,7 +2097,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
* be passed as InvalidOid, and this function will infer the user to be
* recorded as the grantor. In many cases, this will be the current user, but
* things get more complicated when the current user doesn't possess ADMIN
- * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * OPTION on the role but rather relies on having SUPERUSER privileges, or
* on inheriting the privileges of a role which does have ADMIN OPTION. See
* below for details.
*
@@ -2075,7 +2123,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
* not depend on any other existing grants, so always default to this
* interpretation when possible.
*/
- if (has_createrole_privilege(currentUserId))
+ if (superuser_arg(currentUserId))
return BOOTSTRAP_SUPERUSERID;
/*
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..c57d4fd2df 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -6,9 +6,11 @@ CREATE EXTENSION dummy_seclabel;
SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2;
@@ -16,6 +18,8 @@ CREATE FUNCTION dummy_seclabel_four() RETURNS integer AS $$SELECT 4$$ language s
CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
--
@@ -43,16 +47,16 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
-- Test for shared database object
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
ERROR: only superuser can set 'secret' label
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
-ERROR: role "regress_dummy_seclabel_user3" does not exist
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
+ERROR: role "regress_dummy_seclabel_user4" does not exist
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
ERROR: must have CREATEROLE privilege
@@ -81,8 +85,8 @@ SELECT objtype, objname, provider, label FROM pg_seclabels
domain | dummy_seclabel_domain | dummy | classified
function | dummy_seclabel_four() | dummy | classified
publication | dummy_pub | dummy | classified
- role | regress_dummy_seclabel_user1 | dummy | classified
role | regress_dummy_seclabel_user2 | dummy | unclassified
+ role | regress_dummy_seclabel_user3 | dummy | classified
schema | dummy_seclabel_test | dummy | unclassified
subscription | dummy_sub | dummy | classified
table | dummy_seclabel_tbl1 | dummy | top secret
@@ -115,3 +119,4 @@ DROP SUBSCRIPTION dummy_sub;
DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..649409757e 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -8,11 +8,13 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -22,6 +24,8 @@ CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
@@ -47,12 +51,12 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
@@ -113,3 +117,4 @@ DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..6e0cce4579 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -13,7 +13,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -70,20 +70,35 @@ ALTER VIEW tenant_view OWNER TO regress_role_admin;
ERROR: must be owner of view tenant_view
DROP VIEW tenant_view;
ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
ERROR: permission denied to reassign objects
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_monitor IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
+DETAIL: owner of table tenant_table
+owner of view tenant_view
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -114,22 +129,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
-DETAIL: owner of table tenant_table
-owner of view tenant_view
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..d491684db3 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -11,7 +11,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -71,10 +71,10 @@ DROP TABLE tenant_table;
ALTER VIEW tenant_view OWNER TO regress_role_admin;
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
CREATE ROLE regress_monitor IN ROLE pg_monitor;
@@ -86,6 +86,9 @@ CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -109,20 +112,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
-
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.37.1 (Apple Git-137.1)
On Tue, Jan 3, 2023 at 3:11 PM Robert Haas <robertmhaas@gmail.com> wrote:
Committed and back-patched 0001 with fixes for the issues that you pointed out.
Here's a trivial rebase of the rest of the patch set.
I committed 0001 and 0002 after improving the commit messages a bit.
Here's the remaining two patches back. I've done a bit more polishing
of these as well, specifically in terms of fleshing out the regression
tests. I'd like to move forward with these soon, if nobody's too
vehemently opposed to that.
Previous feedback, especially from Tom but also others, was that the
role-level properties the final patch was creating were not good. Now
it doesn't create any new role-level properties, and in fact it has
nothing to say about role-level properties in any way. That might not
be the right thing. Right now, if you have CREATEROLE, you can create
new roles with any combination of attributes you like, except that you
cannot set the SUPERUSER, REPLICATION, or BYPASSRLS properties. While
I think it makes sense that a CREATEROLE user can't hand out SUPERUSER
or REPLICATION privileges, it is really not obvious to me why a
CREATEROLE user shouldn't be permitted to hand out BYPASSRLS, at least
if they have it themselves, and right now there's no way to allow
that. On the other hand, I think that some superusers might want to
restrict a CREATEROLE user's ability to hand out CREATEROLE or
CREATEDB to the users they create, and right now there's no way to
prohibit that.
I don't have a great idea about what a system for handling this
problem ought to look like. In a vacuum, I think it would be
reasonable to change CREATEROLE to only allow CREATEDB, BYPASSRLS, and
similar to be given to new users if the creating user possesses them,
but that approach does not work for CREATEROLE, because if you didn't
have that, you couldn't create any new users at all. It's also pretty
weird for, say, CONNECTION LIMIT. I doubt that there's any connection
between the CONNECTION LIMIT of the CREATEROLE user and the values
that they ought to be able to set for users that they create. Probably
you just want to allow setting CONNECTION LIMIT for downstream users,
or not. Or maybe it's not even worth worrying about -- I think there
might be a decent argument that limiting the ability to set CONNECTION
LIMIT just isn't interesting.
If someone else has a good idea what we ought to do about this part of
the problem, I'd be interested to hear it. Absent such a good idea --
or if that good idea is more work to implement that can be done in the
near term -- I think it would be OK to ship as much as I've done here
and revisit the topic at some later point when we've had a chance to
absorb user feedback.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v4-0001-Restrict-the-privileges-of-CREATEROLE-users.patchapplication/octet-stream; name=v4-0001-Restrict-the-privileges-of-CREATEROLE-users.patchDownload
From ff0407e48fd5505657d96053eeae81fe512a62ff Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 1 Dec 2022 15:07:02 -0500
Subject: [PATCH v4 1/2] Restrict the privileges of CREATEROLE users.
Previously, CREATEROLE users were permitted to make nearly arbitrary
changes to roles that they didn't create, with certain exceptions,
particularly superuser roles. Instead, allow CREATEROLE users to make such
changes to roles for which they possess ADMIN OPTION, and to
grant membership only in roles for which they possess ADMIN OPTION.
When a CREATEROLE user who is not a superuser creates a role, grant
ADMIN OPTION on the newly-created role to the creator, so that they
can administer roles they create or for which they have been given
privileges.
With these changes, CREATEROLE users still have very significant
powers that unprivileged users do not receive: they can alter, rename,
drop, comment on, change the password for, and change security labels
on roles. However, they can now do these things only for roles for
which they possess appropriate privileges, rather than all
non-superuser roles; moreover, they cannot grant a role such as
pg_execute_server_program unless they themselves possess it.
---
doc/src/sgml/ddl.sgml | 10 +-
doc/src/sgml/ref/alter_role.sgml | 8 +-
doc/src/sgml/ref/comment.sgml | 3 +-
doc/src/sgml/ref/create_role.sgml | 4 +-
doc/src/sgml/ref/createuser.sgml | 3 +-
doc/src/sgml/ref/drop_role.sgml | 2 +-
doc/src/sgml/ref/dropuser.sgml | 7 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++--
src/backend/catalog/objectaddress.c | 10 +-
src/backend/commands/user.c | 100 +++++++++++++-----
.../expected/dummy_seclabel.out | 17 +--
.../dummy_seclabel/sql/dummy_seclabel.sql | 13 ++-
src/test/regress/expected/create_role.out | 53 ++++++----
src/test/regress/sql/create_role.sql | 38 +++----
15 files changed, 210 insertions(+), 106 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 6e92bbddd2..ae88266469 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3216,13 +3216,11 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
name. Therefore, if each user has a separate schema, they access
their own schemas by default.) This pattern is a secure schema
usage pattern unless an untrusted user is the database owner or
- holds the <literal>CREATEROLE</literal> privilege, in which case no
- secure schema usage pattern exists.
+ has been granted <literal>ADMIN OPTION</literal> on a relevant role,
+ in which case no secure schema usage pattern exists.
</para>
<!-- A database owner can attack the database's users via "CREATE SCHEMA
- trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". A
- CREATEROLE user can issue "GRANT $dbowner TO $me" and then use the
- database owner attack. -->
+ trojan; ALTER DATABASE $mydb SET search_path = trojan, public;". -->
<para>
In <productname>PostgreSQL</productname> 15 and later, the default
@@ -3250,7 +3248,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
unreliable</link>. If you create functions or extensions in the public
schema, use the first pattern instead. Otherwise, like the first
pattern, this is secure unless an untrusted user is the database owner
- or holds the <literal>CREATEROLE</literal> privilege.
+ or has been granted <literal>ADMIN OPTION</literal> on a relevant role.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 8a8f828137..6a865f21fd 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -73,7 +73,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
Roles having <literal>CREATEROLE</literal> privilege can change any of these
settings except <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>,
and <literal>BYPASSRLS</literal>; but only for non-superuser and
- non-replication roles.
+ non-replication roles for which they have been
+ granted <literal>ADMIN OPTION</literal>.
Ordinary roles can only change their own password.
</para>
@@ -81,7 +82,7 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
The second variant changes the name of the role.
Database superusers can rename any role.
Roles having <literal>CREATEROLE</literal> privilege can rename non-superuser
- roles.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
The current session user cannot be renamed.
(Connect as a different user if you need to do that.)
Because <literal>MD5</literal>-encrypted passwords use the role name as
@@ -116,7 +117,8 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<para>
Superusers can change anyone's session defaults. Roles having
<literal>CREATEROLE</literal> privilege can change defaults for non-superuser
- roles. Ordinary roles can only set defaults for themselves.
+ roles for which they have been granted <literal>ADMIN OPTION</literal>.
+ Ordinary roles can only set defaults for themselves.
Certain configuration variables cannot be set this way, or can only be
set if a superuser issues the command. Only superusers can change a setting
for all roles in all databases.
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 23d9029af9..7499da1d62 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -99,7 +99,8 @@ COMMENT ON
For most kinds of object, only the object's owner can set the comment.
Roles don't have owners, so the rule for <literal>COMMENT ON ROLE</literal> is
that you must be superuser to comment on a superuser role, or have the
- <literal>CREATEROLE</literal> privilege to comment on non-superuser roles.
+ <literal>CREATEROLE</literal> privilege and have been granted
+ <literal>ADMIN OPTION</literal> on the target role.
Likewise, access methods don't have owners either; you must be superuser
to comment on an access method.
Of course, a superuser can comment on anything.
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 1ccc832558..0863acbcac 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -119,8 +119,8 @@ in sync when changing the above synopsis!
<listitem>
<para>
These clauses determine whether a role will be permitted to
- create, alter, drop, comment on, change the security label for,
- and grant or revoke membership in other roles.
+ create, alter, drop, comment on, and change the security label for
+ other roles.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
If not specified, <literal>NOCREATEROLE</literal> is the default.
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index a41a2b24e6..f91dc500a4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -252,8 +252,7 @@ PostgreSQL documentation
<listitem>
<para>
The new user will be allowed to create, alter, drop, comment on,
- change the security label for, and grant or revoke membership in
- other roles; that is,
+ change the security label for other roles; that is,
this user will have <literal>CREATEROLE</literal> privilege.
See <xref linkend='role-creation' /> for more details about what
capabilities are conferred by this privilege.
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..cbcb3cd3d3 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -32,7 +32,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<command>DROP ROLE</command> removes the specified role(s).
To drop a superuser role, you must be a superuser yourself;
to drop non-superuser roles, you must have <literal>CREATEROLE</literal>
- privilege.
+ privilege and have been granted <literal>ADMIN OPTION</literal> on the role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..b6be26d5b0 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,10 @@ PostgreSQL documentation
<para>
<application>dropuser</application> removes an existing
<productname>PostgreSQL</productname> user.
- Only superusers and users with the <literal>CREATEROLE</literal> privilege can
- remove <productname>PostgreSQL</productname> users. (To remove a
- superuser, you must yourself be a superuser.)
+ Superusers can use this command to remove any role; otherwise, only
+ non-superuser roles can be removed, and only by a user who possesses
+ the <literal>CREATEROLE</literal> privilege and has been granted
+ <literal>ADMIN OPTION</literal> on the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 518bdb32d8..85f5f42ea6 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -271,9 +271,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
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. Database superusers can grant or revoke
- membership in any role to anyone. Roles having
- <literal>CREATEROLE</literal> privilege can grant or revoke membership
- in any role that is not a superuser. This option defaults to
+ membership in any role to anyone. This option defaults to
<literal>FALSE</literal>.
</para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 7aa6bdac16..71a2d8f298 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -199,7 +199,12 @@ CREATE USER <replaceable>name</replaceable>;
checks). To create such a role, use <literal>CREATE ROLE
<replaceable>name</replaceable> CREATEROLE</literal>.
A role with <literal>CREATEROLE</literal> privilege can alter and drop
- other roles, too, as well as grant or revoke membership in them.
+ roles which have been granted to the <literal>CREATEROLE</literal>
+ user with the <literal>ADMIN</literal> option. Such a grant occurs
+ automatically when a <literal>CREATEROLE</literal> user that is not
+ a superuser creates a new role, so that by default, a
+ <literal>CREATEROLE</literal> user can alter and drop the roles
+ which they have created.
Altering a role includes most changes that can be made using
<literal>ALTER ROLE</literal>, including, for example, changing
passwords. It also includes modifications to a role that can
@@ -224,15 +229,6 @@ CREATE USER <replaceable>name</replaceable>;
confer the ability to grant or revoke the <literal>BYPASSRLS</literal>
privilege.
</para>
- <para>
- Because the <literal>CREATEROLE</literal> privilege allows a user
- to grant or revoke membership even in roles to which it does not (yet)
- have any access, a <literal>CREATEROLE</literal> user can obtain access
- to the capabilities of every predefined role in the system, including
- highly privileged roles such as
- <literal>pg_execute_server_program</literal> and
- <literal>pg_write_server_files</literal>.
- </para>
</listitem>
</varlistentry>
@@ -329,6 +325,34 @@ ALTER ROLE myname SET enable_indexscan TO off;
<literal>LOGIN</literal> privilege are fairly useless, since they will never
be invoked.
</para>
+
+ <para>
+ When a non-superuser creates a role using the <literal>CREATEROLE</literal>
+ privilege, the created role is automatically granted back to the creating
+ user, just as if the bootstrap superuser had executed the command
+ <literal>GRANT created_user TO creating_user WITH ADMIN TRUE, SET FALSE,
+ INHERIT FALSE</literal>. Since a <literal>CREATEROLE</literal> user can
+ only exercise special privileges with regard to an existing role if they
+ have <literal>ADMIN OPTION</literal> on it, this grant is just sufficient
+ to allow a <literal>CREATEROLE</literal> user to administer the roles they
+ created. However, because it is created with <literal>INHERIT FALSE, SET
+ FALSE</literal>, the <literal>CREATEROLE</literal> user doesn't inherit the
+ privileges of the created role, nor can it access the privileges of that
+ role using <literal>SET ROLE</literal>. However, since any user who has
+ <literal>ADMIN OPTION</literal> on a role can grant membership in that
+ role to any other user, the <literal>CREATEROLE</literal> user can gain
+ access to the created role by simplying granting that role back to
+ themselves with the <literal>INHERIT</literal> and/or <literal>SET</literal>
+ options. Thus, the fact that privileges are not inherited by default nor
+ is <literal>SET ROLE</literal> granted by default is a safeguard against
+ accidents, not a security feature. Also note that, because this automatic
+ grant is granted by the bootstrap user, it cannot be removed or changed by
+ the <literal>CREATEROLE</literal> user; however, any superuser could
+ revoke it, modify it, and/or issue additional such grants to other
+ <literal>CREATEROLE</literal> users. Whichever <literal>CREATEROLE</literal>
+ users have <literal>ADMIN OPTION</literal> on a role at any given time
+ can administer it.
+ </para>
</sect1>
<sect1 id="role-membership">
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 1367b5e7c5..25c50d66fd 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2538,7 +2538,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
/*
* We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
+ * provided that they also have admin option on the role.
+ *
+ * However, superusers are only owned by superusers.
*/
if (superuser_arg(address.objectId))
{
@@ -2553,6 +2555,12 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege")));
+ if (!is_admin_of_role(roleid, address.objectId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ GetUserNameFromId(address.objectId,
+ true))));
}
break;
case OBJECT_TSPARSER:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index bc2b95ac81..1ae2d0a66f 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -519,6 +519,42 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
}
+ /*
+ * If the current user isn't a superuser, make them an admin of the new
+ * role so that they can administer the new object they just created.
+ * Superusers will be able to do that anyway.
+ *
+ * The grantor of record for this implicit grant is the bootstrap
+ * superuser, which means that the CREATEROLE user cannot revoke the
+ * grant. They can however grant the created role back to themselves
+ * with different options, since they enjoy ADMIN OPTION on it.
+ */
+ if (!superuser())
+ {
+ RoleSpec *current_role = makeNode(RoleSpec);
+ GrantRoleOptions poptself;
+
+ current_role->roletype = ROLESPEC_CURRENT_ROLE;
+ current_role->location = -1;
+
+ poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ poptself.admin = true;
+ poptself.inherit = false;
+ poptself.set = false;
+
+ AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
+ list_make1(current_role), list_make1_oid(GetUserId()),
+ BOOTSTRAP_SUPERUSERID, &poptself);
+
+ /*
+ * We must make the implicit grant visible to the code below, else
+ * the additional grants will fail.
+ */
+ CommandCounterIncrement();
+ }
+
/*
* Add the specified members to this new role. adminmembers get the admin
* option, rolemembers don't.
@@ -694,9 +730,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/*
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
- * property. Otherwise, if you don't have createrole, you're only allowed
- * to (1) change your own password or (2) add members to a role for which
- * you have ADMIN OPTION.
+ * property.
*/
if (authform->rolsuper || dissuper)
{
@@ -719,29 +753,35 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+
+ /*
+ * Most changes to a role require that you both have CREATEROLE privileges
+ * and also ADMIN OPTION on the role.
+ */
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
{
- /* things you certainly can't do without CREATEROLE */
+ /* things an unprivileged user certainly can't do */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
- /* without CREATEROLE, can only change your own password */
+ /* an unprivileged user can change their own password */
if (dpassword && roleid != currentUserId)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have CREATEROLE privilege to change another user's password")));
-
- /* without CREATEROLE, can only add members to roles you admin */
- if (drolemembers && !is_admin_of_role(currentUserId, roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have admin option on role \"%s\" to add members",
- rolename)));
}
+ /* To add members to a role, you need ADMIN OPTION. */
+ if (drolemembers && !is_admin_of_role(currentUserId, roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
+
/* Convert validuntil to internal form */
if (dvalidUntil)
{
@@ -935,8 +975,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
shdepLockAndCheckObject(AuthIdRelationId, roleid);
/*
- * To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * To mess with a superuser you gotta be superuser; otherwise you
+ * need CREATEROLE plus admin option on the target role; unless you're
+ * just trying to change your own settings
*/
if (roleform->rolsuper)
{
@@ -947,7 +988,9 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if ((!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
+ && roleid != GetUserId())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1067,13 +1110,18 @@ DropRole(DropRoleStmt *stmt)
/*
* For safety's sake, we allow createrole holders to drop ordinary
- * roles but not superuser roles. This is mainly to avoid the
- * scenario where you accidentally drop the last superuser.
+ * roles but not superuser roles, and only if they also have ADMIN
+ * OPTION.
*/
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\"",
+ role)));
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1312,7 +1360,8 @@ RenameRole(const char *oldname, const char *newname)
errmsg("role \"%s\" already exists", newname)));
/*
- * createrole is enough privilege unless you want to mess with a superuser
+ * Only superusers can mess with superusers. Otherwise, a user with
+ * CREATEROLE can rename a role for which they have ADMIN OPTION.
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1323,7 +1372,8 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!have_createrole_privilege() ||
+ !is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -2023,11 +2073,9 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
else
{
/*
- * Otherwise, must have createrole or admin option on the role to be
- * changed.
+ * Otherwise, must have admin option on the role to be changed.
*/
- if (!has_createrole_privilege(currentUserId) &&
- !is_admin_of_role(currentUserId, roleid))
+ if (!is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -2049,7 +2097,7 @@ check_role_membership_authorization(Oid currentUserId, Oid roleid,
* be passed as InvalidOid, and this function will infer the user to be
* recorded as the grantor. In many cases, this will be the current user, but
* things get more complicated when the current user doesn't possess ADMIN
- * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * OPTION on the role but rather relies on having SUPERUSER privileges, or
* on inheriting the privileges of a role which does have ADMIN OPTION. See
* below for details.
*
@@ -2075,7 +2123,7 @@ check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
* not depend on any other existing grants, so always default to this
* interpretation when possible.
*/
- if (has_createrole_privilege(currentUserId))
+ if (superuser_arg(currentUserId))
return BOOTSTRAP_SUPERUSERID;
/*
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..c57d4fd2df 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -6,9 +6,11 @@ CREATE EXTENSION dummy_seclabel;
SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
CREATE VIEW dummy_seclabel_view1 AS SELECT * FROM dummy_seclabel_tbl2;
@@ -16,6 +18,8 @@ CREATE FUNCTION dummy_seclabel_four() RETURNS integer AS $$SELECT 4$$ language s
CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
--
@@ -43,16 +47,16 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
-- Test for shared database object
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
ERROR: only superuser can set 'secret' label
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
-ERROR: role "regress_dummy_seclabel_user3" does not exist
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
+ERROR: role "regress_dummy_seclabel_user4" does not exist
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
ERROR: must have CREATEROLE privilege
@@ -81,8 +85,8 @@ SELECT objtype, objname, provider, label FROM pg_seclabels
domain | dummy_seclabel_domain | dummy | classified
function | dummy_seclabel_four() | dummy | classified
publication | dummy_pub | dummy | classified
- role | regress_dummy_seclabel_user1 | dummy | classified
role | regress_dummy_seclabel_user2 | dummy | unclassified
+ role | regress_dummy_seclabel_user3 | dummy | classified
schema | dummy_seclabel_test | dummy | unclassified
subscription | dummy_sub | dummy | classified
table | dummy_seclabel_tbl1 | dummy | top secret
@@ -115,3 +119,4 @@ DROP SUBSCRIPTION dummy_sub;
DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..649409757e 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -8,11 +8,13 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
+DROP ROLE IF EXISTS regress_dummy_seclabel_user3;
RESET client_min_messages;
CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
CREATE USER regress_dummy_seclabel_user2;
+CREATE USER regress_dummy_seclabel_user3;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -22,6 +24,8 @@ CREATE DOMAIN dummy_seclabel_domain AS text;
ALTER TABLE dummy_seclabel_tbl1 OWNER TO regress_dummy_seclabel_user1;
ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
+GRANT regress_dummy_seclabel_user2, regress_dummy_seclabel_user3
+ TO regress_dummy_seclabel_user1 WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
--
-- Test of SECURITY LABEL statement with a plugin
@@ -47,12 +51,12 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'classified'; -- OK
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON ROLE regress_dummy_seclabel_user1 IS 'unclassified'; -- fail
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'secret'; -- fail (not superuser)
-SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (not found)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'secret'; -- fail (not superuser)
+SECURITY LABEL ON ROLE regress_dummy_seclabel_user4 IS 'unclassified'; -- fail (not found)
SET SESSION AUTHORIZATION regress_dummy_seclabel_user2;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user2 IS 'unclassified'; -- fail (not privileged)
@@ -113,3 +117,4 @@ DROP PUBLICATION dummy_pub;
DROP ROLE regress_dummy_seclabel_user1;
DROP ROLE regress_dummy_seclabel_user2;
+DROP ROLE regress_dummy_seclabel_user3;
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..f5f745504c 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -1,6 +1,7 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_normal;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
@@ -13,7 +14,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -51,7 +52,19 @@ CREATE ROLE regress_plainrole;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_rolecreator CREATEROLE;
-- ok, roles with CREATEROLE can create new roles with privilege they lack
-CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+CREATE ROLE regress_hasprivs CREATEDB CREATEROLE LOGIN INHERIT
+ CONNECTION LIMIT 5;
+-- ok, we should be able to modify a role we created
+COMMENT ON ROLE regress_hasprivs IS 'some comment';
+ALTER ROLE regress_hasprivs RENAME TO regress_tenant;
+ALTER ROLE regress_tenant NOINHERIT NOLOGIN CONNECTION LIMIT 7;
+-- fail, we should be unable to modify a role we did not create
+COMMENT ON ROLE regress_role_normal IS 'some comment';
+ERROR: must have admin option on role "regress_role_normal"
+ALTER ROLE regress_role_normal RENAME TO regress_role_abnormal;
+ERROR: permission denied to rename role
+ALTER ROLE regress_role_normal NOINHERIT NOLOGIN CONNECTION LIMIT 7;
+ERROR: permission denied
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -70,20 +83,35 @@ ALTER VIEW tenant_view OWNER TO regress_role_admin;
ERROR: must be owner of view tenant_view
DROP VIEW tenant_view;
ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
ERROR: permission denied to reassign objects
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_monitor IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
+DETAIL: owner of table tenant_table
+owner of view tenant_view
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -114,22 +142,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
-DETAIL: owner of table tenant_table
-owner of view tenant_view
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -143,3 +155,4 @@ DROP VIEW tenant_view;
DROP ROLE regress_tenant;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
+DROP ROLE regress_role_normal;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..ddc80578d9 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -1,6 +1,7 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_normal;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
@@ -11,7 +12,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
-CREATE ROLE regress_createrole CREATEROLE;
+CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -54,7 +55,18 @@ CREATE ROLE regress_plainrole;
CREATE ROLE regress_rolecreator CREATEROLE;
-- ok, roles with CREATEROLE can create new roles with privilege they lack
-CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+CREATE ROLE regress_hasprivs CREATEDB CREATEROLE LOGIN INHERIT
+ CONNECTION LIMIT 5;
+
+-- ok, we should be able to modify a role we created
+COMMENT ON ROLE regress_hasprivs IS 'some comment';
+ALTER ROLE regress_hasprivs RENAME TO regress_tenant;
+ALTER ROLE regress_tenant NOINHERIT NOLOGIN CONNECTION LIMIT 7;
+
+-- fail, we should be unable to modify a role we did not create
+COMMENT ON ROLE regress_role_normal IS 'some comment';
+ALTER ROLE regress_role_normal RENAME TO regress_role_abnormal;
+ALTER ROLE regress_role_normal NOINHERIT NOLOGIN CONNECTION LIMIT 7;
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
@@ -71,10 +83,10 @@ DROP TABLE tenant_table;
ALTER VIEW tenant_view OWNER TO regress_role_admin;
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
CREATE ROLE regress_monitor IN ROLE pg_monitor;
@@ -86,6 +98,9 @@ CREATE ROLE regress_write_server_files IN ROLE pg_write_server_files;
CREATE ROLE regress_execute_server_program IN ROLE pg_execute_server_program;
CREATE ROLE regress_signal_backend IN ROLE pg_signal_backend;
+-- fail, role still owns database objects
+DROP ROLE regress_tenant;
+
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_superuser;
@@ -109,20 +124,6 @@ DROP ROLE regress_password_null;
DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
-DROP ROLE regress_rolecreator;
-DROP ROLE regress_read_all_data;
-DROP ROLE regress_write_all_data;
-DROP ROLE regress_monitor;
-DROP ROLE regress_read_all_settings;
-DROP ROLE regress_read_all_stats;
-DROP ROLE regress_stat_scan_tables;
-DROP ROLE regress_read_server_files;
-DROP ROLE regress_write_server_files;
-DROP ROLE regress_execute_server_program;
-DROP ROLE regress_signal_backend;
-
--- fail, role still owns database objects
-DROP ROLE regress_tenant;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
@@ -136,3 +137,4 @@ DROP VIEW tenant_view;
DROP ROLE regress_tenant;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
+DROP ROLE regress_role_normal;
--
2.37.1 (Apple Git-137.1)
v4-0002-Add-new-GUC-createrole_self_grant.patchapplication/octet-stream; name=v4-0002-Add-new-GUC-createrole_self_grant.patchDownload
From 2310de9be1301ea1215288db43d0f9e04c522501 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Thu, 5 Jan 2023 13:56:01 -0500
Subject: [PATCH v4 2/2] Add new GUC createrole_self_grant.
Can be set to the empty string, or to either or both of "set" or
"inherit". If set to a non-empty value, a non-superuser who creates
a role (necessarily by relying up the CREATEROLE privilege) will
grant that role back to themselves with the specified options.
This isn't a security feature, because the grant that this feature
triggers can also be performed explicitly. Instead, it's a user experience
feature. A superuser would necessarily inherit the privileges of any
created role and be able to access all such roles via SET ROLE;
with this patch, you can configure createrole_self_grant = 'set, inherit'
to provide a similar experience for a user who has CREATEROLE but not
SUPERUSER.
---
doc/src/sgml/config.sgml | 33 +++++++
doc/src/sgml/ref/create_role.sgml | 1 +
doc/src/sgml/ref/createuser.sgml | 1 +
src/backend/commands/user.c | 97 ++++++++++++++++++-
src/backend/utils/misc/guc_tables.c | 12 +++
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/commands/user.h | 10 +-
src/test/regress/expected/create_role.out | 33 +++++++
src/test/regress/sql/create_role.sql | 37 +++++++
9 files changed, 220 insertions(+), 5 deletions(-)
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 05b3862d09..d86385d308 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9421,6 +9421,39 @@ SET XML OPTION { DOCUMENT | CONTENT };
</listitem>
</varlistentry>
+ <varlistentry id="guc-createrole-self-grant" xreflabel="createrole_self_grant">
+ <term><varname>createrole_self_grant</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>createrole_self_grant</varname></primary>
+ <secondary>configuration parameter</secondary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ If a user who has <literal>CREATEROLE</literal> but not
+ <literal>SUPERUSER</literal> creates a role, and if this
+ is set to a non-empty value, the newly-created role will be granted
+ to the creating user with the options specified. The value must be
+ <literal>set</literal>, <literal>inherit</literal>, or a
+ comma-separated list of these.
+ </para>
+ <para>
+ The purpose of this option is to allow a <literal>CREATEROLE</literal>
+ user who is not a superuser to automatically inherit, or automatically
+ gain the ability to <literal>SET ROLE</literal> to, any created users.
+ Since a <literal>CREATEROLE</literal> user is always implicitly granted
+ <literal>ADMIN OPTION</literal> on created roles, that user could
+ always execute a <literal>GRANT</literal> statement that would achieve
+ the same effect as this setting. However, it can be convenient for
+ usability reasons if the grant happens automatically. A superuser
+ automatically inherits the privileges of every role and can always
+ <literal>SET ROLE</literal> to any role, and this setting can be used
+ to produce a similar behavior for <literal>CREATEROLE</literal> users
+ for users which they create.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</sect2>
<sect2 id="runtime-config-client-format">
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 0863acbcac..7ce4e38b45 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -506,6 +506,7 @@ CREATE ROLE <replaceable class="parameter">name</replaceable> [ WITH ADMIN <repl
<member><xref linkend="sql-grant"/></member>
<member><xref linkend="sql-revoke"/></member>
<member><xref linkend="app-createuser"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
</refentry>
diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index f91dc500a4..9a1c3d01f4 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -555,6 +555,7 @@ PostgreSQL documentation
<simplelist type="inline">
<member><xref linkend="app-dropuser"/></member>
<member><xref linkend="sql-createrole"/></member>
+ <member><xref linkend="guc-createrole-self-grant"/></member>
</simplelist>
</refsect1>
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 1ae2d0a66f..4d193a6f9a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -39,6 +39,7 @@
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+#include "utils/varlena.h"
/*
* Removing a role grant - or the admin option on it - might recurse to
@@ -81,8 +82,11 @@ typedef struct
#define GRANT_ROLE_SPECIFIED_INHERIT 0x0002
#define GRANT_ROLE_SPECIFIED_SET 0x0004
-/* GUC parameter */
+/* GUC parameters */
int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
+char *createrole_self_grant = "";
+bool createrole_self_grant_enabled = false;
+GrantRoleOptions createrole_self_grant_options;
/* Hook to check passwords in CreateRole() and AlterRole() */
check_password_hook_type check_password_hook = NULL;
@@ -532,10 +536,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (!superuser())
{
RoleSpec *current_role = makeNode(RoleSpec);
- GrantRoleOptions poptself;
+ GrantRoleOptions poptself;
+ List *memberSpecs;
+ List *memberIds = list_make1_oid(currentUserId);
current_role->roletype = ROLESPEC_CURRENT_ROLE;
current_role->location = -1;
+ memberSpecs = list_make1(current_role);
poptself.specified = GRANT_ROLE_SPECIFIED_ADMIN
| GRANT_ROLE_SPECIFIED_INHERIT
@@ -545,7 +552,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
poptself.set = false;
AddRoleMems(BOOTSTRAP_SUPERUSERID, stmt->role, roleid,
- list_make1(current_role), list_make1_oid(GetUserId()),
+ memberSpecs, memberIds,
BOOTSTRAP_SUPERUSERID, &poptself);
/*
@@ -553,6 +560,20 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
* the additional grants will fail.
*/
CommandCounterIncrement();
+
+ /*
+ * Because of the implicit grant above, a CREATEROLE user who creates
+ * a role has the ability to grant that role back to themselves with
+ * the INHERIT or SET options, if they wish to inherit the role's
+ * privileges or be able to SET ROLE to it. The createrole_self_grant
+ * GUC can be used to make this happen automatically. This has no
+ * security implications since the same user is able to make the same
+ * grant using an explicit GRANT statement; it's just convenient.
+ */
+ if (createrole_self_grant_enabled)
+ AddRoleMems(currentUserId, stmt->role, roleid,
+ memberSpecs, memberIds,
+ currentUserId, &createrole_self_grant_options);
}
/*
@@ -2414,3 +2435,73 @@ InitGrantRoleOptions(GrantRoleOptions *popt)
popt->inherit = false;
popt->set = true;
}
+
+/*
+ * GUC check_hook for createrole_self_grant
+ */
+bool
+check_createrole_self_grant(char **newval, void **extra, GucSource source)
+{
+ char *rawstring;
+ List *elemlist;
+ ListCell *l;
+ unsigned options = 0;
+ unsigned *result;
+
+ /* Need a modifiable copy of string */
+ rawstring = pstrdup(*newval);
+
+ if (!SplitIdentifierString(rawstring, ',', &elemlist))
+ {
+ /* syntax error in list */
+ GUC_check_errdetail("List syntax is invalid.");
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+
+ foreach(l, elemlist)
+ {
+ char *tok = (char *) lfirst(l);
+
+ if (pg_strcasecmp(tok, "SET") == 0)
+ options |= GRANT_ROLE_SPECIFIED_SET;
+ else if (pg_strcasecmp(tok, "INHERIT") == 0)
+ options |= GRANT_ROLE_SPECIFIED_INHERIT;
+ else
+ {
+ GUC_check_errdetail("Unrecognized key word: \"%s\".", tok);
+ pfree(rawstring);
+ list_free(elemlist);
+ return false;
+ }
+ }
+
+ pfree(rawstring);
+ list_free(elemlist);
+
+ result = (unsigned *) guc_malloc(LOG, sizeof(unsigned));
+ *result = options;
+ *extra = result;
+
+ return true;
+}
+
+/*
+ * GUC assign_hook for createrole_self_grant
+ */
+void
+assign_createrole_self_grant(const char *newval, void *extra)
+{
+ unsigned options = * (unsigned *) extra;
+
+ createrole_self_grant_enabled = (options != 0);
+ createrole_self_grant_options.specified = GRANT_ROLE_SPECIFIED_ADMIN
+ | GRANT_ROLE_SPECIFIED_INHERIT
+ | GRANT_ROLE_SPECIFIED_SET;
+ createrole_self_grant_options.admin = false;
+ createrole_self_grant_options.inherit =
+ (options & GRANT_ROLE_SPECIFIED_INHERIT) != 0;
+ createrole_self_grant_options.set =
+ (options & GRANT_ROLE_SPECIFIED_SET) != 0;
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 68328b1402..e222b087eb 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3937,6 +3937,18 @@ struct config_string ConfigureNamesString[] =
check_temp_tablespaces, assign_temp_tablespaces, NULL
},
+ {
+ {"createrole_self_grant", PGC_USERSET, CLIENT_CONN_STATEMENT,
+ gettext_noop("Sets whether a CREATEROLE user automatically grants "
+ "the role to themselves, and with which options."),
+ NULL,
+ GUC_LIST_INPUT
+ },
+ &createrole_self_grant,
+ "",
+ check_createrole_self_grant, assign_createrole_self_grant, NULL
+ },
+
{
{"dynamic_library_path", PGC_SUSET, CLIENT_CONN_OTHER,
gettext_noop("Sets the path for dynamically loadable modules."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5afdeb04de..cf8cc12164 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -702,6 +702,7 @@
#xmlbinary = 'base64'
#xmloption = 'content'
#gin_pending_list_limit = 4MB
+#createrole_self_grant = '' # set and/or inherit
# - Locale and Formatting -
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 54c720d880..97dcb93791 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -15,9 +15,11 @@
#include "libpq/crypt.h"
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
+#include "utils/guc.h"
-/* GUC. Is actually of type PasswordType. */
-extern PGDLLIMPORT int Password_encryption;
+/* GUCs */
+extern PGDLLIMPORT int Password_encryption; /* values from enum PasswordType */
+extern PGDLLIMPORT char *createrole_self_grant;
/* Hook to check passwords in CreateRole() and AlterRole() */
typedef void (*check_password_hook_type) (const char *username, const char *shadow_pass, PasswordType password_type, Datum validuntil_time, bool validuntil_null);
@@ -34,4 +36,8 @@ extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern bool check_createrole_self_grant(char **newval, void **extra,
+ GucSource source);
+extern void assign_createrole_self_grant(const char *newval, void *extra);
+
#endif /* USER_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index f5f745504c..bed3749888 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -1,6 +1,7 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_admin WITH GRANT OPTION;
CREATE ROLE regress_role_normal;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
@@ -15,6 +16,7 @@ ERROR: must be superuser to create bypassrls users
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
+GRANT CREATE ON DATABASE regression TO regress_createrole WITH GRANT OPTION;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -83,9 +85,37 @@ ALTER VIEW tenant_view OWNER TO regress_role_admin;
ERROR: must be owner of view tenant_view
DROP VIEW tenant_view;
ERROR: must be owner of view tenant_view
+-- fail, can't create objects owned as regress_tenant
+CREATE SCHEMA regress_tenant_schema AUTHORIZATION regress_tenant;
+ERROR: must be able to SET ROLE "regress_tenant"
-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
ERROR: permission denied to reassign objects
+-- ok, create a role with a value for createrole_self_grant
+SET createrole_self_grant = 'set, inherit';
+CREATE ROLE regress_tenant2;
+GRANT CREATE ON DATABASE regression TO regress_tenant2;
+-- ok, regress_tenant2 can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant2;
+CREATE TABLE tenant2_table (i integer);
+REVOKE ALL PRIVILEGES ON tenant2_table FROM PUBLIC;
+-- ok, because we have SET and INHERIT on regress_tenant2
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE SCHEMA regress_tenant2_schema AUTHORIZATION regress_tenant2;
+ALTER SCHEMA regress_tenant2_schema OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_tenant2;
+-- with SET but not INHERIT, we can give away objects but not take them
+REVOKE INHERIT OPTION FOR regress_tenant2 FROM regress_createrole;
+ALTER SCHEMA regress_tenant2_schema OWNER TO regress_tenant2;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+ERROR: must be owner of table tenant2_table
+-- with INHERIT but not SET, we can take objects but not give them away
+GRANT regress_tenant2 TO regress_createrole WITH INHERIT TRUE, SET FALSE;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_tenant2;
+ERROR: must be able to SET ROLE "regress_tenant2"
+DROP TABLE tenant2_table;
-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"
@@ -131,6 +161,8 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- must revoke privileges before dropping role
+REVOKE CREATE ON DATABASE regression FROM regress_createrole CASCADE;
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
DROP ROLE regress_createrole;
@@ -149,6 +181,7 @@ DROP ROLE regress_role_admin;
ERROR: current user cannot be dropped
-- ok
RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin CASCADE;
DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index ddc80578d9..edaed43588 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -1,6 +1,7 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_admin WITH GRANT OPTION;
CREATE ROLE regress_role_normal;
-- fail, only superusers can create users with these privileges
@@ -13,6 +14,7 @@ CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE NOINHERIT;
+GRANT CREATE ON DATABASE regression TO regress_createrole WITH GRANT OPTION;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
@@ -83,9 +85,40 @@ DROP TABLE tenant_table;
ALTER VIEW tenant_view OWNER TO regress_role_admin;
DROP VIEW tenant_view;
+-- fail, can't create objects owned as regress_tenant
+CREATE SCHEMA regress_tenant_schema AUTHORIZATION regress_tenant;
+
-- fail, we don't inherit permissions from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
+-- ok, create a role with a value for createrole_self_grant
+SET createrole_self_grant = 'set, inherit';
+CREATE ROLE regress_tenant2;
+GRANT CREATE ON DATABASE regression TO regress_tenant2;
+
+-- ok, regress_tenant2 can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant2;
+CREATE TABLE tenant2_table (i integer);
+REVOKE ALL PRIVILEGES ON tenant2_table FROM PUBLIC;
+
+-- ok, because we have SET and INHERIT on regress_tenant2
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE SCHEMA regress_tenant2_schema AUTHORIZATION regress_tenant2;
+ALTER SCHEMA regress_tenant2_schema OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_tenant2;
+
+-- with SET but not INHERIT, we can give away objects but not take them
+REVOKE INHERIT OPTION FOR regress_tenant2 FROM regress_createrole;
+ALTER SCHEMA regress_tenant2_schema OWNER TO regress_tenant2;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+
+-- with INHERIT but not SET, we can take objects but not give them away
+GRANT regress_tenant2 TO regress_createrole WITH INHERIT TRUE, SET FALSE;
+ALTER TABLE tenant2_table OWNER TO regress_createrole;
+ALTER TABLE tenant2_table OWNER TO regress_tenant2;
+DROP TABLE tenant2_table;
+
-- fail, CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
@@ -113,6 +146,9 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- must revoke privileges before dropping role
+REVOKE CREATE ON DATABASE regression FROM regress_createrole CASCADE;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
DROP ROLE regress_createrole;
@@ -131,6 +167,7 @@ DROP ROLE regress_role_admin;
-- ok
RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin CASCADE;
DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
--
2.37.1 (Apple Git-137.1)
On Thu, Jan 5, 2023 at 2:53 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Tue, Jan 3, 2023 at 3:11 PM Robert Haas <robertmhaas@gmail.com> wrote:
Committed and back-patched 0001 with fixes for the issues that you pointed out.
Here's a trivial rebase of the rest of the patch set.
I committed 0001 and 0002 after improving the commit messages a bit.
Here's the remaining two patches back. I've done a bit more polishing
of these as well, specifically in terms of fleshing out the regression
tests. I'd like to move forward with these soon, if nobody's too
vehemently opposed to that.
Done now.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Tue, 10 Jan 2023 at 23:16, Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Jan 5, 2023 at 2:53 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Tue, Jan 3, 2023 at 3:11 PM Robert Haas <robertmhaas@gmail.com> wrote:
Committed and back-patched 0001 with fixes for the issues that you pointed out.
Here's a trivial rebase of the rest of the patch set.
I committed 0001 and 0002 after improving the commit messages a bit.
Here's the remaining two patches back. I've done a bit more polishing
of these as well, specifically in terms of fleshing out the regression
tests. I'd like to move forward with these soon, if nobody's too
vehemently opposed to that.Done now.
I'm not sure if any work is left here, if there is nothing more to do,
can we close this?
Regards,
Vignesh
On Sat, Jan 14, 2023 at 2:26 AM vignesh C <vignesh21@gmail.com> wrote:
I'm not sure if any work is left here, if there is nothing more to do,
can we close this?
There's a discussion on another thread about some follow-up
documentation adjustments, but feel free to close the CF entry for
this patch.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Sun, 15 Jan 2023 at 06:02, Robert Haas <robertmhaas@gmail.com> wrote:
On Sat, Jan 14, 2023 at 2:26 AM vignesh C <vignesh21@gmail.com> wrote:
I'm not sure if any work is left here, if there is nothing more to do,
can we close this?There's a discussion on another thread about some follow-up
documentation adjustments, but feel free to close the CF entry for
this patch.
Thanks, I have marked the CF entry as committed.
Regards,
Vignesh
necro-ing an old thread ...
Robert Haas <robertmhaas@gmail.com> writes:
[ v4-0002-Add-new-GUC-createrole_self_grant.patch ]
I confess to not having paid close enough attention when
these patches went in, or I would have complained about
createrole_self_grant. It changes the user-visible behavior
of SQL commands, specifically CREATE ROLE. We have learned
over and over again that GUCs that do that are generally
a bad idea.
Two years later, it's perhaps too late to take it out again.
However, I'd at least like to complain about the fact that
it breaks pg_dumpall, which is surely not expecting anything
but the default behavior. If for any reason the restore is
run under a non-default setting of createrole_self_grant,
there's a potential of creating role grants that were not
there in the source database. Admittedly the damage is
probably limited by the fact that it only applies if the
restoring user has CREATEROLE but not SUPERUSER, which
I imagine is a rare case. But don't we need to add
createrole_self_grant to the set of GUCs that pg_dump[all]
resets in the emitted SQL?
regards, tom lane
On Wed, Apr 30, 2025 at 1:29 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
But don't we need to add
createrole_self_grant to the set of GUCs that pg_dump[all]
resets in the emitted SQL?
The other approach would be to do what we do for the role options and just
specify everything explicitly in the dump. The GUC is only a default
specifier so let's not leave room for defaults in the dump file.
David J.
On Wed, Apr 30, 2025 at 5:16 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
On Wed, Apr 30, 2025 at 1:29 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
But don't we need to add
createrole_self_grant to the set of GUCs that pg_dump[all]
resets in the emitted SQL?The other approach would be to do what we do for the role options and just specify everything explicitly in the dump. The GUC is only a default specifier so let's not leave room for defaults in the dump file.
+1 for considering that option, although I am not sure which way is better.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, Apr 30, 2025 at 4:29 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I confess to not having paid close enough attention when
these patches went in, or I would have complained about
createrole_self_grant. It changes the user-visible behavior
of SQL commands, specifically CREATE ROLE. We have learned
over and over again that GUCs that do that are generally
a bad idea.
Yeah, that's a fair complaint. I thought it wasn't too bad here
because the cases where it changes the behavior are so narrow, but I
understand why you don't like it. Also, the purpose of the settings is
really to paper over a pretty arbitrary difference between the way
that role management works for superusers and the way it works for
non-superusers. If you want to have a non-superuser administrator, as
every cloud provider does, without this patch, your only real
alternative is to patch the server, which is what everyone was doing
(and maybe they still will, but at least now there's a way to work
around it with just configuration if you want to ship unmodified
PostgreSQL). Consider:
robert.haas=# create role alice;
CREATE ROLE
There is now a role called 'alice' and you have all of alice's
privileges. But now consider:
robert.haas=# create role admin createrole;
CREATE ROLE
robert.haas=# set role admin;
SET
robert.haas=> create role alice;
CREATE ROLE
There is now a role called 'alice' but you do not have alice's
privileges unless you subsequently run "GRANT alice to admin". One
problem, as I say, is that this is confusing and the admin user isn't
likely to understand what they need to do. But maybe the bigger
question is: how do you justify this being randomly different? And if,
hypothetically, you did think it was bad that it was randomly
different, how would you propose fixing it without a behavior-changing
GUC? I guess you could opt for some kind of catalog state someplace
rather than a GUC, but I don't see how else you get around it, unless
you just made a hard behavior change, but that seemed almost certain
to draw objections.
Now I feel like you might object that there's no actual problem here,
but in my opinion, it does nobody any good to refuse to address
problems upstream when multiple large providers are patching around
the issue in more or less the same way. If Microsoft is carrying a
patch to allow for a non-superuser administrator and Amazon is doing
the same thing and EDB is doing the same thing (ok, we're not quite as
big...), to just say "nah, there's no actual problem here" doesn't
really make a lot of sense to me. Besides, even if it were true that
this case wasn't a problem in need of being corrected, surely
sometimes there ARE things we need to correct.
standard_conforming_strings comes to mind as a case when we endured a
lot more pain than I think this will ever cause because the
alternative was to be permanently incompatible with the SQL standard
and we didn't want to do that. And I don't know how we would have
gotten out from under that problem without a behavior-changing GUC,
and I didn't know how to get out from under this one without it,
either.
That's not to say that I feel great about it, though, because I don't.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
Consider:
robert.haas=# create role alice;
CREATE ROLE
There is now a role called 'alice' and you have all of alice's
privileges. But now consider:
robert.haas=# create role admin createrole;
CREATE ROLE
robert.haas=# set role admin;
SET
robert.haas=> create role alice;
CREATE ROLE
There is now a role called 'alice' but you do not have alice's
privileges unless you subsequently run "GRANT alice to admin". One
problem, as I say, is that this is confusing and the admin user isn't
likely to understand what they need to do. But maybe the bigger
question is: how do you justify this being randomly different?
To be blunt, I don't buy this argument in the least. When you say
that "the superuser has all of alice's privileges", that is wrong:
no such grant is recorded in the system, nor does the code consult
alice's privileges to decide what the superuser can do. Reality is
that the superuser can do whatever she wants regardless of alice's
privileges, because she bypasses all privilege checks. Moreover,
there's no way for either the superuser or alice to revoke that
(short of the superuser giving up superuser-ness).
So I think that the idea that admin should implicitly get a grant of
alice's privileges is a misreading of what happens for superusers.
A closer approximation perhaps would be a role property that says
"you automatically have the privileges of any role you have created".
I'm not sure how this would interact with the INHERIT property of
roles and role grants, but there's probably something to think about
there.
In any case, I'd be happier about createrole_self_grant if it had
been a role property bit instead of a GUC. But we'd still need
to worry about whether it corrupts the results of dump/restore
(offhand I think it still would, if it results in GRANTs that
weren't there before).
regards, tom lane
On Thu, May 1, 2025 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
In any case, I'd be happier about createrole_self_grant if it had
been a role property bit instead of a GUC. But we'd still need
to worry about whether it corrupts the results of dump/restore
(offhand I think it still would, if it results in GRANTs that
weren't there before).
Hmm. That might have been a better design. :-(
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, May 1, 2025 at 2:22 PM Robert Haas <robertmhaas@gmail.com> wrote:
The other approach would be to do what we do for the role options and just specify everything explicitly in the dump. The GUC is only a default specifier so let's not leave room for defaults in the dump file.
+1 for considering that option, although I am not sure which way is better.
Actually, on further reflection, this doesn't work, right? I mean,
role properties are things where you mention them when creating or
altering the role, like SUPERUSER or NOSUPERUSER. So you can always
specify all of them and thereby avoid relying on defaults. But that
doesn't work here, because in this case we're talking about whether or
not a completely separate object, namely a GRANT, gets created or not.
createrole_self_grant causes that to happen, and there's nothing you
can say as part of the CREATE ROLE command itself to make it not
happen. So it seems like the only real fix is to do as Tom proposes:
# But don't we need to add
# createrole_self_grant to the set of GUCs that pg_dump[all]
# resets in the emitted SQL?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Monday, May 5, 2025, Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, May 1, 2025 at 2:22 PM Robert Haas <robertmhaas@gmail.com> wrote:
The other approach would be to do what we do for the role options and
just specify everything explicitly in the dump. The GUC is only a default
specifier so let's not leave room for defaults in the dump file.+1 for considering that option, although I am not sure which way is
better.
Actually, on further reflection, this doesn't work, right?
Doh. I was thinking this was controlling admin/inherit/set defaults but
you are correct this controls whether additional commands are emitted
automatically instead of having to make it so manually.
David J.
On Mon, May 5, 2025 at 8:35 AM Robert Haas <robertmhaas@gmail.com> wrote:
So it seems like the only real fix is to do as Tom proposes:
# But don't we need to add
# createrole_self_grant to the set of GUCs that pg_dump[all]
# resets in the emitted SQL?
Tom, are you hoping that I'm going to produce a patch for this, or are
you planning to produce one?
Considering the amount of stuff I still need to do before the
conference next week, I don't think I can realistically work on this
right now. But I can work on it the week of the 19th if you want me
to.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Mon, May 5, 2025 at 8:35 AM Robert Haas <robertmhaas@gmail.com> wrote:
So it seems like the only real fix is to do as Tom proposes:
# But don't we need to add
# createrole_self_grant to the set of GUCs that pg_dump[all]
# resets in the emitted SQL?
Tom, are you hoping that I'm going to produce a patch for this, or are
you planning to produce one?
I wasn't planning to write the patch, no. I don't think there's any
great urgency about it, now that we've missed the deadline for the
May releases. If it gets done by August, it's fine.
regards, tom lane
On Wed, Apr 30, 2025 at 4:29 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
However, I'd at least like to complain about the fact that
it breaks pg_dumpall, which is surely not expecting anything
but the default behavior. If for any reason the restore is
run under a non-default setting of createrole_self_grant,
there's a potential of creating role grants that were not
there in the source database. Admittedly the damage is
probably limited by the fact that it only applies if the
restoring user has CREATEROLE but not SUPERUSER, which
I imagine is a rare case. But don't we need to add
createrole_self_grant to the set of GUCs that pg_dump[all]
resets in the emitted SQL?
I spent some time on this today. As you might imagine, it's quite easy
to make pg_dumpall emit SET createrole_self_grant = '', but doing so
seems less useful than I had expected. I wonder if I'm missing
something important here, so I thought I'd better inquire before
proceeding further.
As I see it, the core difficulty is that the output of pg_dumpall
always contains a CREATE ROLE statement for every single role in the
system, even the bootstrap superuser. For starters, that means that if
you simply run initdb and create cluster #1, run initdb again and
create cluster #2, dump the first and restore to the second, you will
get an error, because the same bootstrap superuser will exist in both
systems, so when you feed the output of pg_dumpall to psql, you end up
trying to create a role that already exists. At this point, my head is
already kind of exploding, because I thought we were pretty careful to
try to make it so that pg_dump output can be restored without error
even in the face of pre-existing objects like the public schema and
the plpgsql language, but apparently we haven't applied the same
principle to pg_dumpall.[1]Exception: When --binary-upgrade is used, we emit only ALTER ROLE and not CREATE ROLE for the bootstrap superuser. Why we think the error is only worth avoiding in the --binary-upgrade case is unclear to me.
But if, as you posit above, we were to try running the output of
pg_dumpall through psql as a non-superuser, the problem is a whole lot
worse. You can imagine a pg_dumpall feature that only tries to dump
(on the source system) roles that the dumping user can administer, and
only tries to recreate those roles on the target system, but we
haven't got that feature, so we're going to try to recreate every
single source role on the target system, including the bootstrap user
and the non-superuser who is restoring the dump if they exist on the
source side and any other superusers and any other users created by
other CREATEROLE superusers and it seems to me that under any set of
somewhat-reasonable assumptions you're going to expect a bunch of
error messages to start showing up at this point. In short, trying to
restore pg_dumpall output as a non-superuser appears to be an
unsupported scenario, so the fact that we don't SET
createrole_self_grant = '' to cater to it doesn't really seem like a
bug to many any more.
In fact, I think there's a decent argument that we ought to let the
prevailing value of createrole_self_grant take effect in this
scenario. One pretty likely scenario, at least as it seems to me, is
that the user was superuser on the source system and is not superuser
on the target system but wants to recreate the same set of roles. If
they want to freely access objects owned by those roles as they could
on the source system, then they're going to need self-grants, and we
have no better clue than the value of createrole_self_grant to help us
figure out whether they want that or not.
To state the concern another way, if this is a bug, it should be
possible to construct a test case that fails without the patch and
passes with the patch. But it appears to me that the only way I could
do that is if I programatically edit the dump. And that seems like
cheating, because if we are talking about a scenario where the user is
editing the dump, they can also add SET createrole_self_grant = '' if
desired.
I don't want to make it sound like I now hate the idea of doing as you
proposed here, because I do see the point of nailing down critical
GUCs that can affect the interpretation of SQL statements in places
like pg_dumpall output, and maybe we should do that here ... kinda
just in case? But I'm not altogether sure that's a sufficient
justification, and at any rate I think we need to be clear on whether
that *is* the justification or whether there's something more concrete
that we're trying to make work.
--
Robert Haas
EDB: http://www.enterprisedb.com
[1]: Exception: When --binary-upgrade is used, we emit only ALTER ROLE and not CREATE ROLE for the bootstrap superuser. Why we think the error is only worth avoiding in the --binary-upgrade case is unclear to me.
and not CREATE ROLE for the bootstrap superuser. Why we think the
error is only worth avoiding in the --binary-upgrade case is unclear
to me.
On Tue, May 20, 2025 at 2:32 PM Robert Haas <robertmhaas@gmail.com> wrote:
trying to create a role that already exists. At this point, my head is
already kind of exploding, because I thought we were pretty careful to
try to make it so that pg_dump output can be restored without error even
in the face of pre-existing objects like the public schema and
the plpgsql language, but apparently we haven't applied the same principle
to pg_dumpall.[1]
This has always been my understanding, even if we are not explicitly
stating it anywhere. pg_dump -> no errors. pg_dumpall -> always at least
one error :)
But if, as you posit above, we were to try running the output of pg_dumpall
through psql as a non-superuser, the problem is a whole lot
worse.
I'm of the camp that pg_dumpall should almost always be run as superuser.
That said, I find myself using pg_dumpall less and less with every year,
and cannot think of the last time I advised a client to use it (other than
a pg_dumpall --globals and ignore the errors as a poor-man's role
duplication system. Even that is getting rarer, as we generally don't want
the same passwords)
Cheers,
Greg
--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support