CREATEROLE and role ownership hierarchies
These patches have been split off the now deprecated monolithic "Delegating superuser tasks to new security roles" thread at [1].
The purpose of these patches is to fix the CREATEROLE escalation attack vector misfeature. (Not everyone will see CREATEROLE that way, but the perceived value of the patch set likely depends on how much you see CREATEROLE in that light.)
Attachments:
v1-0001-Add-tests-of-the-CREATEROLE-attribute.patchapplication/octet-stream; name=v1-0001-Add-tests-of-the-CREATEROLE-attribute.patch; x-unix-mode=0644Download
From 93351528c52921ed1d8833b291a6ee6a7b872c1c Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Sun, 26 Sep 2021 09:12:37 -0700
Subject: [PATCH v1 1/4] Add tests of the CREATEROLE attribute.
While developing alternate rules for what privileges CREATEROLE has,
I noticed that none of the changes to how CREATEROLE works triggered
any regression test failures. This is problematic for two reasons.
It means the existing code has insufficient test coverage, and it
means that unintended changes introduced by subsequent patches may
go unnoticed. Fix that.
---
src/test/regress/expected/create_role.out | 145 ++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/create_role.sql | 138 ++++++++++++++++++++
3 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/create_role.out
create mode 100644 src/test/regress/sql/create_role.sql
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..e6afb72bf7
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_4 REPLICATION;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_5 BYPASSRLS;
+ERROR: must be superuser to create bypassrls users
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+ERROR: role "regress_role_17" is a member of role "regress_role_17"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ROLE regress_role_19;
+ERROR: role "regress_role_19" is a member of role "regress_role_19"
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+ERROR: permission denied to create database
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ERROR: must be owner of index regress_idx_22
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+ERROR: must be owner of table regress_tbl_22
+DROP TABLE regress_tbl_22;
+ERROR: must be owner of table regress_tbl_22
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+ERROR: must be owner of view regress_view_22
+DROP VIEW regress_view_22;
+ERROR: must be owner of view regress_view_22
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+ERROR: permission denied to reassign objects
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+ERROR: role "regress_role_2" does not exist
+DROP ROLE regress_role_3;
+ERROR: role "regress_role_3" does not exist
+DROP ROLE regress_role_4;
+ERROR: role "regress_role_4" does not exist
+DROP ROLE regress_role_5;
+ERROR: role "regress_role_5" does not exist
+DROP ROLE regress_role_14;
+ERROR: role "regress_role_14" does not exist
+DROP ROLE regress_role_15;
+ERROR: role "regress_role_15" does not exist
+DROP ROLE regress_role_17;
+ERROR: role "regress_role_17" does not exist
+DROP ROLE regress_role_19;
+ERROR: role "regress_role_19" does not exist
+DROP ROLE regress_role_20;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
+DETAIL: owner of table regress_tbl_22
+owner of view regress_view_22
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+ERROR: must be superuser to drop superusers
+DROP ROLE regress_role_1;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7be89178f0..2c8580ecde 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -86,7 +86,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..8429b54595
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 REPLICATION;
+CREATE ROLE regress_role_5 BYPASSRLS;
+
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ROLE regress_role_19;
+
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+DROP TABLE regress_tbl_22;
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+DROP VIEW regress_view_22;
+
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
+DROP ROLE regress_role_14;
+DROP ROLE regress_role_15;
+DROP ROLE regress_role_17;
+DROP ROLE regress_role_19;
+DROP ROLE regress_role_20;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+DROP ROLE regress_role_1;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v1-0002-Add-owners-to-roles.patchapplication/octet-stream; name=v1-0002-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 4d22832f4ae9a3d08b03f0fbacbb3d6e1c22cd80 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Mon, 27 Sep 2021 10:09:45 -0700
Subject: [PATCH v1 2/4] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/commands/alter.c | 3 ++
src/backend/commands/user.c | 45 ++++++++++++++++++-
src/backend/parser/gram.y | 12 +++++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 1 +
.../unsafe_tests/expected/rolenames.out | 6 ++-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 32 ++++++++++++-
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +++-
src/test/regress/sql/create_role.sql | 8 +++-
src/test/regress/sql/privileges.sql | 13 +++++-
12 files changed, 127 insertions(+), 7 deletions(-)
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index c47d54e96b..979034ab2f 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -839,6 +839,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index aa69821be4..815c7095ec 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -77,6 +77,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +109,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ /* Make more flexible later */
+ owner = GetUserId();
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -345,6 +349,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +427,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -1078,8 +1085,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1675,3 +1683,38 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_SCHEMA),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ elog(WARNING, "AlterRoleOwner_internal not yet implemented");
+ // AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 08f1bf1031..965c903c71 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1194,6 +1194,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9490,6 +9494,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE RoleSpec OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) $3;
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 2d7115e31d..cce43388d8 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..b49fd2b2c5 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,6 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index e6afb72bf7..385ee74342 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,34 @@ ERROR: role "regress_role_17" does not exist
DROP ROLE regress_role_19;
ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
+-- fail, cannot drop roles that own other roles
+DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_21
+owner of role regress_role_22
+owner of role regress_role_23
+owner of role regress_role_24
+owner of role regress_role_25
+owner of role regress_role_26
+owner of role regress_role_27
+owner of role regress_role_28
+owner of role regress_role_29
+owner of role regress_role_30
+owner of role regress_role_31
+owner of role regress_role_32
+owner of role regress_role_33
+owner of role regress_role_34
+owner of role regress_role_35
+owner of role regress_role_36
+owner of role regress_role_37
+owner of role regress_role_38
+owner of role regress_role_39
+owner of role regress_role_40
+owner of role regress_role_41
+owner of role regress_role_42
+owner of role regress_role_43
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_role_6;
-DROP ROLE regress_role_7;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -130,6 +155,10 @@ DROP ROLE regress_role_22;
ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
DETAIL: owner of table regress_tbl_22
owner of view regress_view_22
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +170,6 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 1461e947cd..beaf942f59 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 83cff902f3..c4456cadce 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
CREATE GROUP regress_priv_group1;
@@ -2327,7 +2329,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 8429b54595..728208f1e1 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_role_17;
DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
+-- fail, cannot drop roles that own other roles
+DROP ROLE regress_role_7;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_role_6;
-DROP ROLE regress_role_7;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -124,6 +126,9 @@ DROP ROLE regress_role_32;
-- fail, role still owns database objects
DROP ROLE regress_role_22;
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
@@ -134,5 +139,6 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 3d1a1db987..bd2d67691c 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -29,9 +29,14 @@ CREATE USER regress_priv_user2;
CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
+
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+
+CREATE USER regress_priv_user6 CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
@@ -1389,8 +1394,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v1-0003-Give-role-owners-control-over-owned-roles.patchapplication/octet-stream; name=v1-0003-Give-role-owners-control-over-owned-roles.patch; x-unix-mode=0644Download
From 84206411750465665ac2076827c39c52f79ebbe6 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Wed, 20 Oct 2021 08:52:25 -0700
Subject: [PATCH v1 3/4] Give role owners control over owned roles
Create a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 68 ++++++++++++++++---
src/backend/catalog/objectaddress.c | 22 +-----
src/backend/commands/user.c | 16 +++--
src/backend/utils/adt/acl.c | 4 ++
src/include/utils/acl.h | 1 +
.../expected/dummy_seclabel.out | 12 ++--
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +++-
src/test/regress/expected/create_role.out | 21 ++----
src/test/regress/sql/create_role.sql | 11 ++-
9 files changed, 102 insertions(+), 65 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 89792b154e..f24b664970 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3418,6 +3418,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_VIEW:
msg = gettext_noop("permission denied for view %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
/* these currently aren't used */
case OBJECT_ACCESS_METHOD:
case OBJECT_AMOP:
@@ -3428,7 +3431,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3543,6 +3545,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_TSDICTIONARY:
msg = gettext_noop("must be owner of text search dictionary %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
/*
* Special cases: For these, the error message talks
@@ -3567,7 +3572,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5428,16 +5432,62 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Check whether specified role is the direct or indirect owner of another
+ * role.
+ */
+bool
+pg_role_ownercheck(Oid role_oid, Oid roleid)
+{
+ HeapTuple tuple;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ /*
+ * Start with the owned role and traverse the ownership hierarchy upward.
+ * We stop when we find the owner we are looking for or when we reach the
+ * top.
+ */
+ while (OidIsValid(role_oid))
+ {
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ owner_oid = ((Form_pg_authid) GETSTRUCT(tuple))->rolowner;
+ ReleaseSysCache(tuple);
+
+ /* Have we found the target role? */
+ if (roleid == owner_oid)
+ return true;
+
+ /*
+ * If we have reached a role which owns itself, we must iterate no
+ * further, else we fall into an infinite loop.
+ */
+ if (role_oid == owner_oid)
+ return false;
+
+ /* Set up for the next iteration. */
+ role_oid = owner_oid;
+ }
+
+ return false;
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8c94939baa..fecbc763de 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2540,25 +2540,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 815c7095ec..902c0473de 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -693,7 +693,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
!rolemembers &&
!validUntil &&
dpassword &&
- roleid == GetUserId()))
+ !pg_role_ownercheck(roleid, GetUserId())))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -894,7 +894,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -946,11 +947,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1022,6 +1018,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434..04eae9d4e5 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4850,6 +4850,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilge the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..ec9d480d67 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 385ee74342..a4eb19c954 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -58,21 +58,18 @@ CREATE TABLE regress_tbl_22 (i integer);
CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
-ERROR: must be owner of index regress_idx_22
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
-ERROR: must be owner of table regress_tbl_22
DROP TABLE regress_tbl_22;
-ERROR: must be owner of table regress_tbl_22
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-ERROR: must be owner of view regress_view_22
+ERROR: must be member of role "regress_role_1"
+-- ok
DROP VIEW regress_view_22;
-ERROR: must be owner of view regress_view_22
-- fail, cannot take ownership of these objects from regress_role_22
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-ERROR: permission denied to reassign objects
-- ok, having CREATEROLE is enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
@@ -152,13 +149,8 @@ DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
-- fail, role still owns database objects
DROP ROLE regress_role_22;
-ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
-DETAIL: owner of table regress_tbl_22
-owner of view regress_view_22
-- fail, role still owns other roles
DROP ROLE regress_role_7;
-ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -166,10 +158,5 @@ DROP ROLE regress_role_1;
ERROR: current user cannot be dropped
-- ok
RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
-DROP ROLE regress_role_7;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 728208f1e1..df8d34a8d5 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -63,12 +63,16 @@ CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
DROP TABLE regress_tbl_22;
+
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+
+-- ok
DROP VIEW regress_view_22;
-- fail, cannot take ownership of these objects from regress_role_22
@@ -135,10 +139,5 @@ DROP ROLE regress_role_1;
-- ok
RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
-DROP ROLE regress_role_7;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v1-0004-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v1-0004-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From 066c4aa444a35058c37bd77023514c2cca7bee71 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Wed, 20 Oct 2021 09:09:35 -0700
Subject: [PATCH v1 4/4] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++---
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 ++++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++----
src/backend/catalog/aclchk.c | 91 +++++++++++++++++++
src/backend/commands/user.c | 58 ++++++-------
src/backend/utils/adt/acl.c | 21 +----
src/include/utils/acl.h | 5 ++
src/test/regress/expected/create_role.out | 101 +++++++++++-----------
src/test/regress/sql/create_role.sql | 54 ++++++------
14 files changed, 271 insertions(+), 182 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 94f745aed0..53fb0ec1c2 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3080,9 +3080,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3094,9 +3092,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3118,8 +3115,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..96e60d5a09 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they own except
+ <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and
+ <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication
+ roles, and only if the role owner does not alter the target role to have a
+ privilege which the role owner itself lacks. Ordinary roles can only change
+ their own password.
</para>
<para>
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.
+ Role owners can rename non-superuser roles they own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Owning roles may change
+ privilege for non-superuser roles they own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index afbf67c28c..e7434f3f86 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index f24b664970..426138b23f 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5527,6 +5527,97 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+bool
+has_rolinherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 902c0473de..68d3cd6c20 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -56,15 +56,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -261,24 +252,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -682,7 +681,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* We already checked issuper, isreplication, and bypassrls */
if (!(inherit < 0 &&
@@ -883,7 +882,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -894,8 +893,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1008,18 +1006,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1200,7 +1192,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1211,7 +1203,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1426,7 +1418,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1436,9 +1428,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1614,9 +1606,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 04eae9d4e5..3438abbcee 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_rolinherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_rolinherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec9d480d67..63cde442a8 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -319,5 +319,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_rolinherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index a4eb19c954..d4a9a07610 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -1,20 +1,21 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
--- fail, only superusers can create users with these privileges
+-- fail, cannot assign SUPERUSER privilege that creator lacks
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_4 REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
--- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
+CREATE ROLE regress_role_8;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
@@ -25,9 +26,9 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
regress_role_super, regress_role_6, regress_role_7, regress_role_8,
@@ -50,8 +51,11 @@ ERROR: permission denied to create database
CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -68,29 +72,33 @@ ALTER VIEW regress_view_22 OWNER TO regress_role_1;
ERROR: must be member of role "regress_role_1"
-- ok
DROP VIEW regress_view_22;
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership of objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, do not have ADMIN option on privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_2;
ERROR: role "regress_role_2" does not exist
-DROP ROLE regress_role_3;
-ERROR: role "regress_role_3" does not exist
-DROP ROLE regress_role_4;
-ERROR: role "regress_role_4" does not exist
-DROP ROLE regress_role_5;
-ERROR: role "regress_role_5" does not exist
DROP ROLE regress_role_14;
ERROR: role "regress_role_14" does not exist
DROP ROLE regress_role_15;
@@ -99,34 +107,36 @@ DROP ROLE regress_role_17;
ERROR: role "regress_role_17" does not exist
DROP ROLE regress_role_19;
ERROR: role "regress_role_19" does not exist
-DROP ROLE regress_role_20;
+DROP ROLE regress_role_23;
+ERROR: role "regress_role_23" does not exist
+DROP ROLE regress_role_24;
+ERROR: role "regress_role_24" does not exist
+DROP ROLE regress_role_25;
+ERROR: role "regress_role_25" does not exist
+DROP ROLE regress_role_26;
+ERROR: role "regress_role_26" does not exist
+DROP ROLE regress_role_27;
+ERROR: role "regress_role_27" does not exist
+DROP ROLE regress_role_28;
+ERROR: role "regress_role_28" does not exist
+DROP ROLE regress_role_29;
+ERROR: role "regress_role_29" does not exist
+DROP ROLE regress_role_30;
+ERROR: role "regress_role_30" does not exist
+DROP ROLE regress_role_31;
+ERROR: role "regress_role_31" does not exist
+DROP ROLE regress_role_32;
+ERROR: role "regress_role_32" does not exist
-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_role_21
+DETAIL: owner of role regress_role_20
+owner of role regress_role_21
owner of role regress_role_22
-owner of role regress_role_23
-owner of role regress_role_24
-owner of role regress_role_25
-owner of role regress_role_26
-owner of role regress_role_27
-owner of role regress_role_28
-owner of role regress_role_29
-owner of role regress_role_30
-owner of role regress_role_31
-owner of role regress_role_32
-owner of role regress_role_33
-owner of role regress_role_34
-owner of role regress_role_35
-owner of role regress_role_36
-owner of role regress_role_37
-owner of role regress_role_38
-owner of role regress_role_39
-owner of role regress_role_40
-owner of role regress_role_41
-owner of role regress_role_42
-owner of role regress_role_43
-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
@@ -136,20 +146,9 @@ DROP ROLE regress_role_12;
DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
+DROP ROLE regress_role_20;
DROP ROLE regress_role_21;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
--- fail, role still owns database objects
DROP ROLE regress_role_22;
--- fail, role still owns other roles
DROP ROLE regress_role_7;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index df8d34a8d5..58b55f5017 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -2,17 +2,22 @@
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
--- fail, only superusers can create users with these privileges
+-- fail, cannot assign SUPERUSER privilege that creator lacks
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
-
--- ok, having CREATEROLE is enough to create users with these privileges
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+
+-- ok, having CREATEROLE is enough for these
+CREATE ROLE regress_role_8;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
@@ -24,7 +29,7 @@ CREATE ROLE regress_role_13 SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-- ok, can grant other users into a role
@@ -53,9 +58,12 @@ CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -75,10 +83,10 @@ ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-- ok
DROP VIEW regress_view_22;
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership of objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, do not have ADMIN option on privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
@@ -93,19 +101,28 @@ CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
-- fail, creation of these roles failed above so they do not now exist
SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_2;
-DROP ROLE regress_role_3;
-DROP ROLE regress_role_4;
-DROP ROLE regress_role_5;
DROP ROLE regress_role_14;
DROP ROLE regress_role_15;
DROP ROLE regress_role_17;
DROP ROLE regress_role_19;
-DROP ROLE regress_role_20;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
@@ -115,22 +132,9 @@ DROP ROLE regress_role_12;
DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
+DROP ROLE regress_role_20;
DROP ROLE regress_role_21;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-
--- fail, role still owns database objects
DROP ROLE regress_role_22;
-
--- fail, role still owns other roles
DROP ROLE regress_role_7;
-- fail, cannot drop ourself nor superusers
--
2.21.1 (Apple Git-122.3)
On 10/20/21, 11:46 AM, "Mark Dilger" <mark.dilger@enterprisedb.com> wrote:
The purpose of these patches is to fix the CREATEROLE escalation
attack vector misfeature. (Not everyone will see CREATEROLE that
way, but the perceived value of the patch set likely depends on how
much you see CREATEROLE in that light.)
Regarding the "attack vector misfeature" comment, I remember being
surprised when I first learned how much roles with CREATEROLE can do.
When I describe CREATEROLE to others, I am sure to emphasize the note
in the docs about such roles being "almost-superuser" roles.
CREATEROLE is a rather big hammer at the moment, so I certainly think
there is value in reducing its almost-superuser-ness.
I mentioned this in the other thread [0]/messages/by-id/53C7DF4C-8463-4647-9DFD-779B5E1861C4@amazon.com already, but the first thing
that comes to mind when I look at these patches is how upgrades might
work. Will we just make the bootstrap superuser the owner for all
roles when you first upgrade to v15? Also, are we just going to strip
the current CREATEROLE roles of much of their powers? Maybe it's
worth keeping a legacy CREATEROLE role attribute for upgraded clusters
that could eventually be removed down the road.
I'd also like to bring up my note about allowing users to transfer
role ownership. When I tested the patches earlier, REASSIGN OWNED BY
was failing with an "unexpected classid" ERROR. Besides REASSIGN
OWNED BY, perhaps there should be another mechanism for transferring
ownership on a role-by-role basis (i.e., ALTER ROLE OWNER TO). I
haven't looked at this new patch set too closely, so my apologies if
this has already been added.
Nathan
[0]: /messages/by-id/53C7DF4C-8463-4647-9DFD-779B5E1861C4@amazon.com
On Oct 21, 2021, at 4:04 PM, Bossart, Nathan <bossartn@amazon.com> wrote:
Regarding the "attack vector misfeature" comment, I remember being
surprised when I first learned how much roles with CREATEROLE can do.
When I describe CREATEROLE to others, I am sure to emphasize the note
in the docs about such roles being "almost-superuser" roles.
CREATEROLE is a rather big hammer at the moment, so I certainly think
there is value in reducing its almost-superuser-ness.
It is hard to know how many people are using CREATEROLE currently. There isn't much reason to give it out, since if you care enough about security to not give out superuser, you probably care too much about security to give away CREATEROLE.
I mentioned this in the other thread [0] already, but the first thing
that comes to mind when I look at these patches is how upgrades might
work. Will we just make the bootstrap superuser the owner for all
roles when you first upgrade to v15?
Yes, that's the idea. After upgrade, all roles will form a tree, with the bootstrap superuser at the root of the tree. The initial tree structure isn't very interesting, with all other roles directly owned by it, but from there the superuser can rearrange the tree, and after that non-superuser roles can manage whatever subtree of roles they are the root of.
Also, are we just going to strip
the current CREATEROLE roles of much of their powers? Maybe it's
worth keeping a legacy CREATEROLE role attribute for upgraded clusters
that could eventually be removed down the road.
The patch as written drastically reduces the power of the CREATEROLE attribute, in a non-backwards compatible way. I wondered if there would be complaints about that. If so, we could instead leave CREATEROLE alone, and create some other privileged role for the same thing, but it does start to look funny having a CREATEROLE privilege bit and also a privileged role named, perhaps, pg_can_create_roles.
I'd also like to bring up my note about allowing users to transfer
role ownership. When I tested the patches earlier, REASSIGN OWNED BY
was failing with an "unexpected classid" ERROR. Besides REASSIGN
OWNED BY, perhaps there should be another mechanism for transferring
ownership on a role-by-role basis (i.e., ALTER ROLE OWNER TO). I
haven't looked at this new patch set too closely, so my apologies if
this has already been added.
Yes, I completely agree with you on that. Both REASSIGN OWNED BY and ALTER ROLE OWNER TO should work. I'll take a look at the patches and repost with any adjustments that I find necessary to make those work.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 2021-10-21 03:40, Mark Dilger wrote:
These patches have been split off the now deprecated monolithic
"Delegating superuser tasks to new security roles" thread at [1].The purpose of these patches is to fix the CREATEROLE escalation
attack vector misfeature. (Not everyone will see CREATEROLE that way,
but the perceived value of the patch set likely depends on how much
you see CREATEROLE in that light.)
Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.
I have three comments.
1. Is there a function to check the owner of a role, it would be nice to
be able to check with \du or pg_roles view.
2. Is it correct that REPLICATION/BYPASSRLS can be granted even if you
are not a super user, but have CREATEROLE and REPLICATION/BYPASSRLS?
3. I think it would be better to have an "DROP ROLE [ IF EXISTS ] name
[, ...] [CASCADE | RESTRICT]" like "DROP TABLE [ IF EXISTS ] name [,
...] [ CASCADE | RESTRICT ]". What do you think?
--
Regards,
--
Shinya Kato
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Oct 25, 2021, at 10:09 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
On 2021-10-21 03:40, Mark Dilger wrote:
These patches have been split off the now deprecated monolithic
"Delegating superuser tasks to new security roles" thread at [1].
The purpose of these patches is to fix the CREATEROLE escalation
attack vector misfeature. (Not everyone will see CREATEROLE that way,
but the perceived value of the patch set likely depends on how much
you see CREATEROLE in that light.)Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.I have three comments.
1. Is there a function to check the owner of a role, it would be nice to be able to check with \du or pg_roles view.
No, but that is a good idea.
2. Is it correct that REPLICATION/BYPASSRLS can be granted even if you are not a super user, but have CREATEROLE and REPLICATION/BYPASSRLS?
It is intentional, yes. Whether it is correct is up for debate, but I think it is.
3. I think it would be better to have an "DROP ROLE [ IF EXISTS ] name [, ...] [CASCADE | RESTRICT]" like "DROP TABLE [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ]". What do you think?
I agree it would be nice to have, but roles are cluster-global and there are technical difficulties in cascading into multiple databases to drop all objects owned by the role. There was also a debate [1]/messages/by-id/20211005025746.GN20998@tamriel.snowman.net about whether we would even want such behavior, leading to no real conclusion regarding how or if such a command should be implemented.
The current solution is to run REASSIGN OWNED in each database where the role owns objects before running DROP ROLE. At that point, the CASCADE option (not implemented) won't be needed. Of course, I need to post the next revision of this patch set addressing the deficiencies that Nathan pointed out upthread to make that work.
[1]: /messages/by-id/20211005025746.GN20998@tamriel.snowman.net
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 10/21/21 19:21, Mark Dilger wrote:
Also, are we just going to strip
the current CREATEROLE roles of much of their powers? Maybe it's
worth keeping a legacy CREATEROLE role attribute for upgraded clusters
that could eventually be removed down the road.The patch as written drastically reduces the power of the CREATEROLE attribute, in a non-backwards compatible way. I wondered if there would be complaints about that. If so, we could instead leave CREATEROLE alone, and create some other privileged role for the same thing, but it does start to look funny having a CREATEROLE privilege bit and also a privileged role named, perhaps, pg_can_create_roles.
Give that CREATEROLE currently just about amounts to being a superuser,
maybe there should be a pg_upgrade option to convert CREATEROLE to
SUPERUSER. I don't want to perpetuate the misfeature though, so let's
just bring it to an end.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Oct 25, 2021, at 10:09 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.I have three comments.
1. Is there a function to check the owner of a role, it would be nice to be able to check with \du or pg_roles view.No, but that is a good idea.
These two ideas are implemented in v2. Both \du and pg_roles show the owner information.
The current solution is to run REASSIGN OWNED in each database where the role owns objects before running DROP ROLE. At that point, the CASCADE option (not implemented) won't be needed. Of course, I need to post the next revision of this patch set addressing the deficiencies that Nathan pointed out upthread to make that work.
REASSIGN OWNED and ALTER ROLE..OWNER TO now work in v2.
Attachments:
v2-0001-Add-tests-of-the-CREATEROLE-attribute.patchapplication/octet-stream; name=v2-0001-Add-tests-of-the-CREATEROLE-attribute.patch; x-unix-mode=0644Download
From e235625bf79dbe78c5450b2cf552a38b4f2a0c7e Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Mon, 25 Oct 2021 17:11:45 -0700
Subject: [PATCH v2 1/4] Add tests of the CREATEROLE attribute.
While developing alternate rules for what privileges CREATEROLE has,
I noticed that none of the changes to how CREATEROLE works triggered
any regression test failures. This is problematic for two reasons.
It means the existing code has insufficient test coverage, and it
means that unintended changes introduced by subsequent patches may
go unnoticed. Fix that.
---
src/test/regress/expected/create_role.out | 145 ++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/create_role.sql | 138 ++++++++++++++++++++
3 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/create_role.out
create mode 100644 src/test/regress/sql/create_role.sql
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..57010bbb58
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_4 REPLICATION;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_5 BYPASSRLS;
+ERROR: must be superuser to create bypassrls users
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+ERROR: role "regress_role_17" is a member of role "regress_role_17"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+ERROR: role "regress_role_19" is a member of role "regress_role_19"
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+ERROR: permission denied to create database
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ERROR: must be owner of index regress_idx_22
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+ERROR: must be owner of table regress_tbl_22
+DROP TABLE regress_tbl_22;
+ERROR: must be owner of table regress_tbl_22
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+ERROR: must be owner of view regress_view_22
+DROP VIEW regress_view_22;
+ERROR: must be owner of view regress_view_22
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+ERROR: permission denied to reassign objects
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+ERROR: role "regress_role_2" does not exist
+DROP ROLE regress_role_3;
+ERROR: role "regress_role_3" does not exist
+DROP ROLE regress_role_4;
+ERROR: role "regress_role_4" does not exist
+DROP ROLE regress_role_5;
+ERROR: role "regress_role_5" does not exist
+DROP ROLE regress_role_14;
+ERROR: role "regress_role_14" does not exist
+DROP ROLE regress_role_15;
+ERROR: role "regress_role_15" does not exist
+DROP ROLE regress_role_17;
+ERROR: role "regress_role_17" does not exist
+DROP ROLE regress_role_19;
+ERROR: role "regress_role_19" does not exist
+DROP ROLE regress_role_20;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
+DETAIL: owner of table regress_tbl_22
+owner of view regress_view_22
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+ERROR: must be superuser to drop superusers
+DROP ROLE regress_role_1;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7be89178f0..2c8580ecde 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -86,7 +86,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..e00893de4e
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 REPLICATION;
+CREATE ROLE regress_role_5 BYPASSRLS;
+
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+DROP TABLE regress_tbl_22;
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+DROP VIEW regress_view_22;
+
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
+DROP ROLE regress_role_14;
+DROP ROLE regress_role_15;
+DROP ROLE regress_role_17;
+DROP ROLE regress_role_19;
+DROP ROLE regress_role_20;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+DROP ROLE regress_role_1;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v2-0002-Add-owners-to-roles.patchapplication/octet-stream; name=v2-0002-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 0a27010622339b7f80e7d622add73137c69a97c0 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Wed, 27 Oct 2021 07:34:18 -0700
Subject: [PATCH v2 2/4] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 59 +++++++-
src/backend/catalog/pg_shdepend.c | 5 +
src/backend/catalog/system_views.sql | 1 +
src/backend/commands/alter.c | 3 +
src/backend/commands/user.c | 142 +++++++++++++++++-
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/parser/gram.y | 23 +++
src/bin/psql/describe.c | 12 ++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 2 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/acl.h | 1 +
.../unsafe_tests/expected/rolenames.out | 6 +-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 133 ++++++++++++++--
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +-
src/test/regress/expected/rules.out | 1 +
src/test/regress/sql/create_role.sql | 64 +++++++-
src/test/regress/sql/privileges.sql | 13 +-
21 files changed, 461 insertions(+), 21 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ce0a4ff14e..ddd205d656 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid role_oid, Oid roleid)
+{
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ /* Otherwise, look up the owner of the role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ return (owner_oid == roleid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 8453d8fefd..88e479ca21 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1557,6 +1558,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 55f6e3711d..b03763695c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 40044070cf..eb407cfc4c 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index aa69821be4..259968f1d4 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +377,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +455,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -1078,8 +1116,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1675,3 +1714,104 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: This is different from most other alter-owner checks in that
+ * the current user is checked for create privileges instead of the
+ * destination owner. This is consistent with the CREATE case for
+ * roles. Because superusers will always have this right, we need no
+ * special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 82464c9889..f406763d71 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4514,6 +4514,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f537d3eb96..0d2c32121b 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index d0eb80e69c..08fd20e239 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1060,9 +1060,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -1201,6 +1212,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9497,6 +9512,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 006661412e..e051cab6f9 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3801,6 +3801,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3837,6 +3843,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3848,6 +3856,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 2d7115e31d..cce43388d8 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 49123e28a4..0d5cfd3eb9 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2621,6 +2621,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..ec9d480d67 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 57010bbb58..e1ce5f3a66 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
@@ -11,14 +12,95 @@ CREATE ROLE regress_role_4 REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+\du+ regress_role_2
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+--------------------+-------------------------+-----------+-------------
+ regress_role_2 | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_role_3
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------------------+-----------+-------------
+ regress_role_3 | regress_role_1 | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_role_4
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_4 | regress_role_1 | Cannot login, Replication | {} |
+
+\du+ regress_role_5
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------------------+-----------+-------------
+ regress_role_5 | regress_role_1 | Cannot login, Bypass RLS | {} |
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+\du+ regress_role_6
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+-------------------------+-----------+-------------
+ regress_role_6 | regress_role_1 | Create DB, Cannot login | {} |
+
+\du+ regress_role_7
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
+
+\du+ regress_role_8
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+------------+-----------+-------------
+ regress_role_8 | regress_role_1 | | {} |
+
+\du+ regress_role_9
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------+-----------+-------------
+ regress_role_9 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_10
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+---------------+-----------+-------------
+ regress_role_10 | regress_role_1 | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_role_11
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------+-----------+-------------
+ regress_role_11 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_12
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+------------------------+------------------------------------------------+-------------
+ regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -84,16 +166,24 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+ERROR: must be owner of role regress_role_20
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+ERROR: permission denied to reassign objects
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
-ERROR: role "regress_role_2" does not exist
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
-ERROR: role "regress_role_3" does not exist
DROP ROLE regress_role_4;
-ERROR: role "regress_role_4" does not exist
DROP ROLE regress_role_5;
-ERROR: role "regress_role_5" does not exist
DROP ROLE regress_role_14;
ERROR: role "regress_role_14" does not exist
DROP ROLE regress_role_15;
@@ -103,9 +193,23 @@ ERROR: role "regress_role_17" does not exist
DROP ROLE regress_role_19;
ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_21
+owner of role regress_role_22
+owner of role regress_role_23
+owner of role regress_role_24
+owner of role regress_role_25
+owner of role regress_role_26
+owner of role regress_role_27
+owner of role regress_role_28
+owner of role regress_role_29
+owner of role regress_role_30
+owner of role regress_role_31
+owner of role regress_role_32
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -130,6 +234,10 @@ DROP ROLE regress_role_22;
ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
DETAIL: owner of table regress_tbl_22
owner of view regress_view_22
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +249,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 83cff902f3..c4456cadce 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
CREATE GROUP regress_priv_group1;
@@ -2327,7 +2329,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2fa00a3c29..c452cc433c 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index e00893de4e..8a4f177574 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
@@ -9,14 +10,42 @@ CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+
+\du+ regress_role_2
+\du+ regress_role_3
+\du+ regress_role_4
+\du+ regress_role_5
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+
+\du+ regress_role_6
+\du+ regress_role_7
+\du+ regress_role_8
+\du+ regress_role_9
+\du+ regress_role_10
+\du+ regress_role_11
+\du+ regress_role_12
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -86,9 +115,22 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
@@ -98,9 +140,11 @@ DROP ROLE regress_role_17;
DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -124,6 +168,9 @@ DROP ROLE regress_role_32;
-- fail, role still owns database objects
DROP ROLE regress_role_22;
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
@@ -134,5 +181,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 3d1a1db987..bd2d67691c 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -29,9 +29,14 @@ CREATE USER regress_priv_user2;
CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
+
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+
+CREATE USER regress_priv_user6 CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
@@ -1389,8 +1394,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v2-0003-Give-role-owners-control-over-owned-roles.patchapplication/octet-stream; name=v2-0003-Give-role-owners-control-over-owned-roles.patch; x-unix-mode=0644Download
From 2528fc733031d2087a67ea9f8cb02edb2d9b1e52 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Wed, 27 Oct 2021 14:21:53 -0700
Subject: [PATCH v2 3/4] Give role owners control over owned roles
Create a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 104 ++++++++++--------
src/backend/catalog/objectaddress.c | 22 +---
src/backend/commands/schemacmds.c | 2 +-
src/backend/commands/user.c | 16 +--
src/backend/utils/adt/acl.c | 4 +
.../expected/dummy_seclabel.out | 12 +-
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +-
src/test/regress/expected/create_role.out | 62 +++--------
src/test/regress/sql/create_role.sql | 39 +++----
9 files changed, 125 insertions(+), 148 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ddd205d656..4a11a0f124 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,79 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
pg_role_ownercheck(Oid role_oid, Oid roleid)
{
- HeapTuple tuple;
- Form_pg_authid authform;
- Oid owner_oid;
-
/* Superusers bypass all permission checking. */
if (superuser_arg(roleid))
return true;
- /* Otherwise, look up the owner of the role */
- tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
- if (!HeapTupleIsValid(tuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("role with OID %u does not exist",
- role_oid)));
- authform = (Form_pg_authid) GETSTRUCT(tuple);
- owner_oid = authform->rolowner;
-
/*
- * Roles must necessarily have owners. Even the bootstrap user has an
- * owner. (It owns itself). Other roles must form a proper tree.
+ * Start with the owned role and traverse the ownership hierarchy upward.
+ * We stop when we find the owner we are looking for or when we reach the
+ * top.
*/
- if (!OidIsValid(owner_oid))
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u has invalid owner",
- authform->rolname.data, authform->oid)));
- if (authform->oid != BOOTSTRAP_SUPERUSERID &&
- authform->rolowner == authform->oid)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owns itself",
- authform->rolname.data, authform->oid)));
- if (authform->oid == BOOTSTRAP_SUPERUSERID &&
- authform->rolowner != BOOTSTRAP_SUPERUSERID)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owned by role with OID %u",
- authform->rolname.data, authform->oid,
- authform->rolowner)));
- ReleaseSysCache(tuple);
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ /* Have we found the target role? */
+ if (roleid == owner_oid)
+ return true;
+
+ /*
+ * If we have reached a role which owns itself, we must iterate no
+ * further, else we fall into an infinite loop.
+ */
+ if (role_oid == owner_oid)
+ return false;
+
+ /* Set up for the next iteration. */
+ role_oid = owner_oid;
+ }
- return (owner_oid == roleid);
+ return false;
}
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2bae3fbb17..cc19409ca3 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 6c6ab9ee34..3447806756 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
+ * NOTE: This is different from most other alter-owner checks in that the
* current user is checked for create privileges instead of the
* destination owner. This is consistent with the CREATE case for
* schemas. Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 259968f1d4..57aede76f0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
!rolemembers &&
!validUntil &&
dpassword &&
- roleid == GetUserId()))
+ !pg_role_ownercheck(roleid, GetUserId())))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434..04eae9d4e5 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4850,6 +4850,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilge the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index e1ce5f3a66..492162e5a1 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -55,8 +55,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -95,11 +96,11 @@ CREATE ROLE regress_role_12
regress_role_11 | regress_role_1 | Cannot login | {} |
\du+ regress_role_12
- List of roles
- Role name | Owner | Attributes | Member of | Description
------------------+----------------+------------------------+------------------------------------------------+-------------
- regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
- | | 2 connections | |
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------------------------------+---------------------------------+--------------------
+ regress_role_12 | regress_role_1 | Create role, Create DB, Cannot login+| {regress_role_6,regress_role_7} | no login test role
+ | | 2 connections | |
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -140,21 +141,18 @@ CREATE TABLE regress_tbl_22 (i integer);
CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
-ERROR: must be owner of index regress_idx_22
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
-ERROR: must be owner of table regress_tbl_22
DROP TABLE regress_tbl_22;
-ERROR: must be owner of table regress_tbl_22
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-ERROR: must be owner of view regress_view_22
+ERROR: must be member of role "regress_role_1"
+-- ok
DROP VIEW regress_view_22;
-ERROR: must be owner of view regress_view_22
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-ERROR: permission denied to reassign objects
-- ok, having CREATEROLE is enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
@@ -166,15 +164,9 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
+-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-ERROR: must be owner of role regress_role_20
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-ERROR: permission denied to reassign objects
--- superuser can do it, though
-RESET SESSION AUTHORIZATION;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-- ok, superuser roles can drop superuser roles they own
SET SESSION AUTHORIZATION regress_role_super;
@@ -184,14 +176,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-ERROR: role "regress_role_14" does not exist
-DROP ROLE regress_role_15;
-ERROR: role "regress_role_15" does not exist
-DROP ROLE regress_role_17;
-ERROR: role "regress_role_17" does not exist
-DROP ROLE regress_role_19;
-ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
@@ -219,6 +203,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -229,28 +214,15 @@ DROP ROLE regress_role_29;
DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
-DETAIL: owner of table regress_tbl_22
-owner of view regress_view_22
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
DROP ROLE regress_role_1;
ERROR: current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
DETAIL: privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 8a4f177574..678f728f52 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -36,8 +36,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
@@ -92,15 +93,19 @@ CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
DROP TABLE regress_tbl_22;
+
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+
+-- ok
DROP VIEW regress_view_22;
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -115,16 +120,11 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
+-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
--- superuser can do it, though
-RESET SESSION AUTHORIZATION;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-
-- ok, superuser roles can drop superuser roles they own
SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
@@ -134,10 +134,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-DROP ROLE regress_role_15;
-DROP ROLE regress_role_17;
-DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
@@ -154,6 +150,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -165,25 +162,15 @@ DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
-- ok, can drop role if we revoke privileges first
--
2.21.1 (Apple Git-122.3)
v2-0004-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v2-0004-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From 4ea93b80464b5a977a0267cf8f0e7aa215485ebf Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Wed, 27 Oct 2021 15:10:19 -0700
Subject: [PATCH v2 4/4] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++---
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 +++++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++-----
src/backend/catalog/aclchk.c | 91 +++++++++++++++++++++++
src/backend/commands/user.c | 60 +++++++--------
src/backend/utils/adt/acl.c | 21 +-----
src/include/utils/acl.h | 5 ++
src/test/regress/expected/create_role.out | 63 ++++++----------
src/test/regress/sql/create_role.sql | 36 ++++-----
14 files changed, 230 insertions(+), 169 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 94f745aed0..53fb0ec1c2 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3080,9 +3080,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3094,9 +3092,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3118,8 +3115,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..96e60d5a09 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they own except
+ <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and
+ <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication
+ roles, and only if the role owner does not alter the target role to have a
+ privilege which the role owner itself lacks. Ordinary roles can only change
+ their own password.
</para>
<para>
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.
+ Role owners can rename non-superuser roles they own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Owning roles may change
+ privilege for non-superuser roles they own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index afbf67c28c..e7434f3f86 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 4a11a0f124..a72f412e90 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5552,6 +5552,97 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+bool
+has_rolinherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 57aede76f0..ccc0f46710 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -276,24 +267,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -713,7 +712,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* We already checked issuper, isreplication, and bypassrls */
if (!(inherit < 0 &&
@@ -914,7 +913,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -925,8 +924,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1039,18 +1037,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1231,7 +1223,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1242,7 +1234,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1457,7 +1449,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1467,9 +1459,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1645,9 +1637,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1803,7 +1795,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* roles. Because superusers will always have this right, we need no
* special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 04eae9d4e5..3438abbcee 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_rolinherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_rolinherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec9d480d67..63cde442a8 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -319,5 +319,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_rolinherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 492162e5a1..7df3683f2b 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -6,22 +6,16 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_4 REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
ERROR: must be superuser to own superusers
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
\du+ regress_role_2
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -50,7 +44,10 @@ CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -70,12 +67,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
----------------+----------------+---------------------------+-----------+-------------
regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
-\du+ regress_role_8
- List of roles
- Role name | Owner | Attributes | Member of | Description
-----------------+----------------+------------+-----------+-------------
- regress_role_8 | regress_role_1 | | {} |
-
\du+ regress_role_9
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -108,19 +99,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
CREATE ROLE regress_role_17 ROLE regress_role_17;
ERROR: role "regress_role_17" is a member of role "regress_role_17"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_role_19 ADMIN regress_role_19;
@@ -133,8 +124,11 @@ ERROR: permission denied to create database
CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -153,17 +147,27 @@ ERROR: must be member of role "regress_role_1"
DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
@@ -182,19 +186,8 @@ DROP ROLE regress_role_7;
ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_role_21
owner of role regress_role_22
-owner of role regress_role_23
-owner of role regress_role_24
-owner of role regress_role_25
-owner of role regress_role_26
-owner of role regress_role_27
-owner of role regress_role_28
-owner of role regress_role_29
-owner of role regress_role_30
-owner of role regress_role_31
-owner of role regress_role_32
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -204,16 +197,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- 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 678f728f52..b1c764cb82 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -6,6 +6,8 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
@@ -17,11 +19,6 @@ CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
-
\du+ regress_role_2
\du+ regress_role_3
\du+ regress_role_4
@@ -31,7 +28,11 @@ CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -42,7 +43,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
-\du+ regress_role_8
\du+ regress_role_9
\du+ regress_role_10
\du+ regress_role_11
@@ -54,12 +54,12 @@ CREATE ROLE regress_role_13 SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
@@ -67,7 +67,7 @@ CREATE ROLE regress_role_17 ROLE regress_role_17;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
@@ -83,9 +83,12 @@ CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -108,7 +111,7 @@ DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
@@ -141,7 +144,6 @@ DROP ROLE regress_role_7;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -151,16 +153,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
On 2021-10-28 07:21, Mark Dilger wrote:
On Oct 25, 2021, at 10:09 PM, Shinya Kato
<Shinya11.Kato@oss.nttdata.com> wrote:Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.I have three comments.
1. Is there a function to check the owner of a role, it would be nice
to be able to check with \du or pg_roles view.No, but that is a good idea.
These two ideas are implemented in v2. Both \du and pg_roles show the
owner information.
Thank you. It seems good to me.
By the way, I got the following execution result.
I was able to add the membership of a role with a different owner.
In brief, "a" was able to change the membership of owner "shinya".
Is this the correct behavior?
---
postgres=# CREATE ROLE a LOGIN;
CREATE ROLE
postgres=# GRANT pg_execute_server_program TO a WITH ADMIN OPTION;
GRANT ROLE
postgres=# CREATE ROLE b;
CREATE ROLE
postgres=# \du a
List of roles
Role name | Owner | Attributes | Member of
-----------+--------+------------+-----------------------------
a | shinya | | {pg_execute_server_program}
postgres=# \du b
List of roles
Role name | Owner | Attributes | Member of
-----------+--------+--------------+-----------
b | shinya | Cannot login | {}
postgres=# \c - a
You are now connected to database "postgres" as user "a".
postgres=> GRANT pg_execute_server_program TO b;
GRANT ROLE
postgres=> \du b
List of roles
Role name | Owner | Attributes | Member of
-----------+--------+--------------+-----------------------------
b | shinya | Cannot login | {pg_execute_server_program}
---
--
Regards,
--
Shinya Kato
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Oct 27, 2021, at 7:32 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
I was able to add the membership of a role with a different owner.
In brief, "a" was able to change the membership of owner "shinya".
Is this the correct behavior?
I believe it is required for backwards compatibility. In a green field, we might consider doing things differently.
The only intentional backward compatibility break in this patch set is the the behavior of CREATEROLE. The general hope is that such a compatibility break will help far more than it hurts, as CREATEROLE does not appear to be a well adopted feature. I would expect that breaking the behavior of the WITH ADMIN OPTION feature would cause a lot more pain.
Trying your example on both the unpatched and the patched sources, things appear to work as they should:
UNPATCHED
------------------
mark.dilger=# CREATE ROLE a LOGIN;
CREATE ROLE
mark.dilger=# GRANT pg_execute_server_program TO a WITH ADMIN OPTION;
GRANT ROLE
mark.dilger=# CREATE ROLE b;
CREATE ROLE
mark.dilger=# \du+ a
List of roles
Role name | Attributes | Member of | Description
-----------+------------+-----------------------------+-------------
a | | {pg_execute_server_program} |
mark.dilger=# \du+ b
List of roles
Role name | Attributes | Member of | Description
-----------+--------------+-----------+-------------
b | Cannot login | {} |
mark.dilger=# \c - a
You are now connected to database "mark.dilger" as user "a".
mark.dilger=> GRANT pg_execute_server_program TO b;
GRANT ROLE
mark.dilger=> \du+ b
List of roles
Role name | Attributes | Member of | Description
-----------+--------------+-----------------------------+-------------
b | Cannot login | {pg_execute_server_program} |
mark.dilger=> \du+ "mark.dilger"
List of roles
Role name | Attributes | Member of | Description
-------------+------------------------------------------------------------+-----------+-------------
mark.dilger | Superuser, Create role, Create DB, Replication, Bypass RLS | {} |
PATCHED:
---------------
mark.dilger=# CREATE ROLE a LOGIN;
CREATE ROLE
mark.dilger=# GRANT pg_execute_server_program TO a WITH ADMIN OPTION;
GRANT ROLE
mark.dilger=# CREATE ROLE b;
CREATE ROLE
mark.dilger=# \du+ a
List of roles
Role name | Owner | Attributes | Member of | Description
-----------+-------------+------------+-----------------------------+-------------
a | mark.dilger | | {pg_execute_server_program} |
mark.dilger=# \du+ b
List of roles
Role name | Owner | Attributes | Member of | Description
-----------+-------------+--------------+-----------+-------------
b | mark.dilger | Cannot login | {} |
mark.dilger=# \c - a
You are now connected to database "mark.dilger" as user "a".
mark.dilger=> GRANT pg_execute_server_program TO b;
GRANT ROLE
mark.dilger=> \du+ b
List of roles
Role name | Owner | Attributes | Member of | Description
-----------+-------------+--------------+-----------------------------+-------------
b | mark.dilger | Cannot login | {pg_execute_server_program} |
mark.dilger=> \du+ "mark.dilger"
List of roles
Role name | Owner | Attributes | Member of | Description
-------------+-------------+------------------------------------------------------------+-----------+-------------
mark.dilger | mark.dilger | Superuser, Create role, Create DB, Replication, Bypass RLS | {} |
You should notice that the owner of role "b" is the superuser "mark.dilger", and that owner's attributes are unchanged. But your point that role "a" can change the attributes of role "mark.dilger" is correct, as shown here:
mark.dilger=> GRANT pg_execute_server_program TO "mark.dilger";
GRANT ROLE
mark.dilger=> \du+ "mark.dilger"
List of roles
Role name | Owner | Attributes | Member of | Description
-------------+-------------+------------------------------------------------------------+-----------------------------+-------------
mark.dilger | mark.dilger | Superuser, Create role, Create DB, Replication, Bypass RLS | {pg_execute_server_program} |
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Mark Dilger <mark.dilger@enterprisedb.com> writes:
The only intentional backward compatibility break in this patch set is the the behavior of CREATEROLE. The general hope is that such a compatibility break will help far more than it hurts, as CREATEROLE does not appear to be a well adopted feature. I would expect that breaking the behavior of the WITH ADMIN OPTION feature would cause a lot more pain.
Even more to the point, WITH ADMIN OPTION is defined by the SQL standard.
The only way you get to mess with that is if you can convince people we
mis-implemented the standard.
regards, tom lane
On 2021-10-29 01:14, Tom Lane wrote:
Mark Dilger <mark.dilger@enterprisedb.com> writes:
The only intentional backward compatibility break in this patch set is
the the behavior of CREATEROLE. The general hope is that such a
compatibility break will help far more than it hurts, as CREATEROLE
does not appear to be a well adopted feature. I would expect that
breaking the behavior of the WITH ADMIN OPTION feature would cause a
lot more pain.Even more to the point, WITH ADMIN OPTION is defined by the SQL
standard.
The only way you get to mess with that is if you can convince people we
mis-implemented the standard.
Thank you for the detailed explanation.
I now understand what you said.
--
Regards,
--
Shinya Kato
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On 2021-10-28 07:21, Mark Dilger wrote:
On Oct 25, 2021, at 10:09 PM, Shinya Kato
<Shinya11.Kato@oss.nttdata.com> wrote:Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.I have three comments.
1. Is there a function to check the owner of a role, it would be nice
to be able to check with \du or pg_roles view.No, but that is a good idea.
These two ideas are implemented in v2. Both \du and pg_roles show the
owner information.The current solution is to run REASSIGN OWNED in each database where
the role owns objects before running DROP ROLE. At that point, the
CASCADE option (not implemented) won't be needed. Of course, I need
to post the next revision of this patch set addressing the
deficiencies that Nathan pointed out upthread to make that work.REASSIGN OWNED and ALTER ROLE..OWNER TO now work in v2.
When ALTER ROLE with the privilege of REPLICATION, only the superuser is
checked.
Therefore, we have a strange situation where we can create a role but
not change it.
---
postgres=> SELECT current_user;
current_user
--------------
test
(1 row)
postgres=> \du test
List of roles
Role name | Owner | Attributes | Member of
-----------+--------+--------------------------+-----------
test | shinya | Create role, Replication | {}
postgres=> CREATE ROLE test2 REPLICATION;
CREATE ROLE
postgres=> ALTER ROLE test2 NOREPLICATION;
2021-11-04 14:24:02.687 JST [2615016] ERROR: must be superuser to alter
replication roles or change replication attribute
2021-11-04 14:24:02.687 JST [2615016] STATEMENT: ALTER ROLE test2
NOREPLICATION;
ERROR: must be superuser to alter replication roles or change
replication attribute
---
Wouldn't it be better to check if the role has CREATEROLE and
REPLICATION?
The same is true for BYPASSRLS.
By the way, is this thread registered to CommitFest?
--
Regards,
--
Shinya Kato
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On 2021-11-04 16:00, Shinya Kato wrote:
On 2021-10-28 07:21, Mark Dilger wrote:
On Oct 25, 2021, at 10:09 PM, Shinya Kato
<Shinya11.Kato@oss.nttdata.com> wrote:Hi! Thank you for the patch.
I too think that CREATEROLE escalation attack is problem.I have three comments.
1. Is there a function to check the owner of a role, it would be
nice to be able to check with \du or pg_roles view.No, but that is a good idea.
These two ideas are implemented in v2. Both \du and pg_roles show the
owner information.The current solution is to run REASSIGN OWNED in each database where
the role owns objects before running DROP ROLE. At that point, the
CASCADE option (not implemented) won't be needed. Of course, I need
to post the next revision of this patch set addressing the
deficiencies that Nathan pointed out upthread to make that work.REASSIGN OWNED and ALTER ROLE..OWNER TO now work in v2.
When ALTER ROLE with the privilege of REPLICATION, only the superuser
is checked.
Therefore, we have a strange situation where we can create a role but
not change it.
---
postgres=> SELECT current_user;
current_user
--------------
test
(1 row)postgres=> \du test
List of roles
Role name | Owner | Attributes | Member of
-----------+--------+--------------------------+-----------
test | shinya | Create role, Replication | {}postgres=> CREATE ROLE test2 REPLICATION;
CREATE ROLE
postgres=> ALTER ROLE test2 NOREPLICATION;
2021-11-04 14:24:02.687 JST [2615016] ERROR: must be superuser to
alter replication roles or change replication attribute
2021-11-04 14:24:02.687 JST [2615016] STATEMENT: ALTER ROLE test2
NOREPLICATION;
ERROR: must be superuser to alter replication roles or change
replication attribute
---
Wouldn't it be better to check if the role has CREATEROLE and
REPLICATION?
The same is true for BYPASSRLS.By the way, is this thread registered to CommitFest?
I fixed the patches because they cannot be applied to HEAD.
--
Regards,
--
Shinya Kato
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
Attachments:
v3-0001-Add-tests-of-the-CREATEROLE-attribute.patchtext/x-diff; name=v3-0001-Add-tests-of-the-CREATEROLE-attribute.patchDownload
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..57010bbb58
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_4 REPLICATION;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_5 BYPASSRLS;
+ERROR: must be superuser to create bypassrls users
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+ERROR: role "regress_role_17" is a member of role "regress_role_17"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+ERROR: role "regress_role_19" is a member of role "regress_role_19"
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+ERROR: permission denied to create database
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ERROR: must be owner of index regress_idx_22
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+ERROR: must be owner of table regress_tbl_22
+DROP TABLE regress_tbl_22;
+ERROR: must be owner of table regress_tbl_22
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+ERROR: must be owner of view regress_view_22
+DROP VIEW regress_view_22;
+ERROR: must be owner of view regress_view_22
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+ERROR: permission denied to reassign objects
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+ERROR: role "regress_role_2" does not exist
+DROP ROLE regress_role_3;
+ERROR: role "regress_role_3" does not exist
+DROP ROLE regress_role_4;
+ERROR: role "regress_role_4" does not exist
+DROP ROLE regress_role_5;
+ERROR: role "regress_role_5" does not exist
+DROP ROLE regress_role_14;
+ERROR: role "regress_role_14" does not exist
+DROP ROLE regress_role_15;
+ERROR: role "regress_role_15" does not exist
+DROP ROLE regress_role_17;
+ERROR: role "regress_role_17" does not exist
+DROP ROLE regress_role_19;
+ERROR: role "regress_role_19" does not exist
+DROP ROLE regress_role_20;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
+DETAIL: owner of table regress_tbl_22
+owner of view regress_view_22
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+ERROR: must be superuser to drop superusers
+DROP ROLE regress_role_1;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 017e962fed..18dfdf5be8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -86,7 +86,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..e00893de4e
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 REPLICATION;
+CREATE ROLE regress_role_5 BYPASSRLS;
+
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+DROP TABLE regress_tbl_22;
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+DROP VIEW regress_view_22;
+
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
+DROP ROLE regress_role_14;
+DROP ROLE regress_role_15;
+DROP ROLE regress_role_17;
+DROP ROLE regress_role_19;
+DROP ROLE regress_role_20;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+DROP ROLE regress_role_1;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
v3-0002-Add-owners-to-roles.patchtext/x-diff; name=v3-0002-Add-owners-to-roles.patchDownload
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ce0a4ff14e..ddd205d656 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid role_oid, Oid roleid)
+{
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ /* Otherwise, look up the owner of the role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ return (owner_oid == roleid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index c20b1fbb96..9842a35a41 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1578,6 +1579,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 61b515cdb8..85f8fc7d4c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 40044070cf..eb407cfc4c 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index c8c0dd0dd5..e1296020b3 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +377,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +455,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -1078,8 +1116,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1686,3 +1725,104 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: This is different from most other alter-owner checks in that
+ * the current user is checked for create privileges instead of the
+ * destination owner. This is consistent with the CREATE case for
+ * roles. Because superusers will always have this right, we need no
+ * special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index df0b747883..939d445abb 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4518,6 +4518,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index cb7ddd463c..f717f5d1e0 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 3d4dd43e47..3512392888 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1077,9 +1077,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -1218,6 +1229,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9587,6 +9602,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c28788e84f..3d3b30f289 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3488,6 +3488,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3508,6 +3514,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3519,6 +3527,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 2d7115e31d..cce43388d8 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4c5a8a39bf..59409edb56 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2622,6 +2622,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..ec9d480d67 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 57010bbb58..e1ce5f3a66 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
@@ -11,14 +12,95 @@ CREATE ROLE regress_role_4 REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+\du+ regress_role_2
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+--------------------+-------------------------+-----------+-------------
+ regress_role_2 | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_role_3
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------------------+-----------+-------------
+ regress_role_3 | regress_role_1 | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_role_4
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_4 | regress_role_1 | Cannot login, Replication | {} |
+
+\du+ regress_role_5
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------------------+-----------+-------------
+ regress_role_5 | regress_role_1 | Cannot login, Bypass RLS | {} |
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+\du+ regress_role_6
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+-------------------------+-----------+-------------
+ regress_role_6 | regress_role_1 | Create DB, Cannot login | {} |
+
+\du+ regress_role_7
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
+
+\du+ regress_role_8
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+------------+-----------+-------------
+ regress_role_8 | regress_role_1 | | {} |
+
+\du+ regress_role_9
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------+-----------+-------------
+ regress_role_9 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_10
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+---------------+-----------+-------------
+ regress_role_10 | regress_role_1 | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_role_11
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------+-----------+-------------
+ regress_role_11 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_12
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+------------------------+------------------------------------------------+-------------
+ regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -84,16 +166,24 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+ERROR: must be owner of role regress_role_20
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+ERROR: permission denied to reassign objects
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
-ERROR: role "regress_role_2" does not exist
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
-ERROR: role "regress_role_3" does not exist
DROP ROLE regress_role_4;
-ERROR: role "regress_role_4" does not exist
DROP ROLE regress_role_5;
-ERROR: role "regress_role_5" does not exist
DROP ROLE regress_role_14;
ERROR: role "regress_role_14" does not exist
DROP ROLE regress_role_15;
@@ -103,9 +193,23 @@ ERROR: role "regress_role_17" does not exist
DROP ROLE regress_role_19;
ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_21
+owner of role regress_role_22
+owner of role regress_role_23
+owner of role regress_role_24
+owner of role regress_role_25
+owner of role regress_role_26
+owner of role regress_role_27
+owner of role regress_role_28
+owner of role regress_role_29
+owner of role regress_role_30
+owner of role regress_role_31
+owner of role regress_role_32
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -130,6 +234,10 @@ DROP ROLE regress_role_22;
ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
DETAIL: owner of table regress_tbl_22
owner of view regress_view_22
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +249,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 0bc79be03d..7ed1ee84a5 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -2358,7 +2360,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..6bce5a005d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index e00893de4e..8a4f177574 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
@@ -9,14 +10,42 @@ CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+
+\du+ regress_role_2
+\du+ regress_role_3
+\du+ regress_role_4
+\du+ regress_role_5
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+
+\du+ regress_role_6
+\du+ regress_role_7
+\du+ regress_role_8
+\du+ regress_role_9
+\du+ regress_role_10
+\du+ regress_role_11
+\du+ regress_role_12
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -86,9 +115,22 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
@@ -98,9 +140,11 @@ DROP ROLE regress_role_17;
DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -124,6 +168,9 @@ DROP ROLE regress_role_32;
-- fail, role still owns database objects
DROP ROLE regress_role_22;
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
@@ -134,5 +181,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..48de54a930 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -29,9 +29,13 @@ CREATE USER regress_priv_user2;
CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
+
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -1426,8 +1430,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
v3-0003-Give-role-owners-control-over-owned-roles.patchtext/x-diff; name=v3-0003-Give-role-owners-control-over-owned-roles.patchDownload
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ddd205d656..4a11a0f124 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,79 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
pg_role_ownercheck(Oid role_oid, Oid roleid)
{
- HeapTuple tuple;
- Form_pg_authid authform;
- Oid owner_oid;
-
/* Superusers bypass all permission checking. */
if (superuser_arg(roleid))
return true;
- /* Otherwise, look up the owner of the role */
- tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
- if (!HeapTupleIsValid(tuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("role with OID %u does not exist",
- role_oid)));
- authform = (Form_pg_authid) GETSTRUCT(tuple);
- owner_oid = authform->rolowner;
-
/*
- * Roles must necessarily have owners. Even the bootstrap user has an
- * owner. (It owns itself). Other roles must form a proper tree.
+ * Start with the owned role and traverse the ownership hierarchy upward.
+ * We stop when we find the owner we are looking for or when we reach the
+ * top.
*/
- if (!OidIsValid(owner_oid))
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u has invalid owner",
- authform->rolname.data, authform->oid)));
- if (authform->oid != BOOTSTRAP_SUPERUSERID &&
- authform->rolowner == authform->oid)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owns itself",
- authform->rolname.data, authform->oid)));
- if (authform->oid == BOOTSTRAP_SUPERUSERID &&
- authform->rolowner != BOOTSTRAP_SUPERUSERID)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owned by role with OID %u",
- authform->rolname.data, authform->oid,
- authform->rolowner)));
- ReleaseSysCache(tuple);
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ /* Have we found the target role? */
+ if (roleid == owner_oid)
+ return true;
+
+ /*
+ * If we have reached a role which owns itself, we must iterate no
+ * further, else we fall into an infinite loop.
+ */
+ if (role_oid == owner_oid)
+ return false;
+
+ /* Set up for the next iteration. */
+ role_oid = owner_oid;
+ }
- return (owner_oid == roleid);
+ return false;
}
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2bae3fbb17..cc19409ca3 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 6c6ab9ee34..3447806756 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
+ * NOTE: This is different from most other alter-owner checks in that the
* current user is checked for create privileges instead of the
* destination owner. This is consistent with the CREATE case for
* schemas. Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index e1296020b3..9c40766d83 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
!rolemembers &&
!validUntil &&
dpassword &&
- roleid == GetUserId()))
+ !pg_role_ownercheck(roleid, GetUserId())))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434..04eae9d4e5 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4850,6 +4850,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilge the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index e1ce5f3a66..492162e5a1 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -55,8 +55,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -95,11 +96,11 @@ CREATE ROLE regress_role_12
regress_role_11 | regress_role_1 | Cannot login | {} |
\du+ regress_role_12
- List of roles
- Role name | Owner | Attributes | Member of | Description
------------------+----------------+------------------------+------------------------------------------------+-------------
- regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
- | | 2 connections | |
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------------------------------+---------------------------------+--------------------
+ regress_role_12 | regress_role_1 | Create role, Create DB, Cannot login+| {regress_role_6,regress_role_7} | no login test role
+ | | 2 connections | |
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -140,21 +141,18 @@ CREATE TABLE regress_tbl_22 (i integer);
CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
-ERROR: must be owner of index regress_idx_22
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
-ERROR: must be owner of table regress_tbl_22
DROP TABLE regress_tbl_22;
-ERROR: must be owner of table regress_tbl_22
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-ERROR: must be owner of view regress_view_22
+ERROR: must be member of role "regress_role_1"
+-- ok
DROP VIEW regress_view_22;
-ERROR: must be owner of view regress_view_22
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-ERROR: permission denied to reassign objects
-- ok, having CREATEROLE is enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
@@ -166,15 +164,9 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
+-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-ERROR: must be owner of role regress_role_20
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-ERROR: permission denied to reassign objects
--- superuser can do it, though
-RESET SESSION AUTHORIZATION;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-- ok, superuser roles can drop superuser roles they own
SET SESSION AUTHORIZATION regress_role_super;
@@ -184,14 +176,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-ERROR: role "regress_role_14" does not exist
-DROP ROLE regress_role_15;
-ERROR: role "regress_role_15" does not exist
-DROP ROLE regress_role_17;
-ERROR: role "regress_role_17" does not exist
-DROP ROLE regress_role_19;
-ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
@@ -219,6 +203,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -229,28 +214,15 @@ DROP ROLE regress_role_29;
DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
-DETAIL: owner of table regress_tbl_22
-owner of view regress_view_22
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
DROP ROLE regress_role_1;
ERROR: current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
DETAIL: privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 8a4f177574..678f728f52 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -36,8 +36,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
@@ -92,15 +93,19 @@ CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
DROP TABLE regress_tbl_22;
+
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+
+-- ok
DROP VIEW regress_view_22;
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -115,16 +120,11 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
+-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
--- superuser can do it, though
-RESET SESSION AUTHORIZATION;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-
-- ok, superuser roles can drop superuser roles they own
SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
@@ -134,10 +134,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-DROP ROLE regress_role_15;
-DROP ROLE regress_role_17;
-DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
@@ -154,6 +150,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -165,25 +162,15 @@ DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
-- ok, can drop role if we revoke privileges first
v3-0004-Restrict-power-granted-via-CREATEROLE.patchtext/x-diff; name=v3-0004-Restrict-power-granted-via-CREATEROLE.patchDownload
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 64d9030652..bfdd6403cd 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3132,9 +3132,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3146,9 +3144,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3170,8 +3167,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..96e60d5a09 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they own except
+ <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and
+ <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication
+ roles, and only if the role owner does not alter the target role to have a
+ privilege which the role owner itself lacks. Ordinary roles can only change
+ their own password.
</para>
<para>
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.
+ Role owners can rename non-superuser roles they own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Owning roles may change
+ privilege for non-superuser roles they own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c..e65b55a004 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 4a11a0f124..a72f412e90 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5552,6 +5552,97 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+bool
+has_rolinherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 9c40766d83..274047c232 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -276,24 +267,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -713,7 +712,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* We already checked issuper, isreplication, and bypassrls */
if (!(inherit < 0 &&
@@ -914,7 +913,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -925,8 +924,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1039,18 +1037,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1231,7 +1223,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1242,7 +1234,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1468,7 +1460,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1478,9 +1470,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1656,9 +1648,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1814,7 +1806,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* roles. Because superusers will always have this right, we need no
* special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 04eae9d4e5..3438abbcee 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_rolinherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_rolinherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec9d480d67..63cde442a8 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -319,5 +319,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_rolinherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 492162e5a1..7df3683f2b 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -6,22 +6,16 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_4 REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
ERROR: must be superuser to own superusers
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
\du+ regress_role_2
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -50,7 +44,10 @@ CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -70,12 +67,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
----------------+----------------+---------------------------+-----------+-------------
regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
-\du+ regress_role_8
- List of roles
- Role name | Owner | Attributes | Member of | Description
-----------------+----------------+------------+-----------+-------------
- regress_role_8 | regress_role_1 | | {} |
-
\du+ regress_role_9
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -108,19 +99,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
CREATE ROLE regress_role_17 ROLE regress_role_17;
ERROR: role "regress_role_17" is a member of role "regress_role_17"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_role_19 ADMIN regress_role_19;
@@ -133,8 +124,11 @@ ERROR: permission denied to create database
CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -153,17 +147,27 @@ ERROR: must be member of role "regress_role_1"
DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
@@ -182,19 +186,8 @@ DROP ROLE regress_role_7;
ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_role_21
owner of role regress_role_22
-owner of role regress_role_23
-owner of role regress_role_24
-owner of role regress_role_25
-owner of role regress_role_26
-owner of role regress_role_27
-owner of role regress_role_28
-owner of role regress_role_29
-owner of role regress_role_30
-owner of role regress_role_31
-owner of role regress_role_32
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -204,16 +197,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- 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 678f728f52..b1c764cb82 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -6,6 +6,8 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
@@ -17,11 +19,6 @@ CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
-
\du+ regress_role_2
\du+ regress_role_3
\du+ regress_role_4
@@ -31,7 +28,11 @@ CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -42,7 +43,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
-\du+ regress_role_8
\du+ regress_role_9
\du+ regress_role_10
\du+ regress_role_11
@@ -54,12 +54,12 @@ CREATE ROLE regress_role_13 SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
@@ -67,7 +67,7 @@ CREATE ROLE regress_role_17 ROLE regress_role_17;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
@@ -83,9 +83,12 @@ CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -108,7 +111,7 @@ DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
@@ -141,7 +144,6 @@ DROP ROLE regress_role_7;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -151,16 +153,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
On Dec 21, 2021, at 5:11 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
I fixed the patches because they cannot be applied to HEAD.
Thank you.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Tue, Dec 21, 2021 at 8:26 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
On Dec 21, 2021, at 5:11 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
I fixed the patches because they cannot be applied to HEAD.
Thank you.
I reviewed and tested these and they LGTM. FYI the rebased v3 patches
upthread are raw diffs so git am won't apply them. I can add myself to
the CF as a reviewer if it is helpful.
On 12/23/21 16:06, Joshua Brindle wrote:
On Tue, Dec 21, 2021 at 8:26 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:On Dec 21, 2021, at 5:11 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
I fixed the patches because they cannot be applied to HEAD.
Thank you.
I reviewed and tested these and they LGTM. FYI the rebased v3 patches
upthread are raw diffs so git am won't apply them.
That's not at all unusual. I normally apply patches just using
patch -p 1 < $patchfile
I can add myself to
the CF as a reviewer if it is helpful.
Please do.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Mon, Jan 3, 2022 at 5:08 PM Andrew Dunstan <andrew@dunslane.net> wrote:
On 12/23/21 16:06, Joshua Brindle wrote:
On Tue, Dec 21, 2021 at 8:26 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:On Dec 21, 2021, at 5:11 PM, Shinya Kato <Shinya11.Kato@oss.nttdata.com> wrote:
I fixed the patches because they cannot be applied to HEAD.
Thank you.
I reviewed and tested these and they LGTM. FYI the rebased v3 patches
upthread are raw diffs so git am won't apply them.That's not at all unusual. I normally apply patches just using
patch -p 1 < $patchfile
I can add myself to
the CF as a reviewer if it is helpful.Please do.
I just ran across this and I don't know if it is intended behavior or
not, can you tell me why this happens?
postgres=> \du+
List of roles
Role name | Owner | Attributes
| Member of | Description
-----------+----------+------------------------------------------------------------+-----------+-------------
brindle | brindle | Password valid until 2022-01-05 00:00:00-05
| {} |
joshua | postgres | Create role
| {} |
postgres | postgres | Superuser, Create role, Create DB,
Replication, Bypass RLS | {} |
postgres=> \password
Enter new password for user "brindle":
Enter it again:
ERROR: role "brindle" with OID 16384 owns itself
On Jan 4, 2022, at 6:35 AM, Joshua Brindle <joshua.brindle@crunchydata.com> wrote:
I just ran across this and I don't know if it is intended behavior or
not
<snip>
postgres=> \password
Enter new password for user "brindle":
Enter it again:
ERROR: role "brindle" with OID 16384 owns itself
No, that looks like a bug. Thanks for reviewing!
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Jan 4, 2022, at 9:07 AM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:
No, that looks like a bug.
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Tue, Jan 4, 2022 at 3:39 PM Mark Dilger <mark.dilger@enterprisedb.com> wrote:
On Jan 4, 2022, at 9:07 AM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:
No, that looks like a bug.
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
I did:
ALTER ROLE brindle OWNER TO brindle;
On Jan 4, 2022, at 12:47 PM, Joshua Brindle <joshua.brindle@crunchydata.com> wrote:
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
I did:
ALTER ROLE brindle OWNER TO brindle;
Ok, thanks. I have rebased, fixed both REASSIGN OWNED BY and ALTER ROLE .. OWNER TO cases, and added regression coverage for them.
The last patch set to contain significant changes was v2, with v3 just being a rebase. Relative to those sets:
0001 -- rebased.
0002 -- rebased; extend AlterRoleOwner_internal to disallow making a role its own immediate owner.
0003 -- rebased; extend AlterRoleOwner_internal to disallow cycles in the role ownership graph.
0004 -- rebased.
0005 -- new; removes the broken pg_auth_members.grantor field.
Attachments:
v4-0001-Add-tests-of-the-CREATEROLE-attribute.patchapplication/octet-stream; name=v4-0001-Add-tests-of-the-CREATEROLE-attribute.patch; x-unix-mode=0644Download
From 2fd69c860c08805f9aaf25f375d952991cd45e00 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 10:35:10 -0800
Subject: [PATCH v4 1/5] Add tests of the CREATEROLE attribute.
While developing alternate rules for what privileges CREATEROLE has,
I noticed that none of the changes to how CREATEROLE works triggered
any regression test failures. This is problematic for two reasons.
It means the existing code has insufficient test coverage, and it
means that unintended changes introduced by subsequent patches may
go unnoticed. Fix that.
---
src/test/regress/expected/create_role.out | 145 ++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/create_role.sql | 138 ++++++++++++++++++++
3 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/create_role.out
create mode 100644 src/test/regress/sql/create_role.sql
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..57010bbb58
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_4 REPLICATION;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_role_5 BYPASSRLS;
+ERROR: must be superuser to create bypassrls users
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+ERROR: role "regress_role_17" is a member of role "regress_role_17"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+ERROR: role "regress_role_19" is a member of role "regress_role_19"
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+ERROR: permission denied to create database
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ERROR: must be owner of index regress_idx_22
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+ERROR: must be owner of table regress_tbl_22
+DROP TABLE regress_tbl_22;
+ERROR: must be owner of table regress_tbl_22
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+ERROR: must be owner of view regress_view_22
+DROP VIEW regress_view_22;
+ERROR: must be owner of view regress_view_22
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+ERROR: permission denied to reassign objects
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+ERROR: role "regress_role_2" does not exist
+DROP ROLE regress_role_3;
+ERROR: role "regress_role_3" does not exist
+DROP ROLE regress_role_4;
+ERROR: role "regress_role_4" does not exist
+DROP ROLE regress_role_5;
+ERROR: role "regress_role_5" does not exist
+DROP ROLE regress_role_14;
+ERROR: role "regress_role_14" does not exist
+DROP ROLE regress_role_15;
+ERROR: role "regress_role_15" does not exist
+DROP ROLE regress_role_17;
+ERROR: role "regress_role_17" does not exist
+DROP ROLE regress_role_19;
+ERROR: role "regress_role_19" does not exist
+DROP ROLE regress_role_20;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
+DETAIL: owner of table regress_tbl_22
+owner of view regress_view_22
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+ERROR: must be superuser to drop superusers
+DROP ROLE regress_role_1;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5b0c73d7e3..861c30a73a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -89,7 +89,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..e00893de4e
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- ok, superuser can create users with any set of privileges
+CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
+CREATE ROLE regress_role_2 SUPERUSER;
+CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 REPLICATION;
+CREATE ROLE regress_role_5 BYPASSRLS;
+
+-- ok, having CREATEROLE is enough to create users with these privileges
+CREATE ROLE regress_role_6 CREATEDB;
+CREATE ROLE regress_role_7 CREATEROLE;
+CREATE ROLE regress_role_8 LOGIN;
+CREATE ROLE regress_role_9 INHERIT;
+CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
+CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_role_12 PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_role_13 SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_role_14 IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_role_16 ROLE
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_role_17 ROLE regress_role_17;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_role_18 ADMIN
+ regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_9, regress_role_10, regress_role_11, regress_role_12;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_role_19 ADMIN regress_role_19;
+
+-- fail, regress_role_7 does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_role_7;
+CREATE DATABASE regress_db_7;
+
+-- ok, regress_role_7 can create new roles
+CREATE ROLE regress_role_20;
+
+-- ok, roles with CREATEROLE can create new roles with it
+CREATE ROLE regress_role_21 CREATEROLE;
+
+-- ok, roles with CREATEROLE can create new roles with privilege they lack
+CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+
+-- ok, regress_role_22 can create objects within the database
+SET SESSION AUTHORIZATION regress_role_22;
+CREATE TABLE regress_tbl_22 (i integer);
+CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
+CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
+
+-- fail, these objects belonging to regress_role_22
+SET SESSION AUTHORIZATION regress_role_7;
+DROP INDEX regress_idx_22;
+ALTER TABLE regress_tbl_22 ADD COLUMN t text;
+DROP TABLE regress_tbl_22;
+ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+DROP VIEW regress_view_22;
+
+-- fail, cannot take ownership of these objects from regress_role_22
+REASSIGN OWNED BY regress_role_22 TO regress_role_7;
+
+-- ok, having CREATEROLE is enough to create roles in privileged roles
+CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+
+-- fail, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_1;
+DROP ROLE regress_role_2;
+DROP ROLE regress_role_3;
+DROP ROLE regress_role_4;
+DROP ROLE regress_role_5;
+DROP ROLE regress_role_14;
+DROP ROLE regress_role_15;
+DROP ROLE regress_role_17;
+DROP ROLE regress_role_19;
+DROP ROLE regress_role_20;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_6;
+DROP ROLE regress_role_7;
+DROP ROLE regress_role_8;
+DROP ROLE regress_role_9;
+DROP ROLE regress_role_10;
+DROP ROLE regress_role_11;
+DROP ROLE regress_role_12;
+DROP ROLE regress_role_13;
+DROP ROLE regress_role_16;
+DROP ROLE regress_role_18;
+DROP ROLE regress_role_21;
+DROP ROLE regress_role_23;
+DROP ROLE regress_role_24;
+DROP ROLE regress_role_25;
+DROP ROLE regress_role_26;
+DROP ROLE regress_role_27;
+DROP ROLE regress_role_28;
+DROP ROLE regress_role_29;
+DROP ROLE regress_role_30;
+DROP ROLE regress_role_31;
+DROP ROLE regress_role_32;
+
+-- fail, role still owns database objects
+DROP ROLE regress_role_22;
+
+-- fail, cannot drop ourself nor superusers
+DROP ROLE regress_role_super;
+DROP ROLE regress_role_1;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX regress_idx_22;
+DROP TABLE regress_tbl_22;
+DROP VIEW regress_view_22;
+DROP ROLE regress_role_22;
+DROP ROLE regress_role_1;
+DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v4-0002-Add-owners-to-roles.patchapplication/octet-stream; name=v4-0002-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 5ed5697400c26e369956c24652c689d72fbb4242 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 11:19:32 -0800
Subject: [PATCH v4 2/5] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 59 ++++++-
src/backend/catalog/pg_shdepend.c | 5 +
src/backend/catalog/system_views.sql | 1 +
src/backend/commands/alter.c | 3 +
src/backend/commands/user.c | 148 +++++++++++++++++-
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/parser/gram.y | 23 +++
src/bin/psql/describe.c | 12 ++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 2 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/acl.h | 1 +
.../unsafe_tests/expected/rolenames.out | 6 +-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 136 ++++++++++++++--
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +-
src/test/regress/expected/rules.out | 1 +
src/test/regress/sql/create_role.sql | 67 +++++++-
src/test/regress/sql/privileges.sql | 10 +-
21 files changed, 470 insertions(+), 21 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ce0a4ff14e..ddd205d656 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid role_oid, Oid roleid)
+{
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ /* Otherwise, look up the owner of the role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ return (owner_oid == roleid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index c20b1fbb96..9842a35a41 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1578,6 +1579,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 61b515cdb8..85f8fc7d4c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 40044070cf..eb407cfc4c 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index aa69821be4..14820744bf 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +377,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +455,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -1078,8 +1116,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1675,3 +1714,110 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: This is different from most other alter-owner checks in that
+ * the current user is checked for create privileges instead of the
+ * destination owner. This is consistent with the CREATE case for
+ * roles. Because superusers will always have this right, we need no
+ * special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Only the bootstrap superuser is allowed to own itself. */
+ if (newOwnerId != BOOTSTRAP_SUPERUSERID && authForm->oid == newOwnerId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role may not own itself")));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 18e778e856..e26f3c0a64 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4519,6 +4519,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index cb7ddd463c..f717f5d1e0 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6dddc07947..48b9b0cfbe 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1077,9 +1077,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -1218,6 +1229,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9585,6 +9600,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c28788e84f..3d3b30f289 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3488,6 +3488,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3508,6 +3514,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3519,6 +3527,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 2d7115e31d..cce43388d8 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 593e301f7a..1a211924a8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2622,6 +2622,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index af771c901d..ec9d480d67 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 57010bbb58..7a5b830de7 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
@@ -11,14 +12,98 @@ CREATE ROLE regress_role_4 REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+\du+ regress_role_2
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+--------------------+-------------------------+-----------+-------------
+ regress_role_2 | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_role_3
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------------------+-----------+-------------
+ regress_role_3 | regress_role_1 | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_role_4
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_4 | regress_role_1 | Cannot login, Replication | {} |
+
+\du+ regress_role_5
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------------------+-----------+-------------
+ regress_role_5 | regress_role_1 | Cannot login, Bypass RLS | {} |
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_role_5 OWNER TO regress_role_5;
+ERROR: role may not own itself
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+\du+ regress_role_6
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+-------------------------+-----------+-------------
+ regress_role_6 | regress_role_1 | Create DB, Cannot login | {} |
+
+\du+ regress_role_7
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+---------------------------+-----------+-------------
+ regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
+
+\du+ regress_role_8
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+------------+-----------+-------------
+ regress_role_8 | regress_role_1 | | {} |
+
+\du+ regress_role_9
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------+----------------+--------------+-----------+-------------
+ regress_role_9 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_10
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+---------------+-----------+-------------
+ regress_role_10 | regress_role_1 | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_role_11
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------+-----------+-------------
+ regress_role_11 | regress_role_1 | Cannot login | {} |
+
+\du+ regress_role_12
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+------------------------+------------------------------------------------+-------------
+ regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -84,16 +169,24 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+ERROR: must be owner of role regress_role_20
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+ERROR: permission denied to reassign objects
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
-ERROR: role "regress_role_2" does not exist
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
-ERROR: role "regress_role_3" does not exist
DROP ROLE regress_role_4;
-ERROR: role "regress_role_4" does not exist
DROP ROLE regress_role_5;
-ERROR: role "regress_role_5" does not exist
DROP ROLE regress_role_14;
ERROR: role "regress_role_14" does not exist
DROP ROLE regress_role_15;
@@ -103,9 +196,23 @@ ERROR: role "regress_role_17" does not exist
DROP ROLE regress_role_19;
ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_21
+owner of role regress_role_22
+owner of role regress_role_23
+owner of role regress_role_24
+owner of role regress_role_25
+owner of role regress_role_26
+owner of role regress_role_27
+owner of role regress_role_28
+owner of role regress_role_29
+owner of role regress_role_30
+owner of role regress_role_31
+owner of role regress_role_32
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -130,6 +237,10 @@ DROP ROLE regress_role_22;
ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
DETAIL: owner of table regress_tbl_22
owner of view regress_view_22
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +252,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..9ce619fd5f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -2356,7 +2358,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..6bce5a005d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index e00893de4e..71fe01ad5d 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
@@ -9,14 +10,45 @@ CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
+
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
+CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
+CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
+
+\du+ regress_role_2
+\du+ regress_role_3
+\du+ regress_role_4
+\du+ regress_role_5
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_role_5 OWNER TO regress_role_5;
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
CREATE ROLE regress_role_8 LOGIN;
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
-CREATE ROLE regress_role_11 ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_role_12 PASSWORD NULL;
+CREATE ROLE regress_role_11 PASSWORD NULL;
+CREATE ROLE regress_role_12
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7, regress_role_8;
+
+\du+ regress_role_6
+\du+ regress_role_7
+\du+ regress_role_8
+\du+ regress_role_9
+\du+ regress_role_10
+\du+ regress_role_11
+\du+ regress_role_12
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -86,9 +118,22 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_role_7
SET SESSION AUTHORIZATION regress_role_1;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_20 OWNER TO regress_role_1;
+REASSIGN OWNED BY regress_role_20 TO regress_role_1;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_role_2;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
@@ -98,9 +143,11 @@ DROP ROLE regress_role_17;
DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_role_6;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_role_6;
DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
@@ -124,6 +171,9 @@ DROP ROLE regress_role_32;
-- fail, role still owns database objects
DROP ROLE regress_role_22;
+-- fail, role still owns other roles
+DROP ROLE regress_role_7;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
@@ -134,5 +184,12 @@ DROP INDEX regress_idx_22;
DROP TABLE regress_tbl_22;
DROP VIEW regress_view_22;
DROP ROLE regress_role_22;
+DROP ROLE regress_role_7;
+
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_1;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_1;
DROP ROLE regress_role_1;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..dcf84f91fd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -30,8 +30,10 @@ CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -1426,8 +1428,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v4-0003-Give-role-owners-control-over-owned-roles.patchapplication/octet-stream; name=v4-0003-Give-role-owners-control-over-owned-roles.patch; x-unix-mode=0644Download
From 1784a5b51d4dbebf99798b5832d92b0f585feb08 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 11:42:27 -0800
Subject: [PATCH v4 3/5] Give role owners control over owned roles
Create a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 51 +-------
src/backend/catalog/objectaddress.c | 22 +---
src/backend/commands/schemacmds.c | 2 +-
src/backend/commands/user.c | 28 +++--
src/backend/utils/adt/acl.c | 118 ++++++++++++++++++
src/include/utils/acl.h | 1 +
.../expected/dummy_seclabel.out | 12 +-
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +-
src/test/regress/expected/create_role.out | 68 ++++------
src/test/regress/sql/create_role.sql | 44 +++----
10 files changed, 207 insertions(+), 151 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ddd205d656..ef36fad700 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,20 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
pg_role_ownercheck(Oid role_oid, Oid roleid)
{
- HeapTuple tuple;
- Form_pg_authid authform;
- Oid owner_oid;
-
/* Superusers bypass all permission checking. */
if (superuser_arg(roleid))
return true;
- /* Otherwise, look up the owner of the role */
- tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
- if (!HeapTupleIsValid(tuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("role with OID %u does not exist",
- role_oid)));
- authform = (Form_pg_authid) GETSTRUCT(tuple);
- owner_oid = authform->rolowner;
-
- /*
- * Roles must necessarily have owners. Even the bootstrap user has an
- * owner. (It owns itself). Other roles must form a proper tree.
- */
- if (!OidIsValid(owner_oid))
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u has invalid owner",
- authform->rolname.data, authform->oid)));
- if (authform->oid != BOOTSTRAP_SUPERUSERID &&
- authform->rolowner == authform->oid)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owns itself",
- authform->rolname.data, authform->oid)));
- if (authform->oid == BOOTSTRAP_SUPERUSERID &&
- authform->rolowner != BOOTSTRAP_SUPERUSERID)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owned by role with OID %u",
- authform->rolname.data, authform->oid,
- authform->rolowner)));
- ReleaseSysCache(tuple);
-
- return (owner_oid == roleid);
+ /* Otherwise, check the role ownership hierarchy */
+ return is_owner_of_role_nosuper(roleid, role_oid);
}
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2bae3fbb17..cc19409ca3 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 6c6ab9ee34..3447806756 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
+ * NOTE: This is different from most other alter-owner checks in that the
* current user is checked for create privileges instead of the
* destination owner. This is consistent with the CREATE case for
* schemas. Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 14820744bf..11d5dffc90 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
!rolemembers &&
!validUntil &&
dpassword &&
- roleid == GetUserId()))
+ !pg_role_ownercheck(roleid, GetUserId())))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1811,6 +1813,18 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("role may not own itself")));
+ /*
+ * Must not create cycles in the role ownership hierarchy. If this
+ * role owns (directly or indirectly) the proposed new owner, disallow
+ * the ownership transfer.
+ */
+ if (is_owner_of_role_nosuper(authForm->oid, newOwnerId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role \"%s\" may not both own and be owned by role \"%s\"",
+ NameStr(authForm->rolname),
+ GetUserNameFromId(newOwnerId, false))));
+
authForm->rolowner = newOwnerId;
CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 67f8b29434..bcb7a50642 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4832,6 +4832,111 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
}
+/*
+ * Get a list of roles which own the given role, directly or indirectly.
+ *
+ * Each role has only one direct owner. The returned list contains the given
+ * role's owner, that role's owner, etc., up to the top of the ownership
+ * hierarchy, which is always the bootstrap superuser.
+ *
+ * Raises an error if any role ownership invariant is violated. Returns NIL if
+ * the given roleid is invalid.
+ */
+static List *
+roles_is_owned_by(Oid roleid)
+{
+ List *owners_list = NIL;
+ Oid role_oid = roleid;
+
+ /*
+ * Start with the current role and follow the ownership chain upwards until
+ * we reach the bootstrap superuser. To defend against getting into an
+ * infinite loop, we must check for ownership cycles. We choose to perform
+ * other corruption checks on the ownership structure while iterating, too.
+ */
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist", role_oid)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself).
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ NameStr(authform->rolname), authform->oid)));
+
+ /* The bootstrap user must own itself */
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ owner_oid != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ NameStr(authform->rolname), authform->oid,
+ authform->rolowner)));
+
+ /*
+ * Roles other than the bootstrap user must not be their own direct
+ * owners.
+ */
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->oid == owner_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ NameStr(authform->rolname), authform->oid)));
+
+ ReleaseSysCache(tuple);
+
+ /* If we have reached the bootstrap user, we're done. */
+ if (role_oid == BOOTSTRAP_SUPERUSERID)
+ {
+ if (!owners_list)
+ owners_list = lappend_oid(owners_list, owner_oid);
+ break;
+ }
+
+ /*
+ * For all other users, check they do not own themselves indirectly
+ * through an ownership cycle.
+ *
+ * Scanning the list each time through this loop results in overall
+ * quadratic work in the depth of the ownership chain, but we're
+ * not on a critical performance path, nor do we expect ownership
+ * hierarchies to be deep.
+ */
+ if (owners_list && list_member_oid(owners_list,
+ ObjectIdGetDatum(owner_oid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u indirectly owns itself",
+ GetUserNameFromId(owner_oid, false),
+ owner_oid)));
+
+ /* Done with sanity checks. Add this owner to the list. */
+ owners_list = lappend_oid(owners_list, owner_oid);
+
+ /* Otherwise, iterate on this iteration's owner_oid. */
+ role_oid = owner_oid;
+ }
+
+ return owners_list;
+}
+
/*
* Does member have the privileges of role (directly or indirectly)?
*
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilge the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
@@ -4921,6 +5030,15 @@ is_member_of_role_nosuper(Oid member, Oid role)
role);
}
+/*
+ * Is owner a direct or indirect owner of the role, not considering
+ * superuserness?
+ */
+bool
+is_owner_of_role_nosuper(Oid owner, Oid role)
+{
+ return list_member_oid(roles_is_owned_by(role), owner);
+}
/*
* Is member an admin of role? That is, is member the role itself (subject to
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index ec9d480d67..7f20aaa9f2 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,6 +209,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern bool is_owner_of_role_nosuper(Oid owner, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 7a5b830de7..b6cc68eed5 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -58,8 +58,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -98,11 +99,11 @@ CREATE ROLE regress_role_12
regress_role_11 | regress_role_1 | Cannot login | {} |
\du+ regress_role_12
- List of roles
- Role name | Owner | Attributes | Member of | Description
------------------+----------------+------------------------+------------------------------------------------+-------------
- regress_role_12 | regress_role_1 | Create role, Create DB+| {regress_role_6,regress_role_7,regress_role_8} |
- | | 2 connections | |
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+----------------+--------------------------------------+---------------------------------+--------------------
+ regress_role_12 | regress_role_1 | Create role, Create DB, Cannot login+| {regress_role_6,regress_role_7} | no login test role
+ | | 2 connections | |
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_role_13 SYSID 12345;
@@ -143,21 +144,18 @@ CREATE TABLE regress_tbl_22 (i integer);
CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
-ERROR: must be owner of index regress_idx_22
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
-ERROR: must be owner of table regress_tbl_22
DROP TABLE regress_tbl_22;
-ERROR: must be owner of table regress_tbl_22
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
-ERROR: must be owner of view regress_view_22
+ERROR: must be member of role "regress_role_1"
+-- ok
DROP VIEW regress_view_22;
-ERROR: must be owner of view regress_view_22
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-ERROR: permission denied to reassign objects
-- ok, having CREATEROLE is enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
@@ -169,14 +167,14 @@ CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
-SET SESSION AUTHORIZATION regress_role_1;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-ERROR: must be owner of role regress_role_20
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-ERROR: permission denied to reassign objects
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_1 TO regress_role_22;
+ERROR: role "regress_role_7" may not both own and be owned by role "regress_role_22"
+ALTER ROLE regress_role_1 OWNER TO regress_role_22;
+ERROR: role "regress_role_1" may not both own and be owned by role "regress_role_22"
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-- ok, superuser roles can drop superuser roles they own
@@ -187,14 +185,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-ERROR: role "regress_role_14" does not exist
-DROP ROLE regress_role_15;
-ERROR: role "regress_role_15" does not exist
-DROP ROLE regress_role_17;
-ERROR: role "regress_role_17" does not exist
-DROP ROLE regress_role_19;
-ERROR: role "regress_role_19" does not exist
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_role_7;
@@ -222,6 +212,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -232,28 +223,15 @@ DROP ROLE regress_role_29;
DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-ERROR: role "regress_role_22" cannot be dropped because some objects depend on it
-DETAIL: owner of table regress_tbl_22
-owner of view regress_view_22
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_role_22
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
DROP ROLE regress_role_1;
ERROR: current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
ERROR: role "regress_role_1" cannot be dropped because some objects depend on it
DETAIL: privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 71fe01ad5d..4a6f7840a2 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -39,8 +39,9 @@ CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
CREATE ROLE regress_role_12
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_role_6, regress_role_7, regress_role_8;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_role_6, regress_role_7;
+COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
@@ -95,15 +96,19 @@ CREATE INDEX regress_idx_22 ON regress_tbl_22(i);
CREATE VIEW regress_view_22 AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON regress_tbl_22 FROM PUBLIC;
--- fail, these objects belonging to regress_role_22
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_role_7;
DROP INDEX regress_idx_22;
ALTER TABLE regress_tbl_22 ADD COLUMN t text;
DROP TABLE regress_tbl_22;
+
+-- fail, not a member of target role
ALTER VIEW regress_view_22 OWNER TO regress_role_1;
+
+-- ok
DROP VIEW regress_view_22;
--- fail, cannot take ownership of these objects from regress_role_22
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -118,13 +123,13 @@ CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
--- fail, cannot take ownership of these objects from regress_role_7
-SET SESSION AUTHORIZATION regress_role_1;
-ALTER ROLE regress_role_20 OWNER TO regress_role_1;
-REASSIGN OWNED BY regress_role_20 TO regress_role_1;
-
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_1 TO regress_role_22;
+ALTER ROLE regress_role_1 OWNER TO regress_role_22;
+
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_1;
ALTER ROLE regress_role_20 OWNER TO regress_role_1;
REASSIGN OWNED BY regress_role_20 TO regress_role_1;
@@ -137,10 +142,6 @@ SET SESSION AUTHORIZATION regress_role_1;
DROP ROLE regress_role_3;
DROP ROLE regress_role_4;
DROP ROLE regress_role_5;
-DROP ROLE regress_role_14;
-DROP ROLE regress_role_15;
-DROP ROLE regress_role_17;
-DROP ROLE regress_role_19;
DROP ROLE regress_role_20;
-- fail, cannot drop roles that own other roles
@@ -157,6 +158,7 @@ DROP ROLE regress_role_13;
DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
+DROP ROLE regress_role_22;
DROP ROLE regress_role_23;
DROP ROLE regress_role_24;
DROP ROLE regress_role_25;
@@ -168,25 +170,15 @@ DROP ROLE regress_role_30;
DROP ROLE regress_role_31;
DROP ROLE regress_role_32;
--- fail, role still owns database objects
-DROP ROLE regress_role_22;
-
--- fail, role still owns other roles
-DROP ROLE regress_role_7;
-
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_1;
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX regress_idx_22;
-DROP TABLE regress_tbl_22;
-DROP VIEW regress_view_22;
-DROP ROLE regress_role_22;
+-- ok, no more owned roles remain
DROP ROLE regress_role_7;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_1;
-- ok, can drop role if we revoke privileges first
--
2.21.1 (Apple Git-122.3)
v4-0004-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v4-0004-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From 070d5d472b30b31be2be4365cbfdd8353bdff2da Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 18:53:34 -0800
Subject: [PATCH v4 4/5] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++---
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 +++++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 ++++++-----
src/backend/catalog/aclchk.c | 91 +++++++++++++++++++++++
src/backend/commands/user.c | 60 +++++++--------
src/backend/utils/adt/acl.c | 21 +-----
src/include/utils/acl.h | 5 ++
src/test/regress/expected/create_role.out | 63 ++++++----------
src/test/regress/sql/create_role.sql | 36 ++++-----
14 files changed, 230 insertions(+), 169 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 64d9030652..bfdd6403cd 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3132,9 +3132,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3146,9 +3144,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3170,8 +3167,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..96e60d5a09 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they own except
+ <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and
+ <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication
+ roles, and only if the role owner does not alter the target role to have a
+ privilege which the role owner itself lacks. Ordinary roles can only change
+ their own password.
</para>
<para>
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.
+ Role owners can rename non-superuser roles they own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Owning roles may change
+ privilege for non-superuser roles they own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c..e65b55a004 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index ef36fad700..6f3434b0b7 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5493,6 +5493,97 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+bool
+has_rolinherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 11d5dffc90..44f3f54b15 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -276,24 +267,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -713,7 +712,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* We already checked issuper, isreplication, and bypassrls */
if (!(inherit < 0 &&
@@ -914,7 +913,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -925,8 +924,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1039,18 +1037,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1231,7 +1223,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1242,7 +1234,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1457,7 +1449,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1467,9 +1459,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1645,9 +1637,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1803,7 +1795,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* roles. Because superusers will always have this right, we need no
* special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index bcb7a50642..764a8436c5 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_rolinherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_rolinherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 7f20aaa9f2..a6a72af5c1 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -320,5 +320,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid role_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_rolinherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index b6cc68eed5..89aaa9969b 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -6,22 +6,16 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_4 REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_role_5 BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
ERROR: must be superuser to own superusers
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
\du+ regress_role_2
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -53,7 +47,10 @@ ERROR: role may not own itself
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -73,12 +70,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
----------------+----------------+---------------------------+-----------+-------------
regress_role_7 | regress_role_1 | Create role, Cannot login | {} |
-\du+ regress_role_8
- List of roles
- Role name | Owner | Attributes | Member of | Description
-----------------+----------------+------------+-----------+-------------
- regress_role_8 | regress_role_1 | | {} |
-
\du+ regress_role_9
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -111,19 +102,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
CREATE ROLE regress_role_17 ROLE regress_role_17;
ERROR: role "regress_role_17" is a member of role "regress_role_17"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_role_19 ADMIN regress_role_19;
@@ -136,8 +127,11 @@ ERROR: permission denied to create database
CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -156,17 +150,27 @@ ERROR: must be member of role "regress_role_1"
DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
CREATE ROLE regress_role_26 IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
CREATE ROLE regress_role_27 IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
CREATE ROLE regress_role_28 IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
CREATE ROLE regress_role_29 IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
CREATE ROLE regress_role_30 IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
CREATE ROLE regress_role_31 IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
CREATE ROLE regress_role_32 IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
REASSIGN OWNED BY regress_role_1 TO regress_role_22;
@@ -191,19 +195,8 @@ DROP ROLE regress_role_7;
ERROR: role "regress_role_7" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_role_21
owner of role regress_role_22
-owner of role regress_role_23
-owner of role regress_role_24
-owner of role regress_role_25
-owner of role regress_role_26
-owner of role regress_role_27
-owner of role regress_role_28
-owner of role regress_role_29
-owner of role regress_role_30
-owner of role regress_role_31
-owner of role regress_role_32
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -213,16 +206,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- 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 4a6f7840a2..11cf26dd82 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -6,6 +6,8 @@ GRANT CREATE ON DATABASE regression TO regress_role_1;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_2 SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_role_3 REPLICATION BYPASSRLS;
CREATE ROLE regress_role_4 REPLICATION;
CREATE ROLE regress_role_5 BYPASSRLS;
@@ -17,11 +19,6 @@ CREATE ROLE regress_role_2 AUTHORIZATION regress_role_1 SUPERUSER;
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_role_2 AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_role_3 AUTHORIZATION regress_role_1 REPLICATION BYPASSRLS;
-CREATE ROLE regress_role_4 AUTHORIZATION regress_role_1 REPLICATION;
-CREATE ROLE regress_role_5 AUTHORIZATION regress_role_1 BYPASSRLS;
-
\du+ regress_role_2
\du+ regress_role_3
\du+ regress_role_4
@@ -34,7 +31,11 @@ ALTER ROLE regress_role_5 OWNER TO regress_role_5;
SET SESSION AUTHORIZATION regress_role_1;
CREATE ROLE regress_role_6 CREATEDB;
CREATE ROLE regress_role_7 CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_role_8 LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_role_9 INHERIT;
CREATE ROLE regress_role_10 CONNECTION LIMIT 5;
CREATE ROLE regress_role_11 PASSWORD NULL;
@@ -45,7 +46,6 @@ COMMENT ON ROLE regress_role_12 IS 'no login test role';
\du+ regress_role_6
\du+ regress_role_7
-\du+ regress_role_8
\du+ regress_role_9
\du+ regress_role_10
\du+ regress_role_11
@@ -57,12 +57,12 @@ CREATE ROLE regress_role_13 SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_role_14 IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_role_15 IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_role_16 ROLE
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself
@@ -70,7 +70,7 @@ CREATE ROLE regress_role_17 ROLE regress_role_17;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_role_18 ADMIN
- regress_role_super, regress_role_6, regress_role_7, regress_role_8,
+ regress_role_super, regress_role_6, regress_role_7,
regress_role_9, regress_role_10, regress_role_11, regress_role_12;
-- fail, cannot grant a role into itself with admin option
@@ -86,9 +86,12 @@ CREATE ROLE regress_role_20;
-- ok, roles with CREATEROLE can create new roles with it
CREATE ROLE regress_role_21 CREATEROLE;
--- ok, roles with CREATEROLE can create new roles with privilege they lack
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_role_22 CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_role_22 CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_role_22 can create objects within the database
SET SESSION AUTHORIZATION regress_role_22;
CREATE TABLE regress_tbl_22 (i integer);
@@ -111,7 +114,7 @@ DROP VIEW regress_view_22;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_role_22 TO regress_role_7;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
CREATE ROLE regress_role_23 IN ROLE pg_read_all_data;
CREATE ROLE regress_role_24 IN ROLE pg_write_all_data;
CREATE ROLE regress_role_25 IN ROLE pg_monitor;
@@ -149,7 +152,6 @@ DROP ROLE regress_role_7;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_role_6;
-DROP ROLE regress_role_8;
DROP ROLE regress_role_9;
DROP ROLE regress_role_10;
DROP ROLE regress_role_11;
@@ -159,16 +161,6 @@ DROP ROLE regress_role_16;
DROP ROLE regress_role_18;
DROP ROLE regress_role_21;
DROP ROLE regress_role_22;
-DROP ROLE regress_role_23;
-DROP ROLE regress_role_24;
-DROP ROLE regress_role_25;
-DROP ROLE regress_role_26;
-DROP ROLE regress_role_27;
-DROP ROLE regress_role_28;
-DROP ROLE regress_role_29;
-DROP ROLE regress_role_30;
-DROP ROLE regress_role_31;
-DROP ROLE regress_role_32;
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v4-0005-Remove-grantor-field-from-pg_auth_members.patchapplication/octet-stream; name=v4-0005-Remove-grantor-field-from-pg_auth_members.patch; x-unix-mode=0644Download
From 3ce6981776a708f4c865e04cfb05b0357f39c1d6 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 19:51:44 -0800
Subject: [PATCH v4 5/5] Remove grantor field from pg_auth_members
The "grantor" field of pg_auth_members is unused and, more to the
point, unreliable. The system does not avoid dangling references
from the grantor field to dropped roles in pg_authid. This creates
the risk that, if an oid is reused for a new role, the grantor field
may point to a role other than the one that performed the grant.
We could fix the bug, but there is no clear solution to the problem
that existing installations may have broken data. Since the field
is not used for any purpose, removing it seems the best option.
---
src/backend/commands/user.c | 17 -----------------
src/bin/pg_dump/pg_dumpall.c | 17 ++---------------
src/include/catalog/pg_auth_members.h | 1 -
src/test/regress/expected/oidjoins.out | 1 -
4 files changed, 2 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 44f3f54b15..c70ee87f9d 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1481,21 +1481,6 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- *
- * Before lifting this restriction, give the member == role case of
- * is_admin_of_role() a fresh look. Ensure that the current role cannot
- * use an explicit grantor specification to take advantage of the session
- * user's self-admin right.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
-
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -1571,12 +1556,10 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record[Anum_pg_auth_members_roleid - 1] = ObjectIdGetDatum(roleid);
new_record[Anum_pg_auth_members_member - 1] = ObjectIdGetDatum(memberid);
- new_record[Anum_pg_auth_members_grantor - 1] = ObjectIdGetDatum(grantorId);
new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(admin_opt);
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9ff0c091a9..a20778299d 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -940,14 +940,12 @@ dumpRoleMembership(PGconn *conn)
printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
- "LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,3", role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
@@ -963,17 +961,6 @@ dumpRoleMembership(PGconn *conn)
fprintf(OPF, " TO %s", fmtId(member));
if (*option == 't')
fprintf(OPF, " WITH ADMIN OPTION");
-
- /*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
- */
- if (!PQgetisnull(res, i, 3))
- {
- char *grantor = PQgetvalue(res, i, 3);
-
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
- }
fprintf(OPF, ";\n");
}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index b9d3ffd1c5..21b805f6de 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -31,7 +31,6 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
{
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
- Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
bool admin_option; /* granted with admin option? */
} FormData_pg_auth_members;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 266a30a85b..a6a73df7ff 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,7 +197,6 @@ NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
-NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
NOTICE: checking pg_shdepend {dbid} => pg_database {oid}
NOTICE: checking pg_shdepend {classid} => pg_class {oid}
NOTICE: checking pg_shdepend {refclassid} => pg_class {oid}
--
2.21.1 (Apple Git-122.3)
On Wed, Jan 5, 2022 at 7:05 PM Mark Dilger <mark.dilger@enterprisedb.com> wrote:
On Jan 4, 2022, at 12:47 PM, Joshua Brindle <joshua.brindle@crunchydata.com> wrote:
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
I did:
ALTER ROLE brindle OWNER TO brindle;Ok, thanks. I have rebased, fixed both REASSIGN OWNED BY and ALTER ROLE .. OWNER TO cases, and added regression coverage for them.
The last patch set to contain significant changes was v2, with v3 just being a rebase. Relative to those sets:
0001 -- rebased.
0002 -- rebased; extend AlterRoleOwner_internal to disallow making a role its own immediate owner.
0003 -- rebased; extend AlterRoleOwner_internal to disallow cycles in the role ownership graph.
0004 -- rebased.
0005 -- new; removes the broken pg_auth_members.grantor field.
LGTM +1
On 1/5/22 19:05, Mark Dilger wrote:
On Jan 4, 2022, at 12:47 PM, Joshua Brindle <joshua.brindle@crunchydata.com> wrote:
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
I did:
ALTER ROLE brindle OWNER TO brindle;Ok, thanks. I have rebased, fixed both REASSIGN OWNED BY and ALTER ROLE .. OWNER TO cases, and added regression coverage for them.
The last patch set to contain significant changes was v2, with v3 just being a rebase. Relative to those sets:
0001 -- rebased.
0002 -- rebased; extend AlterRoleOwner_internal to disallow making a role its own immediate owner.
0003 -- rebased; extend AlterRoleOwner_internal to disallow cycles in the role ownership graph.
0004 -- rebased.
0005 -- new; removes the broken pg_auth_members.grantor field.
In general this looks good. Some nitpicks:
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid role_oid, Oid roleid)
This is a bit confusing. Let's rename these params so it's clear which
is the owner and which the owned role.
+ * Note: In versions prior to PostgreSQL version 15, roles did not have
owners
+ * per se; instead we used this test in places where an ownership-like
+ * permissions test was needed for a role.
No need to talk about what we used to do. People who want to know can
look back at older branches.
+bool
+has_rolinherit_privilege(Oid roleid)
+{
This and similar functions should have header comments.
+ /* Owners of roles have every privilge the owned role has */
s/privlge/privilege/
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
I don't really like this business of just numbering large numbers of
roles in the tests. Let's give them more meaningful names.
+ Role owners can change any of these settings on roles they own except
I would say "on roles they directly or indirectly own", here and
similarly in one or two other places.
...
I will probably do one or two more passes over the patches, but as I say
in general they look fairly good.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Jan 10, 2022, at 2:34 PM, Andrew Dunstan <andrew@dunslane.net> wrote:
In general this looks good. Some nitpicks:
Thanks. Some responses...
+/* + * Ownership check for a role (specified by OID) + */ +bool +pg_role_ownercheck(Oid role_oid, Oid roleid)This is a bit confusing. Let's rename these params so it's clear which
is the owner and which the owned role.
Yeah, I wondered about that when I was writing it. All the neighboring functions follow the pattern:
(Oid <something>_oid, Oid roleid)
so I followed that, but it isn't great. I've changed that in v5-0002 to use
(Oid owned_role_oid, Oid owner_roleid)
I wouldn't choose this naming in a green field, but I'm trying to stay close to the naming scheme of the surrounding functions.
+ * Note: In versions prior to PostgreSQL version 15, roles did not have owners + * per se; instead we used this test in places where an ownership-like + * permissions test was needed for a role.No need to talk about what we used to do. People who want to know can
look back at older branches.
Removed in v5-0003.
+bool +has_rolinherit_privilege(Oid roleid) +{This and similar functions should have header comments.
Header comments added for this and similar functions in v5-0004. This function was misnamed in prior patch sets; the privilege is INHERIT, not ROLINHERIT, so I also fixed the name in v5-0004.
+ /* Owners of roles have every privilge the owned role has */
s/privlge/privilege/
Fixed in v5-0003.
+CREATE ROLE regress_role_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS;
I don't really like this business of just numbering large numbers of
roles in the tests. Let's give them more meaningful names.
Changed in v5-0001.
+ Role owners can change any of these settings on roles they own except
I would say "on roles they directly or indirectly own", here and
similarly in one or two other places.
Changed a few sentences of doc/src/sgml/ref/alter_role.sgml in v5-0004 as you suggest. Please advise if you have other locations in mind. A quick grep -i 'role owner' doesn't show any other relevant locations.
Attachments:
v5-0001-Add-tests-of-the-CREATEROLE-attribute.patchapplication/octet-stream; name=v5-0001-Add-tests-of-the-CREATEROLE-attribute.patch; x-unix-mode=0644Download
From 11f8a16020547d2cb95a750b4f9c92303726a132 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 11:14:23 -0800
Subject: [PATCH v5 1/5] Add tests of the CREATEROLE attribute.
While developing alternate rules for what privileges CREATEROLE has,
I noticed that none of the changes to how CREATEROLE works triggered
any regression test failures. This is problematic for two reasons.
It means the existing code has insufficient test coverage, and it
means that unintended changes introduced by subsequent patches may
go unnoticed. Fix that.
---
src/test/regress/expected/create_role.out | 145 ++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/create_role.sql | 138 ++++++++++++++++++++
3 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/create_role.out
create mode 100644 src/test/regress/sql/create_role.sql
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..4e67d72760
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- 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;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_nosuch_replication REPLICATION;
+ERROR: must be superuser to create replication users
+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_login LOGIN;
+CREATE ROLE regress_inherit INHERIT;
+CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
+CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_password_null PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_noiseword SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_inroles ROLE
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
+ERROR: role "regress_nosuch_recursive" is a member of role "regress_nosuch_recursive"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_adminroles ADMIN
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
+ERROR: role "regress_nosuch_admin_recursive" is a member of role "regress_nosuch_admin_recursive"
+-- fail, regress_createrole does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE DATABASE regress_nosuch_db;
+ERROR: permission denied to create database
+-- ok, regress_createrole can create new roles
+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;
+-- ok, regress_tenant can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant;
+CREATE TABLE tenant_table (i integer);
+CREATE INDEX tenant_idx ON tenant_table(i);
+CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
+-- fail, these objects belonging to regress_tenant
+SET SESSION AUTHORIZATION regress_createrole;
+DROP INDEX tenant_idx;
+ERROR: must be owner of index tenant_idx
+ALTER TABLE tenant_table ADD COLUMN t text;
+ERROR: must be owner of table tenant_table
+DROP TABLE tenant_table;
+ERROR: must be owner of table tenant_table
+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
+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
+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;
+CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+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, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_admin;
+DROP ROLE regress_nosuch_superuser;
+ERROR: role "regress_nosuch_superuser" does not exist
+DROP ROLE regress_nosuch_replication_bypassrls;
+ERROR: role "regress_nosuch_replication_bypassrls" does not exist
+DROP ROLE regress_nosuch_replication;
+ERROR: role "regress_nosuch_replication" does not exist
+DROP ROLE regress_nosuch_bypassrls;
+ERROR: role "regress_nosuch_bypassrls" does not exist
+DROP ROLE regress_nosuch_super;
+ERROR: role "regress_nosuch_super" does not exist
+DROP ROLE regress_nosuch_dbowner;
+ERROR: role "regress_nosuch_dbowner" does not exist
+DROP ROLE regress_nosuch_recursive;
+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;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
+DROP ROLE regress_login;
+DROP ROLE regress_inherit;
+DROP ROLE regress_connection_limit;
+DROP ROLE regress_encrypted_password;
+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
+DROP ROLE regress_role_admin;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX tenant_idx;
+DROP TABLE tenant_table;
+DROP VIEW tenant_view;
+DROP ROLE regress_tenant;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5b0c73d7e3..861c30a73a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -89,7 +89,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..292dc08797
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- 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;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication REPLICATION;
+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_login LOGIN;
+CREATE ROLE regress_inherit INHERIT;
+CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
+CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_password_null PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_noiseword SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_inroles ROLE
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_adminroles ADMIN
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
+
+-- fail, regress_createrole does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE DATABASE regress_nosuch_db;
+
+-- ok, regress_createrole can create new roles
+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;
+
+-- ok, regress_tenant can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant;
+CREATE TABLE tenant_table (i integer);
+CREATE INDEX tenant_idx ON tenant_table(i);
+CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
+
+-- fail, these objects belonging to regress_tenant
+SET SESSION AUTHORIZATION regress_createrole;
+DROP INDEX tenant_idx;
+ALTER TABLE tenant_table ADD COLUMN t text;
+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
+REASSIGN OWNED BY regress_tenant TO regress_createrole;
+
+-- ok, having CREATEROLE is 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;
+CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+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, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_admin;
+DROP ROLE regress_nosuch_superuser;
+DROP ROLE regress_nosuch_replication_bypassrls;
+DROP ROLE regress_nosuch_replication;
+DROP ROLE regress_nosuch_bypassrls;
+DROP ROLE regress_nosuch_super;
+DROP ROLE regress_nosuch_dbowner;
+DROP ROLE regress_nosuch_recursive;
+DROP ROLE regress_nosuch_admin_recursive;
+DROP ROLE regress_plainrole;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
+DROP ROLE regress_login;
+DROP ROLE regress_inherit;
+DROP ROLE regress_connection_limit;
+DROP ROLE regress_encrypted_password;
+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;
+DROP ROLE regress_role_admin;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX tenant_idx;
+DROP TABLE tenant_table;
+DROP VIEW tenant_view;
+DROP ROLE regress_tenant;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v5-0002-Add-owners-to-roles.patchapplication/octet-stream; name=v5-0002-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 83dea225a5baac7efd05e9b835055c031a290c2d Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 11:15:02 -0800
Subject: [PATCH v5 2/5] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 59 ++++++-
src/backend/catalog/pg_shdepend.c | 5 +
src/backend/catalog/system_views.sql | 1 +
src/backend/commands/alter.c | 3 +
src/backend/commands/user.c | 148 +++++++++++++++++-
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/parser/gram.y | 23 +++
src/bin/psql/describe.c | 12 ++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 2 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/acl.h | 1 +
.../unsafe_tests/expected/rolenames.out | 6 +-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 136 ++++++++++++++--
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +-
src/test/regress/expected/rules.out | 1 +
src/test/regress/sql/create_role.sql | 67 +++++++-
src/test/regress/sql/privileges.sql | 10 +-
21 files changed, 470 insertions(+), 21 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1dd03a8e51..e5c7324340 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid)
+{
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(owner_roleid))
+ return true;
+
+ /* Otherwise, look up the owner of the role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(owned_role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ owned_role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ return (owner_oid == owner_roleid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..52f69ad76e 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1578,6 +1579,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 701ff38f76..af08b69864 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 1f64c8aa51..e6c7c84c87 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 3b512a84b3..a4e2223a2d 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +377,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +455,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -1078,8 +1116,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1675,3 +1714,110 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: This is different from most other alter-owner checks in that
+ * the current user is checked for create privileges instead of the
+ * destination owner. This is consistent with the CREATE case for
+ * roles. Because superusers will always have this right, we need no
+ * special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Only the bootstrap superuser is allowed to own itself. */
+ if (newOwnerId != BOOTSTRAP_SUPERUSERID && authForm->oid == newOwnerId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role may not own itself")));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 456d563f34..cb0794e873 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4519,6 +4519,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 53beef1488..05dd05b170 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 879018377b..ef56f87d66 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1077,9 +1077,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -1218,6 +1229,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9585,6 +9600,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 8587b19160..fcf641ec41 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3496,6 +3496,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3516,6 +3522,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3527,6 +3535,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f..3af0f3908c 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 413e7c85a1..06ad44569b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2622,6 +2622,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 1ce4c5556e..42f85d0e84 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..66d4c29cf7 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;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
@@ -11,14 +12,98 @@ CREATE ROLE regress_nosuch_replication REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+\du+ regress_nosuch_superuser
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+-------------------------+-----------+-------------
+ regress_nosuch_superuser | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_nosuch_replication_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------------------+--------------------+---------------------------------------+-----------+-------------
+ regress_nosuch_replication_bypassrls | regress_role_admin | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_nosuch_replication
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------------------+--------------------+---------------------------+-----------+-------------
+ regress_nosuch_replication | regress_role_admin | Cannot login, Replication | {} |
+
+\du+ regress_nosuch_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+--------------------------+-----------+-------------
+ regress_nosuch_bypassrls | regress_role_admin | Cannot login, Bypass RLS | {} |
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
+ERROR: role may not own itself
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole, regress_login;
+\du+ regress_createdb
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+------------------+--------------------+-------------------------+-----------+-------------
+ regress_createdb | regress_role_admin | Create DB, Cannot login | {} |
+
+\du+ regress_createrole
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------+--------------------+---------------------------+-----------+-------------
+ regress_createrole | regress_role_admin | Create role, Cannot login | {} |
+
+\du+ regress_login
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+---------------+--------------------+------------+-----------+-------------
+ regress_login | regress_role_admin | | {} |
+
+\du+ regress_inherit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+--------------------+--------------+-----------+-------------
+ regress_inherit | regress_role_admin | Cannot login | {} |
+
+\du+ regress_connection_limit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+---------------+-----------+-------------
+ regress_connection_limit | regress_role_admin | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_encrypted_password
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------------------+--------------------+--------------+-----------+-------------
+ regress_encrypted_password | regress_role_admin | Cannot login | {} |
+
+\du+ regress_password_null
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------------+--------------------+------------------------+-----------------------------------------------------+-------------
+ regress_password_null | regress_role_admin | Create role, Create DB+| {regress_createdb,regress_createrole,regress_login} |
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -84,16 +169,24 @@ CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
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, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_createrole
SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+ERROR: must be owner of role regress_plainrole
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+ERROR: permission denied to reassign objects
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_nosuch_superuser;
-ERROR: role "regress_nosuch_superuser" does not exist
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
-ERROR: role "regress_nosuch_replication_bypassrls" does not exist
DROP ROLE regress_nosuch_replication;
-ERROR: role "regress_nosuch_replication" does not exist
DROP ROLE regress_nosuch_bypassrls;
-ERROR: role "regress_nosuch_bypassrls" does not exist
DROP ROLE regress_nosuch_super;
ERROR: role "regress_nosuch_super" does not exist
DROP ROLE regress_nosuch_dbowner;
@@ -103,9 +196,23 @@ 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;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_rolecreator
+owner of role regress_tenant
+owner of role regress_read_all_data
+owner of role regress_write_all_data
+owner of role regress_monitor
+owner of role regress_read_all_settings
+owner of role regress_read_all_stats
+owner of role regress_stat_scan_tables
+owner of role regress_read_server_files
+owner of role regress_write_server_files
+owner of role regress_execute_server_program
+owner of role regress_signal_backend
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -130,6 +237,10 @@ 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, role still owns other roles
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_tenant
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +252,12 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_createrole;
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_admin;
+ERROR: role "regress_role_admin" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..9ce619fd5f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -2356,7 +2358,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..6bce5a005d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..091b116dd6 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;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
@@ -9,14 +10,45 @@ CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
+
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+
+\du+ regress_nosuch_superuser
+\du+ regress_nosuch_replication_bypassrls
+\du+ regress_nosuch_replication
+\du+ regress_nosuch_bypassrls
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole, regress_login;
+
+\du+ regress_createdb
+\du+ regress_createrole
+\du+ regress_login
+\du+ regress_inherit
+\du+ regress_connection_limit
+\du+ regress_encrypted_password
+\du+ regress_password_null
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
@@ -86,9 +118,22 @@ 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, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_createrole
SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_nosuch_superuser;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
@@ -98,9 +143,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -124,6 +171,9 @@ DROP ROLE regress_signal_backend;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
+-- fail, role still owns other roles
+DROP ROLE regress_createrole;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_admin;
@@ -134,5 +184,12 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_createrole;
+
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_admin;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..dcf84f91fd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -30,8 +30,10 @@ CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -1426,8 +1428,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v5-0003-Give-role-owners-control-over-owned-roles.patchapplication/octet-stream; name=v5-0003-Give-role-owners-control-over-owned-roles.patch; x-unix-mode=0644Download
From 76bb0669c0792b228a5a7ff455ebe0bc0ffbfbdc Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 11:16:05 -0800
Subject: [PATCH v5 3/5] Give role owners control over owned roles
Create a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.
---
src/backend/catalog/aclchk.c | 49 +-------
src/backend/catalog/objectaddress.c | 22 +---
src/backend/commands/schemacmds.c | 2 +-
src/backend/commands/user.c | 28 +++--
src/backend/utils/adt/acl.c | 118 ++++++++++++++++++
src/include/utils/acl.h | 1 +
.../expected/dummy_seclabel.out | 12 +-
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +-
src/test/regress/expected/create_role.out | 68 ++++------
src/test/regress/sql/create_role.sql | 44 +++----
10 files changed, 204 insertions(+), 152 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index e5c7324340..1e0ee503e4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,16 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid)
{
- HeapTuple tuple;
- Form_pg_authid authform;
- Oid owner_oid;
-
/* Superusers bypass all permission checking. */
if (superuser_arg(owner_roleid))
return true;
- /* Otherwise, look up the owner of the role */
- tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(owned_role_oid));
- if (!HeapTupleIsValid(tuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("role with OID %u does not exist",
- owned_role_oid)));
- authform = (Form_pg_authid) GETSTRUCT(tuple);
- owner_oid = authform->rolowner;
-
- /*
- * Roles must necessarily have owners. Even the bootstrap user has an
- * owner. (It owns itself). Other roles must form a proper tree.
- */
- if (!OidIsValid(owner_oid))
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u has invalid owner",
- authform->rolname.data, authform->oid)));
- if (authform->oid != BOOTSTRAP_SUPERUSERID &&
- authform->rolowner == authform->oid)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owns itself",
- authform->rolname.data, authform->oid)));
- if (authform->oid == BOOTSTRAP_SUPERUSERID &&
- authform->rolowner != BOOTSTRAP_SUPERUSERID)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owned by role with OID %u",
- authform->rolname.data, authform->oid,
- authform->rolowner)));
- ReleaseSysCache(tuple);
-
- return (owner_oid == owner_roleid);
+ /* Otherwise, check the role ownership hierarchy */
+ return is_owner_of_role_nosuper(owner_roleid, owned_role_oid);
}
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
- *
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index f30c742d48..1a47f28829 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 984000a5bc..e5405db658 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
+ * NOTE: This is different from most other alter-owner checks in that the
* current user is checked for create privileges instead of the
* destination owner. This is consistent with the CREATE case for
* schemas. Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index a4e2223a2d..cccb24a498 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
!rolemembers &&
!validUntil &&
dpassword &&
- roleid == GetUserId()))
+ !pg_role_ownercheck(roleid, GetUserId())))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1811,6 +1813,18 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("role may not own itself")));
+ /*
+ * Must not create cycles in the role ownership hierarchy. If this
+ * role owns (directly or indirectly) the proposed new owner, disallow
+ * the ownership transfer.
+ */
+ if (is_owner_of_role_nosuper(authForm->oid, newOwnerId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role \"%s\" may not both own and be owned by role \"%s\"",
+ NameStr(authForm->rolname),
+ GetUserNameFromId(newOwnerId, false))));
+
authForm->rolowner = newOwnerId;
CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156c..97336db058 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4832,6 +4832,111 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
}
+/*
+ * Get a list of roles which own the given role, directly or indirectly.
+ *
+ * Each role has only one direct owner. The returned list contains the given
+ * role's owner, that role's owner, etc., up to the top of the ownership
+ * hierarchy, which is always the bootstrap superuser.
+ *
+ * Raises an error if any role ownership invariant is violated. Returns NIL if
+ * the given roleid is invalid.
+ */
+static List *
+roles_is_owned_by(Oid roleid)
+{
+ List *owners_list = NIL;
+ Oid role_oid = roleid;
+
+ /*
+ * Start with the current role and follow the ownership chain upwards until
+ * we reach the bootstrap superuser. To defend against getting into an
+ * infinite loop, we must check for ownership cycles. We choose to perform
+ * other corruption checks on the ownership structure while iterating, too.
+ */
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist", role_oid)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself).
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ NameStr(authform->rolname), authform->oid)));
+
+ /* The bootstrap user must own itself */
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ owner_oid != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ NameStr(authform->rolname), authform->oid,
+ authform->rolowner)));
+
+ /*
+ * Roles other than the bootstrap user must not be their own direct
+ * owners.
+ */
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->oid == owner_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ NameStr(authform->rolname), authform->oid)));
+
+ ReleaseSysCache(tuple);
+
+ /* If we have reached the bootstrap user, we're done. */
+ if (role_oid == BOOTSTRAP_SUPERUSERID)
+ {
+ if (!owners_list)
+ owners_list = lappend_oid(owners_list, owner_oid);
+ break;
+ }
+
+ /*
+ * For all other users, check they do not own themselves indirectly
+ * through an ownership cycle.
+ *
+ * Scanning the list each time through this loop results in overall
+ * quadratic work in the depth of the ownership chain, but we're
+ * not on a critical performance path, nor do we expect ownership
+ * hierarchies to be deep.
+ */
+ if (owners_list && list_member_oid(owners_list,
+ ObjectIdGetDatum(owner_oid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u indirectly owns itself",
+ GetUserNameFromId(owner_oid, false),
+ owner_oid)));
+
+ /* Done with sanity checks. Add this owner to the list. */
+ owners_list = lappend_oid(owners_list, owner_oid);
+
+ /* Otherwise, iterate on this iteration's owner_oid. */
+ role_oid = owner_oid;
+ }
+
+ return owners_list;
+}
+
/*
* Does member have the privileges of role (directly or indirectly)?
*
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilege the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
@@ -4921,6 +5030,15 @@ is_member_of_role_nosuper(Oid member, Oid role)
role);
}
+/*
+ * Is owner a direct or indirect owner of the role, not considering
+ * superuserness?
+ */
+bool
+is_owner_of_role_nosuper(Oid owner, Oid role)
+{
+ return list_member_oid(roles_is_owned_by(role), owner);
+}
/*
* Is member an admin of role? That is, is member the role itself (subject to
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 42f85d0e84..572cae0f27 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,6 +209,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern bool is_owner_of_role_nosuper(Oid owner, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 66d4c29cf7..2c42c8ad8d 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -58,8 +58,9 @@ CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
CREATE ROLE regress_password_null
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_createdb, regress_createrole, regress_login;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -98,11 +99,11 @@ CREATE ROLE regress_password_null
regress_encrypted_password | regress_role_admin | Cannot login | {} |
\du+ regress_password_null
- List of roles
- Role name | Owner | Attributes | Member of | Description
------------------------+--------------------+------------------------+-----------------------------------------------------+-------------
- regress_password_null | regress_role_admin | Create role, Create DB+| {regress_createdb,regress_createrole,regress_login} |
- | | 2 connections | |
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------------+--------------------+--------------------------------------+---------------------------------------+--------------------
+ regress_password_null | regress_role_admin | Create role, Create DB, Cannot login+| {regress_createdb,regress_createrole} | no login test role
+ | | 2 connections | |
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
@@ -143,21 +144,18 @@ CREATE TABLE tenant_table (i integer);
CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
-ERROR: must be owner of index tenant_idx
ALTER TABLE tenant_table ADD COLUMN t text;
-ERROR: must be owner of table tenant_table
DROP TABLE tenant_table;
-ERROR: must be owner of table tenant_table
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
-ERROR: must be owner of view tenant_view
+ERROR: must be member of role "regress_role_admin"
+-- ok
DROP VIEW tenant_view;
-ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
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
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
@@ -169,14 +167,14 @@ CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
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, cannot take ownership of these objects from regress_createrole
-SET SESSION AUTHORIZATION regress_role_admin;
-ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
-ERROR: must be owner of role regress_plainrole
-REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-ERROR: permission denied to reassign objects
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ERROR: role "regress_createrole" may not both own and be owned by role "regress_tenant"
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+ERROR: role "regress_role_admin" may not both own and be owned by role "regress_tenant"
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_admin;
ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-- ok, superuser roles can drop superuser roles they own
@@ -187,14 +185,6 @@ SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
-DROP ROLE regress_nosuch_super;
-ERROR: role "regress_nosuch_super" does not exist
-DROP ROLE regress_nosuch_dbowner;
-ERROR: role "regress_nosuch_dbowner" does not exist
-DROP ROLE regress_nosuch_recursive;
-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;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
@@ -222,6 +212,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -232,28 +223,15 @@ 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, role still owns other roles
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_tenant
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
DROP ROLE regress_role_admin;
ERROR: current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
+-- ok, no more owned roles remain
DROP ROLE regress_createrole;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_admin;
ERROR: role "regress_role_admin" cannot be dropped because some objects depend on it
DETAIL: privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 091b116dd6..00e63b7d93 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -39,8 +39,9 @@ CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
CREATE ROLE regress_password_null
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_createdb, regress_createrole, regress_login;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
\du+ regress_createrole
@@ -95,15 +96,19 @@ CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
ALTER TABLE tenant_table ADD COLUMN t text;
DROP TABLE tenant_table;
+
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
+
+-- ok
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -118,13 +123,13 @@ 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, cannot take ownership of these objects from regress_createrole
-SET SESSION AUTHORIZATION regress_role_admin;
-ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
-REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_admin;
ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
@@ -137,10 +142,6 @@ SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
-DROP ROLE regress_nosuch_super;
-DROP ROLE regress_nosuch_dbowner;
-DROP ROLE regress_nosuch_recursive;
-DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
-- fail, cannot drop roles that own other roles
@@ -157,6 +158,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -168,25 +170,15 @@ 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, role still owns other roles
-DROP ROLE regress_createrole;
-
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_admin;
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
+-- ok, no more owned roles remain
DROP ROLE regress_createrole;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_admin;
-- ok, can drop role if we revoke privileges first
--
2.21.1 (Apple Git-122.3)
v5-0004-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v5-0004-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From 99b5da77dc05d6269cc4cb53f69754924885f92c Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 12:01:50 -0800
Subject: [PATCH v5 4/5] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++--
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 +++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 +++++----
src/backend/catalog/aclchk.c | 111 ++++++++++++++++++++++
src/backend/commands/user.c | 60 +++++-------
src/backend/utils/adt/acl.c | 21 +---
src/include/utils/acl.h | 5 +
src/test/regress/expected/create_role.out | 63 +++++-------
src/test/regress/sql/create_role.sql | 36 +++----
14 files changed, 250 insertions(+), 169 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 22f6c5c7ab..5596a359e3 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3132,9 +3132,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3146,9 +3144,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3170,8 +3167,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..fc9bea8072 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they directly or
+ indirectly own except <literal>SUPERUSER</literal>,
+ <literal>REPLICATION</literal>, and <literal>BYPASSRLS</literal>; but only
+ for non-superuser and non-replication roles, and only if the role owner does
+ not alter the target role to have a privilege which the role owner itself
+ lacks. Ordinary roles can only change their own password.
</para>
<para>
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 can rename non-superuser roles they directly or indirectly own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Roles may change
+ privilege for non-superuser roles they directly or indirectly own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c..e65b55a004 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1e0ee503e4..8327005404 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5470,6 +5470,9 @@ has_createrole_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has BYPASSRLS privilege (or is a superuser)
+ */
bool
has_bypassrls_privilege(Oid roleid)
{
@@ -5489,6 +5492,114 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has INHERIT privilege (or is a superuser)
+ */
+bool
+has_inherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has CREATEDB privilege (or is a superuser)
+ */
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has LOGIN privilege (or is a superuser)
+ */
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has REPLICATION privilege (or is a superuser)
+ */
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Get the connection limit for the specified role.
+ *
+ * Returns -1 if the role has no connection limit.
+ */
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index cccb24a498..e8fcecc340 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -276,24 +267,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -713,7 +712,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* We already checked issuper, isreplication, and bypassrls */
if (!(inherit < 0 &&
@@ -914,7 +913,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -925,8 +924,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1039,18 +1037,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1231,7 +1223,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1242,7 +1234,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1457,7 +1449,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1467,9 +1459,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1645,9 +1637,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1803,7 +1795,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* roles. Because superusers will always have this right, we need no
* special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 97336db058..cdcce9c569 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_inherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_inherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 572cae0f27..7a6640dfc9 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -320,5 +320,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_inherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 2c42c8ad8d..26d2ef43d9 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -6,22 +6,16 @@ GRANT CREATE ON DATABASE regression TO regress_role_admin;
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_replication REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
ERROR: must be superuser to own superusers
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
\du+ regress_nosuch_superuser
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -53,7 +47,10 @@ ERROR: role may not own itself
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -73,12 +70,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
--------------------+--------------------+---------------------------+-----------+-------------
regress_createrole | regress_role_admin | Create role, Cannot login | {} |
-\du+ regress_login
- List of roles
- Role name | Owner | Attributes | Member of | Description
----------------+--------------------+------------+-----------+-------------
- regress_login | regress_role_admin | | {} |
-
\du+ regress_inherit
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -111,19 +102,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
ERROR: role "regress_nosuch_recursive" is a member of role "regress_nosuch_recursive"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
@@ -136,8 +127,11 @@ ERROR: permission denied to create database
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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -156,17 +150,27 @@ ERROR: must be member of role "regress_role_admin"
DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
REASSIGN OWNED BY regress_role_admin TO regress_tenant;
@@ -191,19 +195,8 @@ DROP ROLE regress_createrole;
ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_rolecreator
owner of role regress_tenant
-owner of role regress_read_all_data
-owner of role regress_write_all_data
-owner of role regress_monitor
-owner of role regress_read_all_settings
-owner of role regress_read_all_stats
-owner of role regress_stat_scan_tables
-owner of role regress_read_server_files
-owner of role regress_write_server_files
-owner of role regress_execute_server_program
-owner of role regress_signal_backend
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -213,16 +206,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, 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 00e63b7d93..c484d77aa7 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -6,6 +6,8 @@ GRANT CREATE ON DATABASE regression TO regress_role_admin;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
@@ -17,11 +19,6 @@ CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
-
\du+ regress_nosuch_superuser
\du+ regress_nosuch_replication_bypassrls
\du+ regress_nosuch_replication
@@ -34,7 +31,11 @@ ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -45,7 +46,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
\du+ regress_createrole
-\du+ regress_login
\du+ regress_inherit
\du+ regress_connection_limit
\du+ regress_encrypted_password
@@ -57,12 +57,12 @@ CREATE ROLE regress_noiseword SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
@@ -70,7 +70,7 @@ CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
@@ -86,9 +86,12 @@ 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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -111,7 +114,7 @@ DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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;
@@ -149,7 +152,6 @@ DROP ROLE regress_createrole;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -159,16 +161,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v5-0005-Remove-grantor-field-from-pg_auth_members.patchapplication/octet-stream; name=v5-0005-Remove-grantor-field-from-pg_auth_members.patch; x-unix-mode=0644Download
From 78b024b5817e337ec1bedc608af36ad92e615113 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 10:26:05 -0800
Subject: [PATCH v5 5/5] Remove grantor field from pg_auth_members
The "grantor" field of pg_auth_members is unused and, more to the
point, unreliable. The system does not avoid dangling references
from the grantor field to dropped roles in pg_authid. This creates
the risk that, if an oid is reused for a new role, the grantor field
may point to a role other than the one that performed the grant.
We could fix the bug, but there is no clear solution to the problem
that existing installations may have broken data. Since the field
is not used for any purpose, removing it seems the best option.
---
src/backend/commands/user.c | 17 -----------------
src/bin/pg_dump/pg_dumpall.c | 17 ++---------------
src/include/catalog/pg_auth_members.h | 1 -
src/test/regress/expected/oidjoins.out | 1 -
4 files changed, 2 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index e8fcecc340..2540963d8a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1481,21 +1481,6 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- *
- * Before lifting this restriction, give the member == role case of
- * is_admin_of_role() a fresh look. Ensure that the current role cannot
- * use an explicit grantor specification to take advantage of the session
- * user's self-admin right.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
-
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -1571,12 +1556,10 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record[Anum_pg_auth_members_roleid - 1] = ObjectIdGetDatum(roleid);
new_record[Anum_pg_auth_members_member - 1] = ObjectIdGetDatum(memberid);
- new_record[Anum_pg_auth_members_grantor - 1] = ObjectIdGetDatum(grantorId);
new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(admin_opt);
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 868f15ed37..fdcbdc2851 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -940,14 +940,12 @@ dumpRoleMembership(PGconn *conn)
printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
- "LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,3", role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
@@ -963,17 +961,6 @@ dumpRoleMembership(PGconn *conn)
fprintf(OPF, " TO %s", fmtId(member));
if (*option == 't')
fprintf(OPF, " WITH ADMIN OPTION");
-
- /*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
- */
- if (!PQgetisnull(res, i, 3))
- {
- char *grantor = PQgetvalue(res, i, 3);
-
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
- }
fprintf(OPF, ";\n");
}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c873201688 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -31,7 +31,6 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
{
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
- Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
bool admin_option; /* granted with admin option? */
} FormData_pg_auth_members;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 266a30a85b..a6a73df7ff 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,7 +197,6 @@ NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
-NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
NOTICE: checking pg_shdepend {dbid} => pg_database {oid}
NOTICE: checking pg_shdepend {classid} => pg_class {oid}
NOTICE: checking pg_shdepend {refclassid} => pg_class {oid}
--
2.21.1 (Apple Git-122.3)
Rebased:
Attachments:
v6-0001-Add-tests-of-the-CREATEROLE-attribute.patchapplication/octet-stream; name=v6-0001-Add-tests-of-the-CREATEROLE-attribute.patch; x-unix-mode=0644Download
From 6cd7df4189dfca42a09e04beecc532962bf3e562 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 11:14:23 -0800
Subject: [PATCH v6 1/5] Add tests of the CREATEROLE attribute.
While developing alternate rules for what privileges CREATEROLE has,
I noticed that none of the changes to how CREATEROLE works triggered
any regression test failures. This is problematic for two reasons.
It means the existing code has insufficient test coverage, and it
means that unintended changes introduced by subsequent patches may
go unnoticed. Fix that.
---
src/test/regress/expected/create_role.out | 145 ++++++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/create_role.sql | 138 ++++++++++++++++++++
3 files changed, 284 insertions(+), 1 deletion(-)
create mode 100644 src/test/regress/expected/create_role.out
create mode 100644 src/test/regress/sql/create_role.sql
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
new file mode 100644
index 0000000000..4e67d72760
--- /dev/null
+++ b/src/test/regress/expected/create_role.out
@@ -0,0 +1,145 @@
+-- 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;
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+ERROR: must be superuser to create replication users
+CREATE ROLE regress_nosuch_replication REPLICATION;
+ERROR: must be superuser to create replication users
+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_login LOGIN;
+CREATE ROLE regress_inherit INHERIT;
+CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
+CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_password_null PASSWORD NULL;
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_noiseword SYSID 12345;
+NOTICE: SYSID can no longer be specified
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
+ERROR: must be superuser to alter superusers
+-- fail, database owner cannot have members
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+ERROR: role "pg_database_owner" cannot have explicit members
+-- ok, can grant other users into a role
+CREATE ROLE regress_inroles ROLE
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
+ERROR: role "regress_nosuch_recursive" is a member of role "regress_nosuch_recursive"
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_adminroles ADMIN
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
+ERROR: role "regress_nosuch_admin_recursive" is a member of role "regress_nosuch_admin_recursive"
+-- fail, regress_createrole does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE DATABASE regress_nosuch_db;
+ERROR: permission denied to create database
+-- ok, regress_createrole can create new roles
+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;
+-- ok, regress_tenant can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant;
+CREATE TABLE tenant_table (i integer);
+CREATE INDEX tenant_idx ON tenant_table(i);
+CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
+-- fail, these objects belonging to regress_tenant
+SET SESSION AUTHORIZATION regress_createrole;
+DROP INDEX tenant_idx;
+ERROR: must be owner of index tenant_idx
+ALTER TABLE tenant_table ADD COLUMN t text;
+ERROR: must be owner of table tenant_table
+DROP TABLE tenant_table;
+ERROR: must be owner of table tenant_table
+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
+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
+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;
+CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+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, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_admin;
+DROP ROLE regress_nosuch_superuser;
+ERROR: role "regress_nosuch_superuser" does not exist
+DROP ROLE regress_nosuch_replication_bypassrls;
+ERROR: role "regress_nosuch_replication_bypassrls" does not exist
+DROP ROLE regress_nosuch_replication;
+ERROR: role "regress_nosuch_replication" does not exist
+DROP ROLE regress_nosuch_bypassrls;
+ERROR: role "regress_nosuch_bypassrls" does not exist
+DROP ROLE regress_nosuch_super;
+ERROR: role "regress_nosuch_super" does not exist
+DROP ROLE regress_nosuch_dbowner;
+ERROR: role "regress_nosuch_dbowner" does not exist
+DROP ROLE regress_nosuch_recursive;
+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;
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
+DROP ROLE regress_login;
+DROP ROLE regress_inherit;
+DROP ROLE regress_connection_limit;
+DROP ROLE regress_encrypted_password;
+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
+DROP ROLE regress_role_admin;
+ERROR: current user cannot be dropped
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX tenant_idx;
+DROP TABLE tenant_table;
+DROP VIEW tenant_view;
+DROP ROLE regress_tenant;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_super;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5b0c73d7e3..861c30a73a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -89,7 +89,7 @@ test: brin_bloom brin_multi
# ----------
# Another group of parallel tests
# ----------
-test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort
+test: create_table_like alter_generic alter_operator misc async dbsize misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role
# rules cannot run concurrently with any test that creates
# a view or rule in the public schema
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
new file mode 100644
index 0000000000..292dc08797
--- /dev/null
+++ b/src/test/regress/sql/create_role.sql
@@ -0,0 +1,138 @@
+-- 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;
+
+-- fail, only superusers can create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication REPLICATION;
+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_login LOGIN;
+CREATE ROLE regress_inherit INHERIT;
+CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
+CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
+CREATE ROLE regress_password_null PASSWORD NULL;
+
+-- ok, backwards compatible noise words should be ignored
+CREATE ROLE regress_noiseword SYSID 12345;
+
+-- fail, cannot grant membership in superuser role
+CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
+
+-- fail, database owner cannot have members
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+
+-- ok, can grant other users into a role
+CREATE ROLE regress_inroles ROLE
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+
+-- fail, cannot grant a role into itself
+CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
+
+-- ok, can grant other users into a role with admin option
+CREATE ROLE regress_adminroles ADMIN
+ regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
+
+-- fail, cannot grant a role into itself with admin option
+CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
+
+-- fail, regress_createrole does not have CREATEDB privilege
+SET SESSION AUTHORIZATION regress_createrole;
+CREATE DATABASE regress_nosuch_db;
+
+-- ok, regress_createrole can create new roles
+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;
+
+-- ok, regress_tenant can create objects within the database
+SET SESSION AUTHORIZATION regress_tenant;
+CREATE TABLE tenant_table (i integer);
+CREATE INDEX tenant_idx ON tenant_table(i);
+CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
+REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
+
+-- fail, these objects belonging to regress_tenant
+SET SESSION AUTHORIZATION regress_createrole;
+DROP INDEX tenant_idx;
+ALTER TABLE tenant_table ADD COLUMN t text;
+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
+REASSIGN OWNED BY regress_tenant TO regress_createrole;
+
+-- ok, having CREATEROLE is 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;
+CREATE ROLE regress_read_all_settings IN ROLE pg_read_all_settings;
+CREATE ROLE regress_read_all_stats IN ROLE pg_read_all_stats;
+CREATE ROLE regress_stat_scan_tables IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
+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, creation of these roles failed above so they do not now exist
+SET SESSION AUTHORIZATION regress_role_admin;
+DROP ROLE regress_nosuch_superuser;
+DROP ROLE regress_nosuch_replication_bypassrls;
+DROP ROLE regress_nosuch_replication;
+DROP ROLE regress_nosuch_bypassrls;
+DROP ROLE regress_nosuch_super;
+DROP ROLE regress_nosuch_dbowner;
+DROP ROLE regress_nosuch_recursive;
+DROP ROLE regress_nosuch_admin_recursive;
+DROP ROLE regress_plainrole;
+
+-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
+DROP ROLE regress_login;
+DROP ROLE regress_inherit;
+DROP ROLE regress_connection_limit;
+DROP ROLE regress_encrypted_password;
+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;
+DROP ROLE regress_role_admin;
+
+-- ok
+RESET SESSION AUTHORIZATION;
+DROP INDEX tenant_idx;
+DROP TABLE tenant_table;
+DROP VIEW tenant_view;
+DROP ROLE regress_tenant;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v6-0002-Add-owners-to-roles.patchapplication/octet-stream; name=v6-0002-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 499d7d5e41c9b3abc6b8d492e207b1e585a388f5 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 11:15:02 -0800
Subject: [PATCH v6 2/5] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
This is a preparatory patch for changing how CREATEROLE works.
---
src/backend/catalog/aclchk.c | 59 ++++++-
src/backend/catalog/pg_shdepend.c | 5 +
src/backend/catalog/system_views.sql | 1 +
src/backend/commands/alter.c | 3 +
src/backend/commands/user.c | 148 +++++++++++++++++-
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/parser/gram.y | 23 +++
src/bin/psql/describe.c | 12 ++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 2 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/acl.h | 1 +
.../unsafe_tests/expected/rolenames.out | 6 +-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 136 ++++++++++++++--
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +-
src/test/regress/expected/rules.out | 1 +
src/test/regress/sql/create_role.sql | 67 +++++++-
src/test/regress/sql/privileges.sql | 10 +-
21 files changed, 470 insertions(+), 21 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1dd03a8e51..e5c7324340 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid)
+{
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(owner_roleid))
+ return true;
+
+ /* Otherwise, look up the owner of the role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(owned_role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist",
+ owned_role_oid)));
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself). Other roles must form a proper tree.
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner == authform->oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ authform->rolname.data, authform->oid)));
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ authform->rolowner != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ authform->rolname.data, authform->oid,
+ authform->rolowner)));
+ ReleaseSysCache(tuple);
+
+ return (owner_oid == owner_roleid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..52f69ad76e 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1578,6 +1579,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87..057d17460b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 1f64c8aa51..e6c7c84c87 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246b..3903791ff0 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +377,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +455,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -1051,8 +1089,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1648,3 +1687,110 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: This is different from most other alter-owner checks in that
+ * the current user is checked for create privileges instead of the
+ * destination owner. This is consistent with the CREATE case for
+ * roles. Because superusers will always have this right, we need no
+ * special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Only the bootstrap superuser is allowed to own itself. */
+ if (newOwnerId != BOOTSTRAP_SUPERUSERID && authForm->oid == newOwnerId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role may not own itself")));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 90b5da51c9..f43c92a70f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4522,6 +4522,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 06345da3ba..328f155208 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b5966712ce..df2f84143f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1077,9 +1077,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -1218,6 +1229,10 @@ CreateOptRoleElem:
{
$$ = makeDefElem("addroleto", (Node *)$3, @1);
}
+ | OWNER RoleSpec
+ {
+ $$ = makeDefElem("owner", (Node *)$2, @1);
+ }
;
@@ -9585,6 +9600,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 40433e32fa..e6198c0332 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3518,6 +3518,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3538,6 +3544,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3549,6 +3557,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f..3af0f3908c 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3e9bdc781f..b9d124d38f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2623,6 +2623,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 1ce4c5556e..42f85d0e84 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -316,6 +316,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..66d4c29cf7 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;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
@@ -11,14 +12,98 @@ CREATE ROLE regress_nosuch_replication REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+\du+ regress_nosuch_superuser
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+-------------------------+-----------+-------------
+ regress_nosuch_superuser | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_nosuch_replication_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------------------+--------------------+---------------------------------------+-----------+-------------
+ regress_nosuch_replication_bypassrls | regress_role_admin | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_nosuch_replication
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------------------+--------------------+---------------------------+-----------+-------------
+ regress_nosuch_replication | regress_role_admin | Cannot login, Replication | {} |
+
+\du+ regress_nosuch_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+--------------------------+-----------+-------------
+ regress_nosuch_bypassrls | regress_role_admin | Cannot login, Bypass RLS | {} |
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
+ERROR: role may not own itself
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole, regress_login;
+\du+ regress_createdb
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+------------------+--------------------+-------------------------+-----------+-------------
+ regress_createdb | regress_role_admin | Create DB, Cannot login | {} |
+
+\du+ regress_createrole
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------+--------------------+---------------------------+-----------+-------------
+ regress_createrole | regress_role_admin | Create role, Cannot login | {} |
+
+\du+ regress_login
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+---------------+--------------------+------------+-----------+-------------
+ regress_login | regress_role_admin | | {} |
+
+\du+ regress_inherit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+--------------------+--------------+-----------+-------------
+ regress_inherit | regress_role_admin | Cannot login | {} |
+
+\du+ regress_connection_limit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+---------------+-----------+-------------
+ regress_connection_limit | regress_role_admin | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_encrypted_password
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------------------+--------------------+--------------+-----------+-------------
+ regress_encrypted_password | regress_role_admin | Cannot login | {} |
+
+\du+ regress_password_null
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------------+--------------------+------------------------+-----------------------------------------------------+-------------
+ regress_password_null | regress_role_admin | Create role, Create DB+| {regress_createdb,regress_createrole,regress_login} |
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -84,16 +169,24 @@ CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
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, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_createrole
SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+ERROR: must be owner of role regress_plainrole
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+ERROR: permission denied to reassign objects
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_nosuch_superuser;
-ERROR: role "regress_nosuch_superuser" does not exist
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
-ERROR: role "regress_nosuch_replication_bypassrls" does not exist
DROP ROLE regress_nosuch_replication;
-ERROR: role "regress_nosuch_replication" does not exist
DROP ROLE regress_nosuch_bypassrls;
-ERROR: role "regress_nosuch_bypassrls" does not exist
DROP ROLE regress_nosuch_super;
ERROR: role "regress_nosuch_super" does not exist
DROP ROLE regress_nosuch_dbowner;
@@ -103,9 +196,23 @@ 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;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_rolecreator
+owner of role regress_tenant
+owner of role regress_read_all_data
+owner of role regress_write_all_data
+owner of role regress_monitor
+owner of role regress_read_all_settings
+owner of role regress_read_all_stats
+owner of role regress_stat_scan_tables
+owner of role regress_read_server_files
+owner of role regress_write_server_files
+owner of role regress_execute_server_program
+owner of role regress_signal_backend
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -130,6 +237,10 @@ 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, role still owns other roles
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_tenant
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
@@ -141,5 +252,12 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_createrole;
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_admin;
+ERROR: role "regress_role_admin" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..9ce619fd5f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -2356,7 +2358,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index d652f7b5fb..b6e3c508ba 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..091b116dd6 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;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
@@ -9,14 +10,45 @@ CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
+
+-- ok, superuser can create users with these privileges for normal role
+CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+
+\du+ regress_nosuch_superuser
+\du+ regress_nosuch_replication_bypassrls
+\du+ regress_nosuch_replication
+\du+ regress_nosuch_bypassrls
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole, regress_login;
+
+\du+ regress_createdb
+\du+ regress_createrole
+\du+ regress_login
+\du+ regress_inherit
+\du+ regress_connection_limit
+\du+ regress_encrypted_password
+\du+ regress_password_null
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
@@ -86,9 +118,22 @@ 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, creation of these roles failed above so they do not now exist
+-- fail, cannot take ownership of these objects from regress_createrole
SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+
+-- superuser can do it, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
DROP ROLE regress_nosuch_superuser;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
@@ -98,9 +143,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -124,6 +171,9 @@ DROP ROLE regress_signal_backend;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
+-- fail, role still owns other roles
+DROP ROLE regress_createrole;
+
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_admin;
@@ -134,5 +184,12 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_createrole;
+
+-- fail, cannot drop role with remaining privileges
+DROP ROLE regress_role_admin;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..dcf84f91fd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -30,8 +30,10 @@ CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -1426,8 +1428,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v6-0003-Give-role-owners-control-over-owned-roles.patchapplication/octet-stream; name=v6-0003-Give-role-owners-control-over-owned-roles.patch; x-unix-mode=0644Download
From 0b670278d7bc64641e6f8b77f9506705d92ceb02 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 18 Jan 2022 10:18:12 -0800
Subject: [PATCH v6 3/5] Give role owners control over owned roles
Create a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.
---
src/backend/catalog/aclchk.c | 49 +-------
src/backend/catalog/objectaddress.c | 22 +---
src/backend/commands/schemacmds.c | 2 +-
src/backend/commands/user.c | 29 +++--
src/backend/utils/adt/acl.c | 118 ++++++++++++++++++
src/include/utils/acl.h | 1 +
.../expected/dummy_seclabel.out | 12 +-
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +-
src/test/regress/expected/create_role.out | 68 ++++------
src/test/regress/sql/create_role.sql | 44 +++----
10 files changed, 205 insertions(+), 152 deletions(-)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index e5c7324340..1e0ee503e4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5440,61 +5440,16 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid)
{
- HeapTuple tuple;
- Form_pg_authid authform;
- Oid owner_oid;
-
/* Superusers bypass all permission checking. */
if (superuser_arg(owner_roleid))
return true;
- /* Otherwise, look up the owner of the role */
- tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(owned_role_oid));
- if (!HeapTupleIsValid(tuple))
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("role with OID %u does not exist",
- owned_role_oid)));
- authform = (Form_pg_authid) GETSTRUCT(tuple);
- owner_oid = authform->rolowner;
-
- /*
- * Roles must necessarily have owners. Even the bootstrap user has an
- * owner. (It owns itself). Other roles must form a proper tree.
- */
- if (!OidIsValid(owner_oid))
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u has invalid owner",
- authform->rolname.data, authform->oid)));
- if (authform->oid != BOOTSTRAP_SUPERUSERID &&
- authform->rolowner == authform->oid)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owns itself",
- authform->rolname.data, authform->oid)));
- if (authform->oid == BOOTSTRAP_SUPERUSERID &&
- authform->rolowner != BOOTSTRAP_SUPERUSERID)
- ereport(ERROR,
- (errcode(ERRCODE_DATA_CORRUPTED),
- errmsg("role \"%s\" with OID %u owned by role with OID %u",
- authform->rolname.data, authform->oid,
- authform->rolowner)));
- ReleaseSysCache(tuple);
-
- return (owner_oid == owner_roleid);
+ /* Otherwise, check the role ownership hierarchy */
+ return is_owner_of_role_nosuper(owner_roleid, owned_role_oid);
}
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
- *
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index f30c742d48..1a47f28829 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 984000a5bc..e5405db658 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
+ * NOTE: This is different from most other alter-owner checks in that the
* current user is checked for create privileges instead of the
* destination owner. This is consistent with the CREATE case for
* schemas. Because superusers will always have this right, we need
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 3903791ff0..3a24934679 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -693,7 +693,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
{
/* check the rest */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
- drolemembers || dvalidUntil || !dpassword || roleid != GetUserId())
+ drolemembers || dvalidUntil || !dpassword ||
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -898,7 +899,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -950,11 +952,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -1026,6 +1023,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1784,6 +1787,18 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("role may not own itself")));
+ /*
+ * Must not create cycles in the role ownership hierarchy. If this
+ * role owns (directly or indirectly) the proposed new owner, disallow
+ * the ownership transfer.
+ */
+ if (is_owner_of_role_nosuper(authForm->oid, newOwnerId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role \"%s\" may not both own and be owned by role \"%s\"",
+ NameStr(authForm->rolname),
+ GetUserNameFromId(newOwnerId, false))));
+
authForm->rolowner = newOwnerId;
CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156c..97336db058 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4832,6 +4832,111 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
}
+/*
+ * Get a list of roles which own the given role, directly or indirectly.
+ *
+ * Each role has only one direct owner. The returned list contains the given
+ * role's owner, that role's owner, etc., up to the top of the ownership
+ * hierarchy, which is always the bootstrap superuser.
+ *
+ * Raises an error if any role ownership invariant is violated. Returns NIL if
+ * the given roleid is invalid.
+ */
+static List *
+roles_is_owned_by(Oid roleid)
+{
+ List *owners_list = NIL;
+ Oid role_oid = roleid;
+
+ /*
+ * Start with the current role and follow the ownership chain upwards until
+ * we reach the bootstrap superuser. To defend against getting into an
+ * infinite loop, we must check for ownership cycles. We choose to perform
+ * other corruption checks on the ownership structure while iterating, too.
+ */
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist", role_oid)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself).
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ NameStr(authform->rolname), authform->oid)));
+
+ /* The bootstrap user must own itself */
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ owner_oid != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ NameStr(authform->rolname), authform->oid,
+ authform->rolowner)));
+
+ /*
+ * Roles other than the bootstrap user must not be their own direct
+ * owners.
+ */
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->oid == owner_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ NameStr(authform->rolname), authform->oid)));
+
+ ReleaseSysCache(tuple);
+
+ /* If we have reached the bootstrap user, we're done. */
+ if (role_oid == BOOTSTRAP_SUPERUSERID)
+ {
+ if (!owners_list)
+ owners_list = lappend_oid(owners_list, owner_oid);
+ break;
+ }
+
+ /*
+ * For all other users, check they do not own themselves indirectly
+ * through an ownership cycle.
+ *
+ * Scanning the list each time through this loop results in overall
+ * quadratic work in the depth of the ownership chain, but we're
+ * not on a critical performance path, nor do we expect ownership
+ * hierarchies to be deep.
+ */
+ if (owners_list && list_member_oid(owners_list,
+ ObjectIdGetDatum(owner_oid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u indirectly owns itself",
+ GetUserNameFromId(owner_oid, false),
+ owner_oid)));
+
+ /* Done with sanity checks. Add this owner to the list. */
+ owners_list = lappend_oid(owners_list, owner_oid);
+
+ /* Otherwise, iterate on this iteration's owner_oid. */
+ role_oid = owner_oid;
+ }
+
+ return owners_list;
+}
+
/*
* Does member have the privileges of role (directly or indirectly)?
*
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilege the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
@@ -4921,6 +5030,15 @@ is_member_of_role_nosuper(Oid member, Oid role)
role);
}
+/*
+ * Is owner a direct or indirect owner of the role, not considering
+ * superuserness?
+ */
+bool
+is_owner_of_role_nosuper(Oid owner, Oid role)
+{
+ return list_member_oid(roles_is_owned_by(role), owner);
+}
/*
* Is member an admin of role? That is, is member the role itself (subject to
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 42f85d0e84..572cae0f27 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,6 +209,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern bool is_owner_of_role_nosuper(Oid owner, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 66d4c29cf7..2c42c8ad8d 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -58,8 +58,9 @@ CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
CREATE ROLE regress_password_null
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_createdb, regress_createrole, regress_login;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -98,11 +99,11 @@ CREATE ROLE regress_password_null
regress_encrypted_password | regress_role_admin | Cannot login | {} |
\du+ regress_password_null
- List of roles
- Role name | Owner | Attributes | Member of | Description
------------------------+--------------------+------------------------+-----------------------------------------------------+-------------
- regress_password_null | regress_role_admin | Create role, Create DB+| {regress_createdb,regress_createrole,regress_login} |
- | | 2 connections | |
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------------+--------------------+--------------------------------------+---------------------------------------+--------------------
+ regress_password_null | regress_role_admin | Create role, Create DB, Cannot login+| {regress_createdb,regress_createrole} | no login test role
+ | | 2 connections | |
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
@@ -143,21 +144,18 @@ CREATE TABLE tenant_table (i integer);
CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
-ERROR: must be owner of index tenant_idx
ALTER TABLE tenant_table ADD COLUMN t text;
-ERROR: must be owner of table tenant_table
DROP TABLE tenant_table;
-ERROR: must be owner of table tenant_table
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
-ERROR: must be owner of view tenant_view
+ERROR: must be member of role "regress_role_admin"
+-- ok
DROP VIEW tenant_view;
-ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
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
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
@@ -169,14 +167,14 @@ CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
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, cannot take ownership of these objects from regress_createrole
-SET SESSION AUTHORIZATION regress_role_admin;
-ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
-ERROR: must be owner of role regress_plainrole
-REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-ERROR: permission denied to reassign objects
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ERROR: role "regress_createrole" may not both own and be owned by role "regress_tenant"
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+ERROR: role "regress_role_admin" may not both own and be owned by role "regress_tenant"
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_admin;
ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-- ok, superuser roles can drop superuser roles they own
@@ -187,14 +185,6 @@ SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
-DROP ROLE regress_nosuch_super;
-ERROR: role "regress_nosuch_super" does not exist
-DROP ROLE regress_nosuch_dbowner;
-ERROR: role "regress_nosuch_dbowner" does not exist
-DROP ROLE regress_nosuch_recursive;
-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;
-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
@@ -222,6 +212,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -232,28 +223,15 @@ 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, role still owns other roles
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: owner of role regress_tenant
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
ERROR: must be superuser to drop superusers
DROP ROLE regress_role_admin;
ERROR: current user cannot be dropped
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
+-- ok, no more owned roles remain
DROP ROLE regress_createrole;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_admin;
ERROR: role "regress_role_admin" cannot be dropped because some objects depend on it
DETAIL: privileges for database regression
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 091b116dd6..00e63b7d93 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -39,8 +39,9 @@ CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
CREATE ROLE regress_password_null
- CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
- IN ROLE regress_createdb, regress_createrole, regress_login;
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
\du+ regress_createrole
@@ -95,15 +96,19 @@ CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
ALTER TABLE tenant_table ADD COLUMN t text;
DROP TABLE tenant_table;
+
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
+
+-- ok
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -118,13 +123,13 @@ 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, cannot take ownership of these objects from regress_createrole
-SET SESSION AUTHORIZATION regress_role_admin;
-ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
-REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
-
--- superuser can do it, though
+-- fail, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_admin;
ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
@@ -137,10 +142,6 @@ SET SESSION AUTHORIZATION regress_role_admin;
DROP ROLE regress_nosuch_replication_bypassrls;
DROP ROLE regress_nosuch_replication;
DROP ROLE regress_nosuch_bypassrls;
-DROP ROLE regress_nosuch_super;
-DROP ROLE regress_nosuch_dbowner;
-DROP ROLE regress_nosuch_recursive;
-DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
-- fail, cannot drop roles that own other roles
@@ -157,6 +158,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -168,25 +170,15 @@ 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, role still owns other roles
-DROP ROLE regress_createrole;
-
-- fail, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
DROP ROLE regress_role_admin;
--- ok
-RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
+-- ok, no more owned roles remain
DROP ROLE regress_createrole;
-- fail, cannot drop role with remaining privileges
+RESET SESSION AUTHORIZATION;
DROP ROLE regress_role_admin;
-- ok, can drop role if we revoke privileges first
--
2.21.1 (Apple Git-122.3)
v6-0004-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v6-0004-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From d949f35209b8de4723b0612ef823e0385ae6a338 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 18 Jan 2022 10:24:55 -0800
Subject: [PATCH v6 4/5] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++--
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 +++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 +++++----
src/backend/catalog/aclchk.c | 111 ++++++++++++++++++++++
src/backend/commands/user.c | 60 +++++-------
src/backend/utils/adt/acl.c | 21 +---
src/include/utils/acl.h | 5 +
src/test/regress/expected/create_role.out | 63 +++++-------
src/test/regress/sql/create_role.sql | 36 +++----
14 files changed, 250 insertions(+), 169 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 22f6c5c7ab..5596a359e3 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3132,9 +3132,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3146,9 +3144,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3170,8 +3167,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..fc9bea8072 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they directly or
+ indirectly own except <literal>SUPERUSER</literal>,
+ <literal>REPLICATION</literal>, and <literal>BYPASSRLS</literal>; but only
+ for non-superuser and non-replication roles, and only if the role owner does
+ not alter the target role to have a privilege which the role owner itself
+ lacks. Ordinary roles can only change their own password.
</para>
<para>
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 can rename non-superuser roles they directly or indirectly own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Roles may change
+ privilege for non-superuser roles they directly or indirectly own. 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 e07fc47fd3..1ba374f3a1 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -92,12 +92,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..2e73102562 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -107,8 +107,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -120,8 +122,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -163,6 +163,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -194,8 +196,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -281,6 +283,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -301,6 +307,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -313,6 +323,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c..e65b55a004 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1e0ee503e4..8327005404 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5470,6 +5470,9 @@ has_createrole_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has BYPASSRLS privilege (or is a superuser)
+ */
bool
has_bypassrls_privilege(Oid roleid)
{
@@ -5489,6 +5492,114 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has INHERIT privilege (or is a superuser)
+ */
+bool
+has_inherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has CREATEDB privilege (or is a superuser)
+ */
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has LOGIN privilege (or is a superuser)
+ */
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has REPLICATION privilege (or is a superuser)
+ */
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Get the connection limit for the specified role.
+ *
+ * Returns -1 if the role has no connection limit.
+ */
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 3a24934679..cc33277c20 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -276,24 +267,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -689,7 +688,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* check the rest */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
@@ -888,7 +887,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -899,8 +898,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1013,18 +1011,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1205,7 +1197,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1216,7 +1208,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1431,7 +1423,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1441,9 +1433,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1619,9 +1611,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1777,7 +1769,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* roles. Because superusers will always have this right, we need no
* special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 97336db058..cdcce9c569 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_inherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_inherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 572cae0f27..7a6640dfc9 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -320,5 +320,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_inherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 2c42c8ad8d..26d2ef43d9 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -6,22 +6,16 @@ GRANT CREATE ON DATABASE regression TO regress_role_admin;
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
ERROR: must be superuser to create superusers
+-- ok, can assign privileges the creator has
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_replication REPLICATION;
-ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
ERROR: must be superuser to own superusers
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
\du+ regress_nosuch_superuser
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -53,7 +47,10 @@ ERROR: role may not own itself
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -73,12 +70,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
--------------------+--------------------+---------------------------+-----------+-------------
regress_createrole | regress_role_admin | Create role, Cannot login | {} |
-\du+ regress_login
- List of roles
- Role name | Owner | Attributes | Member of | Description
----------------+--------------------+------------+-----------+-------------
- regress_login | regress_role_admin | | {} |
-
\du+ regress_inherit
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -111,19 +102,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
ERROR: role "regress_nosuch_recursive" is a member of role "regress_nosuch_recursive"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
@@ -136,8 +127,11 @@ ERROR: permission denied to create database
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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -156,17 +150,27 @@ ERROR: must be member of role "regress_role_admin"
DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
REASSIGN OWNED BY regress_role_admin TO regress_tenant;
@@ -191,19 +195,8 @@ DROP ROLE regress_createrole;
ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_rolecreator
owner of role regress_tenant
-owner of role regress_read_all_data
-owner of role regress_write_all_data
-owner of role regress_monitor
-owner of role regress_read_all_settings
-owner of role regress_read_all_stats
-owner of role regress_stat_scan_tables
-owner of role regress_read_server_files
-owner of role regress_write_server_files
-owner of role regress_execute_server_program
-owner of role regress_signal_backend
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -213,16 +206,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, 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 00e63b7d93..c484d77aa7 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -6,6 +6,8 @@ GRANT CREATE ON DATABASE regression TO regress_role_admin;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
+
+-- ok, can assign privileges the creator has
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
@@ -17,11 +19,6 @@ CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
-- ok, superuser can create superusers belonging to other superusers
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_super SUPERUSER;
--- ok, superuser can create users with these privileges for normal role
-CREATE ROLE regress_nosuch_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_nosuch_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_nosuch_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
-
\du+ regress_nosuch_superuser
\du+ regress_nosuch_replication_bypassrls
\du+ regress_nosuch_replication
@@ -34,7 +31,11 @@ ALTER ROLE regress_nosuch_bypassrls OWNER TO regress_nosuch_bypassrls;
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -45,7 +46,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
\du+ regress_createrole
-\du+ regress_login
\du+ regress_inherit
\du+ regress_connection_limit
\du+ regress_encrypted_password
@@ -57,12 +57,12 @@ CREATE ROLE regress_noiseword SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
@@ -70,7 +70,7 @@ CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
@@ -86,9 +86,12 @@ 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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -111,7 +114,7 @@ DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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;
@@ -149,7 +152,6 @@ DROP ROLE regress_createrole;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -159,16 +161,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
v6-0005-Remove-grantor-field-from-pg_auth_members.patchapplication/octet-stream; name=v6-0005-Remove-grantor-field-from-pg_auth_members.patch; x-unix-mode=0644Download
From 13000e50d611071eafc0b5bebfa33c2cedec1f90 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 11 Jan 2022 10:26:05 -0800
Subject: [PATCH v6 5/5] Remove grantor field from pg_auth_members
The "grantor" field of pg_auth_members is unused and, more to the
point, unreliable. The system does not avoid dangling references
from the grantor field to dropped roles in pg_authid. This creates
the risk that, if an oid is reused for a new role, the grantor field
may point to a role other than the one that performed the grant.
We could fix the bug, but there is no clear solution to the problem
that existing installations may have broken data. Since the field
is not used for any purpose, removing it seems the best option.
---
src/backend/commands/user.c | 17 -----------------
src/bin/pg_dump/pg_dumpall.c | 17 ++---------------
src/include/catalog/pg_auth_members.h | 1 -
src/test/regress/expected/oidjoins.out | 1 -
4 files changed, 2 insertions(+), 34 deletions(-)
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index cc33277c20..b36b00d46c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1455,21 +1455,6 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- *
- * Before lifting this restriction, give the member == role case of
- * is_admin_of_role() a fresh look. Ensure that the current role cannot
- * use an explicit grantor specification to take advantage of the session
- * user's self-admin right.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
-
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
@@ -1545,12 +1530,10 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record[Anum_pg_auth_members_roleid - 1] = ObjectIdGetDatum(roleid);
new_record[Anum_pg_auth_members_member - 1] = ObjectIdGetDatum(memberid);
- new_record[Anum_pg_auth_members_grantor - 1] = ObjectIdGetDatum(grantorId);
new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(admin_opt);
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 10383c713f..cfc997bb73 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -945,14 +945,12 @@ dumpRoleMembership(PGconn *conn)
printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
- "LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,3", role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
@@ -968,17 +966,6 @@ dumpRoleMembership(PGconn *conn)
fprintf(OPF, " TO %s", fmtId(member));
if (*option == 't')
fprintf(OPF, " WITH ADMIN OPTION");
-
- /*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
- */
- if (!PQgetisnull(res, i, 3))
- {
- char *grantor = PQgetvalue(res, i, 3);
-
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
- }
fprintf(OPF, ";\n");
}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c873201688 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -31,7 +31,6 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
{
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
- Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
bool admin_option; /* granted with admin option? */
} FormData_pg_auth_members;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 266a30a85b..a6a73df7ff 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -197,7 +197,6 @@ NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
-NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
NOTICE: checking pg_shdepend {dbid} => pg_database {oid}
NOTICE: checking pg_shdepend {classid} => pg_class {oid}
NOTICE: checking pg_shdepend {refclassid} => pg_class {oid}
--
2.21.1 (Apple Git-122.3)
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 4, 2022, at 12:47 PM, Joshua Brindle <joshua.brindle@crunchydata.com> wrote:
I was able to reproduce that using REASSIGN OWNED BY to cause a user to own itself. Is that how you did it, or is there yet another way to get into that state?
I did:
ALTER ROLE brindle OWNER TO brindle;Ok, thanks. I have rebased, fixed both REASSIGN OWNED BY and ALTER ROLE .. OWNER TO cases, and added regression coverage for them.
The last patch set to contain significant changes was v2, with v3 just being a rebase. Relative to those sets:
0001 -- rebased.
0002 -- rebased; extend AlterRoleOwner_internal to disallow making a role its own immediate owner.
0003 -- rebased; extend AlterRoleOwner_internal to disallow cycles in the role ownership graph.
0004 -- rebased.
0005 -- new; removes the broken pg_auth_members.grantor field.
Subject: [PATCH v4 1/5] Add tests of the CREATEROLE attribute.
No particular issue with this one.
Subject: [PATCH v4 2/5] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.
... database superuser, not 'POSTGRES'.
+++ b/src/backend/catalog/aclchk.c @@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid) return has_privs_of_role(roleid, ownerId); }+/* + * Ownership check for a role (specified by OID) + */ +bool +pg_role_ownercheck(Oid role_oid, Oid roleid) +{ + HeapTuple tuple; + Form_pg_authid authform; + Oid owner_oid; + + /* Superusers bypass all permission checking. */ + if (superuser_arg(roleid)) + return true; + + /* Otherwise, look up the owner of the role */ + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("role with OID %u does not exist", + role_oid))); + authform = (Form_pg_authid) GETSTRUCT(tuple); + owner_oid = authform->rolowner; + + /* + * Roles must necessarily have owners. Even the bootstrap user has an + * owner. (It owns itself). Other roles must form a proper tree. + */ + if (!OidIsValid(owner_oid)) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u has invalid owner", + authform->rolname.data, authform->oid))); + if (authform->oid != BOOTSTRAP_SUPERUSERID && + authform->rolowner == authform->oid) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owns itself", + authform->rolname.data, authform->oid))); + if (authform->oid == BOOTSTRAP_SUPERUSERID && + authform->rolowner != BOOTSTRAP_SUPERUSERID) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owned by role with OID %u", + authform->rolname.data, authform->oid, + authform->rolowner))); + ReleaseSysCache(tuple); + + return (owner_oid == roleid); +}
Do we really need all of these checks on every call of this function..?
Also, there isn't much point in including the role OID twice in the last
error message, is there? Unless things have gotten quite odd, it's
goint to be the same value both times as we just proved to ourselves
that it is, in fact, the same value (and that it's not the
BOOTSTRAP_SUPERUSERID).
This function also doesn't actually do any kind of checking to see if
the role ownership forms a proper tree, so it seems a bit odd to have
the comment talking about that here where it's doing other checks.
+++ b/src/backend/commands/user.c @@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) Datum new_record[Natts_pg_authid]; bool new_record_nulls[Natts_pg_authid]; Oid roleid; + Oid owner_uid; + Oid saved_uid; + int save_sec_context;
Seems a bit odd to introduce 'uid' into this file, which hasn't got any
such anywhere in it, and I'm not entirely sure that any of these are
actually needed..?
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;+ GetUserIdAndSecContext(&saved_uid, &save_sec_context); + + /* + * Who is supposed to own the new role? + */ + if (stmt->authrole) + owner_uid = get_rolespec_oid(stmt->authrole, false); + else + owner_uid = saved_uid; + /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) { @@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create superusers"))); + if (!superuser_arg(owner_uid)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to own superusers"))); } else if (isreplication) {
So, we're telling a superuser (which is the only way you could get to
this point...) that they aren't allowed to create a superuser role which
is owned by a non-superuser... Why?
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));+ /* + * If the requested authorization is different from the current user, + * temporarily set the current user so that the object(s) will be created + * with the correct ownership. + * + * (The setting will be restored at the end of this routine, or in case of + * error, transaction abort will clean things up.) + */ + if (saved_uid != owner_uid) + SetUserIdAndSecContext(owner_uid, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
Err, why is this needed? This looks copied from the CreateSchemaCommand
but, unlike with the create schema command, CreateRole doesn't actually
allow sub-commands to be run to create other objects in the way that
CreateSchemaCommand does.
@@ -478,6 +513,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);+ /* Reset current user and security context */ + SetUserIdAndSecContext(saved_uid, save_sec_context); + return roleid; }
... ditto with this.
@@ -1675,3 +1714,110 @@ DelRoleMems(const char *rolename, Oid roleid, +static void +AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId) +{ + Form_pg_authid authForm; + + Assert(tup->t_tableOid == AuthIdRelationId); + Assert(RelationGetRelid(rel) == AuthIdRelationId); + + authForm = (Form_pg_authid) GETSTRUCT(tup); + + /* + * If the new owner is the same as the existing owner, consider the + * command to have succeeded. This is for dump restoration purposes. + */ + if (authForm->rolowner != newOwnerId) + { + /* Otherwise, must be owner of the existing object */ + if (!pg_role_ownercheck(authForm->oid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE, + NameStr(authForm->rolname)); + + /* Must be able to become new owner */ + check_is_member_of_role(GetUserId(), newOwnerId);
Feels like we should be saying a bit more about why we check for role
membership vs. has_privs_of_role() here. I'm generally of the opinion
that membership is the right thing to check here, just feel like we
should try to explain more why that's the right thing.
+ /* + * must have CREATEROLE rights + * + * NOTE: This is different from most other alter-owner checks in that + * the current user is checked for create privileges instead of the + * destination owner. This is consistent with the CREATE case for + * roles. Because superusers will always have this right, we need no + * special case for them. + */ + if (!have_createrole_privilege()) + aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE, + NameStr(authForm->rolname)); +
I would think we'd be trying to get away from the role attribute stuff.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList + { + CreateRoleStmt *n = makeNode(CreateRoleStmt); + n->stmt_type = ROLESTMT_ROLE; + n->role = $3; + n->authrole = $5; + n->options = $7; + $$ = (Node *)n; + } ;
...
@@ -1218,6 +1229,10 @@ CreateOptRoleElem: { $$ = makeDefElem("addroleto", (Node *)$3, @1); } + | OWNER RoleSpec + { + $$ = makeDefElem("owner", (Node *)$2, @1); + } ;
Not sure why we'd have both AUTHORIZATION and OWNER for CREATE ROLE..?
We don't do that for other objects.
diff --git 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS; +GRANT CREATE ON DATABASE regression TO regress_role_1;
Seems odd to add this as part of this patch, or am I missing something?
From 1784a5b51d4dbebf99798b5832d92b0f585feb08 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 11:42:27 -0800
Subject: [PATCH v4 3/5] Give role owners control over owned rolesCreate a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.This is a preparatory patch for changing how CREATEROLE works.
This feels odd to have be an independent commit.
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index ddd205d656..ef36fad700 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -5440,61 +5440,20 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid) bool pg_role_ownercheck(Oid role_oid, Oid roleid) { - HeapTuple tuple; - Form_pg_authid authform; - Oid owner_oid; - /* Superusers bypass all permission checking. */ if (superuser_arg(roleid)) return true;- /* Otherwise, look up the owner of the role */ - tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid)); - if (!HeapTupleIsValid(tuple)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("role with OID %u does not exist", - role_oid))); - authform = (Form_pg_authid) GETSTRUCT(tuple); - owner_oid = authform->rolowner; - - /* - * Roles must necessarily have owners. Even the bootstrap user has an - * owner. (It owns itself). Other roles must form a proper tree. - */ - if (!OidIsValid(owner_oid)) - ereport(ERROR, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("role \"%s\" with OID %u has invalid owner", - authform->rolname.data, authform->oid))); - if (authform->oid != BOOTSTRAP_SUPERUSERID && - authform->rolowner == authform->oid) - ereport(ERROR, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("role \"%s\" with OID %u owns itself", - authform->rolname.data, authform->oid))); - if (authform->oid == BOOTSTRAP_SUPERUSERID && - authform->rolowner != BOOTSTRAP_SUPERUSERID) - ereport(ERROR, - (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("role \"%s\" with OID %u owned by role with OID %u", - authform->rolname.data, authform->oid, - authform->rolowner))); - ReleaseSysCache(tuple); - - return (owner_oid == roleid); + /* Otherwise, check the role ownership hierarchy */ + return is_owner_of_role_nosuper(roleid, role_oid); }
The function being basically entirely rewritten in this patch would be
one reason why it seems an odd split.
/* * Check whether specified role has CREATEROLE privilege (or is a superuser) * - * Note: roles do not have owners per se; instead we use this test in - * places where an ownership-like permissions test is needed for a role. - * Be sure to apply it to the role trying to do the operation, not the - * role being operated on! Also note that this generally should not be - * considered enough privilege if the target role is a superuser. - * (We don't handle that consideration here because we want to give a - * separate error message for such cases, so the caller has to deal with it.) + * Note: In versions prior to PostgreSQL version 15, roles did not have owners + * per se; instead we used this test in places where an ownership-like + * permissions test was needed for a role. */ bool has_createrole_privilege(Oid roleid)
Surely this should be in the prior commit, if the split is kept..
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId) /* * must have create-schema rights * - * NOTE: This is different from other alter-owner checks in that the + * NOTE: This is different from most other alter-owner checks in that the * current user is checked for create privileges instead of the * destination owner. This is consistent with the CREATE case for * schemas. Because superusers will always have this right, we need
Not a fan of just dropping 'most' in here, doesn't really help someone
understand what is being talked about. I'd suggest adjusting the
comment to talk about alter-owner checks for objects which exist in
schemas, as that's really what is being referred to.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 14820744bf..11d5dffc90 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -724,7 +724,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) !rolemembers && !validUntil && dpassword && - roleid == GetUserId())) + !pg_role_ownercheck(roleid, GetUserId()))) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied"))); @@ -925,7 +925,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt) } else { - if (!have_createrole_privilege() && roleid != GetUserId()) + if (!have_createrole_privilege() && + !pg_role_ownercheck(roleid, GetUserId())) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied"))); @@ -977,11 +978,6 @@ DropRole(DropRoleStmt *stmt) pg_auth_members_rel; ListCell *item;- if (!have_createrole_privilege()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied to drop role"))); - /* * Scan the pg_authid relation to find the Oid of the role(s) to be * deleted. @@ -1053,6 +1049,12 @@ DropRole(DropRoleStmt *stmt) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to drop superusers")));+ if (!have_createrole_privilege() && + !pg_role_ownercheck(roleid, GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to drop role"))); + /* DROP hook for the role being removed */ InvokeObjectDropHook(AuthIdRelationId, roleid, 0);@@ -1811,6 +1813,18 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("role may not own itself")));+ /* + * Must not create cycles in the role ownership hierarchy. If this + * role owns (directly or indirectly) the proposed new owner, disallow + * the ownership transfer. + */ + if (is_owner_of_role_nosuper(authForm->oid, newOwnerId)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("role \"%s\" may not both own and be owned by role \"%s\"", + NameStr(authForm->rolname), + GetUserNameFromId(newOwnerId, false)))); + authForm->rolowner = newOwnerId; CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
+/* + * Get a list of roles which own the given role, directly or indirectly. + * + * Each role has only one direct owner. The returned list contains the given + * role's owner, that role's owner, etc., up to the top of the ownership + * hierarchy, which is always the bootstrap superuser. + * + * Raises an error if any role ownership invariant is violated. Returns NIL if + * the given roleid is invalid. + */ +static List * +roles_is_owned_by(Oid roleid) +{ + List *owners_list = NIL; + Oid role_oid = roleid; + + /* + * Start with the current role and follow the ownership chain upwards until + * we reach the bootstrap superuser. To defend against getting into an + * infinite loop, we must check for ownership cycles. We choose to perform + * other corruption checks on the ownership structure while iterating, too. + */ + while (OidIsValid(role_oid)) + { + HeapTuple tuple; + Form_pg_authid authform; + Oid owner_oid; + + /* Find the owner of the current iteration's role */ + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("role with OID %u does not exist", role_oid))); + + authform = (Form_pg_authid) GETSTRUCT(tuple); + owner_oid = authform->rolowner; + + /* + * Roles must necessarily have owners. Even the bootstrap user has an + * owner. (It owns itself). + */ + if (!OidIsValid(owner_oid)) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u has invalid owner", + NameStr(authform->rolname), authform->oid))); + + /* The bootstrap user must own itself */ + if (authform->oid == BOOTSTRAP_SUPERUSERID && + owner_oid != BOOTSTRAP_SUPERUSERID) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owned by role with OID %u", + NameStr(authform->rolname), authform->oid, + authform->rolowner))); + + /* + * Roles other than the bootstrap user must not be their own direct + * owners. + */ + if (authform->oid != BOOTSTRAP_SUPERUSERID && + authform->oid == owner_oid) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owns itself", + NameStr(authform->rolname), authform->oid))); + + ReleaseSysCache(tuple); + + /* If we have reached the bootstrap user, we're done. */ + if (role_oid == BOOTSTRAP_SUPERUSERID) + { + if (!owners_list) + owners_list = lappend_oid(owners_list, owner_oid); + break; + } + + /* + * For all other users, check they do not own themselves indirectly + * through an ownership cycle. + * + * Scanning the list each time through this loop results in overall + * quadratic work in the depth of the ownership chain, but we're + * not on a critical performance path, nor do we expect ownership + * hierarchies to be deep. + */ + if (owners_list && list_member_oid(owners_list, + ObjectIdGetDatum(owner_oid))) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u indirectly owns itself", + GetUserNameFromId(owner_oid, false), + owner_oid))); + + /* Done with sanity checks. Add this owner to the list. */ + owners_list = lappend_oid(owners_list, owner_oid); + + /* Otherwise, iterate on this iteration's owner_oid. */ + role_oid = owner_oid; + } + + return owners_list; +}
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
+ /* Owners of roles have every privilge the owned role has */ + if (pg_role_ownercheck(role, member)) + return true;
Whoah, really? No, I don't agree with this, it's throwing away the
entire concept around inheritance of role rights and how you can have
roles which you can get the privileges of by doing a SET ROLE to them
but you don't automatically have those rights.
+/* + * Is owner a direct or indirect owner of the role, not considering + * superuserness? + */ +bool +is_owner_of_role_nosuper(Oid owner, Oid role) +{ + return list_member_oid(roles_is_owned_by(role), owner); +}
Surely if you're a member of a role which owns another role, you should
be considered to be an owner of that role too..? Just checking if the
current role is a member of the roles which directly own the specified
role misses that case.
That is:
CREATE ROLE r1;
CREATE ROLE r2;
GRANT r2 to r1;
CREATE ROLE r3 AUTHORIZATION r2;
Surely, r1 is to be considered an owner of r3 in this case, but the
above check wouldn't consider that to be the case- it would only return
true if the current role is r2.
We do need some kind of direct membership check in the list of owners to
avoid creating loops, so maybe this function is kept as that and the
pg_role_ownership() check is changed to address the above case, but I
don't think we should just ignore role membership when it comes to role
ownership- we don't do that for any other kind of ownership check.
Subject: [PATCH v4 4/5] Restrict power granted via CREATEROLE.
I would think this would be done independently of the other patches and
probably be first.
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A <link linkend="sql-revoke"><command>REVOKE</command></link> for that.) Attributes not mentioned in the command retain their previous settings. Database superusers can change any of these settings for any role. - 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. - Ordinary roles can only change their own password. + Role owners can change any of these settings on roles they own except + <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and + <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication + roles, and only if the role owner does not alter the target role to have a + privilege which the role owner itself lacks. Ordinary roles can only change + their own password. </para>
Having contemplated this a bit more, I don't like it, and it's not how
things work when it comes to regular privileges.
Consider that I can currently GRANT someone UPDATE privileges on an
object, but they can't GRANT that privilege to someone else unless I
explicitly allow it. The same could certainly be said for roles-
perhaps I want to allow someone the privilege to create non-login roles,
but I don't want them to be able to create new login roles, even if they
themselves have LOGIN.
As another point, I might want to have an 'admin' role that I want
admins to SET ROLE to before they go creating other roles, because I
don't want them to be creating roles as their regular user and so that
those other roles are owned by the 'admin' role, but I don't want that
role to have the 'login' attribute.
In other words, we should really consider what role attributes a given
role has to be independent of what role attributes that role is allowed
to set on roles they create. I appreciate that "just whatever the
current role has" is simpler and less work but also will be difficult to
walk back from once it's in the wild.
@@ -1457,7 +1449,7 @@ AddRoleMems(const char *rolename, Oid roleid,
/* - * Check permissions: must have createrole or admin option on the role to + * Check permissions: must be owner or have admin option on the role to * be changed. To mess with a superuser role, you gotta be superuser. */ if (superuser_arg(roleid))
...
@@ -1467,9 +1459,9 @@ AddRoleMems(const char *rolename, Oid roleid, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to alter superusers"))); } - else + else if (!superuser()) { - if (!have_createrole_privilege() && + if (!pg_role_ownercheck(roleid, grantorId) && !is_admin_of_role(grantorId, roleid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
I'm not entirely sure about including owners here though I'm not
completely against it either. This conflation of what the 'admin'
privileges on a role means vs. the 'ownership' of a role is part of what
I dislike about having two distinct systems for saying who is allowed to
GRANT one role to another.
Also, if we're going to always consider owners to be admins of roles
they own, why not push that into is_admin_of_role()?
Subject: [PATCH v4 5/5] Remove grantor field from pg_auth_members
While I do think we should fix the issue with dangling references, I
dislike just getting rid of this entirely. While I don't really agree
with the spec about running around DROP'ing objects when a user's
privilege to create those objects has been revoked, I do think we should
be REVOKE'ing rights when a user's right to GRANT has been revoked, and
tracking the information about who GRANT'd what role to what other role
is needed for that. Further, we track who GRANT'd access to what in the
regular ACL system and I don't like the idea of removing that for roles.
We could fix the bug, but there is no clear solution to the problem
that existing installations may have broken data. Since the field
is not used for any purpose, removing it seems the best option.
Existing broken systems will have to eventually be upgraded and the
admin will have to deal with such cases then, so I don't really consider
this to be that big of an issue or reason to entirely remove this.
If we're going to do this, it should also be done independently of the
role ownership stuff too.
Thanks,
Stephen
On 1/22/22 16:20, Stephen Frost wrote:
Subject: [PATCH v4 1/5] Add tests of the CREATEROLE attribute.
No particular issue with this one.
I'm going to commit this piece forthwith so we get it out of the way.
That will presumably make the cfbot unhappy until Mark submits a new
patch set.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Sat, Jan 22, 2022 at 4:20 PM Stephen Frost <sfrost@snowman.net> wrote:
Whoah, really? No, I don't agree with this, it's throwing away the
entire concept around inheritance of role rights and how you can have
roles which you can get the privileges of by doing a SET ROLE to them
but you don't automatically have those rights.
I see it differently. In my opinion, what that does is make the patch
actually useful instead of largely a waste of time. If you are a
service provider, you want to give your customers a super-user-like
experience without actually making them superuser. You don't want to
actually make them superuser, because then they could do things like
change archive_command or install plperlu and shell out to the OS
account, which you don't want. But you do want them to be able to
administer objects within the database just as a superuser could. And
a superuser has privileges over objects they own and objects belonging
to other users automatically, without needing to SET ROLE.
Imagine what happens if we adopt your proposal here. Everybody now has
to understand the behavior of a regular account, the behavior of a
superuser account, and the behavior of this third type of account
which is sort of like a superuser but requires a lot more SET ROLE
commands. And also every tool. So for example pg_dump and restore
isn't going to work, not even on the set of objects this
elevated-privilege user can access. pgAdmin isn't going to understand
that it needs to insert a bunch of extra SET ROLE commands to
administer objects. Ditto literally every other tool anyone has ever
written to administer PostgreSQL. And for all of that pain, we get
exactly zero extra security.
--
Robert Haas
EDB: http://www.enterprisedb.com
On 1/24/22 15:33, Robert Haas wrote:
On Sat, Jan 22, 2022 at 4:20 PM Stephen Frost <sfrost@snowman.net> wrote:
Whoah, really? No, I don't agree with this, it's throwing away the
entire concept around inheritance of role rights and how you can have
roles which you can get the privileges of by doing a SET ROLE to them
but you don't automatically have those rights.I see it differently. In my opinion, what that does is make the patch
actually useful instead of largely a waste of time. If you are a
service provider, you want to give your customers a super-user-like
experience without actually making them superuser. You don't want to
actually make them superuser, because then they could do things like
change archive_command or install plperlu and shell out to the OS
account, which you don't want. But you do want them to be able to
administer objects within the database just as a superuser could. And
a superuser has privileges over objects they own and objects belonging
to other users automatically, without needing to SET ROLE.
+many
I encountered such issues on a cloud provider several years ago, and
blogged about the difficulties, which would have been solved very nicely
and cleanly by this proposal. It was when I understood properly how this
proposal worked, precisely as Robert states, that I became more
enthusiastic about it.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
Greetings,
On Mon, Jan 24, 2022 at 15:33 Robert Haas <robertmhaas@gmail.com> wrote:
On Sat, Jan 22, 2022 at 4:20 PM Stephen Frost <sfrost@snowman.net> wrote:
Whoah, really? No, I don't agree with this, it's throwing away the
entire concept around inheritance of role rights and how you can have
roles which you can get the privileges of by doing a SET ROLE to them
but you don't automatically have those rights.I see it differently. In my opinion, what that does is make the patch
actually useful instead of largely a waste of time.
The idea behind this patch is to enable creation and dropping of roles,
which isn’t possible now without being effectively a superuser.
Forcing owners to also implicitly have all rights of the roles they create
is orthogonal to that and an unnecessary change.
If you are a
service provider, you want to give your customers a super-user-like
experience without actually making them superuser. You don't want to
actually make them superuser, because then they could do things like
change archive_command or install plperlu and shell out to the OS
account, which you don't want. But you do want them to be able to
administer objects within the database just as a superuser could. And
a superuser has privileges over objects they own and objects belonging
to other users automatically, without needing to SET ROLE.
I am not saying that we would explicitly set all cases to be noninherit or
that we would even change the default away from what it is today, only that
we should use the existing role system and it’s concept of
inherit-vs-noninherit rather than throwing all of that away.
Everybody now has
to understand the behavior of a regular account, the behavior of a
superuser account, and the behavior of this third type of account
which is sort of like a superuser but requires a lot more SET ROLE
commands.
Inherit vs. noninherit roles is not a new concept, it has existed since the
role system was implemented. Further, that system does not require a lot
of SET ROLE commands unless and until an admin sets up a non-inherit role.
At that time, however, it’s expected that the rights of a role which has
inherit set to false are not automatically allowed for the role to which it
was GRANT’d. That’s how roles have always worked since they were
introduced.
And also every tool. So for example pg_dump and restore
isn't going to work, not even on the set of objects this
elevated-privilege user can access. pgAdmin isn't going to understand
that it needs to insert a bunch of extra SET ROLE commands to
administer objects. Ditto literally every other tool anyone has ever
written to administer PostgreSQL. And for all of that pain, we get
exactly zero extra security.
We have an inherit system today and pg_dump works just fine, as far as I’m
aware, and it does, indeed, issue SET ROLE at various points. Perhaps you
could explain with PG today what the issue is that is caused? Or what
issue pgAdmin has with PG’s existing role inherit system?
Further, being able to require a SET ROLE before running a given operation
is certainly a benefit in much the same way that having a user have to sudo
before running an operation is.
Thanks,
Stephen
Show quoted text
On Mon, Jan 24, 2022 at 4:23 PM Stephen Frost <sfrost@snowman.net> wrote:
The idea behind this patch is to enable creation and dropping of roles, which isn’t possible now without being effectively a superuser.
Forcing owners to also implicitly have all rights of the roles they create is orthogonal to that and an unnecessary change.
I just took a look at the first email on this thread and it says this:
These patches have been split off the now deprecated monolithic "Delegating superuser tasks to new security roles" thread at [1].
Therefore I think it is pretty clear that the goals of this patch set
include being able to delegate superuser tasks to new security roles.
And having those tasks be delegated but *work randomly differently* is
much less useful.
I am not saying that we would explicitly set all cases to be noninherit or that we would even change the default away from what it is today, only that we should use the existing role system and it’s concept of inherit-vs-noninherit rather than throwing all of that away.
INHERIT vs. NOINHERIT is documented to control the behavior of role
*membership*. This patch is introducing a new concept of role
*ownership*. It's not self-evident that what applies to one case
should apply to the other.
Further, being able to require a SET ROLE before running a given operation is certainly a benefit in much the same way that having a user have to sudo before running an operation is.
That's a reasonable point of view, but having things work similarly to
what happens for a superuser is ALSO a very big benefit. In my
opinion, in fact, it is a far larger benefit.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
On Mon, Jan 24, 2022 at 16:42 Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Jan 24, 2022 at 4:23 PM Stephen Frost <sfrost@snowman.net> wrote:
The idea behind this patch is to enable creation and dropping of roles,
which isn’t possible now without being effectively a superuser.
Forcing owners to also implicitly have all rights of the roles they
create is orthogonal to that and an unnecessary change.
I just took a look at the first email on this thread and it says this:
These patches have been split off the now deprecated monolithic
"Delegating superuser tasks to new security roles" thread at [1].
Therefore I think it is pretty clear that the goals of this patch set
include being able to delegate superuser tasks to new security roles.
And having those tasks be delegated but *work randomly differently* is
much less useful.
Being able to create and drop users is, in fact, effectively a
superuser-only task today. We could throw out the entire idea of role
ownership, in fact, as being entirely unnecessary when talking about that
specific task.
I am not saying that we would explicitly set all cases to be noninherit
or that we would even change the default away from what it is today, only
that we should use the existing role system and it’s concept of
inherit-vs-noninherit rather than throwing all of that away.INHERIT vs. NOINHERIT is documented to control the behavior of role
*membership*. This patch is introducing a new concept of role
*ownership*. It's not self-evident that what applies to one case
should apply to the other.
This is an argument to drop the role ownership concept, as I view it.
Privileges are driven by membership today and inventing some new
independent way to do that is increasing confusion, not improving things.
I disagree that adding role ownership should necessarily change how the
regular GRANT privilege system works or throw away basic concepts of that
system which have been in place for decades. Increasing the number of
independent ways to answer the question of “what users have what rights on
object X” is an active bad thing. Anything that cares about object access
will now also have to address role ownership to answer that question, while
if we don’t include this one change then they don’t need to directly have
any concern for ownership because regular object privileges still work the
same way they did before.
Further, being able to require a SET ROLE before running a given
operation is certainly a benefit in much the same way that having a user
have to sudo before running an operation is.That's a reasonable point of view, but having things work similarly to
what happens for a superuser is ALSO a very big benefit. In my
opinion, in fact, it is a far larger benefit.
Superuser is a problem specifically because it gives people access to do
absolutely anything, both for security and safety concerns. Disallowing a
way to curtail that same risk when it comes to role ownership invites
exactly those same problems.
I appreciate that there’s an edge between the ownership system being
proposed and the existing role membership system, but we’d be much better
off trying to minimize the amount that they end up overlapping- role
ownership should be about managing roles.
To push back on the original “tenant” argument, consider that one of the
bigger issues in cloud computing today is exactly the problem that the
cloud managers can potentially gain access to the sensitive data of their
tenants and that’s not generally viewed as a positive thing. This change
would make it so that every landlord can go and SELECT from the tables of
their tenants without so much as a by-your-leave. The tenants likely don’t
like that idea, and almost as likely the landlords in many cases aren’t
thrilled with it either. Should the landlords be able to DROP the tenant
due to the tenant not paying their bill? Of course, and that should then
eliminate the tenant’s tables and other objects which take up resources,
but that’s not the same thing as saying that a landlord should be able to
unlock a tenant’s old phone that they left behind (and yeah, maybe the
analogy falls apart a bit there, but the point I’m trying to get at is that
it’s not as simple as it’s being made out to be here and we should think
about these things and not just implicitly grant all access to the owner
because that’s an easy thing to do- and is exactly what viewing owners as
“mini superusers” does and leads to many of the same issues we already have
with superusers).
Thanks,
Stephen
Show quoted text
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
Being able to create and drop users is, in fact, effectively a superuser-only task today. We could throw out the entire idea of role ownership, in fact, as being entirely unnecessary when talking about that specific task.
Wow, that's totally contrary to how I see this patch. The heart and soul of this patch is to fix the fact that CREATEROLE is currently overpowered. Everything else is gravy.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
Superuser is a problem specifically because it gives people access to do absolutely anything, both for security and safety concerns. Disallowing a way to curtail that same risk when it comes to role ownership invites exactly those same problems.
Before the patch, users with CREATEROLE can do mischief. After the patch, users with CREATEROLE can do mischief. The difference is that the mischief that can be done after the patch is a proper subset of the mischief that can be done before the patch. (Counter-examples highly welcome.)
Specifically, I claim that before the patch, non-superuser "bob" with CREATEROLE can interfere with *any* non-superuser. After the patch, non-superuser "bob" with CREATEROLE can interfere with *some* non-superusers; specifically, with non-superusers he created himself, or which have had ownership transferred to him.
Restricting the scope of bob's mischief is a huge win, in my view.
The argument about whether owners should always implicitly inherit privileges from roles they own is a bit orthogonal to my point about mischief-making. Do we at least agree on the mischief-abatement aspect of this patch set?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 2022/01/25 8:18, Mark Dilger wrote:
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
Superuser is a problem specifically because it gives people access to do absolutely anything, both for security and safety concerns. Disallowing a way to curtail that same risk when it comes to role ownership invites exactly those same problems.
Before the patch, users with CREATEROLE can do mischief. After the patch, users with CREATEROLE can do mischief. The difference is that the mischief that can be done after the patch is a proper subset of the mischief that can be done before the patch. (Counter-examples highly welcome.)
Specifically, I claim that before the patch, non-superuser "bob" with CREATEROLE can interfere with *any* non-superuser. After the patch, non-superuser "bob" with CREATEROLE can interfere with *some* non-superusers; specifically, with non-superusers he created himself, or which have had ownership transferred to him.
Restricting the scope of bob's mischief is a huge win, in my view.
+1
One of "mischiefs" I'm thinking problematic is that users with CREATEROLE can give any predefined role that they don't have, to other users including themselves. For example, users with CREATEROLE can give pg_execute_server_program to themselves and run any OS commands by COPY PROGRAM. This would be an issue when providing something like PostgreSQL cloud service that wants to prevent end users from running OS commands but allow them to create/drop roles. Does the proposed patch fix also this issue?
Regards,
--
Fujii Masao
Advanced Computing Technology Center
Research and Development Headquarters
NTT DATA CORPORATION
On Jan 24, 2022, at 10:55 PM, Fujii Masao <masao.fujii@oss.nttdata.com> wrote:
+1
One of "mischiefs" I'm thinking problematic is that users with CREATEROLE can give any predefined role that they don't have, to other users including themselves. For example, users with CREATEROLE can give pg_execute_server_program to themselves and run any OS commands by COPY PROGRAM. This would be an issue when providing something like PostgreSQL cloud service that wants to prevent end users from running OS commands but allow them to create/drop roles. Does the proposed patch fix also this issue?
Yes, the patch restricts CREATEROLE privilege from granting any privilege they themselves lack. There is a regression test in the patch set which demonstrates this. See src/test/regress/expected/create_role.out. The diffs from v6-0004-Restrict-power-granted-via-CREATEROLE.patch are quoted here for ease of viewing:
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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"
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
To push back on the original “tenant” argument, consider that one of the bigger issues in cloud computing today is exactly the problem that the cloud managers can potentially gain access to the sensitive data of their tenants and that’s not generally viewed as a positive thing.
+1. This is a real problem. I have been viewing this problem as separate from the one which role ownership is intended to fix. Do you have a suggestion about how to tackle the problems together with less work than tackling them separately?
This change would make it so that every landlord can go and SELECT from the tables of their tenants without so much as a by-your-leave.
I would expect that is already true. A user with CREATEROLE can do almost everything. This patch closes some CREATEROLE related security problems, but not this one you mention.
The tenants likely don’t like that idea
+1
, and almost as likely the landlords in many cases aren’t thrilled with it either.
+1
Should the landlords be able to DROP the tenant due to the tenant not paying their bill? Of course, and that should then eliminate the tenant’s tables and other objects which take up resources, but that’s not the same thing as saying that a landlord should be able to unlock a tenant’s old phone that they left behind (and yeah, maybe the analogy falls apart a bit there, but the point I’m trying to get at is that it’s not as simple as it’s being made out to be here and we should think about these things and not just implicitly grant all access to the owner because that’s an easy thing to do- and is exactly what viewing owners as “mini superusers” does and leads to many of the same issues we already have with superusers).
This is a pretty interesting argument. I don't believe it will work to do as you say unconditionally, as there is still a need to have CREATEROLE users who have privileges on their created roles' objects, even if for no other purpose than to be able to REASSIGN OWNED BY those objects before dropping roles. But maybe there is also a need to have CREATEROLE users who lack that privilege? Would that be a privilege bit akin to (but not the same as!) the INHERIT privilege? Should I redesign for something like that?
I like that the current patch restricts CREATEROLE users from granting privileges they themselves lack. Would such a new privilege bit work the same way? Imagine that you, "stephen", have CREATEROLE but not this new bit, and you create me, "mark" as a tenant with CREATEROLE. Can you give me the bit? Or does the fact that you lack the bit mean you can't give it to me, either?
Other suggestions?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Mon, Jan 24, 2022 at 5:21 PM Stephen Frost <sfrost@snowman.net> wrote:
This is an argument to drop the role ownership concept, as I view it. Privileges are driven by membership today and inventing some new independent way to do that is increasing confusion, not improving things. I disagree that adding role ownership should necessarily change how the regular GRANT privilege system works or throw away basic concepts of that system which have been in place for decades. Increasing the number of independent ways to answer the question of “what users have what rights on object X” is an active bad thing. Anything that cares about object access will now also have to address role ownership to answer that question, while if we don’t include this one change then they don’t need to directly have any concern for ownership because regular object privileges still work the same way they did before.
It really feels to me like you just keep moving the goalposts. We
started out with a conversation where Mark said he'd like to be able
to grant permissions on GUCs to non-superusers.[1]/messages/by-id/F9408A5A-B20B-42D2-9E7F-49CD3D1547BC@enterprisedb.com You argued
repeatedly that we really needed to do something about CREATEROLE
[2,3,4]. Mark argued that this was an unrelated problem[5]/messages/by-id/92AA9A52-A644-42FE-B699-8ECAEE12E635@enterprisedb.com but you
argued that unless it were addressed, users would still be able to
break out of the sandbox[6]/messages/by-id/20210823195130.GF17906@tamriel.snowman.net which must mean either the OS user, or at
least PostgreSQL users other than the ones they were supposed to be
able to control.
That led *directly* to the patch at hand, which solves the problem by
inventing the notion of role ownership, so that you can distinguish
the roles you can administer from the ones you drop. You are now
proposing that we get rid of that concept, a concept that was added
four months ago[7]/messages/by-id/67BB2F92-704B-415C-8D47-149327CA8F4B@enterprisedb.com as a direct response to your previous feedback.
It's completely unfair to make an argument that results in the
addition of a complex piece of machinery to a body of work that was
initially on an only marginally related topic and then turn around and
argue, quite close to the end of the release cycle, for the removal of
that exact same mechanism.
And your argument about whether the privileges should be able to be
exercised without SET ROLE is also just completely baffling to me
given the previous conversation. It seems 100% clear from the previous
discussion that we were talking about service provider environments
and trying to deliver a good user experience to "lead tenants" in such
environments. Regardless of the technical details of how INHERIT or
anything else work, an actual superuser would not be subject to a
restriction similar to the one you're talking about, so arguing that
it ought to be present here for some technical reason is placing
technicalities ahead of what seemed at the time to be a shared goal.
There's a perfectly good argument to be made that the superuser role
should not work the way it does, but it's too late to relitigate that.
And I can't imagine why any service provider would find any value in a
new role that requires all of the extra push-ups you're trying to
impose on it.
I just can't shake the feeling that you're trying to redesign this
patch out of (a) getting committed and (b) solving any of the problems
it intends to solve, problems with which you largely seemed to agree.
I assume that is not actually your intention, but I can't think of
anything you'd be doing differently here if it were.
[1]: /messages/by-id/F9408A5A-B20B-42D2-9E7F-49CD3D1547BC@enterprisedb.com
[2]: /messages/by-id/20210726200542.GX20766@tamriel.snowman.net
[3]: /messages/by-id/20210726205433.GA20766@tamriel.snowman.net
[4]: /messages/by-id/20210823181351.GB17906@tamriel.snowman.net
[5]: /messages/by-id/92AA9A52-A644-42FE-B699-8ECAEE12E635@enterprisedb.com
[6]: /messages/by-id/20210823195130.GF17906@tamriel.snowman.net
[7]: /messages/by-id/67BB2F92-704B-415C-8D47-149327CA8F4B@enterprisedb.com
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
Being able to create and drop users is, in fact, effectively a superuser-only task today. We could throw out the entire idea of role ownership, in fact, as being entirely unnecessary when talking about that specific task.Wow, that's totally contrary to how I see this patch. The heart and soul of this patch is to fix the fact that CREATEROLE is currently overpowered. Everything else is gravy.
I agree that CREATEROLE is overpowered and that the goal of this should
be to provide a way for roles to be created and dropped that doesn't
give the user who has that power everything that CREATEROLE currently
does. The point I was making is that the concept of role ownership
isn't intrinsically linked to that and is, therefore, as you say, gravy.
That isn't to say that I'm entirely against the role ownership idea but
I'd want it to be focused on the goal of providing ways of creating and
dropping users and otherwise performing that kind of administration and
that doesn't require the specific change to make owners be members of
all roles they own and automatically have all privileges of those roles
all the time.
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
Superuser is a problem specifically because it gives people access to do absolutely anything, both for security and safety concerns. Disallowing a way to curtail that same risk when it comes to role ownership invites exactly those same problems.
Before the patch, users with CREATEROLE can do mischief. After the patch, users with CREATEROLE can do mischief. The difference is that the mischief that can be done after the patch is a proper subset of the mischief that can be done before the patch. (Counter-examples highly welcome.)
Specifically, I claim that before the patch, non-superuser "bob" with CREATEROLE can interfere with *any* non-superuser. After the patch, non-superuser "bob" with CREATEROLE can interfere with *some* non-superusers; specifically, with non-superusers he created himself, or which have had ownership transferred to him.
Restricting the scope of bob's mischief is a huge win, in my view.
The argument about whether owners should always implicitly inherit privileges from roles they own is a bit orthogonal to my point about mischief-making. Do we at least agree on the mischief-abatement aspect of this patch set?
I don't know how many bites at this particular apple we're going to get,
but I doubt folks are going to be happy if we change our minds every
release. Further, I suspect we'll be better off going too far in the
direction of 'mischief reduction' than not far enough. If we restrict
things too far then we can provide ways to add those things back, but
it's harder to remove things we didn't take away.
This particular case is even an oddity on that spectrum though-
CREATEROLE users, today, don't have access to all the objects created by
roles which they create. Yes, they can get such access if they go
through some additional hoops, but that could then be caught by someone
auditing the logs, a consideration that I don't think we appreciate
enough today.
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 24, 2022, at 2:21 PM, Stephen Frost <sfrost@snowman.net> wrote:
To push back on the original “tenant” argument, consider that one of the bigger issues in cloud computing today is exactly the problem that the cloud managers can potentially gain access to the sensitive data of their tenants and that’s not generally viewed as a positive thing.
+1. This is a real problem. I have been viewing this problem as separate from the one which role ownership is intended to fix. Do you have a suggestion about how to tackle the problems together with less work than tackling them separately?
I don't know about less work or not, but in this particular case I was
asking for a few lines to be removed from the patch. I can believe that
doing so would create some issues in terms of the use-cases that you
want to solve with this and if we agree on those being sensible cases to
address then we'd need to implement something to address those, though
it's also possibly not the case and maybe removing those few lines
doesn't impact anything beyond then allowing owners to not automatically
inherit the rights of the roles they own if they don't wish to.
Instead of talking about those cases concretely though, it seems like
we've shifted to abstractly talking about ownership and landlords.
Maybe some of that is helpful, but it seems to increasingly be an area
that's causing more division than helping to move forward towards a
mutually agreeable result.
This change would make it so that every landlord can go and SELECT from the tables of their tenants without so much as a by-your-leave.
I would expect that is already true. A user with CREATEROLE can do almost everything. This patch closes some CREATEROLE related security problems, but not this one you mention.
Yes, such a role *can* do almost anything, but they can't do this today:
=> create role r3;
CREATE ROLE
=*> set role r3;
ERROR: permission denied to set role "r3"
Nor, should 'r3' log in and create tables, can the creating role SELECT
from r3's tables or otherwise have any effect on them. That has
positives and negatives- we do want the 'owning' role to be able to do
certain things, like DROP the role, and once a role has created objects
that isn't able to be done unless those objects are reassigned or
dropped themselves. How do we allow explicitly that then? That's the
general direction I would think we'd be wanting to go in, rather than
just blanketly giving the owner all privileges of the roles they create
without any further say by anyone.
The tenants likely don’t like that idea
+1
, and almost as likely the landlords in many cases aren’t thrilled with it either.
+1
Glad we agree on those.
Should the landlords be able to DROP the tenant due to the tenant not paying their bill? Of course, and that should then eliminate the tenant’s tables and other objects which take up resources, but that’s not the same thing as saying that a landlord should be able to unlock a tenant’s old phone that they left behind (and yeah, maybe the analogy falls apart a bit there, but the point I’m trying to get at is that it’s not as simple as it’s being made out to be here and we should think about these things and not just implicitly grant all access to the owner because that’s an easy thing to do- and is exactly what viewing owners as “mini superusers” does and leads to many of the same issues we already have with superusers).
This is a pretty interesting argument. I don't believe it will work to do as you say unconditionally, as there is still a need to have CREATEROLE users who have privileges on their created roles' objects, even if for no other purpose than to be able to REASSIGN OWNED BY those objects before dropping roles. But maybe there is also a need to have CREATEROLE users who lack that privilege? Would that be a privilege bit akin to (but not the same as!) the INHERIT privilege? Should I redesign for something like that?
We have INHERIT today already for roles and I'm not really thrilled with
the idea of coming up with some new and independent way to make that
work, or having something that works effectively the same way as role
membership does today but is called something else (which is what this
patch set is doing with ownership, hence my concern).
There's a couple of thoughts I have about addressing things around DROP
and REASSIGN- one is that those could perhaps just be made to work for
owners, but another is to allow owners to manage the role memberships of
roles they own, to include allowing the role to be granted to
themselves, and maybe that's even the default? With today's CREATEROLE,
that looks like:
=> create role r3 admin sfrost;
CREATE ROLE
=*> set role r3;
SET
but we could possibly change that to be the default, or maybe we don't,
since that isn't how it works today.
Either way, we likely would need to allow owners to modify the role
membership of roles they own, but that doesn't strike me as a terribly
difficult thing to allow. A more interesting question is about if a
role can manage their *own* membership- something we allow today but, as
I've brought up before, we should probably curtail to some extent.
Ultimately, that makes it possible for this:
SELECT * FROM secret_table;
to fail when secret_table was created by a tenant and the query is run
by a landlord. A landlord would still be able to get access to
secret_table, but they'd have to do:
GRANT tenant TO landlord;
SELECT * FROM secret_table;
which may not seem like a lot to us, but it shows clear forethought and
very likely that GRANT would be an audited statement. If tenant also
doesn't have 'inherit' set then a SET ROLE might also be required.
Perhaps additional requirements could be added to the GRANT/SET ROLE to
make those operations not be trivial to do (certainly we've been asked
in the past for a way for SET ROLE to require a password, and, indeed,
some other database systems support that; consider that one day a
landlord might have to reset the PW for the role, GRANT themselves into
the role, and then SET ROLE with the reset password...).
I'd also like to share that while we talk about 'landlords' and
'tenants' here, the real world is more complicated- I'm sure the various
cloud providers have employees who have different levels of access,
perhaps some of whom are able to reset passwords for users, while others
are able to create new accounts, and yet others are able to authorize
access to customer data, something which hopefully most of the
organization isn't able to do and requires some additional hoops.
I like that the current patch restricts CREATEROLE users from granting privileges they themselves lack. Would such a new privilege bit work the same way? Imagine that you, "stephen", have CREATEROLE but not this new bit, and you create me, "mark" as a tenant with CREATEROLE. Can you give me the bit? Or does the fact that you lack the bit mean you can't give it to me, either?
Other suggestions?
As I mentioned in the patch review, having a particular bit set doesn't
necessarily mean you should be able to pass it on- the existing object
GRANT system distinguishes those two and it seems like we should too.
In other words, I'm saying that we should be able to explicitly say just
what privileges a CREATEROLE user is able to grant to some other role
rather than basing it on what that user themselves has. This might
already be possible with the proposed patch by creating a role with
CREATEROLE that then has the privileges we want to be allowed to be
passed on, and then GRANT'ing that role to the user who we want to allow
to create roles, though they would then have to SET ROLE to that role to
run the CREATE ROLE since role attributes aren't inherited by role
memberships. That doesn't seem like a terrible approach to solving that
particular issue, but then perhaps others feel differently.
Thanks,
Stephen
Import Notes
Reply to msg id not found: 61DB6A06-8625-49A2-8EB6-6295AA5AE1EC@enterprisedb.com8043E300-4968-4284-9A3C-84532C8F47BE@enterprisedb.com8961992A-F04B-478F-95DA-B38DD48E89FC@enterprisedb.com | Resolved by subject fallback
On Jan 25, 2022, at 12:44 PM, Stephen Frost <sfrost@snowman.net> wrote:
As I mentioned in the patch review, having a particular bit set doesn't
necessarily mean you should be able to pass it on- the existing object
GRANT system distinguishes those two and it seems like we should too.
In other words, I'm saying that we should be able to explicitly say just
what privileges a CREATEROLE user is able to grant to some other role
rather than basing it on what that user themselves has.
I like the way you are thinking, but I'm not sure I agree with the facts you are asserting.
I agree that "CREATE ROLE.. ROLE .." differs from "CREATE ROLE .. ADMIN ..", and "GRANT..WITH GRANT OPTION" differs from "GRANT..", but those only cover privileges tracked in an aclitem array. The privileges CREATEDB, CREATEROLE, REPLICATION, and BYPASSRLS don't work that way. There isn't a with/without grant option distinction for them. So I'm forced to say that a role without those privileges must not give them away.
I'd be happier if we could get rid of all privileges of that kind, leaving only those that can be granted with/without grant option, tracked in an aclitem, and use that to determine if the user creating the role can give them away. But that's a bigger redesign of the system. Just touching how CREATEROLE works entails backwards compatibility problems. I'd hate to try to change all these other things; we'd be breaking a lot more, and features that appear more commonly used.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Jan 22, 2022, at 1:20 PM, Stephen Frost <sfrost@snowman.net> wrote:
Subject: [PATCH v4 1/5] Add tests of the CREATEROLE attribute.
No particular issue with this one.
Andrew already committed this, forcing the remaining patches to be renumbered. Per your comments below, I have combined what was 0002+0003 into 0001, renumbered 0004 as 0002, and abandoned 0005. (It may come back as an independent patch.) Also owing to the fact that 0001 has been committed, I really need to post another patch set right away, to make the cfbot happy. I'm fixing non-controversial deficits you call out in your review, but leaving other things unchanged, in the interest of getting a patch posted sooner rather than later.
Subject: [PATCH v4 2/5] Add owners to roles
All roles now have owners. By default, roles belong to the role
that created them, and initdb-time roles are owned by POSTGRES.... database superuser, not 'POSTGRES'.
I rephrased this as "bootstrap superuser" in the commit message.
+++ b/src/backend/catalog/aclchk.c @@ -5430,6 +5434,57 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid) return has_privs_of_role(roleid, ownerId); }+/* + * Ownership check for a role (specified by OID) + */ +bool +pg_role_ownercheck(Oid role_oid, Oid roleid) +{ + HeapTuple tuple; + Form_pg_authid authform; + Oid owner_oid; + + /* Superusers bypass all permission checking. */ + if (superuser_arg(roleid)) + return true; + + /* Otherwise, look up the owner of the role */ + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("role with OID %u does not exist", + role_oid))); + authform = (Form_pg_authid) GETSTRUCT(tuple); + owner_oid = authform->rolowner; + + /* + * Roles must necessarily have owners. Even the bootstrap user has an + * owner. (It owns itself). Other roles must form a proper tree. + */ + if (!OidIsValid(owner_oid)) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u has invalid owner", + authform->rolname.data, authform->oid))); + if (authform->oid != BOOTSTRAP_SUPERUSERID && + authform->rolowner == authform->oid) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owns itself", + authform->rolname.data, authform->oid))); + if (authform->oid == BOOTSTRAP_SUPERUSERID && + authform->rolowner != BOOTSTRAP_SUPERUSERID) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("role \"%s\" with OID %u owned by role with OID %u", + authform->rolname.data, authform->oid, + authform->rolowner))); + ReleaseSysCache(tuple); + + return (owner_oid == roleid); +}Do we really need all of these checks on every call of this function..?
Since the function is following the ownership chain upwards, it seems necessary to check that the chain is wellformed, else we might get into an infinite loop or return the wrong answer. These would only happen under corrupt conditions, but it seems sensible to check for those, since they are cheap to check. (Actually, the check for nontrivial cycles included in the patch is not as efficient as it could be, but I'm punting the work of improving that algorithm from quadratic to linear until a later patch version, in the interest of posting the patch soon.)
Also, there isn't much point in including the role OID twice in the last
error message, is there? Unless things have gotten quite odd, it's
goint to be the same value both times as we just proved to ourselves
that it is, in fact, the same value (and that it's not the
BOOTSTRAP_SUPERUSERID).
It is comparing the authform->oid against the authform->rolowner, which are not the same. The first is the owned role, the second is the owning role. We could hardcode the message to say something like "bootstrap superuser owned by role with Oid %u", but that hardcodes "bootstrap superuser" into the message, rather than something like "stephen". I don't feel strongly about the wording. Let me know if you still want me to change it.
This function also doesn't actually do any kind of checking to see if
the role ownership forms a proper tree, so it seems a bit odd to have
the comment talking about that here where it's doing other checks.
Right. The comment simply explains the structure we expect, not the structure we are fully validating. The point is that each link in the hierarchy must be compatible with the expected structure. It would be overkill to validate the whole tree in this one function. I don't mind rewording the code comment, if you have a less confusing suggestion.
+++ b/src/backend/commands/user.c @@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) Datum new_record[Natts_pg_authid]; bool new_record_nulls[Natts_pg_authid]; Oid roleid; + Oid owner_uid; + Oid saved_uid; + int save_sec_context;Seems a bit odd to introduce 'uid' into this file, which hasn't got any
such anywhere in it, and I'm not entirely sure that any of these are
actually needed..?
Good catch! The implementation in v6 was wrong. It didn't enforce that the creating role was a member of the target owner, something this next patch set does.
@@ -108,6 +113,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;+ GetUserIdAndSecContext(&saved_uid, &save_sec_context); + + /* + * Who is supposed to own the new role? + */ + if (stmt->authrole) + owner_uid = get_rolespec_oid(stmt->authrole, false); + else + owner_uid = saved_uid; + /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) { @@ -254,6 +269,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create superusers"))); + if (!superuser_arg(owner_uid)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to own superusers"))); } else if (isreplication) {So, we're telling a superuser (which is the only way you could get to
this point...) that they aren't allowed to create a superuser role which
is owned by a non-superuser... Why?
The reason is one you won't like very much. Given that roles have the privileges of roles they own (which you don't like), allowing a non-superuser to own a superuser effectively promotes that owner to superuser status. That's a pretty obscure way of making someone a superuser, probably not what was intended, and quite a high-caliber foot-gun.
Even if roles didn't inherit privileges from roles they own, I think it would be odd for a non-superuser to own a superuser. The definition of "ownership" would have to be extremely restricted to prevent the owner from using their ownership to obtain superuser.
@@ -310,6 +329,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));+ /* + * If the requested authorization is different from the current user, + * temporarily set the current user so that the object(s) will be created + * with the correct ownership. + * + * (The setting will be restored at the end of this routine, or in case of + * error, transaction abort will clean things up.) + */ + if (saved_uid != owner_uid) + SetUserIdAndSecContext(owner_uid, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE);Err, why is this needed? This looks copied from the CreateSchemaCommand
but, unlike with the create schema command, CreateRole doesn't actually
allow sub-commands to be run to create other objects in the way that
CreateSchemaCommand does.
Not quite. There are still the check_password_hook and RunObjectPostCreateHook() to consider. The check_password_hook might want to validate the validuntil_time parameter against the owner's validuntil time, or some other property of the owner. And the RunObjectPostCreateHook (called via InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0)) may want the information, too.
I'm not saying these are super strong arguments. If people generally feel that CREATE ROLE ... AUTHORIZATION shouldn't call SetUserIdAndSecContext, feel free to argue that.
@@ -1675,3 +1714,110 @@ DelRoleMems(const char *rolename, Oid roleid, +static void +AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId) +{ + Form_pg_authid authForm; + + Assert(tup->t_tableOid == AuthIdRelationId); + Assert(RelationGetRelid(rel) == AuthIdRelationId); + + authForm = (Form_pg_authid) GETSTRUCT(tup); + + /* + * If the new owner is the same as the existing owner, consider the + * command to have succeeded. This is for dump restoration purposes. + */ + if (authForm->rolowner != newOwnerId) + { + /* Otherwise, must be owner of the existing object */ + if (!pg_role_ownercheck(authForm->oid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE, + NameStr(authForm->rolname)); + + /* Must be able to become new owner */ + check_is_member_of_role(GetUserId(), newOwnerId);Feels like we should be saying a bit more about why we check for role
membership vs. has_privs_of_role() here. I'm generally of the opinion
that membership is the right thing to check here, just feel like we
should try to explain more why that's the right thing.
For orthogonality with how ALTER .. OWNER TO works for everything else? AlterEventTriggerOwner_internal doesn't check this explicitly, but that's because it has already checked that the new owner is superuser, so the check must necessarily succeed. I'm not aware of any ALTER .. OWNER TO commands that don't require this, at least implicitly.
We could explain this in AlterRoleOwner_internal, as you suggest, but if we need it there, do we need to put the same explanation in functions which handle other object types? I don't see why this one function would require the explanation if other equivalent functions do not.
+ /* + * must have CREATEROLE rights + * + * NOTE: This is different from most other alter-owner checks in that + * the current user is checked for create privileges instead of the + * destination owner. This is consistent with the CREATE case for + * roles. Because superusers will always have this right, we need no + * special case for them. + */ + if (!have_createrole_privilege()) + aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE, + NameStr(authForm->rolname)); +I would think we'd be trying to get away from the role attribute stuff.
That's not a bad idea, but I thought it was discussed months ago. The two options were (1) keep using CREATEROLE but change it to be less powerful, and (2) add a new built-in role, say "pg_create_role", and have membership in that role be what we use. Option (2) was generally viewed less favorably, or that was my sense of people's opinions, on the theory that we'd be better off fixing how CREATEROLE works than having two different ways of doing roughly the same thing.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList + { + CreateRoleStmt *n = makeNode(CreateRoleStmt); + n->stmt_type = ROLESTMT_ROLE; + n->role = $3; + n->authrole = $5; + n->options = $7; + $$ = (Node *)n; + } ;...
@@ -1218,6 +1229,10 @@ CreateOptRoleElem: { $$ = makeDefElem("addroleto", (Node *)$3, @1); } + | OWNER RoleSpec + { + $$ = makeDefElem("owner", (Node *)$2, @1); + } ;Not sure why we'd have both AUTHORIZATION and OWNER for CREATE ROLE..?
We don't do that for other objects.
Good catch! The "OWNER RoleSpec" here was unused. I have removed it from the new patch set.
diff --git 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_1 CREATEDB CREATEROLE REPLICATION BYPASSRLS; +GRANT CREATE ON DATABASE regression TO regress_role_1;Seems odd to add this as part of this patch, or am I missing something?
It's not used much in patch 0001 where it gets introduced, but gets used more in patch 0002. I put it here to reduce the number of diffs the next patch creates.
From 1784a5b51d4dbebf99798b5832d92b0f585feb08 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 4 Jan 2022 11:42:27 -0800
Subject: [PATCH v4 3/5] Give role owners control over owned rolesCreate a role ownership hierarchy. The previous commit added owners
to roles. This goes further, making role ownership transitive. If
role A owns role B, and role B owns role C, then role A can act as
the owner of role C. Also, roles A and B can perform any action on
objects belonging to role C that role C could itself perform.This is a preparatory patch for changing how CREATEROLE works.
This feels odd to have be an independent commit.
Reworked the v6-0002 and v6-0003 patches into just one, as discussed at the top of this email.
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c@@ -363,7 +363,7 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId) /* * must have create-schema rights * - * NOTE: This is different from other alter-owner checks in that the + * NOTE: This is different from most other alter-owner checks in that the * current user is checked for create privileges instead of the * destination owner. This is consistent with the CREATE case for * schemas. Because superusers will always have this right, we needNot a fan of just dropping 'most' in here, doesn't really help someone
understand what is being talked about. I'd suggest adjusting the
comment to talk about alter-owner checks for objects which exist in
schemas, as that's really what is being referred to.
Yeah, that's a better approach. This next patch set changes the comment in both AlterSchemaOwner_internal and AlterRoleOwner_internal to make that clear.
...<snip>...
Whoah, really? No, I don't agree with this, it's throwing away the
entire concept around inheritance of role rights and how you can have
roles which you can get the privileges of by doing a SET ROLE to them
but you don't automatically have those rights.
I didn't change any of this for the next patch set, not because I'm ignoring you, but because we're still arguing out what the right behavior should be. Whatever we come up with, I think it should allow the use case that Robert has been talking about. Doing that and also doing what you are talking about might be hard, but I'm still hoping to find some solution.
Recall that upthread, months ago, we discussed that it is abnormal for any role to be a member of a login role. You can think of "login role" as a synonym for "user", and "non-login role" as a synonym for "group", and that language makes it easier to think about how weird it is for users to be members of other users.
It's perfectly sensible to have users own users, but not for users to be members of users. If not for that, I'd be in favor of what you suggest, excepting that I'd accommodate Robert's requirements by having the owner of a role have ADMIN on that role by default, with grammar for requesting the alternative. Maybe there is something I'm forgetting to consider just now, but I'd think that would handle Robert's "tenant" type argument while also making it easy to operate the way that you want. But, again, it does require having users be members of users, something which was rejected in the discussion months ago.
+/* + * Is owner a direct or indirect owner of the role, not considering + * superuserness? + */ +bool +is_owner_of_role_nosuper(Oid owner, Oid role) +{ + return list_member_oid(roles_is_owned_by(role), owner); +}Surely if you're a member of a role which owns another role, you should
be considered to be an owner of that role too..? Just checking if the
current role is a member of the roles which directly own the specified
role misses that case.That is:
CREATE ROLE r1;
CREATE ROLE r2;GRANT r2 to r1;
CREATE ROLE r3 AUTHORIZATION r2;
Surely, r1 is to be considered an owner of r3 in this case, but the
above check wouldn't consider that to be the case- it would only return
true if the current role is r2.We do need some kind of direct membership check in the list of owners to
avoid creating loops, so maybe this function is kept as that and the
pg_role_ownership() check is changed to address the above case, but I
don't think we should just ignore role membership when it comes to role
ownership- we don't do that for any other kind of ownership check.
I like this line of reasoning, and it appears to be an argument in your favor where the larger question is concerned. If role ownership is transitive, and role membership is transitive, it gets weird trying to work out larger relationship chains.
This deserves more attention.
Subject: [PATCH v4 4/5] Restrict power granted via CREATEROLE.
I would think this would be done independently of the other patches and
probably be first.
The way I'm trying to fix CREATEROLE is first by introducing the concept of role owners, then second by restricting what roles can do based on whether they own a target role. I don't see how I can reverse the order.
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A <link linkend="sql-revoke"><command>REVOKE</command></link> for that.) Attributes not mentioned in the command retain their previous settings. Database superusers can change any of these settings for any role. - 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. - Ordinary roles can only change their own password. + Role owners can change any of these settings on roles they own except + <literal>SUPERUSER</literal>, <literal>REPLICATION</literal>, and + <literal>BYPASSRLS</literal>; but only for non-superuser and non-replication + roles, and only if the role owner does not alter the target role to have a + privilege which the role owner itself lacks. Ordinary roles can only change + their own password. </para>Having contemplated this a bit more, I don't like it, and it's not how
things work when it comes to regular privileges.Consider that I can currently GRANT someone UPDATE privileges on an
object, but they can't GRANT that privilege to someone else unless I
explicitly allow it. The same could certainly be said for roles-
perhaps I want to allow someone the privilege to create non-login roles,
but I don't want them to be able to create new login roles, even if they
themselves have LOGIN.
This comment conflates privileges like LOGIN for which there isn't any "with grant option" logic with privileges that do. Granting someone UPDATE privileges on a relation will be tracked in an aclitem including whether the "with grant option" bit is set. Nothing like that will exist for LOGIN. I'm not dead-set against having that functionality for the privileges that currently lack it, but we'd have to do so in a way that doesn't gratuitously break backward compatibility, and how to do so has not been discussed.
As another point, I might want to have an 'admin' role that I want
admins to SET ROLE to before they go creating other roles, because I
don't want them to be creating roles as their regular user and so that
those other roles are owned by the 'admin' role, but I don't want that
role to have the 'login' attribute.
Same problem. We don't have aclitem bits for this.
In other words, we should really consider what role attributes a given
role has to be independent of what role attributes that role is allowed
to set on roles they create. I appreciate that "just whatever the
current role has" is simpler and less work but also will be difficult to
walk back from once it's in the wild.
I don't feel there is any fundamental disagreement here, except perhaps whether it needs to be done as part of this patch, vs. implemented in a future development cycle. We don't currently have any syntax for "CREATE ROLE bob LOGIN WITH GRANT OPTION". I can see some advantages in doing it all in one go, but also some advantage in being incremental. More discussion is needed here.
@@ -1457,7 +1449,7 @@ AddRoleMems(const char *rolename, Oid roleid,
/* - * Check permissions: must have createrole or admin option on the role to + * Check permissions: must be owner or have admin option on the role to * be changed. To mess with a superuser role, you gotta be superuser. */ if (superuser_arg(roleid))...
@@ -1467,9 +1459,9 @@ AddRoleMems(const char *rolename, Oid roleid, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to alter superusers"))); } - else + else if (!superuser()) { - if (!have_createrole_privilege() && + if (!pg_role_ownercheck(roleid, grantorId) && !is_admin_of_role(grantorId, roleid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),I'm not entirely sure about including owners here though I'm not
completely against it either. This conflation of what the 'admin'
privileges on a role means vs. the 'ownership' of a role is part of what
I dislike about having two distinct systems for saying who is allowed to
GRANT one role to another.Also, if we're going to always consider owners to be admins of roles
they own, why not push that into is_admin_of_role()?
Unchanged in this patch set, but worth further discussion and evaluation.
Subject: [PATCH v4 5/5] Remove grantor field from pg_auth_members
...<snip>...
If we're going to do this, it should also be done independently of the
role ownership stuff too.
I've withdrawn 0005 from this patch set, and we can come back to it separately.
Thanks,
Stephen
Thanks for the review! I hope we can keep pushing this forward. Again, no offense is intended in having not addressed all your concerns in the v7 patch set:
Attachments:
v7-0001-Add-owners-to-roles.patchapplication/octet-stream; name=v7-0001-Add-owners-to-roles.patch; x-unix-mode=0644Download
From 08b59a68ed3a333bfb92de20f6ea74ccbc42b652 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 25 Jan 2022 11:15:19 -0800
Subject: [PATCH v7 1/2] Add owners to roles
All roles now have owners. By default, created roles belong to the
role that created them, and initdb-time roles are owned by the
bootstrap superuser. Role ownership is transitive; if role A owns
role B, and role B owns role C, then role A can act as the owner of
role C. Roles may drop or alter roles that they own, and have all
privileges that any role they own has.
---
doc/src/sgml/ref/create_role.sgml | 23 ++-
src/backend/catalog/aclchk.c | 30 ++-
src/backend/catalog/objectaddress.c | 22 +--
src/backend/catalog/pg_shdepend.c | 5 +
src/backend/catalog/system_views.sql | 1 +
src/backend/commands/alter.c | 3 +
src/backend/commands/schemacmds.c | 10 +-
src/backend/commands/user.c | 180 +++++++++++++++++-
src/backend/nodes/copyfuncs.c | 1 +
src/backend/nodes/equalfuncs.c | 1 +
src/backend/parser/gram.y | 19 ++
src/backend/utils/adt/acl.c | 118 ++++++++++++
src/bin/psql/describe.c | 12 ++
src/include/catalog/pg_authid.h | 1 +
src/include/commands/user.h | 2 +
src/include/nodes/parsenodes.h | 1 +
src/include/utils/acl.h | 2 +
.../expected/dummy_seclabel.out | 12 +-
.../dummy_seclabel/sql/dummy_seclabel.sql | 12 +-
.../unsafe_tests/expected/rolenames.out | 6 +-
.../modules/unsafe_tests/sql/rolenames.sql | 3 +-
src/test/regress/expected/create_role.out | 177 ++++++++++++++---
src/test/regress/expected/oidjoins.out | 1 +
src/test/regress/expected/privileges.out | 9 +-
src/test/regress/expected/rules.out | 1 +
src/test/regress/sql/create_role.sql | 100 ++++++++--
src/test/regress/sql/privileges.sql | 10 +-
27 files changed, 657 insertions(+), 105 deletions(-)
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..1e9347b2ce 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -21,9 +21,16 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
-CREATE ROLE <replaceable class="parameter">name</replaceable> [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
+CREATE ROLE <replaceable class="parameter">name</replaceable> [ AUTHORIZATION <replaceable class="parameter">role_specification</replaceable> ] [ [ WITH ] <replaceable class="parameter">option</replaceable> [ ... ] ]
-<phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>
+<phrase>where <replaceable class="parameter">role_specification</replaceable> can be:</phrase>
+
+ <replaceable class="parameter">user_name</replaceable>
+ | CURRENT_ROLE
+ | CURRENT_USER
+ | SESSION_USER
+
+<phrase>and <replaceable class="parameter">option</replaceable> can be:</phrase>
SUPERUSER | NOSUPERUSER
| CREATEDB | NOCREATEDB
@@ -83,6 +90,18 @@ in sync when changing the above synopsis!
</listitem>
</varlistentry>
+ <varlistentry>
+ <term><replaceable class="parameter">user_name</replaceable></term>
+ <listitem>
+ <para>
+ The role name of the user who will own the new role. If omitted,
+ defaults to the user executing the command. To create a role
+ owned by another role, you must be a direct or indirect member of
+ that role, directly or indirectly own that role, or be a superuser.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry>
<term><literal>SUPERUSER</literal></term>
<term><literal>NOSUPERUSER</literal></term>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1dd03a8e51..1e0ee503e4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3385,6 +3385,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("permission denied for publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("permission denied for role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("permission denied for routine %s");
break;
@@ -3429,7 +3432,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
@@ -3511,6 +3513,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_PUBLICATION:
msg = gettext_noop("must be owner of publication %s");
break;
+ case OBJECT_ROLE:
+ msg = gettext_noop("must be owner of role %s");
+ break;
case OBJECT_ROUTINE:
msg = gettext_noop("must be owner of routine %s");
break;
@@ -3569,7 +3574,6 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
case OBJECT_DOMCONSTRAINT:
case OBJECT_PUBLICATION_NAMESPACE:
case OBJECT_PUBLICATION_REL:
- case OBJECT_ROLE:
case OBJECT_TRANSFORM:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
@@ -5430,16 +5434,22 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+/*
+ * Ownership check for a role (specified by OID)
+ */
+bool
+pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid)
+{
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(owner_roleid))
+ return true;
+
+ /* Otherwise, check the role ownership hierarchy */
+ return is_owner_of_role_nosuper(owner_roleid, owned_role_oid);
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
- *
- * Note: roles do not have owners per se; instead we use this test in
- * places where an ownership-like permissions test is needed for a role.
- * Be sure to apply it to the role trying to do the operation, not the
- * role being operated on! Also note that this generally should not be
- * considered enough privilege if the target role is a superuser.
- * (We don't handle that consideration here because we want to give a
- * separate error message for such cases, so the caller has to deal with it.)
*/
bool
has_createrole_privilege(Oid roleid)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index f30c742d48..1a47f28829 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2596,25 +2596,9 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
NameListToString(castNode(List, object)));
break;
case OBJECT_ROLE:
-
- /*
- * We treat roles as being "owned" by those with CREATEROLE priv,
- * except that superusers are only owned by superusers.
- */
- if (superuser_arg(address.objectId))
- {
- if (!superuser_arg(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser")));
- }
- else
- {
- if (!has_createrole_privilege(roleid))
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must have CREATEROLE privilege")));
- }
+ if (!pg_role_ownercheck(address.objectId, roleid))
+ aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+ strVal(object));
break;
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..52f69ad76e 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -61,6 +61,7 @@
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
#include "commands/typecmds.h"
+#include "commands/user.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
@@ -1578,6 +1579,10 @@ shdepReassignOwned(List *roleids, Oid newrole)
AlterSubscriptionOwner_oid(sdepForm->objid, newrole);
break;
+ case AuthIdRelationId:
+ AlterRoleOwner_oid(sdepForm->objid, newrole);
+ break;
+
/* Generic alter owner cases */
case CollationRelationId:
case ConversionRelationId:
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87..057d17460b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -17,6 +17,7 @@
CREATE VIEW pg_roles AS
SELECT
rolname,
+ pg_get_userbyid(rolowner) AS rolowner,
rolsuper,
rolinherit,
rolcreaterole,
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 1f64c8aa51..e6c7c84c87 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -840,6 +840,9 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
case OBJECT_DATABASE:
return AlterDatabaseOwner(strVal(stmt->object), newowner);
+ case OBJECT_ROLE:
+ return AlterRoleOwner(strVal(stmt->object), newowner);
+
case OBJECT_SCHEMA:
return AlterSchemaOwner(strVal(stmt->object), newowner);
diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c
index 984000a5bc..320e8378c4 100644
--- a/src/backend/commands/schemacmds.c
+++ b/src/backend/commands/schemacmds.c
@@ -363,11 +363,11 @@ AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
/*
* must have create-schema rights
*
- * NOTE: This is different from other alter-owner checks in that the
- * current user is checked for create privileges instead of the
- * destination owner. This is consistent with the CREATE case for
- * schemas. Because superusers will always have this right, we need
- * no special case for them.
+ * NOTE: alter-schema and alter-role are different from other
+ * alter-owner checks in that the current user is checked for create
+ * privileges instead of the destination owner. Alter-schema is
+ * consistent with the CREATE case for schemas. Because superusers
+ * will always have this right, we need no special case for them.
*/
aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(),
ACL_CREATE);
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246b..3726e40f36 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -55,6 +55,8 @@ static void AddRoleMems(const char *rolename, Oid roleid,
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
+static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
+ Oid newOwnerId);
/* Check if current user has createrole privileges */
@@ -77,6 +79,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
Datum new_record[Natts_pg_authid];
bool new_record_nulls[Natts_pg_authid];
Oid roleid;
+ Oid owner_uid;
+ Oid saved_uid;
+ int save_sec_context;
ListCell *item;
ListCell *option;
char *password = NULL; /* user password */
@@ -108,6 +113,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ GetUserIdAndSecContext(&saved_uid, &save_sec_context);
+
+ /*
+ * Who is supposed to own the new role?
+ */
+ if (stmt->authrole)
+ {
+ owner_uid = get_rolespec_oid(stmt->authrole, false);
+ check_is_member_of_role(saved_uid, owner_uid);
+ }
+ else
+ owner_uid = saved_uid;
+
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
{
@@ -254,6 +272,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
+ if (!superuser_arg(owner_uid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must be superuser to own superusers")));
}
else if (isreplication)
{
@@ -310,6 +332,19 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
errmsg("role \"%s\" already exists",
stmt->role)));
+ /*
+ * If the requested authorization is different from the current user,
+ * temporarily set the current user so that the object(s) will be created
+ * with the correct ownership.
+ *
+ * (The setting will be restored at the end of this routine, or in case of
+ * error, transaction abort will clean things up.)
+ */
+ if (saved_uid != owner_uid)
+ SetUserIdAndSecContext(owner_uid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+
/* Convert validuntil to internal form */
if (validUntil)
{
@@ -345,6 +380,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
+ new_record[Anum_pg_authid_rolowner - 1] = ObjectIdGetDatum(owner_uid);
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
@@ -422,6 +458,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
CatalogTupleInsert(pg_authid_rel, tuple);
+ recordDependencyOnOwner(AuthIdRelationId, roleid, owner_uid);
+
/*
* Advance command counter so we can see new record; else tests in
* AddRoleMems may fail.
@@ -478,6 +516,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
table_close(pg_authid_rel, NoLock);
+ /* Reset current user and security context */
+ SetUserIdAndSecContext(saved_uid, save_sec_context);
+
return roleid;
}
@@ -655,7 +696,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
{
/* check the rest */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
- drolemembers || dvalidUntil || !dpassword || roleid != GetUserId())
+ drolemembers || dvalidUntil || !dpassword ||
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -860,7 +902,8 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() && roleid != GetUserId())
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -912,11 +955,6 @@ DropRole(DropRoleStmt *stmt)
pg_auth_members_rel;
ListCell *item;
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to drop role")));
-
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
* deleted.
@@ -988,6 +1026,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
+ if (!have_createrole_privilege() &&
+ !pg_role_ownercheck(roleid, GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to drop role")));
+
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
@@ -1051,8 +1095,9 @@ DropRole(DropRoleStmt *stmt)
systable_endscan(sscan);
/*
- * Remove any comments or security labels on this role.
+ * Remove any dependencies, comments or security labels on this role.
*/
+ deleteSharedDependencyRecordsFor(AuthIdRelationId, roleid, 0);
DeleteSharedComments(roleid, AuthIdRelationId);
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
@@ -1648,3 +1693,122 @@ DelRoleMems(const char *rolename, Oid roleid,
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Change role owner
+ */
+ObjectAddress
+AlterRoleOwner(const char *name, Oid newOwnerId)
+{
+ Oid roleid;
+ HeapTuple tup;
+ Relation rel;
+ ObjectAddress address;
+ Form_pg_authid authform;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHNAME, CStringGetDatum(name));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role \"%s\" does not exist", name)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tup);
+ roleid = authform->oid;
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ObjectAddressSet(address, AuthIdRelationId, roleid);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+
+ return address;
+}
+
+void
+AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId)
+{
+ HeapTuple tup;
+ Relation rel;
+
+ rel = table_open(AuthIdRelationId, RowExclusiveLock);
+
+ tup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for role %u", roleOid);
+
+ AlterRoleOwner_internal(tup, rel, newOwnerId);
+
+ ReleaseSysCache(tup);
+
+ table_close(rel, RowExclusiveLock);
+}
+
+static void
+AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
+{
+ Form_pg_authid authForm;
+
+ Assert(tup->t_tableOid == AuthIdRelationId);
+ Assert(RelationGetRelid(rel) == AuthIdRelationId);
+
+ authForm = (Form_pg_authid) GETSTRUCT(tup);
+
+ /*
+ * If the new owner is the same as the existing owner, consider the
+ * command to have succeeded. This is for dump restoration purposes.
+ */
+ if (authForm->rolowner != newOwnerId)
+ {
+ /* Otherwise, must be owner of the existing object */
+ if (!pg_role_ownercheck(authForm->oid, GetUserId()))
+ aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Must be able to become new owner */
+ check_is_member_of_role(GetUserId(), newOwnerId);
+
+ /*
+ * must have CREATEROLE rights
+ *
+ * NOTE: Alter-role and alter-schema are different from other
+ * alter-owner checks in that the current user is checked for create
+ * privileges instead of the destination owner. Alter-role is
+ * consistent with the CREATE case for roles. Because superusers will
+ * always have this right, we need no special case for them.
+ */
+ if (!have_createrole_privilege())
+ aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
+ NameStr(authForm->rolname));
+
+ /* Only the bootstrap superuser is allowed to own itself. */
+ if (newOwnerId != BOOTSTRAP_SUPERUSERID && authForm->oid == newOwnerId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role may not own itself")));
+
+ /*
+ * Must not create cycles in the role ownership hierarchy. If this
+ * role owns (directly or indirectly) the proposed new owner, disallow
+ * the ownership transfer.
+ */
+ if (is_owner_of_role_nosuper(authForm->oid, newOwnerId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("role \"%s\" may not both own and be owned by role \"%s\"",
+ NameStr(authForm->rolname),
+ GetUserNameFromId(newOwnerId, false))));
+
+ authForm->rolowner = newOwnerId;
+ CatalogTupleUpdate(rel, &tup->t_self, tup);
+
+ /* Update owner dependency reference */
+ changeDependencyOnOwner(AuthIdRelationId, authForm->oid, newOwnerId);
+ }
+
+ InvokeObjectPostAlterHook(AuthIdRelationId,
+ authForm->oid, 0);
+}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 90b5da51c9..f43c92a70f 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -4522,6 +4522,7 @@ _copyCreateRoleStmt(const CreateRoleStmt *from)
COPY_SCALAR_FIELD(stmt_type);
COPY_STRING_FIELD(role);
+ COPY_NODE_FIELD(authrole);
COPY_NODE_FIELD(options);
return newnode;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 06345da3ba..328f155208 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2128,6 +2128,7 @@ _equalCreateRoleStmt(const CreateRoleStmt *a, const CreateRoleStmt *b)
{
COMPARE_SCALAR_FIELD(stmt_type);
COMPARE_STRING_FIELD(role);
+ COMPARE_NODE_FIELD(authrole);
COMPARE_NODE_FIELD(options);
return true;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b5966712ce..024d96470f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -1077,9 +1077,20 @@ CreateRoleStmt:
CreateRoleStmt *n = makeNode(CreateRoleStmt);
n->stmt_type = ROLESTMT_ROLE;
n->role = $3;
+ n->authrole = NULL;
n->options = $5;
$$ = (Node *)n;
}
+ |
+ CREATE ROLE RoleId AUTHORIZATION RoleSpec opt_with OptRoleList
+ {
+ CreateRoleStmt *n = makeNode(CreateRoleStmt);
+ n->stmt_type = ROLESTMT_ROLE;
+ n->role = $3;
+ n->authrole = $5;
+ n->options = $7;
+ $$ = (Node *)n;
+ }
;
@@ -9585,6 +9596,14 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
n->newowner = $6;
$$ = (Node *)n;
}
+ | ALTER ROLE name OWNER TO RoleSpec
+ {
+ AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+ n->objectType = OBJECT_ROLE;
+ n->object = (Node *) makeString($3);
+ n->newowner = $6;
+ $$ = (Node *)n;
+ }
| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
{
AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156c..97336db058 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4832,6 +4832,111 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
}
+/*
+ * Get a list of roles which own the given role, directly or indirectly.
+ *
+ * Each role has only one direct owner. The returned list contains the given
+ * role's owner, that role's owner, etc., up to the top of the ownership
+ * hierarchy, which is always the bootstrap superuser.
+ *
+ * Raises an error if any role ownership invariant is violated. Returns NIL if
+ * the given roleid is invalid.
+ */
+static List *
+roles_is_owned_by(Oid roleid)
+{
+ List *owners_list = NIL;
+ Oid role_oid = roleid;
+
+ /*
+ * Start with the current role and follow the ownership chain upwards until
+ * we reach the bootstrap superuser. To defend against getting into an
+ * infinite loop, we must check for ownership cycles. We choose to perform
+ * other corruption checks on the ownership structure while iterating, too.
+ */
+ while (OidIsValid(role_oid))
+ {
+ HeapTuple tuple;
+ Form_pg_authid authform;
+ Oid owner_oid;
+
+ /* Find the owner of the current iteration's role */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(role_oid));
+ if (!HeapTupleIsValid(tuple))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("role with OID %u does not exist", role_oid)));
+
+ authform = (Form_pg_authid) GETSTRUCT(tuple);
+ owner_oid = authform->rolowner;
+
+ /*
+ * Roles must necessarily have owners. Even the bootstrap user has an
+ * owner. (It owns itself).
+ */
+ if (!OidIsValid(owner_oid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u has invalid owner",
+ NameStr(authform->rolname), authform->oid)));
+
+ /* The bootstrap user must own itself */
+ if (authform->oid == BOOTSTRAP_SUPERUSERID &&
+ owner_oid != BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owned by role with OID %u",
+ NameStr(authform->rolname), authform->oid,
+ authform->rolowner)));
+
+ /*
+ * Roles other than the bootstrap user must not be their own direct
+ * owners.
+ */
+ if (authform->oid != BOOTSTRAP_SUPERUSERID &&
+ authform->oid == owner_oid)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u owns itself",
+ NameStr(authform->rolname), authform->oid)));
+
+ ReleaseSysCache(tuple);
+
+ /* If we have reached the bootstrap user, we're done. */
+ if (role_oid == BOOTSTRAP_SUPERUSERID)
+ {
+ if (!owners_list)
+ owners_list = lappend_oid(owners_list, owner_oid);
+ break;
+ }
+
+ /*
+ * For all other users, check they do not own themselves indirectly
+ * through an ownership cycle.
+ *
+ * Scanning the list each time through this loop results in overall
+ * quadratic work in the depth of the ownership chain, but we're
+ * not on a critical performance path, nor do we expect ownership
+ * hierarchies to be deep.
+ */
+ if (owners_list && list_member_oid(owners_list,
+ ObjectIdGetDatum(owner_oid)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("role \"%s\" with OID %u indirectly owns itself",
+ GetUserNameFromId(owner_oid, false),
+ owner_oid)));
+
+ /* Done with sanity checks. Add this owner to the list. */
+ owners_list = lappend_oid(owners_list, owner_oid);
+
+ /* Otherwise, iterate on this iteration's owner_oid. */
+ role_oid = owner_oid;
+ }
+
+ return owners_list;
+}
+
/*
* Does member have the privileges of role (directly or indirectly)?
*
@@ -4850,6 +4955,10 @@ has_privs_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /* Owners of roles have every privilege the owned role has */
+ if (pg_role_ownercheck(role, member))
+ return true;
+
/*
* Find all the roles that member has the privileges of, including
* multi-level recursion, then see if target role is any one of them.
@@ -4921,6 +5030,15 @@ is_member_of_role_nosuper(Oid member, Oid role)
role);
}
+/*
+ * Is owner a direct or indirect owner of the role, not considering
+ * superuserness?
+ */
+bool
+is_owner_of_role_nosuper(Oid owner, Oid role)
+{
+ return list_member_oid(roles_is_owned_by(role), owner);
+}
/*
* Is member an admin of role? That is, is member the role itself (subject to
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 346cd92793..406138609d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3518,6 +3518,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
appendPQExpBufferStr(&buf, "\n, r.rolbypassrls");
}
+ if (pset.sversion >= 150000)
+ {
+ appendPQExpBufferStr(&buf, "\n, r.rolowner");
+ ncols++;
+ }
+
appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n");
if (!showSystem && !pattern)
@@ -3538,6 +3544,8 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
printTableAddHeader(&cont, gettext_noop("Role name"), true, align);
+ if (pset.sversion >= 150000)
+ printTableAddHeader(&cont, gettext_noop("Owner"), true, align);
printTableAddHeader(&cont, gettext_noop("Attributes"), true, align);
/* ignores implicit memberships from superuser & pg_database_owner */
printTableAddHeader(&cont, gettext_noop("Member of"), true, align);
@@ -3549,6 +3557,10 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
{
printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false);
+ if (pset.sversion >= 150000)
+ printTableAddCell(&cont, PQgetvalue(res, i, (verbose ? 12 : 11)),
+ false, false);
+
resetPQExpBuffer(&buf);
if (strcmp(PQgetvalue(res, i, 1), "t") == 0)
add_role_attribute(&buf, _("Superuser"));
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f..3af0f3908c 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -32,6 +32,7 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
{
Oid oid; /* oid */
NameData rolname; /* name of role */
+ Oid rolowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); /* owner of this role */
bool rolsuper; /* read this field via superuser() only! */
bool rolinherit; /* inherit privileges from other roles? */
bool rolcreaterole; /* allowed to create more roles? */
diff --git a/src/include/commands/user.h b/src/include/commands/user.h
index 0b7a3cd65f..c32127e41e 100644
--- a/src/include/commands/user.h
+++ b/src/include/commands/user.h
@@ -33,5 +33,7 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname);
extern void DropOwnedObjects(DropOwnedStmt *stmt);
extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt);
extern List *roleSpecsToIds(List *memberNames);
+extern ObjectAddress AlterRoleOwner(const char *name, Oid newOwnerId);
+extern void AlterRoleOwner_oid(Oid roleOid, Oid newOwnerId);
#endif /* USER_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3e9bdc781f..b9d124d38f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2623,6 +2623,7 @@ typedef struct CreateRoleStmt
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
+ RoleSpec *authrole; /* the owner of the created role */
List *options; /* List of DefElem nodes */
} CreateRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 1ce4c5556e..572cae0f27 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -209,6 +209,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern bool is_owner_of_role_nosuper(Oid owner, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
@@ -316,6 +317,7 @@ extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid);
extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
+extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
diff --git a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
index b2d898a7d1..93cf82b750 100644
--- a/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
+++ b/src/test/modules/dummy_seclabel/expected/dummy_seclabel.out
@@ -7,8 +7,11 @@ SET client_min_messages TO 'warning';
DROP ROLE IF EXISTS regress_dummy_seclabel_user1;
DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
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;
@@ -19,7 +22,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
@@ -29,6 +32,7 @@ ERROR: '...invalid label...' is not a valid security label
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
ERROR: security label provider "unknown_seclabel" is not loaded
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
ERROR: must be owner of table dummy_seclabel_tbl2
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
@@ -42,7 +46,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
ERROR: '...invalid label...' is not a valid security label
@@ -55,7 +59,7 @@ SECURITY LABEL ON ROLE regress_dummy_seclabel_user3 IS 'unclassified'; -- fail (
ERROR: role "regress_dummy_seclabel_user3" 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
+ERROR: must be owner of role regress_dummy_seclabel_user2
RESET SESSION AUTHORIZATION;
--
-- Test for various types of object
diff --git a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
index 8c347b6a68..bf575343cf 100644
--- a/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
+++ b/src/test/modules/dummy_seclabel/sql/dummy_seclabel.sql
@@ -11,8 +11,12 @@ DROP ROLE IF EXISTS regress_dummy_seclabel_user2;
RESET client_min_messages;
-CREATE USER regress_dummy_seclabel_user1 WITH CREATEROLE;
+CREATE USER regress_dummy_seclabel_user0 WITH CREATEROLE;
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
+CREATE USER regress_dummy_seclabel_user1;
CREATE USER regress_dummy_seclabel_user2;
+RESET SESSION AUTHORIZATION;
CREATE TABLE dummy_seclabel_tbl1 (a int, b text);
CREATE TABLE dummy_seclabel_tbl2 (x int, y text);
@@ -26,7 +30,7 @@ ALTER TABLE dummy_seclabel_tbl2 OWNER TO regress_dummy_seclabel_user2;
--
-- Test of SECURITY LABEL statement with a plugin
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- OK
SECURITY LABEL ON COLUMN dummy_seclabel_tbl1.a IS 'unclassified'; -- OK
@@ -34,6 +38,8 @@ SECURITY LABEL ON COLUMN dummy_seclabel_tbl1 IS 'unclassified'; -- fail
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS '...invalid label...'; -- fail
SECURITY LABEL FOR 'dummy' ON TABLE dummy_seclabel_tbl1 IS 'unclassified'; -- OK
SECURITY LABEL FOR 'unknown_seclabel' ON TABLE dummy_seclabel_tbl1 IS 'classified'; -- fail
+
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'unclassified'; -- fail (not owner)
SECURITY LABEL ON TABLE dummy_seclabel_tbl1 IS 'secret'; -- fail (not superuser)
SECURITY LABEL ON TABLE dummy_seclabel_tbl3 IS 'unclassified'; -- fail (not found)
@@ -45,7 +51,7 @@ SECURITY LABEL ON TABLE dummy_seclabel_tbl2 IS 'classified'; -- OK
--
-- Test for shared database object
--
-SET SESSION AUTHORIZATION regress_dummy_seclabel_user1;
+SET SESSION AUTHORIZATION regress_dummy_seclabel_user0;
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS 'classified'; -- OK
SECURITY LABEL ON ROLE regress_dummy_seclabel_user1 IS '...invalid label...'; -- fail
diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out
index eb608fdc2e..8b79a63b80 100644
--- a/src/test/modules/unsafe_tests/expected/rolenames.out
+++ b/src/test/modules/unsafe_tests/expected/rolenames.out
@@ -1086,6 +1086,10 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
\c
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
+ERROR: role "regress_testrol2" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_role_haspriv
+owner of role regress_role_nopriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql
index adac36536d..95a54ce70d 100644
--- a/src/test/modules/unsafe_tests/sql/rolenames.sql
+++ b/src/test/modules/unsafe_tests/sql/rolenames.sql
@@ -499,6 +499,7 @@ REVOKE pg_read_all_settings FROM regress_role_haspriv;
DROP SCHEMA test_roles_schema;
DROP OWNED BY regress_testrol0, "Public", "current_role", "current_user", regress_testrol1, regress_testrol2, regress_testrolx CASCADE;
-DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- fails with owner of role regress_role_haspriv
DROP ROLE "Public", "None", "current_role", "current_user", "session_user", "user";
DROP ROLE regress_role_haspriv, regress_role_nopriv;
+DROP ROLE regress_testrol0, regress_testrol1, regress_testrol2, regress_testrolx; -- ok now
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..4ad6fd4161 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -1,6 +1,8 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_bystander;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_admin;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
@@ -11,14 +13,108 @@ CREATE ROLE regress_nosuch_replication REPLICATION;
ERROR: must be superuser to create replication users
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
ERROR: must be superuser to create bypassrls users
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+ERROR: must be superuser to own superusers
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_superuser AUTHORIZATION regress_role_super SUPERUSER;
+-- fail, can only create roles belonging to other roles that we belong to
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_alice AUTHORIZATION regress_role_super;
+ERROR: must be member of role "regress_role_super"
+CREATE ROLE regress_nosuch_bob AUTHORIZATION regress_superuser;
+ERROR: must be member of role "regress_superuser"
+CREATE ROLE regress_nosuch_charlie AUTHORIZATION regress_role_bystander;
+ERROR: must be member of role "regress_role_bystander"
+-- ok, superuser can create users with these privileges for normal role
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+\du+ regress_superuser
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-------------------+--------------------+-------------------------+-----------+-------------
+ regress_superuser | regress_role_super | Superuser, Cannot login | {} |
+
+\du+ regress_replication_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-------------------------------+--------------------+---------------------------------------+-----------+-------------
+ regress_replication_bypassrls | regress_role_admin | Cannot login, Replication, Bypass RLS | {} |
+
+\du+ regress_replication
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+---------------------+--------------------+---------------------------+-----------+-------------
+ regress_replication | regress_role_admin | Cannot login, Replication | {} |
+
+\du+ regress_bypassrls
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-------------------+--------------------+--------------------------+-----------+-------------
+ regress_bypassrls | regress_role_admin | Cannot login, Bypass RLS | {} |
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_bypassrls OWNER TO regress_bypassrls;
+ERROR: role may not own itself
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
+\du+ regress_createdb
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+------------------+--------------------+-------------------------+-----------+-------------
+ regress_createdb | regress_role_admin | Create DB, Cannot login | {} |
+
+\du+ regress_createrole
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------+--------------------+---------------------------+-----------+-------------
+ regress_createrole | regress_role_admin | Create role, Cannot login | {} |
+
+\du+ regress_login
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+---------------+--------------------+------------+-----------+-------------
+ regress_login | regress_role_admin | | {} |
+
+\du+ regress_inherit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------+--------------------+--------------+-----------+-------------
+ regress_inherit | regress_role_admin | Cannot login | {} |
+
+\du+ regress_connection_limit
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+--------------------------+--------------------+---------------+-----------+-------------
+ regress_connection_limit | regress_role_admin | Cannot login +| {} |
+ | | 5 connections | |
+
+\du+ regress_encrypted_password
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+----------------------------+--------------------+--------------+-----------+-------------
+ regress_encrypted_password | regress_role_admin | Cannot login | {} |
+
+\du+ regress_password_null
+ List of roles
+ Role name | Owner | Attributes | Member of | Description
+-----------------------+--------------------+--------------------------------------+---------------------------------------+--------------------
+ regress_password_null | regress_role_admin | Create role, Create DB, Cannot login+| {regress_createdb,regress_createrole} | no login test role
+ | | 2 connections | |
+
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
NOTICE: SYSID can no longer be specified
@@ -58,21 +154,18 @@ CREATE TABLE tenant_table (i integer);
CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
-ERROR: must be owner of index tenant_idx
ALTER TABLE tenant_table ADD COLUMN t text;
-ERROR: must be owner of table tenant_table
DROP TABLE tenant_table;
-ERROR: must be owner of table tenant_table
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
-ERROR: must be owner of view tenant_view
+ERROR: must be member of role "regress_role_admin"
+-- ok
DROP VIEW tenant_view;
-ERROR: must be owner of view tenant_view
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
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
CREATE ROLE regress_read_all_data IN ROLE pg_read_all_data;
CREATE ROLE regress_write_all_data IN ROLE pg_write_all_data;
@@ -84,16 +177,24 @@ CREATE ROLE regress_read_server_files IN ROLE pg_read_server_files;
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, creation of these roles failed above so they do not now exist
+-- fail, cannot create ownership cycles
+RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ERROR: role "regress_createrole" may not both own and be owned by role "regress_tenant"
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+ERROR: role "regress_role_admin" may not both own and be owned by role "regress_tenant"
+-- ok, can take ownership from owned roles
+SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
+DROP ROLE regress_superuser;
+-- ok, non-superuser roles can drop non-superuser roles they own
SET SESSION AUTHORIZATION regress_role_admin;
-DROP ROLE regress_nosuch_superuser;
-ERROR: role "regress_nosuch_superuser" does not exist
-DROP ROLE regress_nosuch_replication_bypassrls;
-ERROR: role "regress_nosuch_replication_bypassrls" does not exist
-DROP ROLE regress_nosuch_replication;
-ERROR: role "regress_nosuch_replication" does not exist
-DROP ROLE regress_nosuch_bypassrls;
-ERROR: role "regress_nosuch_bypassrls" does not exist
+DROP ROLE regress_replication_bypassrls;
+DROP ROLE regress_replication;
+DROP ROLE regress_bypassrls;
DROP ROLE regress_nosuch_super;
ERROR: role "regress_nosuch_super" does not exist
DROP ROLE regress_nosuch_dbowner;
@@ -103,9 +204,23 @@ 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;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_rolecreator
+owner of role regress_tenant
+owner of role regress_read_all_data
+owner of role regress_write_all_data
+owner of role regress_monitor
+owner of role regress_read_all_settings
+owner of role regress_read_all_stats
+owner of role regress_stat_scan_tables
+owner of role regress_read_server_files
+owner of role regress_write_server_files
+owner of role regress_execute_server_program
+owner of role regress_signal_backend
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -115,6 +230,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -125,21 +241,20 @@ 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
DROP ROLE regress_role_admin;
ERROR: current user cannot be dropped
--- ok
+-- ok, no more owned roles remain
+DROP ROLE regress_createrole;
+-- fail, cannot drop role with remaining privileges
RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
DROP ROLE regress_role_admin;
+ERROR: role "regress_role_admin" cannot be dropped because some objects depend on it
+DETAIL: privileges for database regression
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_bystander;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..266a30a85b 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -194,6 +194,7 @@ NOTICE: checking pg_database {dattablespace} => pg_tablespace {oid}
NOTICE: checking pg_db_role_setting {setdatabase} => pg_database {oid}
NOTICE: checking pg_db_role_setting {setrole} => pg_authid {oid}
NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid}
+NOTICE: checking pg_authid {rolowner} => pg_authid {oid}
NOTICE: checking pg_auth_members {roleid} => pg_authid {oid}
NOTICE: checking pg_auth_members {member} => pg_authid {oid}
NOTICE: checking pg_auth_members {grantor} => pg_authid {oid}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..9ce619fd5f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -27,8 +27,10 @@ CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
ERROR: role "regress_priv_user5" already exists
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -2356,7 +2358,12 @@ DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
DROP USER regress_priv_user6;
+ERROR: role "regress_priv_user6" cannot be dropped because some objects depend on it
+DETAIL: owner of role regress_priv_user7
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
ERROR: role "regress_priv_user8" does not exist
-- permissions with LOCK TABLE
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index d652f7b5fb..b6e3c508ba 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1482,6 +1482,7 @@ pg_replication_slots| SELECT l.slot_name,
FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase)
LEFT JOIN pg_database d ON ((l.datoid = d.oid)));
pg_roles| SELECT pg_authid.rolname,
+ pg_get_userbyid(pg_authid.rolowner) AS rolowner,
pg_authid.rolsuper,
pg_authid.rolinherit,
pg_authid.rolcreaterole,
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..6266586b8b 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -1,6 +1,8 @@
-- ok, superuser can create users with any set of privileges
CREATE ROLE regress_role_super SUPERUSER;
+CREATE ROLE regress_role_bystander;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
+GRANT CREATE ON DATABASE regression TO regress_role_admin;
-- fail, only superusers can create users with these privileges
SET SESSION AUTHORIZATION regress_role_admin;
@@ -9,14 +11,53 @@ CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+-- fail, only superusers can own superusers
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
+
+-- ok, superuser can create superusers belonging to other superusers
+CREATE ROLE regress_superuser AUTHORIZATION regress_role_super SUPERUSER;
+
+-- fail, can only create roles belonging to other roles that we belong to
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_alice AUTHORIZATION regress_role_super;
+CREATE ROLE regress_nosuch_bob AUTHORIZATION regress_superuser;
+CREATE ROLE regress_nosuch_charlie AUTHORIZATION regress_role_bystander;
+
+-- ok, superuser can create users with these privileges for normal role
+RESET SESSION AUTHORIZATION;
+CREATE ROLE regress_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
+CREATE ROLE regress_replication AUTHORIZATION regress_role_admin REPLICATION;
+CREATE ROLE regress_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
+
+\du+ regress_superuser
+\du+ regress_replication_bypassrls
+\du+ regress_replication
+\du+ regress_bypassrls
+
+-- fail, roles are not allowed to own themselves
+ALTER ROLE regress_bypassrls OWNER TO regress_bypassrls;
+
-- ok, having CREATEROLE is enough to create users with these privileges
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
-CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
-CREATE ROLE regress_password_null PASSWORD NULL;
+CREATE ROLE regress_encrypted_password PASSWORD NULL;
+CREATE ROLE regress_password_null
+ CREATEDB CREATEROLE INHERIT CONNECTION LIMIT 2 ENCRYPTED PASSWORD 'foo'
+ IN ROLE regress_createdb, regress_createrole;
+COMMENT ON ROLE regress_password_null IS 'no login test role';
+
+\du+ regress_createdb
+\du+ regress_createrole
+\du+ regress_login
+\du+ regress_inherit
+\du+ regress_connection_limit
+\du+ regress_encrypted_password
+\du+ regress_password_null
-- ok, backwards compatible noise words should be ignored
CREATE ROLE regress_noiseword SYSID 12345;
@@ -63,15 +104,19 @@ CREATE INDEX tenant_idx ON tenant_table(i);
CREATE VIEW tenant_view AS SELECT * FROM pg_catalog.pg_class;
REVOKE ALL PRIVILEGES ON tenant_table FROM PUBLIC;
--- fail, these objects belonging to regress_tenant
+-- ok, owning role can manage owned role's objects
SET SESSION AUTHORIZATION regress_createrole;
DROP INDEX tenant_idx;
ALTER TABLE tenant_table ADD COLUMN t text;
DROP TABLE tenant_table;
+
+-- fail, not a member of target role
ALTER VIEW tenant_view OWNER TO regress_role_admin;
+
+-- ok
DROP VIEW tenant_view;
--- fail, cannot take ownership of these objects from regress_tenant
+-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
-- ok, having CREATEROLE is enough to create roles in privileged roles
@@ -86,21 +131,36 @@ 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, creation of these roles failed above so they do not now exist
+-- fail, cannot create ownership cycles
+RESET SESSION AUTHORIZATION;
+REASSIGN OWNED BY regress_role_admin TO regress_tenant;
+ALTER ROLE regress_role_admin OWNER TO regress_tenant;
+
+-- ok, can take ownership from owned roles
SET SESSION AUTHORIZATION regress_role_admin;
-DROP ROLE regress_nosuch_superuser;
-DROP ROLE regress_nosuch_replication_bypassrls;
-DROP ROLE regress_nosuch_replication;
-DROP ROLE regress_nosuch_bypassrls;
+ALTER ROLE regress_plainrole OWNER TO regress_role_admin;
+REASSIGN OWNED BY regress_plainrole TO regress_role_admin;
+
+-- ok, superuser roles can drop superuser roles they own
+SET SESSION AUTHORIZATION regress_role_super;
+DROP ROLE regress_superuser;
+
+-- ok, non-superuser roles can drop non-superuser roles they own
+SET SESSION AUTHORIZATION regress_role_admin;
+DROP ROLE regress_replication_bypassrls;
+DROP ROLE regress_replication;
+DROP ROLE regress_bypassrls;
DROP ROLE regress_nosuch_super;
DROP ROLE regress_nosuch_dbowner;
DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- ok, should be able to drop non-superuser roles we created
-DROP ROLE regress_createdb;
+-- fail, cannot drop roles that own other roles
DROP ROLE regress_createrole;
+
+-- ok, should be able to drop these non-superuser roles
+DROP ROLE regress_createdb;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -110,6 +170,7 @@ DROP ROLE regress_noiseword;
DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
+DROP ROLE regress_tenant;
DROP ROLE regress_read_all_data;
DROP ROLE regress_write_all_data;
DROP ROLE regress_monitor;
@@ -121,18 +182,19 @@ 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;
DROP ROLE regress_role_admin;
--- ok
+-- ok, no more owned roles remain
+DROP ROLE regress_createrole;
+
+-- fail, cannot drop role with remaining privileges
RESET SESSION AUTHORIZATION;
-DROP INDEX tenant_idx;
-DROP TABLE tenant_table;
-DROP VIEW tenant_view;
-DROP ROLE regress_tenant;
DROP ROLE regress_role_admin;
+
+-- ok, can drop role if we revoke privileges first
+REVOKE CREATE ON DATABASE regression FROM regress_role_admin;
+DROP ROLE regress_role_admin;
+DROP ROLE regress_role_bystander;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..dcf84f91fd 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -30,8 +30,10 @@ CREATE USER regress_priv_user3;
CREATE USER regress_priv_user4;
CREATE USER regress_priv_user5;
CREATE USER regress_priv_user5; -- duplicate
-CREATE USER regress_priv_user6;
+CREATE USER regress_priv_user6 CREATEROLE;
+SET SESSION AUTHORIZATION regress_priv_user6;
CREATE USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
@@ -1426,8 +1428,14 @@ DROP USER regress_priv_user2;
DROP USER regress_priv_user3;
DROP USER regress_priv_user4;
DROP USER regress_priv_user5;
+
DROP USER regress_priv_user6;
+
+SET SESSION AUTHORIZATION regress_priv_user6;
DROP USER regress_priv_user7;
+RESET SESSION AUTHORIZATION;
+
+DROP USER regress_priv_user6;
DROP USER regress_priv_user8; -- does not exist
--
2.21.1 (Apple Git-122.3)
v7-0002-Restrict-power-granted-via-CREATEROLE.patchapplication/octet-stream; name=v7-0002-Restrict-power-granted-via-CREATEROLE.patch; x-unix-mode=0644Download
From e6faa2a27cf01fd85c8d95101d89eb78416d4df6 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Tue, 25 Jan 2022 14:54:06 -0800
Subject: [PATCH v7 2/2] Restrict power granted via CREATEROLE.
The CREATEROLE attribute no longer has anything to do with the power
to alter roles or to grant or revoke role membership, but merely the
ability to create new roles, as its name suggests. The ability to
alter a role is based on role ownership; the ability to grant and
revoke role membership is based on having admin privilege on the
relevant role or alternatively on role ownership, as owners now
implicitly have admin privileges on roles they own.
A role must either be superuser or have the CREATEROLE attribute to
create roles. This is unchanged from the prior behavior. A new
principle is adopted, though, to make CREATEROLE less dangerous: a
role may not create new roles with privileges that the creating role
lacks. This new principle is intended to prevent privilege
escalation attacks stemming from giving CREATEROLE to a user. This
is not backwards compatible. The idea is to fix the CREATEROLE
privilege to not be pathway to gaining superuser, and no
non-breaking change to accomplish that is apparent.
SUPERUSER, REPLICATION, BYPASSRLS, CREATEDB, CREATEROLE and LOGIN
privilege can only be given to new roles by creators who have the
same privilege. In the case of the CREATEROLE privilege, this is
trivially true, as the creator must necessarily have it or they
couldn't be creating the role to begin with.
The INHERIT attribute is not considered a privilege, and since a
user who belongs to a role may SET ROLE to that role and do anything
that role can do, it isn't clear that treating it as a privilege
would stop any privilege escalation attacks.
The CONNECTION LIMIT and VALID UNTIL attributes are also not
considered privileges, but this design choice is debatable. One
could think of the ability to log in during a given window of time,
or up to a certain number of connections as a privilege, and
allowing such a restricted role to create a new role with unlimited
connections or no expiration as a privilege escalation which escapes
the intended restrictions. However, it is just as easy to think of
these limitations as being used to guard against badly written
client programs connecting too many times, or connecting at a time
of day that is not intended. Since it is unclear which design is
better, this commit is conservative and the handling of these
attributes is unchanged relative to prior behavior.
Since the grammar of the CREATE ROLE command allows specifying roles
into which the new role should be enrolled, and also lists of roles
which become members of the newly created role (as admin or not),
the CREATE ROLE command may now fail if the creating role has
insufficient privilege on the roles so listed. Such failures were
not possible before, since the CREATEROLE privilege was always
sufficient.
---
doc/src/sgml/ddl.sgml | 12 +--
doc/src/sgml/ref/alter_role.sgml | 20 ++--
doc/src/sgml/ref/comment.sgml | 8 +-
doc/src/sgml/ref/create_role.sgml | 26 +++--
doc/src/sgml/ref/drop_role.sgml | 3 +-
doc/src/sgml/ref/dropuser.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 4 +-
doc/src/sgml/user-manag.sgml | 44 +++++----
src/backend/catalog/aclchk.c | 111 ++++++++++++++++++++++
src/backend/commands/user.c | 60 +++++-------
src/backend/utils/adt/acl.c | 21 +---
src/include/utils/acl.h | 5 +
src/test/regress/expected/create_role.out | 73 ++++++--------
src/test/regress/sql/create_role.sql | 45 ++++-----
14 files changed, 264 insertions(+), 174 deletions(-)
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 7cf0f0da3b..85c133d00d 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -3132,9 +3132,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
doesn't preserve that DROP.
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>
Constrain ordinary users to user-private schemas. To implement this,
first issue <literal>REVOKE CREATE ON SCHEMA public FROM
@@ -3146,9 +3144,8 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
pattern in a database where untrusted users had already logged in,
consider auditing the public schema for objects named like objects in
schema <literal>pg_catalog</literal>. 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.
+ usage pattern unless an untrusted user is the database owner, in which
+ case no secure schema usage pattern exists.
</para>
<para>
If the database originated in an upgrade
@@ -3170,8 +3167,7 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC;
schema <link linkend="typeconv-func">will be unsafe or
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.
+ pattern, this is secure unless an untrusted user is the database owner.
</para>
</listitem>
diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index 5aa5648ae7..fc9bea8072 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -70,18 +70,18 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
<link linkend="sql-revoke"><command>REVOKE</command></link> for that.)
Attributes not mentioned in the command retain their previous settings.
Database superusers can change any of these settings for any role.
- 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.
- Ordinary roles can only change their own password.
+ Role owners can change any of these settings on roles they directly or
+ indirectly own except <literal>SUPERUSER</literal>,
+ <literal>REPLICATION</literal>, and <literal>BYPASSRLS</literal>; but only
+ for non-superuser and non-replication roles, and only if the role owner does
+ not alter the target role to have a privilege which the role owner itself
+ lacks. Ordinary roles can only change their own password.
</para>
<para>
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 can rename non-superuser roles they directly or indirectly own.
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
@@ -114,9 +114,9 @@ ALTER ROLE { <replaceable class="parameter">role_specification</replaceable> | A
</para>
<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.
+ Superusers can change anyone's session defaults. Roles may change
+ privilege for non-superuser roles they directly or indirectly own. 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 b12796095f..38c78c9971 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -97,12 +97,8 @@ COMMENT ON
<para>
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.
- 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.
+ Access methods don't have owners; you must be superuser to comment on an
+ access method. Of course, a superuser can comment on anything.
</para>
<para>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 1e9347b2ce..59bbd418f4 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -126,8 +126,10 @@ in sync when changing the above synopsis!
<literal>CREATEDB</literal> is specified, the role being
defined will be allowed to create new databases. Specifying
<literal>NOCREATEDB</literal> will deny a role the ability to
- create databases. If not specified,
- <literal>NOCREATEDB</literal> is the default.
+ create databases. Only roles with the <literal>CREATEDB</literal>
+ attribute may create roles with the <literal>CREATEDB</literal>
+ attribute. If not specified, <literal>NOCREATEDB</literal> is the
+ default.
</para>
</listitem>
</varlistentry>
@@ -139,8 +141,6 @@ in sync when changing the above synopsis!
<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.
</para>
@@ -182,6 +182,8 @@ in sync when changing the above synopsis!
<literal>NOLOGIN</literal> is the default, except when
<command>CREATE ROLE</command> is invoked through its alternative spelling
<link linkend="sql-createuser"><command>CREATE USER</command></link>.
+ You must have the <literal>LOGIN</literal> attribute to create a new role
+ with the <literal>LOGIN</literal> attribute.
</para>
</listitem>
</varlistentry>
@@ -213,8 +215,8 @@ in sync when changing the above synopsis!
<para>
These clauses determine whether a role bypasses every row-level
security (RLS) policy. <literal>NOBYPASSRLS</literal> is the default.
- You must be a superuser to create a new role having
- the <literal>BYPASSRLS</literal> attribute.
+ You must have the <literal>BYPASSRLS</literal> attribute to create a
+ new role having the <literal>BYPASSRLS</literal> attribute.
</para>
<para>
@@ -300,6 +302,10 @@ in sync when changing the above synopsis!
member. (Note that there is no option to add the new role as an
administrator; use a separate <command>GRANT</command> command to do that.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -320,6 +326,10 @@ in sync when changing the above synopsis!
roles which are automatically added as members of the new role.
(This in effect makes the new role a <quote>group</quote>.)
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
@@ -332,6 +342,10 @@ in sync when changing the above synopsis!
OPTION</literal>, giving them the right to grant membership in this role
to others.
</para>
+ <para>
+ If not a superuser, the creating role must either own or have admin
+ privilege on each listed role.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/drop_role.sgml b/doc/src/sgml/ref/drop_role.sgml
index 13dc1cc649..c3d57ee8db 100644
--- a/doc/src/sgml/ref/drop_role.sgml
+++ b/doc/src/sgml/ref/drop_role.sgml
@@ -31,8 +31,7 @@ DROP ROLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...
<para>
<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.
+ to drop non-superuser roles, you must own the target role.
</para>
<para>
diff --git a/doc/src/sgml/ref/dropuser.sgml b/doc/src/sgml/ref/dropuser.sgml
index 81580507e8..30a99eaf68 100644
--- a/doc/src/sgml/ref/dropuser.sgml
+++ b/doc/src/sgml/ref/dropuser.sgml
@@ -35,9 +35,9 @@ 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.)
+ A <productname>PostgreSQL</productname> user may only be removed by its
+ owner or by a superuser. (To remove a superuser, you must yourself be a
+ superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..86fc387af2 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -254,8 +254,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
OPTION</literal> on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ to anyone. Roles can revoke membership in any role they own, and
+ may grant membership in any role they own to any role they own.
</para>
<para>
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 9067be1d9c..e65b55a004 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -198,9 +198,10 @@ CREATE USER <replaceable>name</replaceable>;
(except for superusers, since those bypass all permission
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.
- However, to create, alter, drop, or change membership of a
+ A role which creates a new role becomes the new role's owner, able to
+ alter or drop that new role, and sharing ownership of any additional
+ objects (including additional roles) that new role creates.
+ To create, alter, drop, or change membership of a
superuser role, superuser status is required;
<literal>CREATEROLE</literal> is insufficient for that.
</para>
@@ -246,11 +247,14 @@ CREATE USER <replaceable>name</replaceable>;
<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
+ It is good practice to create a role that has the
+ <literal>CREATEDB</literal>, <literal>LOGIN</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.
+ approach avoids the dangers of operating as a superuser for tasks that do
+ not really require it. This role must also have
+ <literal>REPLICATION</literal> if it will create replication users, and
+ must have <literal>BYPASSRLS</literal> if it will create bypassrls users.
</para>
</tip>
@@ -387,15 +391,22 @@ RESET ROLE;
<para>
The role attributes <literal>LOGIN</literal>, <literal>SUPERUSER</literal>,
- <literal>CREATEDB</literal>, and <literal>CREATEROLE</literal> can be thought of as
- special privileges, but they are never inherited as ordinary privileges
- on database objects are. You must actually <command>SET ROLE</command> to a
- specific role having one of these attributes in order to make use of
- the attribute. Continuing the above example, we might choose to
+ <literal>CREATEDB</literal>, <literal>REPLICATION</literal>,
+ <literal>BYPASSRLS</literal>, and <literal>CREATEROLE</literal> can be
+ thought of as special privileges, but they are never inherited as ordinary
+ privileges on database objects are. You must actually <command>SET
+ ROLE</command> to a specific role having one of these attributes in order to
+ make use of the attribute. Continuing the above example, we might choose to
grant <literal>CREATEDB</literal> and <literal>CREATEROLE</literal> to the
- <literal>admin</literal> role. Then a session connecting as role <literal>joe</literal>
- would not have these privileges immediately, only after doing
- <command>SET ROLE admin</command>.
+ <literal>admin</literal> role. Then a session connecting as role
+ <literal>joe</literal> would not have these privileges immediately, only
+ after doing <command>SET ROLE admin</command>. Roles with these attributes
+ may only be created by roles which themselves have these attributes.
+ Superusers may always do so, but non-superuser roles with
+ <literal>CREATEROLE</literal> may only create new roles with
+ <literal>LOGIN</literal>, <literal>CREATEDB</literal>,
+ <literal>REPLICATION</literal>, or <literal>BYPASSRLS</literal> if they
+ themselves have the same attribute.
</para>
<para>
@@ -493,8 +504,7 @@ DROP ROLE doomed_role;
<para>
<productname>PostgreSQL</productname> provides a set of predefined roles
that provide access to certain, commonly needed, privileged capabilities
- and information. Administrators (including roles that have the
- <literal>CREATEROLE</literal> privilege) can <command>GRANT</command> these
+ and information. Administrators can <command>GRANT</command> these
roles to users and/or other roles in their environment, providing those
users with access to the specified capabilities and information.
</para>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1e0ee503e4..8327005404 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5470,6 +5470,9 @@ has_createrole_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has BYPASSRLS privilege (or is a superuser)
+ */
bool
has_bypassrls_privilege(Oid roleid)
{
@@ -5489,6 +5492,114 @@ has_bypassrls_privilege(Oid roleid)
return result;
}
+/*
+ * Check whether specified role has INHERIT privilege (or is a superuser)
+ */
+bool
+has_inherit_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has CREATEDB privilege (or is a superuser)
+ */
+bool
+has_createdb_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreatedb;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has LOGIN privilege (or is a superuser)
+ */
+bool
+has_login_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolcanlogin;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Check whether specified role has REPLICATION privilege (or is a superuser)
+ */
+bool
+has_replication_privilege(Oid roleid)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolreplication;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
+/*
+ * Get the connection limit for the specified role.
+ *
+ * Returns -1 if the role has no connection limit.
+ */
+int32
+role_connection_limit(Oid roleid)
+{
+ int32 result = -1;
+ HeapTuple utup;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ result = ((Form_pg_authid) GETSTRUCT(utup))->rolconnlimit;
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Fetch pg_default_acl entry for given role, namespace and object type
* (object type must be given in pg_default_acl's encoding).
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 3726e40f36..13265a0ed2 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -58,15 +58,6 @@ static void DelRoleMems(const char *rolename, Oid roleid,
static void AlterRoleOwner_internal(HeapTuple tup, Relation rel,
Oid newOwnerId);
-
-/* Check if current user has createrole privileges */
-static bool
-have_createrole_privilege(void)
-{
- return has_createrole_privilege(GetUserId());
-}
-
-
/*
* CREATE ROLE
*/
@@ -279,24 +270,32 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
}
else if (isreplication)
{
- if (!superuser())
+ if (!has_replication_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
+ errmsg("must have replication privilege to create replication users")));
}
else if (bypassrls)
{
- if (!superuser())
+ if (!has_bypassrls_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
+ errmsg("must have bypassrls privilege to create bypassrls users")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create role")));
+ if (createdb && !has_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have createdb privilege to create createdb users")));
+ if (canlogin && !has_login_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have login privilege to create login users")));
}
/*
@@ -692,7 +691,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to change bypassrls attribute")));
}
- else if (!have_createrole_privilege())
+ else if (!superuser())
{
/* check the rest */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
@@ -891,7 +890,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
/*
* To mess with a superuser you gotta be superuser; else you need
- * createrole, or just want to change your own settings
+ * to own the role, or just want to change your own settings
*/
if (roleform->rolsuper)
{
@@ -902,8 +901,7 @@ AlterRoleSet(AlterRoleSetStmt *stmt)
}
else
{
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
@@ -1016,18 +1014,12 @@ DropRole(DropRoleStmt *stmt)
(errcode(ERRCODE_OBJECT_IN_USE),
errmsg("session user cannot be dropped")));
- /*
- * 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.
- */
if (roleform->rolsuper && !superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to drop superusers")));
- if (!have_createrole_privilege() &&
- !pg_role_ownercheck(roleid, GetUserId()))
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
@@ -1208,7 +1200,7 @@ 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
+ * role ownership is enough privilege unless you want to mess with a superuser
*/
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
{
@@ -1219,7 +1211,7 @@ RenameRole(const char *oldname, const char *newname)
}
else
{
- if (!have_createrole_privilege())
+ if (!pg_role_ownercheck(roleid, GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to rename role")));
@@ -1434,7 +1426,7 @@ AddRoleMems(const char *rolename, Oid roleid,
return;
/*
- * Check permissions: must have createrole or admin option on the role to
+ * Check permissions: must be owner or have admin option on the role to
* be changed. To mess with a superuser role, you gotta be superuser.
*/
if (superuser_arg(roleid))
@@ -1444,9 +1436,9 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, grantorId) &&
!is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1622,9 +1614,9 @@ DelRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superusers")));
}
- else
+ else if (!superuser())
{
- if (!have_createrole_privilege() &&
+ if (!pg_role_ownercheck(roleid, GetUserId()) &&
!is_admin_of_role(GetUserId(), roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
@@ -1780,7 +1772,7 @@ AlterRoleOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerId)
* consistent with the CREATE case for roles. Because superusers will
* always have this right, we need no special case for them.
*/
- if (!have_createrole_privilege())
+ if (!has_createrole_privilege(GetUserId()))
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_ROLE,
NameStr(authForm->rolname));
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 97336db058..cdcce9c569 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4656,7 +4656,7 @@ initialize_acl(void)
/*
* In normal mode, set a callback on any syscache invalidation of rows
* of pg_auth_members (for roles_is_member_of()), pg_authid (for
- * has_rolinherit()), or pg_database (for roles_is_member_of())
+ * has_inherit_privilege()), or pg_database (for roles_is_member_of())
*/
CacheRegisterSyscacheCallback(AUTHMEMROLEMEM,
RoleMembershipCacheCallback,
@@ -4690,23 +4690,6 @@ RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
}
-/* Check if specified role has rolinherit set */
-static bool
-has_rolinherit(Oid roleid)
-{
- bool result = false;
- HeapTuple utup;
-
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolinherit;
- ReleaseSysCache(utup);
- }
- return result;
-}
-
-
/*
* Get a list of roles that the specified roleid is a member of
*
@@ -4776,7 +4759,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
CatCList *memlist;
int i;
- if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid))
+ if (type == ROLERECURSE_PRIVS && !has_inherit_privilege(memberid))
continue; /* ignore non-inheriting roles */
/* Find roles that memberid is directly a member of */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 572cae0f27..7a6640dfc9 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -320,5 +320,10 @@ extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool pg_role_ownercheck(Oid owned_role_oid, Oid owner_roleid);
extern bool has_createrole_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool has_inherit_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_login_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
+extern int32 role_connection_limit(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4ad6fd4161..61f143a191 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -3,16 +3,17 @@ CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_bystander;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
GRANT CREATE ON DATABASE regression TO regress_role_admin;
--- fail, only superusers can create users with these privileges
+-- fail, only superusers can create users with superuser
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
ERROR: must be superuser to create superusers
-CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
-CREATE ROLE regress_nosuch_replication REPLICATION;
-ERROR: must be superuser to create replication users
-CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
-ERROR: must be superuser to create bypassrls users
+-- fail, only login users can create other login users
+CREATE ROLE regress_nosuch_login LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, can assign privileges the creator has
+CREATE ROLE regress_replication_bypassrls REPLICATION BYPASSRLS;
+CREATE ROLE regress_replication REPLICATION;
+CREATE ROLE regress_bypassrls BYPASSRLS;
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
CREATE ROLE regress_nosuch_superuser AUTHORIZATION regress_role_admin SUPERUSER;
@@ -29,9 +30,6 @@ CREATE ROLE regress_nosuch_charlie AUTHORIZATION regress_role_bystander;
ERROR: must be member of role "regress_role_bystander"
-- ok, superuser can create users with these privileges for normal role
RESET SESSION AUTHORIZATION;
-CREATE ROLE regress_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
\du+ regress_superuser
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -63,7 +61,10 @@ ERROR: role may not own itself
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+ERROR: must have login privilege to create login users
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -83,12 +84,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
--------------------+--------------------+---------------------------+-----------+-------------
regress_createrole | regress_role_admin | Create role, Cannot login | {} |
-\du+ regress_login
- List of roles
- Role name | Owner | Attributes | Member of | Description
----------------+--------------------+------------+-----------+-------------
- regress_login | regress_role_admin | | {} |
-
\du+ regress_inherit
List of roles
Role name | Owner | Attributes | Member of | Description
@@ -121,19 +116,19 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-ERROR: role "pg_database_owner" cannot have explicit members
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
ERROR: role "regress_nosuch_recursive" is a member of role "regress_nosuch_recursive"
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
CREATE ROLE regress_nosuch_admin_recursive ADMIN regress_nosuch_admin_recursive;
@@ -146,8 +141,11 @@ ERROR: permission denied to create database
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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+ERROR: must have createdb privilege to create createdb users
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -166,17 +164,27 @@ ERROR: must be member of role "regress_role_admin"
DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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, cannot create ownership cycles
RESET SESSION AUTHORIZATION;
REASSIGN OWNED BY regress_role_admin TO regress_tenant;
@@ -209,19 +217,8 @@ DROP ROLE regress_createrole;
ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
DETAIL: owner of role regress_rolecreator
owner of role regress_tenant
-owner of role regress_read_all_data
-owner of role regress_write_all_data
-owner of role regress_monitor
-owner of role regress_read_all_settings
-owner of role regress_read_all_stats
-owner of role regress_stat_scan_tables
-owner of role regress_read_server_files
-owner of role regress_write_server_files
-owner of role regress_execute_server_program
-owner of role regress_signal_backend
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -231,16 +228,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, 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 6266586b8b..36b9b04322 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -4,12 +4,17 @@ CREATE ROLE regress_role_bystander;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
GRANT CREATE ON DATABASE regression TO regress_role_admin;
--- fail, only superusers can create users with these privileges
+-- fail, only superusers can create users with superuser
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
-CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
-CREATE ROLE regress_nosuch_replication REPLICATION;
-CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+
+-- fail, only login users can create other login users
+CREATE ROLE regress_nosuch_login LOGIN;
+
+-- ok, can assign privileges the creator has
+CREATE ROLE regress_replication_bypassrls REPLICATION BYPASSRLS;
+CREATE ROLE regress_replication REPLICATION;
+CREATE ROLE regress_bypassrls BYPASSRLS;
-- fail, only superusers can own superusers
RESET SESSION AUTHORIZATION;
@@ -26,9 +31,6 @@ CREATE ROLE regress_nosuch_charlie AUTHORIZATION regress_role_bystander;
-- ok, superuser can create users with these privileges for normal role
RESET SESSION AUTHORIZATION;
-CREATE ROLE regress_replication_bypassrls AUTHORIZATION regress_role_admin REPLICATION BYPASSRLS;
-CREATE ROLE regress_replication AUTHORIZATION regress_role_admin REPLICATION;
-CREATE ROLE regress_bypassrls AUTHORIZATION regress_role_admin BYPASSRLS;
\du+ regress_superuser
\du+ regress_replication_bypassrls
@@ -42,7 +44,11 @@ ALTER ROLE regress_bypassrls OWNER TO regress_bypassrls;
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
+
+-- fail, cannot assign LOGIN privilege that creator lacks
CREATE ROLE regress_login LOGIN;
+
+-- ok, having CREATEROLE is enough for these
CREATE ROLE regress_inherit INHERIT;
CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
CREATE ROLE regress_encrypted_password PASSWORD NULL;
@@ -53,7 +59,6 @@ COMMENT ON ROLE regress_password_null IS 'no login test role';
\du+ regress_createdb
\du+ regress_createrole
-\du+ regress_login
\du+ regress_inherit
\du+ regress_connection_limit
\du+ regress_encrypted_password
@@ -65,12 +70,12 @@ CREATE ROLE regress_noiseword SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
--- fail, database owner cannot have members
+-- fail, do not have ADMIN privilege on database owner
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself
@@ -78,7 +83,7 @@ CREATE ROLE regress_nosuch_recursive ROLE regress_nosuch_recursive;
-- ok, can grant other users into a role with admin option
CREATE ROLE regress_adminroles ADMIN
- regress_role_super, regress_createdb, regress_createrole, regress_login,
+ regress_role_super, regress_createdb, regress_createrole,
regress_inherit, regress_connection_limit, regress_encrypted_password, regress_password_null;
-- fail, cannot grant a role into itself with admin option
@@ -94,9 +99,12 @@ 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
+-- fail, roles with CREATEROLE cannot create new roles with privilege they lack
CREATE ROLE regress_tenant CREATEDB CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5;
+-- ok, roles with CREATEROLE can create new roles with privilege they have
+CREATE ROLE regress_tenant CREATEROLE INHERIT CONNECTION LIMIT 5;
+
-- ok, regress_tenant can create objects within the database
SET SESSION AUTHORIZATION regress_tenant;
CREATE TABLE tenant_table (i integer);
@@ -119,7 +127,7 @@ DROP VIEW tenant_view;
-- ok, can take ownership objects from owned roles
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having 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;
@@ -161,7 +169,6 @@ DROP ROLE regress_createrole;
-- ok, should be able to drop these non-superuser roles
DROP ROLE regress_createdb;
-DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
DROP ROLE regress_encrypted_password;
@@ -171,16 +178,6 @@ DROP ROLE regress_inroles;
DROP ROLE regress_adminroles;
DROP ROLE regress_rolecreator;
DROP ROLE regress_tenant;
-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, cannot drop ourself nor superusers
DROP ROLE regress_role_super;
--
2.21.1 (Apple Git-122.3)
On Jan 25, 2022, at 12:44 PM, Stephen Frost <sfrost@snowman.net> wrote:
I agree that CREATEROLE is overpowered and that the goal of this should
be to provide a way for roles to be created and dropped that doesn't
give the user who has that power everything that CREATEROLE currently
does.
I'm attaching a patch that attempts to fix CREATEROLE without any connection to role ownership.
The point I was making is that the concept of role ownership
isn't intrinsically linked to that and is, therefore, as you say, gravy.
I agree, they aren't intrinsically linked, though the solution to one might interact in some ways with the solution to the other.
That isn't to say that I'm entirely against the role ownership idea but
I'd want it to be focused on the goal of providing ways of creating and
dropping users and otherwise performing that kind of administration and
that doesn't require the specific change to make owners be members of
all roles they own and automatically have all privileges of those roles
all the time.
The attached WIP patch attempts to solve most of the CREATEROLE problems but not the problem of which role who can drop which other role. That will likely require an ownership concept.
The main idea here is that having CREATEROLE doesn't give you ADMIN on roles, nor on role attributes. For role attributes, the syntax has been extended. An excerpt from the patch's regression test illustrates some of that concept:
-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away
-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;
-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"
-- fail, cannot change attributes without ADMIN for them
SET SESSION AUTHORIZATION regress_role_minoradmin;
ALTER ROLE regress_role_login LOGIN;
ERROR: must have admin on login to change login attribute
ALTER ROLE regress_role_login NOLOGIN;
ERROR: must have admin on login to change login attribute
Whether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied hinges on whether the role is given CREATEROLE. That hackery is necessary to preserve backwards compatibility. If we don't care about compatibility, I could change the patch to make "WITHOUT ADMIN OPTION" implied for all attributes when not specified.
I'd appreciate feedback on the direction this patch is going.
Attachments:
v8-0001-Adding-admin-options-for-role-attributes.patch.WIPapplication/octet-stream; name=v8-0001-Adding-admin-options-for-role-attributes.patch.WIP; x-unix-mode=0644Download
From 82d235b39b32ca0cd0b94d47a54ee6806645a365 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Fri, 28 Jan 2022 07:57:57 -0800
Subject: [PATCH v8] Adding admin options for role attributes
When creating roles, attributes such as BYPASSRLS can be optionally
specified WITH ADMIN OPTION or WITHOUT ADMIN OPTION. If these
optional clauses are unspecified, they all default to WITHOUT
unless the role being created is given CREATEROLE, in which case
they default to WITHOUT for SUPERUSER, REPLICATION, and BYPASSRLS
and true for all others. This preserves backwards compatible
behavior.
The CREATEROLE attribute no longer makes up for lacking the ADMIN
option on a role. The creator of a role only has the ADMIN-like
right to grant other roles into the new role during the creation
statement itself. After that, the creator may only do so if the
creator has ADMIN on the role. Note that creators may add
themselves to the list of ADMINs on the new role during creation
time.
SUPERUSER can still only be granted by superusers.
---
doc/src/sgml/ref/create_role.sgml | 50 ++--
src/backend/catalog/aclchk.c | 179 ++++++++++++--
src/backend/commands/user.c | 278 +++++++++++++++++-----
src/backend/parser/gram.y | 161 ++++++++++---
src/include/catalog/pg_authid.dat | 52 +++-
src/include/catalog/pg_authid.h | 10 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 11 +-
src/include/utils/acl.h | 12 +
src/test/regress/expected/create_role.out | 188 ++++++++++++++-
src/test/regress/expected/privileges.out | 4 +
src/test/regress/sql/create_role.sql | 153 +++++++++++-
src/test/regress/sql/privileges.sql | 3 +
src/tools/pgindent/typedefs.list | 1 +
14 files changed, 936 insertions(+), 167 deletions(-)
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index b6a4ea1f72..7163779e0a 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -26,15 +26,22 @@ CREATE ROLE <replaceable class="parameter">name</replaceable> [ [ WITH ] <replac
<phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>
SUPERUSER | NOSUPERUSER
- | CREATEDB | NOCREATEDB
- | CREATEROLE | NOCREATEROLE
- | INHERIT | NOINHERIT
- | LOGIN | NOLOGIN
- | REPLICATION | NOREPLICATION
- | BYPASSRLS | NOBYPASSRLS
- | CONNECTION LIMIT <replaceable class="parameter">connlimit</replaceable>
- | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' | PASSWORD NULL
- | VALID UNTIL '<replaceable class="parameter">timestamp</replaceable>'
+ | INHERIT [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOINHERIT [ { WITH | WITHOUT } GRANT OPTION ]
+ | CREATEDB [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOCREATEDB [ { WITH | WITHOUT } GRANT OPTION ]
+ | CREATEROLE [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOCREATEROLE [ { WITH | WITHOUT } GRANT OPTION ]
+ | LOGIN [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOLOGIN [ { WITH | WITHOUT } GRANT OPTION ]
+ | REPLICATION [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOREPLICATION [ { WITH | WITHOUT } GRANT OPTION ]
+ | BYPASSRLS [ { WITH | WITHOUT } GRANT OPTION ]
+ | NOBYPASSRLS [ { WITH | WITHOUT } GRANT OPTION ]
+ | CONNECTION LIMIT [ <replaceable class="parameter">connlimit</replaceable> | NONE ] [ { WITH | WITHOUT } GRANT OPTION ]
+ | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' [ { WITH | WITHOUT } GRANT OPTION ]
+ | PASSWORD NULL [ { WITH | WITHOUT } GRANT OPTION ]
+ | VALID [ UNTIL '<replaceable class="parameter">timestamp</replaceable>' | ALWAYS ] [ { WITH | WITHOUT } GRANT OPTION ]
| IN ROLE <replaceable class="parameter">role_name</replaceable> [, ...]
| IN GROUP <replaceable class="parameter">role_name</replaceable> [, ...]
| ROLE <replaceable class="parameter">role_name</replaceable> [, ...]
@@ -356,6 +363,18 @@ in sync when changing the above synopsis!
<link linkend="sql-revoke"><command>REVOKE</command></link>.
</para>
+ <para>
+ Some parameters allow the <literal>WITH ADMIN OPTION</literal> or
+ <literal>WITHOUT ADMIN OPTION</literal> clause to be specified. For roles
+ with the <literal>CREATEROLE</literal> attribute, these clauses govern
+ whether new roles may be created with the attribute. If not given, for
+ reasons of backwards compatibility, <literal>WITHOUT ADMIN OPTION</literal>
+ is the default for <literal>REPLICATION</literal> and
+ <literal>BYPASSRLS</literal>, but <literal>WITH ADMIN OPTION</literal> is
+ the default for <literal>CREATEDB</literal>, <literal>CREATEROLE</literal>,
+ and <literal>LOGIN</literal>.
+ </para>
+
<para>
The <literal>VALID UNTIL</literal> clause defines an expiration time for a
password only, not for the role per se. In
@@ -383,19 +402,6 @@ in sync when changing the above synopsis!
specified in the SQL standard.
</para>
- <para>
- Be careful with the <literal>CREATEROLE</literal> privilege. There is no concept of
- inheritance for the privileges of a <literal>CREATEROLE</literal>-role. That
- means that even if a role does not have a certain privilege but is allowed
- to create other roles, it can easily create another role with different
- privileges than its own (except for creating roles with superuser
- privileges). For example, if the role <quote>user</quote> has the
- <literal>CREATEROLE</literal> privilege but not the <literal>CREATEDB</literal> privilege,
- nonetheless it can create a new role with the <literal>CREATEDB</literal>
- privilege. Therefore, regard roles that have the <literal>CREATEROLE</literal>
- privilege as almost-superuser-roles.
- </para>
-
<para>
<productname>PostgreSQL</productname> includes a program <xref
linkend="app-createuser"/> that has
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1dd03a8e51..c66f545f36 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -5430,6 +5430,91 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
return has_privs_of_role(roleid, ownerId);
}
+typedef enum ROLPRIV
+{
+ CREATEROLE,
+ CREATEDB,
+ CANLOGIN,
+ REPLICATION,
+ BYPASSRLS,
+ INHERIT,
+ CONNLIMIT,
+ VALIDUNTIL,
+ PASSWORD
+} ROLPRIV;
+
+static bool
+has_privilege(Oid roleid, ROLPRIV priv, bool self, bool grant)
+{
+ bool result = false;
+ HeapTuple utup;
+
+ /* Superusers bypass all permission checking. */
+ if (superuser_arg(roleid))
+ return true;
+
+ utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (HeapTupleIsValid(utup))
+ {
+ Form_pg_authid uform = (Form_pg_authid) GETSTRUCT(utup);
+
+ result = true;
+ switch (priv)
+ {
+ case CREATEROLE:
+ if (self && !uform->rolcreaterole)
+ result = false;
+ if (grant && !uform->admcreaterole)
+ result = false;
+ break;
+ case CREATEDB:
+ if (self && !uform->rolcreatedb)
+ result = false;
+ if (grant && !uform->admcreatedb)
+ result = false;
+ break;
+ case CANLOGIN:
+ if (self && !uform->rolcanlogin)
+ result = false;
+ if (grant && !uform->admcanlogin)
+ result = false;
+ break;
+ case REPLICATION:
+ if (self && !uform->rolreplication)
+ result = false;
+ if (grant && !uform->admreplication)
+ result = false;
+ break;
+ case BYPASSRLS:
+ if (self && !uform->rolbypassrls)
+ result = false;
+ if (grant && !uform->admbypassrls)
+ result = false;
+ break;
+ case INHERIT:
+ if (grant && !uform->adminherit)
+ result = false;
+ break;
+ case CONNLIMIT:
+ if (grant && !uform->admconnlimit)
+ result = false;
+ break;
+ case VALIDUNTIL:
+ if (grant && !uform->admvaliduntil)
+ result = false;
+ break;
+ case PASSWORD:
+ if (grant && !uform->admpassword)
+ result = false;
+ break;
+
+ /* Intentionally no default here */
+ }
+ ReleaseSysCache(utup);
+ }
+ return result;
+}
+
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
@@ -5444,39 +5529,85 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid)
bool
has_createrole_privilege(Oid roleid)
{
- bool result = false;
- HeapTuple utup;
+ return has_privilege(roleid, CREATEROLE, true, false);
+}
- /* Superusers bypass all permission checking. */
- if (superuser_arg(roleid))
- return true;
+bool
+has_createdb_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CREATEDB, true, false);
+}
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolcreaterole;
- ReleaseSysCache(utup);
- }
- return result;
+bool
+has_canlogin_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CANLOGIN, true, false);
+}
+
+bool
+has_replication_privilege(Oid roleid)
+{
+ return has_privilege(roleid, REPLICATION, true, false);
}
bool
has_bypassrls_privilege(Oid roleid)
{
- bool result = false;
- HeapTuple utup;
+ return has_privilege(roleid, BYPASSRLS, true, false);
+}
- /* Superusers bypass all permission checking. */
- if (superuser_arg(roleid))
- return true;
+bool
+may_admin_createrole_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CREATEROLE, false, true);
+}
- utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
- if (HeapTupleIsValid(utup))
- {
- result = ((Form_pg_authid) GETSTRUCT(utup))->rolbypassrls;
- ReleaseSysCache(utup);
- }
- return result;
+bool
+may_admin_createdb_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CREATEDB, false, true);
+}
+
+bool
+may_admin_canlogin_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CANLOGIN, false, true);
+}
+
+bool
+may_admin_replication_privilege(Oid roleid)
+{
+ return has_privilege(roleid, REPLICATION, false, true);
+}
+
+bool
+may_admin_bypassrls_privilege(Oid roleid)
+{
+ return has_privilege(roleid, BYPASSRLS, false, true);
+}
+
+bool
+may_admin_inherit_privilege(Oid roleid)
+{
+ return has_privilege(roleid, INHERIT, false, true);
+}
+
+bool
+may_admin_connlimit_privilege(Oid roleid)
+{
+ return has_privilege(roleid, CONNLIMIT, false, true);
+}
+
+bool
+may_admin_validuntil_privilege(Oid roleid)
+{
+ return has_privilege(roleid, VALIDUNTIL, false, true);
+}
+
+bool
+may_admin_password_privilege(Oid roleid)
+{
+ return has_privilege(roleid, PASSWORD, false, true);
}
/*
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246b..501613a840 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -51,7 +51,7 @@ check_password_hook_type check_password_hook = NULL;
static void AddRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- Oid grantorId, bool admin_opt);
+ Oid grantorId, bool admin_opt, bool creating);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
bool admin_opt);
@@ -107,6 +107,24 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
DefElem *dadminmembers = NULL;
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ int inherit_spec = 0;
+ int createrole_spec = 0;
+ int createdb_spec = 0;
+ int canlogin_spec = 0;
+ int replication_spec = 0;
+ int bypassrls_spec = 0;
+ int connlimit_spec = 0;
+ int password_spec = 0;
+ int validuntil_spec = 0;
+ bool admin_inherit = false;
+ bool admin_createrole = false;
+ bool admin_createdb = false;
+ bool admin_canlogin = false;
+ bool admin_replication = false;
+ bool admin_bypassrls = false;
+ bool admin_connlimit = false;
+ bool admin_password = false;
+ bool admin_validuntil = false;
/* The defaults can vary depending on the original statement type */
switch (stmt->stmt_type)
@@ -124,13 +142,15 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
/* Extract options from the statement node tree */
foreach(option, stmt->options)
{
- DefElem *defel = (DefElem *) lfirst(option);
+ RoleElem *relem = (RoleElem *) lfirst(option);
+ DefElem *defel = relem->elem;
if (strcmp(defel->defname, "password") == 0)
{
if (dpassword)
errorConflictingDefElem(defel, pstate);
dpassword = defel;
+ password_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "sysid") == 0)
{
@@ -148,36 +168,42 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (dinherit)
errorConflictingDefElem(defel, pstate);
dinherit = defel;
+ inherit_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "createrole") == 0)
{
if (dcreaterole)
errorConflictingDefElem(defel, pstate);
dcreaterole = defel;
+ createrole_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "createdb") == 0)
{
if (dcreatedb)
errorConflictingDefElem(defel, pstate);
dcreatedb = defel;
+ createdb_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "canlogin") == 0)
{
if (dcanlogin)
errorConflictingDefElem(defel, pstate);
dcanlogin = defel;
+ canlogin_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "isreplication") == 0)
{
if (disreplication)
errorConflictingDefElem(defel, pstate);
disreplication = defel;
+ replication_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "connectionlimit") == 0)
{
if (dconnlimit)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
+ connlimit_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "addroleto") == 0)
{
@@ -202,12 +228,14 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (dvalidUntil)
errorConflictingDefElem(defel, pstate);
dvalidUntil = defel;
+ validuntil_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "bypassrls") == 0)
{
if (dbypassRLS)
errorConflictingDefElem(defel, pstate);
dbypassRLS = defel;
+ bypassrls_spec = relem->admin_spec;
}
else
elog(ERROR, "option \"%s\" not recognized",
@@ -247,6 +275,28 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
if (dbypassRLS)
bypassrls = boolVal(dbypassRLS->arg);
+ /*
+ * For backward-compatibility, we allow CREATEROLE to imply the ability to
+ * grant all privileges except SUPERUSER, REPLICATION, and BYPASSRLS when
+ * neither WITH ADMIN OPTION nor WITHOUT ADMIN OPTION is specified.
+ */
+ admin_inherit = ((inherit_spec == 1) ||
+ (createrole && inherit_spec != -1));
+ admin_createrole = ((createrole_spec == 1) ||
+ (createrole && createrole_spec != -1));
+ admin_createdb = ((createdb_spec == 1) ||
+ (createrole && createdb_spec != -1));
+ admin_canlogin = ((canlogin_spec == 1) ||
+ (createrole && canlogin_spec != -1));
+ admin_replication = (replication_spec == 1);
+ admin_bypassrls = (bypassrls_spec == 1);
+ admin_connlimit = ((connlimit_spec == 1) ||
+ (createrole && connlimit_spec != -1));
+ admin_password = ((password_spec == 1) ||
+ (createrole && password_spec != -1));
+ admin_validuntil = ((validuntil_spec == 1) ||
+ (createrole && validuntil_spec != -1));
+
/* Check some permissions first */
if (issuper)
{
@@ -255,27 +305,36 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to create superusers")));
}
- else if (isreplication)
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create replication users")));
- }
- else if (bypassrls)
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to create bypassrls users")));
- }
- else
- {
- if (!have_createrole_privilege())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied to create role")));
- }
+
+ if (createrole && !may_admin_createrole_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have grant option on CREATEROLE privilege to create createrole users")));
+
+ if (createdb && !may_admin_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have grant option on CREATEDB privilege to create createdb users")));
+
+ if (canlogin && !may_admin_canlogin_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have grant option on LOGIN privilege to create login users")));
+
+ if (isreplication && !may_admin_replication_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have grant option on REPLICATION privilege to create replication users")));
+
+ if (bypassrls && !may_admin_bypassrls_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have grant option on BYPASSRLS privilege to create bypassrls users")));
+
+ if (!have_createrole_privilege())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to create role")));
/*
* Check that the user is not trying to create a role in the reserved
@@ -311,7 +370,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
stmt->role)));
/* Convert validuntil to internal form */
- if (validUntil)
+ if (validUntil && strcmp(validUntil, "always") != 0)
{
validUntil_datum = DirectFunctionCall3(timestamptz_in,
CStringGetDatum(validUntil),
@@ -393,6 +452,16 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls);
+ new_record[Anum_pg_authid_adminherit - 1] = BoolGetDatum(admin_inherit);
+ new_record[Anum_pg_authid_admcreaterole - 1] = BoolGetDatum(admin_createrole);
+ new_record[Anum_pg_authid_admcreatedb - 1] = BoolGetDatum(admin_createdb);
+ new_record[Anum_pg_authid_admcanlogin - 1] = BoolGetDatum(admin_canlogin);
+ new_record[Anum_pg_authid_admreplication - 1] = BoolGetDatum(admin_replication);
+ new_record[Anum_pg_authid_admbypassrls - 1] = BoolGetDatum(admin_bypassrls);
+ new_record[Anum_pg_authid_admconnlimit - 1] = BoolGetDatum(admin_connlimit);
+ new_record[Anum_pg_authid_admpassword - 1] = BoolGetDatum(admin_password);
+ new_record[Anum_pg_authid_admvaliduntil - 1] = BoolGetDatum(admin_validuntil);
+
/*
* pg_largeobject_metadata contains pg_authid.oid's, so we use the
* binary-upgrade override.
@@ -453,7 +522,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ GetUserId(), false, false);
ReleaseSysCache(oldroletup);
}
@@ -465,10 +534,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ GetUserId(), true, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ GetUserId(), false, true);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -518,6 +587,15 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
DefElem *drolemembers = NULL;
DefElem *dvalidUntil = NULL;
DefElem *dbypassRLS = NULL;
+ int inherit_spec = 0;
+ int createrole_spec = 0;
+ int createdb_spec = 0;
+ int canlogin_spec = 0;
+ int replication_spec = 0;
+ int bypassrls_spec = 0;
+ int connlimit_spec = 0;
+ int password_spec = 0;
+ int validuntil_spec = 0;
Oid roleid;
check_rolespec_name(stmt->role,
@@ -526,13 +604,15 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
/* Extract options from the statement node tree */
foreach(option, stmt->options)
{
- DefElem *defel = (DefElem *) lfirst(option);
+ RoleElem *relem = (RoleElem *) lfirst(option);
+ DefElem *defel = relem->elem;
if (strcmp(defel->defname, "password") == 0)
{
if (dpassword)
errorConflictingDefElem(defel, pstate);
dpassword = defel;
+ password_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "superuser") == 0)
{
@@ -545,36 +625,42 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (dinherit)
errorConflictingDefElem(defel, pstate);
dinherit = defel;
+ inherit_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "createrole") == 0)
{
if (dcreaterole)
errorConflictingDefElem(defel, pstate);
dcreaterole = defel;
+ createrole_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "createdb") == 0)
{
if (dcreatedb)
errorConflictingDefElem(defel, pstate);
dcreatedb = defel;
+ createdb_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "canlogin") == 0)
{
if (dcanlogin)
errorConflictingDefElem(defel, pstate);
dcanlogin = defel;
+ canlogin_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "isreplication") == 0)
{
if (disreplication)
errorConflictingDefElem(defel, pstate);
disreplication = defel;
+ replication_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "connectionlimit") == 0)
{
if (dconnlimit)
errorConflictingDefElem(defel, pstate);
dconnlimit = defel;
+ connlimit_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "rolemembers") == 0 &&
stmt->action != 0)
@@ -588,12 +674,14 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (dvalidUntil)
errorConflictingDefElem(defel, pstate);
dvalidUntil = defel;
+ validuntil_spec = relem->admin_spec;
}
else if (strcmp(defel->defname, "bypassrls") == 0)
{
if (dbypassRLS)
errorConflictingDefElem(defel, pstate);
dbypassRLS = defel;
+ bypassrls_spec = relem->admin_spec;
}
else
elog(ERROR, "option \"%s\" not recognized",
@@ -630,6 +718,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
* property. Otherwise, if you don't have createrole, you're only allowed
* to change your own password.
*/
+
+ /* To mess with a superuser in any way you gotta be superuser. */
if (authform->rolsuper || dissuper)
{
if (!superuser())
@@ -637,32 +727,57 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must be superuser to alter superuser roles or change superuser attribute")));
}
- else if (authform->rolreplication || disreplication)
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to alter replication roles or change replication attribute")));
- }
- else if (dbypassRLS)
- {
- if (!superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to change bypassrls attribute")));
- }
- else if (!have_createrole_privilege())
- {
- /* check the rest */
- if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
- drolemembers || dvalidUntil || !dpassword || roleid != GetUserId())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("permission denied")));
- }
+
+ /* To mess with replication roles, must have admin on REPLICATION */
+ if ((authform->rolreplication || disreplication) &&
+ !may_admin_replication_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on replication to alter replication roles or change replication attribute")));
+
+ /* For other privileges, must have admin on the privilege being altered */
+ if (dbypassRLS && !may_admin_bypassrls_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on bypassrls to change bypassrls attribute")));
+
+ if (dinherit && !may_admin_inherit_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on inherit to change inherit attribute")));
+
+ if (dcreaterole && !may_admin_createrole_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on createrole to change createrole attribute")));
+
+ if (dcreatedb && !may_admin_createdb_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on createdb to change createdb attribute")));
+
+ if (dcanlogin && !may_admin_canlogin_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on login to change login attribute")));
+
+ if (dconnlimit && !may_admin_connlimit_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on connection limit to change connection limit attribute")));
+
+ if (dvalidUntil && !may_admin_validuntil_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on valid until to change valid until attribute")));
+
+ if (dpassword && roleid != GetUserId() && !may_admin_password_privilege(GetUserId()))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin on password to change password attribute")));
/* Convert validuntil to internal form */
- if (dvalidUntil)
+ if (validUntil && strcmp(validUntil, "always") != 0)
{
validUntil_datum = DirectFunctionCall3(timestamptz_in,
CStringGetDatum(validUntil),
@@ -670,6 +785,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
Int32GetDatum(-1));
validUntil_null = false;
}
+ else if (validUntil && strcmp(validUntil, "always") == 0)
+ {
+ validUntil_null = true;
+ }
else
{
/* fetch existing setting in case hook needs it */
@@ -709,36 +828,66 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(boolVal(dinherit->arg));
new_record_repl[Anum_pg_authid_rolinherit - 1] = true;
}
+ if (inherit_spec != 0)
+ {
+ new_record[Anum_pg_authid_adminherit - 1] = BoolGetDatum(inherit_spec == 1);
+ new_record_repl[Anum_pg_authid_adminherit - 1] = true;
+ }
if (dcreaterole)
{
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(boolVal(dcreaterole->arg));
new_record_repl[Anum_pg_authid_rolcreaterole - 1] = true;
}
+ if (createrole_spec != 0)
+ {
+ new_record[Anum_pg_authid_admcreaterole - 1] = BoolGetDatum(createrole_spec == 1);
+ new_record_repl[Anum_pg_authid_admcreaterole - 1] = true;
+ }
if (dcreatedb)
{
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(boolVal(dcreatedb->arg));
new_record_repl[Anum_pg_authid_rolcreatedb - 1] = true;
}
+ if (createdb_spec != 0)
+ {
+ new_record[Anum_pg_authid_admcreatedb - 1] = BoolGetDatum(createdb_spec == 1);
+ new_record_repl[Anum_pg_authid_admcreatedb - 1] = true;
+ }
if (dcanlogin)
{
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(boolVal(dcanlogin->arg));
new_record_repl[Anum_pg_authid_rolcanlogin - 1] = true;
}
+ if (canlogin_spec != 0)
+ {
+ new_record[Anum_pg_authid_admcanlogin - 1] = BoolGetDatum(canlogin_spec == 1);
+ new_record_repl[Anum_pg_authid_admcanlogin - 1] = true;
+ }
if (disreplication)
{
new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(boolVal(disreplication->arg));
new_record_repl[Anum_pg_authid_rolreplication - 1] = true;
}
+ if (replication_spec != 0)
+ {
+ new_record[Anum_pg_authid_admreplication - 1] = BoolGetDatum(replication_spec == 1);
+ new_record_repl[Anum_pg_authid_admreplication - 1] = true;
+ }
if (dconnlimit)
{
new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
new_record_repl[Anum_pg_authid_rolconnlimit - 1] = true;
}
+ if (connlimit_spec != 0)
+ {
+ new_record[Anum_pg_authid_admconnlimit - 1] = BoolGetDatum(connlimit_spec == 1);
+ new_record_repl[Anum_pg_authid_admconnlimit - 1] = true;
+ }
/* password */
if (password)
@@ -771,17 +920,32 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
}
+ if (password_spec != 0)
+ {
+ new_record[Anum_pg_authid_admpassword - 1] = BoolGetDatum(password_spec == 1);
+ new_record_repl[Anum_pg_authid_admpassword - 1] = true;
+ }
/* valid until */
new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true;
+ if (validuntil_spec != 0)
+ {
+ new_record[Anum_pg_authid_admvaliduntil - 1] = BoolGetDatum(validuntil_spec == 1);
+ new_record_repl[Anum_pg_authid_admvaliduntil - 1] = true;
+ }
if (dbypassRLS)
{
new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(boolVal(dbypassRLS->arg));
new_record_repl[Anum_pg_authid_rolbypassrls - 1] = true;
}
+ if (bypassrls_spec != 0)
+ {
+ new_record[Anum_pg_authid_admbypassrls - 1] = BoolGetDatum(bypassrls_spec == 1);
+ new_record_repl[Anum_pg_authid_admbypassrls - 1] = true;
+ }
new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record,
new_record_nulls, new_record_repl);
@@ -805,7 +969,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ GetUserId(), false, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
@@ -1264,7 +1428,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->is_grant)
AddRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- grantor, stmt->admin_opt);
+ grantor, stmt->admin_opt, false);
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
@@ -1371,11 +1535,12 @@ roleSpecsToIds(List *memberNames)
* memberIds: OIDs of roles to add
* grantorId: who is granting the membership
* admin_opt: granting admin option?
+ * creating: is roleid presently being created?
*/
static void
AddRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- Oid grantorId, bool admin_opt)
+ Oid grantorId, bool admin_opt, bool creating)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
@@ -1401,8 +1566,7 @@ AddRoleMems(const char *rolename, Oid roleid,
}
else
{
- if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ if (!creating && !is_admin_of_role(grantorId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index b5966712ce..7503d3ead6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -327,7 +327,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
PLpgSQL_Expr PLAssignStmt
%type <node> alter_column_default opclass_item opclass_drop alter_using
-%type <ival> add_drop opt_asc_desc opt_nulls_order
+%type <ival> add_drop opt_asc_desc opt_nulls_order opt_admin_spec
%type <node> alter_table_cmd alter_type_cmd opt_collate_clause
replica_identity partition_cmd index_partition_cmd
@@ -356,8 +356,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
opt_transaction_chain
%type <ival> opt_nowait_or_skip
-%type <list> OptRoleList AlterOptRoleList
-%type <defelt> CreateOptRoleElem AlterOptRoleElem
+%type <list> OptRoleList AlterOptRoleList user_role_list
+%type <node> CreateOptRoleElem AlterOptRoleElem
%type <str> opt_type
%type <str> foreign_server_version opt_foreign_server_version
@@ -1104,24 +1104,33 @@ AlterOptRoleList:
;
AlterOptRoleElem:
- PASSWORD Sconst
+ PASSWORD Sconst opt_admin_spec
{
- $$ = makeDefElem("password",
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("password",
(Node *)makeString($2), @1);
+ n->admin_spec = $3;
+ $$ = (Node *)n;
}
- | PASSWORD NULL_P
+ | PASSWORD NULL_P opt_admin_spec
{
- $$ = makeDefElem("password", NULL, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("password", NULL, @1);
+ n->admin_spec = $3;
+ $$ = (Node *)n;
}
- | ENCRYPTED PASSWORD Sconst
+ | ENCRYPTED PASSWORD Sconst opt_admin_spec
{
/*
* These days, passwords are always stored in encrypted
* form, so there is no difference between PASSWORD and
* ENCRYPTED PASSWORD.
*/
- $$ = makeDefElem("password",
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("password",
(Node *)makeString($3), @1);
+ n->admin_spec = $4;
+ $$ = (Node *)n;
}
| UNENCRYPTED PASSWORD Sconst
{
@@ -1131,67 +1140,111 @@ AlterOptRoleElem:
errhint("Remove UNENCRYPTED to store the password in encrypted form instead."),
parser_errposition(@1)));
}
- | INHERIT
+ | INHERIT opt_admin_spec
+ {
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
+ n->admin_spec = $2;
+ $$ = (Node *)n;
+ }
+ | CONNECTION LIMIT SignedIconst opt_admin_spec
{
- $$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("connectionlimit", (Node *)makeInteger($3), @1);
+ n->admin_spec = $4;
+ $$ = (Node *)n;
}
- | CONNECTION LIMIT SignedIconst
+ | CONNECTION LIMIT NONE opt_admin_spec
{
- $$ = makeDefElem("connectionlimit", (Node *)makeInteger($3), @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("connectionlimit", (Node *)makeInteger(-1), @1);
+ n->admin_spec = $4;
+ $$ = (Node *)n;
}
- | VALID UNTIL Sconst
+ | VALID UNTIL Sconst opt_admin_spec
{
- $$ = makeDefElem("validUntil", (Node *)makeString($3), @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("validUntil", (Node *)makeString($3), @1);
+ n->admin_spec = $4;
+ $$ = (Node *)n;
+ }
+ | VALID ALWAYS opt_admin_spec
+ {
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("validUntil", (Node *)makeString("always"), @1);
+ n->admin_spec = $3;
+ $$ = (Node *)n;
}
/* Supported but not documented for roles, for use by ALTER GROUP. */
- | USER role_list
+ | USER role_list opt_admin_spec
{
- $$ = makeDefElem("rolemembers", (Node *)$2, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("rolemembers", (Node *)$2, @1);
+ n->admin_spec = $3;
+ $$ = (Node *)n;
}
- | IDENT
+ | IDENT opt_admin_spec
{
/*
* We handle identifiers that aren't parser keywords with
* the following special-case codes, to avoid bloating the
* size of the main parser.
*/
+ RoleElem *n = makeNode(RoleElem);
+
+ /*
+ * Record whether the user specified WITH GRANT OPTION.
+ * Note that for some privileges this is always implied,
+ * such as SUPERUSER, but we don't reflect that here.
+ */
+ n->admin_spec = $2;
+
if (strcmp($1, "superuser") == 0)
- $$ = makeDefElem("superuser", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("superuser", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "nosuperuser") == 0)
- $$ = makeDefElem("superuser", (Node *)makeBoolean(false), @1);
+ {
+ n->elem = makeDefElem("superuser", (Node *)makeBoolean(false), @1);
+ if (n->admin_spec == 1)
+ ereport(ERROR,
+ errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("only superusers may create new superusers"),
+ parser_errposition(@2));
+ }
else if (strcmp($1, "createrole") == 0)
- $$ = makeDefElem("createrole", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("createrole", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "nocreaterole") == 0)
- $$ = makeDefElem("createrole", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("createrole", (Node *)makeBoolean(false), @1);
else if (strcmp($1, "replication") == 0)
- $$ = makeDefElem("isreplication", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("isreplication", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "noreplication") == 0)
- $$ = makeDefElem("isreplication", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("isreplication", (Node *)makeBoolean(false), @1);
else if (strcmp($1, "createdb") == 0)
- $$ = makeDefElem("createdb", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("createdb", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "nocreatedb") == 0)
- $$ = makeDefElem("createdb", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("createdb", (Node *)makeBoolean(false), @1);
else if (strcmp($1, "login") == 0)
- $$ = makeDefElem("canlogin", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("canlogin", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "nologin") == 0)
- $$ = makeDefElem("canlogin", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("canlogin", (Node *)makeBoolean(false), @1);
else if (strcmp($1, "bypassrls") == 0)
- $$ = makeDefElem("bypassrls", (Node *)makeBoolean(true), @1);
+ n->elem = makeDefElem("bypassrls", (Node *)makeBoolean(true), @1);
else if (strcmp($1, "nobypassrls") == 0)
- $$ = makeDefElem("bypassrls", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("bypassrls", (Node *)makeBoolean(false), @1);
else if (strcmp($1, "noinherit") == 0)
{
/*
* Note that INHERIT is a keyword, so it's handled by main parser, but
* NOINHERIT is handled here.
*/
- $$ = makeDefElem("inherit", (Node *)makeBoolean(false), @1);
+ n->elem = makeDefElem("inherit", (Node *)makeBoolean(false), @1);
}
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized role option \"%s\"", $1),
parser_errposition(@1)));
+
+ $$ = (Node *)n;
}
;
@@ -1200,23 +1253,38 @@ CreateOptRoleElem:
/* The following are not supported by ALTER ROLE/USER/GROUP */
| SYSID Iconst
{
- $$ = makeDefElem("sysid", (Node *)makeInteger($2), @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("sysid", (Node *)makeInteger($2), @1);
+ n->admin_spec = 0;
+ $$ = (Node *)n;
}
| ADMIN role_list
{
- $$ = makeDefElem("adminmembers", (Node *)$2, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("adminmembers", (Node *)$2, @1);
+ n->admin_spec = 0;
+ $$ = (Node *)n;
}
| ROLE role_list
{
- $$ = makeDefElem("rolemembers", (Node *)$2, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("rolemembers", (Node *)$2, @1);
+ n->admin_spec = 0;
+ $$ = (Node *)n;
}
| IN_P ROLE role_list
{
- $$ = makeDefElem("addroleto", (Node *)$3, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("addroleto", (Node *)$3, @1);
+ n->admin_spec = 0;
+ $$ = (Node *)n;
}
| IN_P GROUP_P role_list
{
- $$ = makeDefElem("addroleto", (Node *)$3, @1);
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("addroleto", (Node *)$3, @1);
+ n->admin_spec = 0;
+ $$ = (Node *)n;
}
;
@@ -1385,13 +1453,12 @@ CreateGroupStmt:
*****************************************************************************/
AlterGroupStmt:
- ALTER GROUP_P RoleSpec add_drop USER role_list
+ ALTER GROUP_P RoleSpec add_drop user_role_list
{
AlterRoleStmt *n = makeNode(AlterRoleStmt);
n->role = $3;
n->action = $4;
- n->options = list_make1(makeDefElem("rolemembers",
- (Node *)$6, @6));
+ n->options = $5;
$$ = (Node *)n;
}
;
@@ -1400,6 +1467,16 @@ add_drop: ADD_P { $$ = +1; }
| DROP { $$ = -1; }
;
+user_role_list:
+ USER role_list
+ {
+ RoleElem *n = makeNode(RoleElem);
+ n->elem = makeDefElem("rolemembers", (Node *)$2, @2);
+ n->admin_spec = 0;
+ $$ = list_make1(n);
+ }
+ ;
+
/*****************************************************************************
*
@@ -7257,6 +7334,12 @@ opt_grant_grant_option:
| /*EMPTY*/ { $$ = false; }
;
+opt_admin_spec:
+ WITH ADMIN OPTION { $$ = 1; }
+ | WITHOUT ADMIN OPTION { $$ = -1; }
+ | /* EMPTY */ { $$ = 0; }
+ ;
+
/*****************************************************************************
*
* GRANT and REVOKE ROLE statements
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 6c28119fa1..4829a6dbd2 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -22,67 +22,93 @@
{ oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID',
rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't',
rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't',
- rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1',
+ rolreplication => 't', rolbypassrls => 't', adminherit => 't', admcreaterole => 't',
+ admcreatedb => 't', admcanlogin => 't', admreplication => 't', admbypassrls => 't',
+ admconnlimit => 't', admpassword => 't', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => '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',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
{ oid => '4544', oid_symbol => 'ROLE_PG_CHECKPOINTER',
rolname => 'pg_checkpointer', rolsuper => 'f', rolinherit => 't',
rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
- rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+ rolreplication => 'f', rolbypassrls => 'f', adminherit => 'f', admcreaterole => 'f',
+ admcreatedb => 'f', admcanlogin => 'f', admreplication => 'f', admbypassrls => 'f',
+ admconnlimit => 'f', admpassword => 'f', admvaliduntil => 'f', rolconnlimit => '-1',
rolpassword => '_null_', rolvaliduntil => '_null_' },
]
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h
index 4b65e39a1f..4acdcaa685 100644
--- a/src/include/catalog/pg_authid.h
+++ b/src/include/catalog/pg_authid.h
@@ -39,6 +39,16 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284
bool rolcanlogin; /* allowed to log in as session user? */
bool rolreplication; /* role used for streaming replication */
bool rolbypassrls; /* bypasses row-level security? */
+
+ bool adminherit; /* allowed to administer inherit? */
+ bool admcreaterole; /* allowed to administer createrole? */
+ bool admcreatedb; /* allowed to administer createdb?? */
+ bool admcanlogin; /* allowed to administer login? */
+ bool admreplication; /* allowed to administer replication? */
+ bool admbypassrls; /* allowed to administer bypassesrls? */
+ bool admconnlimit; /* allowed to administer connlimit? */
+ bool admpassword; /* allowed to administer password? */
+ bool admvaliduntil; /* allowed to administer validuntil? */
int32 rolconnlimit; /* max connections allowed (-1=no limit) */
/* remaining fields may be null; use heap_getattr to read them! */
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index da35f2c272..065f58c485 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -361,6 +361,7 @@ typedef enum NodeTag
T_DiscardStmt,
T_CreateTrigStmt,
T_CreatePLangStmt,
+ T_RoleElem,
T_CreateRoleStmt,
T_AlterRoleStmt,
T_DropRoleStmt,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 3e9bdc781f..e19980d5ff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2618,19 +2618,26 @@ typedef enum RoleStmtType
ROLESTMT_GROUP
} RoleStmtType;
+typedef struct RoleElem
+{
+ NodeTag type;
+ DefElem *elem;
+ int admin_spec;
+} RoleElem;
+
typedef struct CreateRoleStmt
{
NodeTag type;
RoleStmtType stmt_type; /* ROLE/USER/GROUP */
char *role; /* role name */
- List *options; /* List of DefElem nodes */
+ List *options; /* List of RoleElem nodes */
} CreateRoleStmt;
typedef struct AlterRoleStmt
{
NodeTag type;
RoleSpec *role; /* role */
- List *options; /* List of DefElem nodes */
+ List *options; /* List of RoleElem nodes */
int action; /* +1 = add members, -1 = drop members */
} AlterRoleStmt;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 1ce4c5556e..7cdc454869 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -317,6 +317,18 @@ extern bool pg_publication_ownercheck(Oid pub_oid, Oid roleid);
extern bool pg_subscription_ownercheck(Oid sub_oid, Oid roleid);
extern bool pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid);
extern bool has_createrole_privilege(Oid roleid);
+extern bool has_createdb_privilege(Oid roleid);
+extern bool has_canlogin_privilege(Oid roleid);
+extern bool has_replication_privilege(Oid roleid);
extern bool has_bypassrls_privilege(Oid roleid);
+extern bool may_admin_createrole_privilege(Oid roleid);
+extern bool may_admin_createdb_privilege(Oid roleid);
+extern bool may_admin_canlogin_privilege(Oid roleid);
+extern bool may_admin_replication_privilege(Oid roleid);
+extern bool may_admin_bypassrls_privilege(Oid roleid);
+extern bool may_admin_inherit_privilege(Oid roleid);
+extern bool may_admin_connlimit_privilege(Oid roleid);
+extern bool may_admin_validuntil_privilege(Oid roleid);
+extern bool may_admin_password_privilege(Oid roleid);
#endif /* ACL_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..20da6644b7 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -1,17 +1,72 @@
-- 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;
--- fail, only superusers can create users with these privileges
+-- ok, superuser can create a role that can create login replication users, but
+-- cannot itself login, nor perform replication
+CREATE ROLE regress_role_repladmin
+ CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
+ NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
+ NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
+ NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
+ NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away
+-- ok, superuser can create a role with CREATEROLE but restrict give-aways
+CREATE ROLE regress_role_minoradmin
+ NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
+ CREATEROLE WITHOUT ADMIN OPTION
+ NOCREATEDB WITHOUT ADMIN OPTION
+ NOLOGIN WITHOUT ADMIN OPTION
+ NOREPLICATION -- WITHOUT ADMIN OPTION is implied
+ NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
+ NOINHERIT WITHOUT ADMIN OPTION
+ CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
+ VALID ALWAYS WITHOUT ADMIN OPTION
+ PASSWORD NULL WITHOUT ADMIN OPTION;
+-- fail, not granted privilege to create these users
+SET SESSION AUTHORIZATION regress_role_repladmin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_nosuch_createrole CREATEROLE;
+ERROR: must have grant option on CREATEROLE privilege to create createrole users
+CREATE ROLE regress_nosuch_createdb CREATEDB;
+ERROR: must have grant option on CREATEDB privilege to create createdb users
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+ERROR: must have grant option on BYPASSRLS privilege to create bypassrls users
+CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+ERROR: must have grant option on BYPASSRLS privilege to create bypassrls users
+-- ok, can create login and replication users
+CREATE ROLE regress_role_login LOGIN;
+CREATE ROLE regress_role_replication REPLICATION;
+CREATE ROLE regress_role_login_replication LOGIN REPLICATION;
+-- fail, cannot create privileged users
+SET SESSION AUTHORIZATION regress_role_minoradmin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+ERROR: must be superuser to create superusers
+CREATE ROLE regress_nosuch_createrole CREATEROLE;
+ERROR: must have grant option on CREATEROLE privilege to create createrole users
+CREATE ROLE regress_nosuch_createdb CREATEDB;
+ERROR: must have grant option on CREATEDB privilege to create createdb users
+CREATE ROLE regress_nosuch_replication REPLICATION;
+ERROR: must have grant option on REPLICATION privilege to create replication users
+CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+ERROR: must have grant option on BYPASSRLS privilege to create bypassrls users
+-- ok, can create unprivileged roles
+CREATE ROLE regress_role_unprivileged;
+-- fail, CREATEROLE does not imply ADMIN on roles
+CREATE ROLE regress_nosuch_inrole IN ROLE regress_role_unprivileged;
+ERROR: must have admin option on role "regress_role_unprivileged"
+CREATE ROLE regress_nosuch_inrole IN GROUP regress_role_unprivileged;
+ERROR: must have admin option on role "regress_role_unprivileged"
+-- fail, having CREATEROLE does not by default give ADMIN OPTION on these
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
ERROR: must be superuser to create superusers
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
-ERROR: must be superuser to create replication users
+ERROR: must have grant option on REPLICATION privilege to create replication users
CREATE ROLE regress_nosuch_replication REPLICATION;
-ERROR: must be superuser to create replication users
+ERROR: must have grant option on REPLICATION privilege to create replication users
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
+ERROR: must have grant option on BYPASSRLS privilege to create bypassrls users
+-- ok, having CREATEROLE does by default give ADMIN OPTION on these
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
@@ -25,9 +80,16 @@ NOTICE: SYSID can no longer be specified
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
ERROR: must be superuser to alter superusers
--- fail, database owner cannot have members
+-- fail, lack ADMIN privilege on role pg_database_owner;
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+ERROR: must have admin option on role "pg_database_owner"
+-- fail, database owner cannot have members
+RESET SESSION AUTHORIZATION;
+GRANT pg_database_owner TO regress_role_admin WITH ADMIN OPTION;
ERROR: role "pg_database_owner" cannot have explicit members
+SET SESSION AUTHORIZATION regress_role_admin;
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+ERROR: must have admin option on role "pg_database_owner"
-- ok, can grant other users into a role
CREATE ROLE regress_inroles ROLE
regress_role_super, regress_createdb, regress_createrole, regress_login,
@@ -73,7 +135,35 @@ ERROR: must be owner of view tenant_view
-- fail, cannot take ownership of these objects 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, having CREATEROLE is not enough to create roles in privileged roles
+CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
+ERROR: must have admin option on role "pg_read_all_data"
+CREATE ROLE regress_nosuch_write_all_data IN ROLE pg_write_all_data;
+ERROR: must have admin option on role "pg_write_all_data"
+CREATE ROLE regress_nosuch_monitor IN ROLE pg_monitor;
+ERROR: must have admin option on role "pg_monitor"
+CREATE ROLE regress_nosuch_read_all_settings IN ROLE pg_read_all_settings;
+ERROR: must have admin option on role "pg_read_all_settings"
+CREATE ROLE regress_nosuch_read_all_stats IN ROLE pg_read_all_stats;
+ERROR: must have admin option on role "pg_read_all_stats"
+CREATE ROLE regress_nosuch_stat_scan_tables IN ROLE pg_stat_scan_tables;
+ERROR: must have admin option on role "pg_stat_scan_tables"
+CREATE ROLE regress_nosuch_read_server_files IN ROLE pg_read_server_files;
+ERROR: must have admin option on role "pg_read_server_files"
+CREATE ROLE regress_nosuch_write_server_files IN ROLE pg_write_server_files;
+ERROR: must have admin option on role "pg_write_server_files"
+CREATE ROLE regress_nosuch_execute_server_program IN ROLE pg_execute_server_program;
+ERROR: must have admin option on role "pg_execute_server_program"
+CREATE ROLE regress_nosuch_signal_backend IN ROLE pg_signal_backend;
+ERROR: must have admin option on role "pg_signal_backend"
+-- ok, superuser can grant ADMIN on privileged roles
+SET SESSION AUTHORIZATION regress_role_super;
+GRANT pg_read_all_data, pg_write_all_data, pg_monitor, pg_read_all_settings,
+ pg_read_all_stats, pg_stat_scan_tables, pg_read_server_files,
+ pg_write_server_files, pg_execute_server_program, pg_signal_backend
+TO regress_createrole WITH ADMIN OPTION;
+-- ok, having CREATEROLE plus ADMIN is enough to create roles in privileged roles
+SET SESSION AUTHORIZATION regress_createrole;
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;
@@ -103,7 +193,89 @@ 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;
+-- fail, cannot change attributes without ADMIN for them
+SET SESSION AUTHORIZATION regress_role_minoradmin;
+ALTER ROLE regress_role_login LOGIN;
+ERROR: must have admin on login to change login attribute
+ALTER ROLE regress_role_login NOLOGIN;
+ERROR: must have admin on login to change login attribute
+ALTER ROLE regress_role_login REPLICATION;
+ERROR: must have admin on replication to alter replication roles or change replication attribute
+ALTER ROLE regress_role_login NOREPLICATION;
+ERROR: must have admin on replication to alter replication roles or change replication attribute
+ALTER ROLE regress_role_login CREATEDB;
+ERROR: must have admin on createdb to change createdb attribute
+ALTER ROLE regress_role_login NOCREATEDB;
+ERROR: must have admin on createdb to change createdb attribute
+ALTER ROLE regress_role_login CREATEROLE;
+ERROR: must have admin on createrole to change createrole attribute
+ALTER ROLE regress_role_login NOCREATEROLE;
+ERROR: must have admin on createrole to change createrole attribute
+ALTER ROLE regress_role_login INHERIT;
+ERROR: must have admin on inherit to change inherit attribute
+ALTER ROLE regress_role_login NOINHERIT;
+ERROR: must have admin on inherit to change inherit attribute
+ALTER ROLE regress_role_login CONNECTION LIMIT 5;
+ERROR: must have admin on connection limit to change connection limit attribute
+ALTER ROLE regress_role_login VALID ALWAYS;
+ERROR: must have admin on valid until to change valid until attribute
+ALTER ROLE regress_role_login PASSWORD 'foobar';
+ERROR: must have admin on password to change password attribute
+-- ok, regress_role_admin got ADMIN on attributes by way of having CREATEROLE
+SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_role_login LOGIN;
+ALTER ROLE regress_role_login NOLOGIN;
+ALTER ROLE regress_role_login CREATEDB;
+ALTER ROLE regress_role_login NOCREATEDB;
+ALTER ROLE regress_role_login CREATEROLE;
+ALTER ROLE regress_role_login NOCREATEROLE;
+ALTER ROLE regress_role_login INHERIT;
+ALTER ROLE regress_role_login NOINHERIT;
+ALTER ROLE regress_role_login CONNECTION LIMIT 5;
+ALTER ROLE regress_role_login VALID ALWAYS;
+ALTER ROLE regress_role_login PASSWORD 'foobar';
+-- fail, regress_role_admin did not get ADMIN on these
+ALTER ROLE regress_role_login SUPERUSER;
+ERROR: must be superuser to alter superuser roles or change superuser attribute
+ALTER ROLE regress_role_login NOSUPERUSER;
+ERROR: must be superuser to alter superuser roles or change superuser attribute
+ALTER ROLE regress_role_login REPLICATION;
+ERROR: must have admin on replication to alter replication roles or change replication attribute
+ALTER ROLE regress_role_login NOREPLICATION;
+ERROR: must have admin on replication to alter replication roles or change replication attribute
+ALTER ROLE regress_role_login BYPASSRLS;
+ERROR: must have admin on bypassrls to change bypassrls attribute
+ALTER ROLE regress_role_login NOBYPASSRLS;
+ERROR: must have admin on bypassrls to change bypassrls attribute
+-- superuser can grant them now, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_admin
+ NOREPLICATION WITH ADMIN OPTION
+ NOBYPASSRLS WITH ADMIN OPTION;
+-- ok, regress_role_admin can now grant these
+SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_role_login REPLICATION;
+ALTER ROLE regress_role_login NOREPLICATION;
+ALTER ROLE regress_role_login BYPASSRLS;
+ALTER ROLE regress_role_login NOBYPASSRLS;
+-- fail, but regress_role_admin still cannot grant this
+ALTER ROLE regress_role_login SUPERUSER;
+ERROR: must be superuser to alter superuser roles or change superuser attribute
+ALTER ROLE regress_role_login NOSUPERUSER;
+ERROR: must be superuser to alter superuser roles or change superuser attribute
+-- ok, regress_role_admin can grant attributes with ADMIN
+ALTER ROLE regress_role_unprivileged LOGIN WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CREATEDB WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CREATEROLE WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged INHERIT WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CONNECTION LIMIT 5 WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged VALID ALWAYS WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged PASSWORD 'foobar' WITH ADMIN OPTION;
-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_login;
+DROP ROLE regress_role_replication;
+DROP ROLE regress_role_login_replication;
+DROP ROLE regress_role_unprivileged;
DROP ROLE regress_createdb;
DROP ROLE regress_createrole;
DROP ROLE regress_login;
@@ -141,5 +313,7 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_role_minoradmin;
+DROP ROLE regress_role_repladmin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..60370626f4 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -59,6 +59,10 @@ CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user4, regress_priv_user2; -- duplicates
+NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user5, regress_priv_user6, regress_priv_user7;
+ALTER GROUP regress_priv_group2 DROP USER regress_priv_user7, regress_priv_user6, regress_priv_user5;
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..c15fc33be3 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -2,14 +2,64 @@
CREATE ROLE regress_role_super SUPERUSER;
CREATE ROLE regress_role_admin CREATEDB CREATEROLE REPLICATION BYPASSRLS;
--- fail, only superusers can create users with these privileges
+-- ok, superuser can create a role that can create login replication users, but
+-- cannot itself login, nor perform replication
+CREATE ROLE regress_role_repladmin
+ CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
+ NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
+ NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
+ NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
+ NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away
+
+-- ok, superuser can create a role with CREATEROLE but restrict give-aways
+CREATE ROLE regress_role_minoradmin
+ NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
+ CREATEROLE WITHOUT ADMIN OPTION
+ NOCREATEDB WITHOUT ADMIN OPTION
+ NOLOGIN WITHOUT ADMIN OPTION
+ NOREPLICATION -- WITHOUT ADMIN OPTION is implied
+ NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
+ NOINHERIT WITHOUT ADMIN OPTION
+ CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
+ VALID ALWAYS WITHOUT ADMIN OPTION
+ PASSWORD NULL WITHOUT ADMIN OPTION;
+
+-- fail, not granted privilege to create these users
+SET SESSION AUTHORIZATION regress_role_repladmin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+CREATE ROLE regress_nosuch_createrole CREATEROLE;
+CREATE ROLE regress_nosuch_createdb CREATEDB;
+CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
+CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+
+-- ok, can create login and replication users
+CREATE ROLE regress_role_login LOGIN;
+CREATE ROLE regress_role_replication REPLICATION;
+CREATE ROLE regress_role_login_replication LOGIN REPLICATION;
+
+-- fail, cannot create privileged users
+SET SESSION AUTHORIZATION regress_role_minoradmin;
+CREATE ROLE regress_nosuch_superuser SUPERUSER;
+CREATE ROLE regress_nosuch_createrole CREATEROLE;
+CREATE ROLE regress_nosuch_createdb CREATEDB;
+CREATE ROLE regress_nosuch_replication REPLICATION;
+CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
+
+-- ok, can create unprivileged roles
+CREATE ROLE regress_role_unprivileged;
+
+-- fail, CREATEROLE does not imply ADMIN on roles
+CREATE ROLE regress_nosuch_inrole IN ROLE regress_role_unprivileged;
+CREATE ROLE regress_nosuch_inrole IN GROUP regress_role_unprivileged;
+
+-- fail, having CREATEROLE does not by default give ADMIN OPTION on these
SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_superuser SUPERUSER;
CREATE ROLE regress_nosuch_replication_bypassrls REPLICATION BYPASSRLS;
CREATE ROLE regress_nosuch_replication REPLICATION;
CREATE ROLE regress_nosuch_bypassrls BYPASSRLS;
--- ok, having CREATEROLE is enough to create users with these privileges
+-- ok, having CREATEROLE does by default give ADMIN OPTION on these
CREATE ROLE regress_createdb CREATEDB;
CREATE ROLE regress_createrole CREATEROLE;
CREATE ROLE regress_login LOGIN;
@@ -24,7 +74,14 @@ CREATE ROLE regress_noiseword SYSID 12345;
-- fail, cannot grant membership in superuser role
CREATE ROLE regress_nosuch_super IN ROLE regress_role_super;
+-- fail, lack ADMIN privilege on role pg_database_owner;
+CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
+
-- fail, database owner cannot have members
+RESET SESSION AUTHORIZATION;
+GRANT pg_database_owner TO regress_role_admin WITH ADMIN OPTION;
+
+SET SESSION AUTHORIZATION regress_role_admin;
CREATE ROLE regress_nosuch_dbowner IN ROLE pg_database_owner;
-- ok, can grant other users into a role
@@ -74,7 +131,27 @@ DROP VIEW tenant_view;
-- fail, cannot take ownership of these objects from regress_tenant
REASSIGN OWNED BY regress_tenant TO regress_createrole;
--- ok, having CREATEROLE is enough to create roles in privileged roles
+-- fail, having CREATEROLE is not enough to create roles in privileged roles
+CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
+CREATE ROLE regress_nosuch_write_all_data IN ROLE pg_write_all_data;
+CREATE ROLE regress_nosuch_monitor IN ROLE pg_monitor;
+CREATE ROLE regress_nosuch_read_all_settings IN ROLE pg_read_all_settings;
+CREATE ROLE regress_nosuch_read_all_stats IN ROLE pg_read_all_stats;
+CREATE ROLE regress_nosuch_stat_scan_tables IN ROLE pg_stat_scan_tables;
+CREATE ROLE regress_nosuch_read_server_files IN ROLE pg_read_server_files;
+CREATE ROLE regress_nosuch_write_server_files IN ROLE pg_write_server_files;
+CREATE ROLE regress_nosuch_execute_server_program IN ROLE pg_execute_server_program;
+CREATE ROLE regress_nosuch_signal_backend IN ROLE pg_signal_backend;
+
+-- ok, superuser can grant ADMIN on privileged roles
+SET SESSION AUTHORIZATION regress_role_super;
+GRANT pg_read_all_data, pg_write_all_data, pg_monitor, pg_read_all_settings,
+ pg_read_all_stats, pg_stat_scan_tables, pg_read_server_files,
+ pg_write_server_files, pg_execute_server_program, pg_signal_backend
+TO regress_createrole WITH ADMIN OPTION;
+
+-- ok, having CREATEROLE plus ADMIN is enough to create roles in privileged roles
+SET SESSION AUTHORIZATION regress_createrole;
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;
@@ -98,7 +175,75 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, cannot change attributes without ADMIN for them
+SET SESSION AUTHORIZATION regress_role_minoradmin;
+ALTER ROLE regress_role_login LOGIN;
+ALTER ROLE regress_role_login NOLOGIN;
+ALTER ROLE regress_role_login REPLICATION;
+ALTER ROLE regress_role_login NOREPLICATION;
+ALTER ROLE regress_role_login CREATEDB;
+ALTER ROLE regress_role_login NOCREATEDB;
+ALTER ROLE regress_role_login CREATEROLE;
+ALTER ROLE regress_role_login NOCREATEROLE;
+ALTER ROLE regress_role_login INHERIT;
+ALTER ROLE regress_role_login NOINHERIT;
+ALTER ROLE regress_role_login CONNECTION LIMIT 5;
+ALTER ROLE regress_role_login VALID ALWAYS;
+ALTER ROLE regress_role_login PASSWORD 'foobar';
+
+-- ok, regress_role_admin got ADMIN on attributes by way of having CREATEROLE
+SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_role_login LOGIN;
+ALTER ROLE regress_role_login NOLOGIN;
+ALTER ROLE regress_role_login CREATEDB;
+ALTER ROLE regress_role_login NOCREATEDB;
+ALTER ROLE regress_role_login CREATEROLE;
+ALTER ROLE regress_role_login NOCREATEROLE;
+ALTER ROLE regress_role_login INHERIT;
+ALTER ROLE regress_role_login NOINHERIT;
+ALTER ROLE regress_role_login CONNECTION LIMIT 5;
+ALTER ROLE regress_role_login VALID ALWAYS;
+ALTER ROLE regress_role_login PASSWORD 'foobar';
+
+-- fail, regress_role_admin did not get ADMIN on these
+ALTER ROLE regress_role_login SUPERUSER;
+ALTER ROLE regress_role_login NOSUPERUSER;
+ALTER ROLE regress_role_login REPLICATION;
+ALTER ROLE regress_role_login NOREPLICATION;
+ALTER ROLE regress_role_login BYPASSRLS;
+ALTER ROLE regress_role_login NOBYPASSRLS;
+
+-- superuser can grant them now, though
+RESET SESSION AUTHORIZATION;
+ALTER ROLE regress_role_admin
+ NOREPLICATION WITH ADMIN OPTION
+ NOBYPASSRLS WITH ADMIN OPTION;
+
+-- ok, regress_role_admin can now grant these
+SET SESSION AUTHORIZATION regress_role_admin;
+ALTER ROLE regress_role_login REPLICATION;
+ALTER ROLE regress_role_login NOREPLICATION;
+ALTER ROLE regress_role_login BYPASSRLS;
+ALTER ROLE regress_role_login NOBYPASSRLS;
+
+-- fail, but regress_role_admin still cannot grant this
+ALTER ROLE regress_role_login SUPERUSER;
+ALTER ROLE regress_role_login NOSUPERUSER;
+
+-- ok, regress_role_admin can grant attributes with ADMIN
+ALTER ROLE regress_role_unprivileged LOGIN WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CREATEDB WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CREATEROLE WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged INHERIT WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged CONNECTION LIMIT 5 WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged VALID ALWAYS WITH ADMIN OPTION;
+ALTER ROLE regress_role_unprivileged PASSWORD 'foobar' WITH ADMIN OPTION;
+
-- ok, should be able to drop non-superuser roles we created
+DROP ROLE regress_role_login;
+DROP ROLE regress_role_replication;
+DROP ROLE regress_role_login_replication;
+DROP ROLE regress_role_unprivileged;
DROP ROLE regress_createdb;
DROP ROLE regress_createrole;
DROP ROLE regress_login;
@@ -134,5 +279,7 @@ DROP INDEX tenant_idx;
DROP TABLE tenant_table;
DROP VIEW tenant_view;
DROP ROLE regress_tenant;
+DROP ROLE regress_role_minoradmin;
+DROP ROLE regress_role_repladmin;
DROP ROLE regress_role_admin;
DROP ROLE regress_role_super;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..8658b6e83c 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -72,6 +72,9 @@ CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user4, regress_priv_user2; -- duplicates
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user5, regress_priv_user6, regress_priv_user7;
+ALTER GROUP regress_priv_group2 DROP USER regress_priv_user7, regress_priv_user6, regress_priv_user5;
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 89249ecc97..2bb87b3b45 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2260,6 +2260,7 @@ RmgrData
RmgrDescData
RmgrId
RmgrIds
+RoleElem
RoleSpec
RoleSpecType
RoleStmtType
--
2.21.1 (Apple Git-122.3)
Hi,
On Sat, Jan 29, 2022 at 09:58:38PM -0800, Mark Dilger wrote:
On Jan 25, 2022, at 12:44 PM, Stephen Frost <sfrost@snowman.net> wrote:
I agree that CREATEROLE is overpowered and that the goal of this should
be to provide a way for roles to be created and dropped that doesn't
give the user who has that power everything that CREATEROLE currently
does.I'm attaching a patch that attempts to fix CREATEROLE without any
connection to role ownership.
Sounds like a useful way forward.
The point I was making is that the concept of role ownership
isn't intrinsically linked to that and is, therefore, as you say, gravy.I agree, they aren't intrinsically linked, though the solution to one
might interact in some ways with the solution to the other.That isn't to say that I'm entirely against the role ownership idea but
I'd want it to be focused on the goal of providing ways of creating and
dropping users and otherwise performing that kind of administration and
that doesn't require the specific change to make owners be members of
all roles they own and automatically have all privileges of those roles
all the time.The attached WIP patch attempts to solve most of the CREATEROLE
problems but not the problem of which role who can drop which other
role. That will likely require an ownership concept.The main idea here is that having CREATEROLE doesn't give you ADMIN on
roles, nor on role attributes. For role attributes, the syntax has
been extended. An excerpt from the patch's regression test
illustrates some of that concept:-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"
Great.
-- fail, cannot change attributes without ADMIN for them
SET SESSION AUTHORIZATION regress_role_minoradmin;
ALTER ROLE regress_role_login LOGIN;
ERROR: must have admin on login to change login attributeALTER ROLE regress_role_login NOLOGIN;
ERROR: must have admin on login to change login attributeWhether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied
hinges on whether the role is given CREATEROLE. That hackery is
necessary to preserve backwards compatibility. If we don't care about
compatibility, I could change the patch to make "WITHOUT ADMIN OPTION"
implied for all attributes when not specified.I'd appreciate feedback on the direction this patch is going.
One thing I noticed (and which will likely make DBAs grumpy) is that it
seems being able to create users (as opposed to non-login roles/groups)
depends on when you get the CREATEROLE attribute (on role creation or
later), viz:
postgres=# CREATE USER admin CREATEROLE;
CREATE ROLE
postgres=# SET ROLE admin;
SET
postgres=> CREATE USER testuser; -- this works
CREATE ROLE
postgres=> RESET ROLE;
RESET
postgres=# CREATE USER admin2;
CREATE ROLE
postgres=# ALTER ROLE admin2 CREATEROLE; -- we get CREATEROLE after the fact
ALTER ROLE
postgres=# SET ROLE admin2;
SET
postgres=> CREATE USER testuser2; -- bam
ERROR: must have grant option on LOGIN privilege to create login users
postgres=# SELECT rolname, admcreaterole, admcanlogin FROM pg_authid
WHERE rolname LIKE 'admin%';
rolname | admcreaterole | admcanlogin
---------+---------------+-------------
admin | t | t
admin2 | f | f
(2 rows)
Is that intentional? If it is, I think it would be nice if this could be
changed, unless I'm missing some serious security concerns or so.
Some light review of the patch (I haven't read all the previous ones, so
please excuse me if I rehash old discussions):
From 82d235b39b32ca0cd0b94d47a54ee6806645a365 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Fri, 28 Jan 2022 07:57:57 -0800
Subject: [PATCH v8] Adding admin options for role attributesWhen creating roles, attributes such as BYPASSRLS can be optionally
specified WITH ADMIN OPTION or WITHOUT ADMIN OPTION. If these
optional clauses are unspecified, they all default to WITHOUT
unless the role being created is given CREATEROLE, in which case
they default to WITHOUT for SUPERUSER, REPLICATION, and BYPASSRLS
and true for all others. This preserves backwards compatible
behavior.The CREATEROLE attribute no longer makes up for lacking the ADMIN
option on a role. The creator of a role only has the ADMIN-like
right to grant other roles into the new role during the creation
statement itself. After that, the creator may only do so if the
creator has ADMIN on the role. Note that creators may add
themselves to the list of ADMINs on the new role during creation
time.SUPERUSER can still only be granted by superusers.
---
doc/src/sgml/ref/create_role.sgml | 50 ++--
src/backend/catalog/aclchk.c | 179 ++++++++++++--
src/backend/commands/user.c | 278 +++++++++++++++++-----
src/backend/parser/gram.y | 161 ++++++++++---
src/include/catalog/pg_authid.dat | 52 +++-
src/include/catalog/pg_authid.h | 10 +
src/include/nodes/nodes.h | 1 +
src/include/nodes/parsenodes.h | 11 +-
src/include/utils/acl.h | 12 +
src/test/regress/expected/create_role.out | 188 ++++++++++++++-
src/test/regress/expected/privileges.out | 4 +
src/test/regress/sql/create_role.sql | 153 +++++++++++-
src/test/regress/sql/privileges.sql | 3 +
src/tools/pgindent/typedefs.list | 1 +
14 files changed, 936 insertions(+), 167 deletions(-)diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml index b6a4ea1f72..7163779e0a 100644 --- a/doc/src/sgml/ref/create_role.sgml +++ b/doc/src/sgml/ref/create_role.sgml @@ -26,15 +26,22 @@ CREATE ROLE <replaceable class="parameter">name</replaceable> [ [ WITH ] <replac <phrase>where <replaceable class="parameter">option</replaceable> can be:</phrase>SUPERUSER | NOSUPERUSER - | CREATEDB | NOCREATEDB - | CREATEROLE | NOCREATEROLE - | INHERIT | NOINHERIT - | LOGIN | NOLOGIN - | REPLICATION | NOREPLICATION - | BYPASSRLS | NOBYPASSRLS - | CONNECTION LIMIT <replaceable class="parameter">connlimit</replaceable> - | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' | PASSWORD NULL - | VALID UNTIL '<replaceable class="parameter">timestamp</replaceable>' + | INHERIT [ { WITH | WITHOUT } GRANT OPTION ] + | NOINHERIT [ { WITH | WITHOUT } GRANT OPTION ]
Spaces vs. tabs here...
+ | CREATEDB [ { WITH | WITHOUT } GRANT OPTION ] + | NOCREATEDB [ { WITH | WITHOUT } GRANT OPTION ] + | CREATEROLE [ { WITH | WITHOUT } GRANT OPTION ] + | NOCREATEROLE [ { WITH | WITHOUT } GRANT OPTION ] + | LOGIN [ { WITH | WITHOUT } GRANT OPTION ] + | NOLOGIN [ { WITH | WITHOUT } GRANT OPTION ] + | REPLICATION [ { WITH | WITHOUT } GRANT OPTION ] + | NOREPLICATION [ { WITH | WITHOUT } GRANT OPTION ] + | BYPASSRLS [ { WITH | WITHOUT } GRANT OPTION ] + | NOBYPASSRLS [ { WITH | WITHOUT } GRANT OPTION ] + | CONNECTION LIMIT [ <replaceable class="parameter">connlimit</replaceable> | NONE ] [ { WITH | WITHOUT } GRANT OPTION ] + | [ ENCRYPTED ] PASSWORD '<replaceable class="parameter">password</replaceable>' [ { WITH | WITHOUT } GRANT OPTION ] + | PASSWORD NULL [ { WITH | WITHOUT } GRANT OPTION ]
... and here, is that intentional?
@@ -356,6 +363,18 @@ in sync when changing the above synopsis!
<link linkend="sql-revoke"><command>REVOKE</command></link>.
</para>+ <para> + Some parameters allow the <literal>WITH ADMIN OPTION</literal> or + <literal>WITHOUT ADMIN OPTION</literal> clause to be specified. For roles + with the <literal>CREATEROLE</literal> attribute, these clauses govern + whether new roles may be created with the attribute. If not given, for + reasons of backwards compatibility, <literal>WITHOUT ADMIN OPTION</literal> + is the default for <literal>REPLICATION</literal> and + <literal>BYPASSRLS</literal>, but <literal>WITH ADMIN OPTION</literal> is + the default for <literal>CREATEDB</literal>, <literal>CREATEROLE</literal>, + and <literal>LOGIN</literal>. + </para> + <para> The <literal>VALID UNTIL</literal> clause defines an expiration time for a password only, not for the role per se. In diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 1dd03a8e51..c66f545f36 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -5430,6 +5430,91 @@ pg_statistics_object_ownercheck(Oid stat_oid, Oid roleid) return has_privs_of_role(roleid, ownerId); }+typedef enum ROLPRIV
I think typdefs usually go at the top of the file, not at line 5441...
+{ + CREATEROLE, + CREATEDB, + CANLOGIN, + REPLICATION, + BYPASSRLS, + INHERIT, + CONNLIMIT, + VALIDUNTIL, + PASSWORD +} ROLPRIV; +
[...]
/*
* Check whether specified role has CREATEROLE privilege (or is a superuser)
*
I feel this function comment needs revision; we now have a dozen similar
functions that all do the same, but only the first one
(has_createrole_privilege) is being explained.
I guess the comment overall is still applicable, so as a minimum maybe
just change the CREATEROLE above for a generic "has some privilege", and
add a space in order to make it clear this applies to all of the
following functions.
Hrm, maybe also mention why there may_admin_*_privilege for all
privileges, but has_*_privilege only for some.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index f9d3c1246b..501613a840 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -255,27 +305,36 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create superusers"))); } - else if (isreplication) - { - if (!superuser()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("must be superuser to create replication users"))); - } - else if (bypassrls) - { - if (!superuser()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("must be superuser to create bypassrls users"))); - } - else - { - if (!have_createrole_privilege()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied to create role"))); - } + + if (createrole && !may_admin_createrole_privilege(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must have grant option on CREATEROLE privilege to create createrole users")));
Shouldn't this (and the following) be "must have admin option on
CREATEROLE"?
@@ -311,7 +370,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
stmt->role)));/* Convert validuntil to internal form */ - if (validUntil) + if (validUntil && strcmp(validUntil, "always") != 0)
This (there are other similar hunks further down) looks like an
independent patch/feature?
@@ -637,32 +727,57 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to alter superuser roles or change superuser attribute"))); } - else if (authform->rolreplication || disreplication) - { - if (!superuser()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("must be superuser to alter replication roles or change replication attribute"))); - } - else if (dbypassRLS) - { - if (!superuser()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("must be superuser to change bypassrls attribute"))); - } - else if (!have_createrole_privilege()) - { - /* check the rest */ - if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit || - drolemembers || dvalidUntil || !dpassword || roleid != GetUserId()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied"))); - } + + /* To mess with replication roles, must have admin on REPLICATION */ + if ((authform->rolreplication || disreplication) && + !may_admin_replication_privilege(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must have admin on replication to alter replication roles or change replication attribute")));
"have admin" sounds a bit weird, but I understand the error message is
too long already to spell out "must have admin option"? Or am I mistaken
and "admin" is what it's actually called (same for the ones below)?
Also, I think those role options are usually capitalized like
REPLICATION in other error messages.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b5966712ce..7503d3ead6 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -1131,67 +1140,111 @@ AlterOptRoleElem:
[...]
+ | VALID ALWAYS opt_admin_spec + { + RoleElem *n = makeNode(RoleElem); + n->elem = makeDefElem("validUntil", (Node *)makeString("always"), @1); + n->admin_spec = $3; + $$ = (Node *)n;
This one is from another patch as well I think.
} /* Supported but not documented for roles, for use by ALTER GROUP. */ - | USER role_list + | USER role_list opt_admin_spec { - $$ = makeDefElem("rolemembers", (Node *)$2, @1); + RoleElem *n = makeNode(RoleElem); + n->elem = makeDefElem("rolemembers", (Node *)$2, @1); + n->admin_spec = $3; + $$ = (Node *)n; } - | IDENT + | IDENT opt_admin_spec { /* * We handle identifiers that aren't parser keywords with * the following special-case codes, to avoid bloating the * size of the main parser. */ + RoleElem *n = makeNode(RoleElem); + + /* + * Record whether the user specified WITH GRANT OPTION.
WITH ADMIN OPTION rather?
+ * Note that for some privileges this is always implied, + * such as SUPERUSER, but we don't reflect that here. + */ + n->admin_spec = $2; +
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 6c28119fa1..4829a6dbd2 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -22,67 +22,93 @@ { oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID', rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', - rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', + rolreplication => 't', rolbypassrls => 't', adminherit => 't', admcreaterole => 't', + admcreatedb => 't', admcanlogin => 't', admreplication => 't', admbypassrls => 't', + admconnlimit => 't', admpassword => 't', admvaliduntil => 't', rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' },
Those sure are a couple of new columns in pg_authid, but oh well...
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index 4b65e39a1f..4acdcaa685 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -39,6 +39,16 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 bool rolcanlogin; /* allowed to log in as session user? */ bool rolreplication; /* role used for streaming replication */ bool rolbypassrls; /* bypasses row-level security? */ + + bool adminherit; /* allowed to administer inherit? */ + bool admcreaterole; /* allowed to administer createrole? */ + bool admcreatedb; /* allowed to administer createdb?? */ + bool admcanlogin; /* allowed to administer login? */ + bool admreplication; /* allowed to administer replication? */ + bool admbypassrls; /* allowed to administer bypassesrls? */ + bool admconnlimit; /* allowed to administer connlimit? */ + bool admpassword; /* allowed to administer password? */ + bool admvaliduntil; /* allowed to administer validuntil? */ int32 rolconnlimit; /* max connections allowed (-1=no limit) */
It's cosmetic, but the space between rolbypassrls and adminherit is
maybe not needed, and I'd put rolconnlimit first (even though it has a
different type).
Michael
--
Michael Banck
Teamleiter PostgreSQL-Team
Projektleiter
Tel.: +49 2166 9901-171
Email: michael.banck@credativ.de
credativ GmbH, HRB M�nchengladbach 12080
USt-ID-Nummer: DE204566209
Trompeterallee 108, 41189 M�nchengladbach
Gesch�ftsf�hrung: Dr. Michael Meskes, Geoff Richardson, Peter Lilley
Unser Umgang mit personenbezogenen Daten unterliegt
folgenden Bestimmungen: https://www.credativ.de/datenschutz
On Jan 30, 2022, at 2:38 PM, Michael Banck <michael.banck@credativ.de> wrote:
Hi,
Your review is greatly appreciated!
The attached WIP patch attempts to solve most of the CREATEROLE
I'm mostly looking for whether the general approach in this Work In Progress patch is acceptable, so I was a bit sloppy with whitespace and such....
One thing I noticed (and which will likely make DBAs grumpy) is that it
seems being able to create users (as opposed to non-login roles/groups)
depends on when you get the CREATEROLE attribute (on role creation or
later), viz:postgres=# CREATE USER admin CREATEROLE;
CREATE ROLE
postgres=# SET ROLE admin;
SET
postgres=> CREATE USER testuser; -- this works
CREATE ROLE
postgres=> RESET ROLE;
RESET
postgres=# CREATE USER admin2;
CREATE ROLE
postgres=# ALTER ROLE admin2 CREATEROLE; -- we get CREATEROLE after the fact
ALTER ROLE
postgres=# SET ROLE admin2;
SET
postgres=> CREATE USER testuser2; -- bam
ERROR: must have grant option on LOGIN privilege to create login users
postgres=# SELECT rolname, admcreaterole, admcanlogin FROM pg_authid
WHERE rolname LIKE 'admin%';
rolname | admcreaterole | admcanlogin
---------+---------------+-------------
admin | t | t
admin2 | f | f
(2 rows)Is that intentional? If it is, I think it would be nice if this could be
changed, unless I'm missing some serious security concerns or so.
It's intentional, but part of what I wanted review comments about. The issue is that historically:
CREATE USER michael CREATEROLE
meant that you could go on to do things like create users with LOGIN privilege. I could take that away, which would be a backwards compatibility break, or I can do the weird thing this patch does. Or I could have your
ALTER ROLE admin2 CREATEROLE;
also grant the other privileges like LOGIN unless you explicitly say otherwise with a bunch of explicit WITHOUT ADMIN OPTION clauses. Finding out which of those this is preferred was a big part of why I put this up for review. Thanks for calling it out in under 24 hours!
Some light review of the patch (I haven't read all the previous ones, so
please excuse me if I rehash old discussions):
Not a problem.
Spaces vs. tabs here...
... and here, is that intentional?
I think typdefs usually go at the top of the file, not at line 5441...
I feel this function comment needs revision...
Hrm, maybe also mention ...
All good comments, but I'm not doing code cleanup on this WIP patch just yet. Forgive me.
Shouldn't this (and the following) be "must have admin option on
CREATEROLE"?
Yes, there may be other places where I failed to replace the verbiage "grant option" with "admin option". Earlier drafts of the patch were using that language. I wouldn't mind review comments on which language people thinks is better.
@@ -311,7 +370,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
stmt->role)));/* Convert validuntil to internal form */ - if (validUntil) + if (validUntil && strcmp(validUntil, "always") != 0)This (there are other similar hunks further down) looks like an
independent patch/feature?
Part of the problem with the grammar introduced in this patch is that you are not normally required to mention attributes like VALID UNTIL, but if you want to change whether the created role gets WITH ADMIN OPTION, you have to. That leaves the problem of what to do if you *only* want to specify the ADMIN part. The grammar needs some sort of "dummy" value that intentionally has no effect, but sets up for the WITH/WITHOUT ADMIN OPTION clause. I think I left a few bits of cruft around like that. But what I'd really like to know is if people think this sort of thing is even headed in the right direction? Are there problems with SQL spec compliance? Does it just feel icky? I don't have any pride-of-ownership in the grammar this WIP patch introduces. I just needed something to put out there for people to attack/improve.
+ + /* To mess with replication roles, must have admin on REPLICATION */ + if ((authform->rolreplication || disreplication) && + !may_admin_replication_privilege(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must have admin on replication to alter replication roles or change replication attribute")));"have admin" sounds a bit weird, but I understand the error message is
too long already to spell out "must have admin option"? Or am I mistaken
and "admin" is what it's actually called (same for the ones below)?
If it is the officially correct language, I arrived at it by accident. I didn't take any time to wordsmith those error messages. Improvements welcome!
Also, I think those role options are usually capitalized like
REPLICATION in other error messages.
Yeah, I noticed some amount of inconsistency there. For a brief time I was trying to make them all the same, but got a bit confused on what would be correct, and didn't waste the time. The sort of thing I'm thinking about is the pre-existing message text, "must be superuser to change bypassrls attribute". Note that neither "superuser" nor "bypassrls" are capitalized. If people like where this patch is going, I'll no doubt need to clean it up.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b5966712ce..7503d3ead6 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -1131,67 +1140,111 @@ AlterOptRoleElem:[...]
+ | VALID ALWAYS opt_admin_spec + { + RoleElem *n = makeNode(RoleElem); + n->elem = makeDefElem("validUntil", (Node *)makeString("always"), @1); + n->admin_spec = $3; + $$ = (Node *)n;This one is from another patch as well I think.
That was an attempt at a "dummy" type value. I agree it probably doesn't belong.
} /* Supported but not documented for roles, for use by ALTER GROUP. */ - | USER role_list + | USER role_list opt_admin_spec { - $$ = makeDefElem("rolemembers", (Node *)$2, @1); + RoleElem *n = makeNode(RoleElem); + n->elem = makeDefElem("rolemembers", (Node *)$2, @1); + n->admin_spec = $3; + $$ = (Node *)n; } - | IDENT + | IDENT opt_admin_spec { /* * We handle identifiers that aren't parser keywords with * the following special-case codes, to avoid bloating the * size of the main parser. */ + RoleElem *n = makeNode(RoleElem); + + /* + * Record whether the user specified WITH GRANT OPTION.WITH ADMIN OPTION rather?
Yes.
+ * Note that for some privileges this is always implied, + * such as SUPERUSER, but we don't reflect that here. + */ + n->admin_spec = $2; +diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index 6c28119fa1..4829a6dbd2 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -22,67 +22,93 @@ { oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID', rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', - rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', + rolreplication => 't', rolbypassrls => 't', adminherit => 't', admcreaterole => 't', + admcreatedb => 't', admcanlogin => 't', admreplication => 't', admbypassrls => 't', + admconnlimit => 't', admpassword => 't', admvaliduntil => 't', rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' },Those sure are a couple of new columns in pg_authid, but oh well...
Yes, that's also a big part of what people might object to. I think it's a reasonable objection, but I don't know where else to put the information, given the lack of an aclitem[]?
diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index 4b65e39a1f..4acdcaa685 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -39,6 +39,16 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 bool rolcanlogin; /* allowed to log in as session user? */ bool rolreplication; /* role used for streaming replication */ bool rolbypassrls; /* bypasses row-level security? */ + + bool adminherit; /* allowed to administer inherit? */ + bool admcreaterole; /* allowed to administer createrole? */ + bool admcreatedb; /* allowed to administer createdb?? */ + bool admcanlogin; /* allowed to administer login? */ + bool admreplication; /* allowed to administer replication? */ + bool admbypassrls; /* allowed to administer bypassesrls? */ + bool admconnlimit; /* allowed to administer connlimit? */ + bool admpassword; /* allowed to administer password? */ + bool admvaliduntil; /* allowed to administer validuntil? */ int32 rolconnlimit; /* max connections allowed (-1=no limit) */It's cosmetic, but the space between rolbypassrls and adminherit is
maybe not needed, and I'd put rolconnlimit first (even though it has a
different type).
Oh, totally agree. I had that blank there during development because the "rol..." and "adm..." all started to blur together.
Thanks again! If the patch stays mostly like it is, I'll incorporate all your review comments into a next version.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Hi,
Am Sonntag, dem 30.01.2022 um 17:11 -0800 schrieb Mark Dilger:
On Jan 30, 2022, at 2:38 PM, Michael Banck <
michael.banck@credativ.de> wrote:The attached WIP patch attempts to solve most of the CREATEROLE
I'm mostly looking for whether the general approach in this Work In
Progress patch is acceptable, so I was a bit sloppy with whitespace
and such....
Ok, sure. I think this topic is hugely important and as I read the
patch anyway, I added some comments, but yeah, we need to figure out
the fundamentals first.
One thing I noticed (and which will likely make DBAs grumpy) is that it
seems being able to create users (as opposed to non-login roles/groups)
depends on when you get the CREATEROLE attribute (on role creation or
later), viz:postgres=# CREATE USER admin CREATEROLE;
CREATE ROLE
postgres=# SET ROLE admin;
SET
postgres=> CREATE USER testuser; -- this works
CREATE ROLE
postgres=> RESET ROLE;
RESET
postgres=# CREATE USER admin2;
CREATE ROLE
postgres=# ALTER ROLE admin2 CREATEROLE; -- we get CREATEROLE after the fact
ALTER ROLE
postgres=# SET ROLE admin2;
SET
postgres=> CREATE USER testuser2; -- bam
ERROR: must have grant option on LOGIN privilege to create login users
postgres=# SELECT rolname, admcreaterole, admcanlogin FROM
pg_authid
WHERE rolname LIKE 'admin%';
rolname | admcreaterole | admcanlogin
---------+---------------+-------------
admin | t | t
admin2 | f | f
(2 rows)Is that intentional? If it is, I think it would be nice if this
could be
changed, unless I'm missing some serious security concerns or so.It's intentional, but part of what I wanted review comments about.
The issue is that historically:CREATE USER michael CREATEROLE
meant that you could go on to do things like create users with LOGIN
privilege. I could take that away, which would be a backwards
compatibility break, or I can do the weird thing this patch does. Or
I could have yourALTER ROLE admin2 CREATEROLE;
also grant the other privileges like LOGIN unless you explicitly say
otherwise with a bunch of explicit WITHOUT ADMIN OPTION clauses.
Finding out which of those this is preferred was a big part of why I
put this up for review. Thanks for calling it out in under 24 hours!
Ok, so what I would have needed to do in the above in order to have
"admin2" and "admin" be the same as far as creating login users is (I
believe):
ALTER ROLE admin2 CREATEROLE LOGIN WITH ADMIN OPTION;
I think if possible, it would be nice to just have this part as default
if possible. I.e. CREATEROLE and HASLOGIN are historically so much
intertwined that I think the above should be implicit (again, if that
is possible); I don't care and/or haven't made up my mind about any of
the other options so far...
Ok, so now that I had another look, I see we are going down Pandora's
box: For any of the other things a role admin would like to do (change
password, change conn limit), one would have to go with this weird
disconnect between CREATE USER admin CREATEROLE and ALTER USER admin2
CREATEROLE [massive list of WITH ADMIN OPTION], and then I'm not sure
where we stop.
By the way, is there now even a way to add admpassword to a role after
it got created?
postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
ERROR: must have admin on password to change password attribute
postgres=> RESET ROLE;
RESET
postgres=# ALTER ROLE admin2 PASSWORD WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
UPDATE pg_authid SET admpassword = 't' WHERE rolname = 'admin2';
UPDATE 1
postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
postgres=>
However, the next thing is:
postgres=# SET ROLE admin;
SET
postgres=> CREATE GROUP testgroup;
CREATE ROLE
postgres=> GRANT testgroup TO test;
ERROR: must have admin option on role "testgroup"
First off, what does "admin option" mean on a role?
I then tried this:
postgres=# CREATE USER admin3 CREATEROLE WITH ADMIN OPTION;
CREATE ROLE
postgres=# SET ROLE admin3;
SET
postgres=> CREATE USER test3;
CREATE ROLE
postgres=> CREATE GROUP testgroup3;
CREATE ROLE
postgres=> GRANT testgroup3 TO test3;
ERROR: must have admin option on role "testgroup3"
So I created both user and group, I have the CREATEROLE priv (with or
without admin option), but I still can't assign the group. Is that
(tracking who created a role and letting the creator do more thing) the
part that got chopped away in your last patch in order to find a common
ground?
Is there now any way non-Superusers can assign groups to other users? I
feel this (next to creating users/groups) is the primary thing those
CREATEROLE admins are supposed to do/where doing up to now.
Again, sorry if this was all discussed previously, I only skimmed this
thread.
Two more comments regarding the code:
b/src/include/catalog/pg_authid.dat index 6c28119fa1..4829a6dbd2 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -22,67 +22,93 @@ { oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID', rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', rolcreaterole => 't', rolcreatedb => 't', rolcanlogin => 't', - rolreplication => 't', rolbypassrls => 't', rolconnlimit => '-1', + rolreplication => 't', rolbypassrls => 't', adminherit => 't', admcreaterole => 't', + admcreatedb => 't', admcanlogin => 't', admreplication => 't', admbypassrls => 't', + admconnlimit => 't', admpassword => 't', admvaliduntil => 't', rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' },Those sure are a couple of new columns in pg_authid, but oh well...
Yes, that's also a big part of what people might object to. I think
it's a reasonable objection, but I don't know where else to put the
information, given the lack of an aclitem[]?
Yeah, it crossed my mind that an array might not be bad. In any case,
if we can fix CREATEROLE for good, a couple of extra columns in
pg_authid might be a small price to pay.
diff --git a/src/include/catalog/pg_authid.h
b/src/include/catalog/pg_authid.h index 4b65e39a1f..4acdcaa685 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -39,6 +39,16 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 bool rolcanlogin; /* allowed to log in as session user? */ bool rolreplication; /* role used for streaming replication */ bool rolbypassrls; /* bypasses row-level security? */ + + bool adminherit; /* allowed to administer inherit? */ + bool admcreaterole; /* allowed to administer createrole? */ + bool admcreatedb; /* allowed to administer createdb?? */ + bool admcanlogin; /* allowed to administer login? */ + bool admreplication; /* allowed to administer replication? */ + bool admbypassrls; /* allowed to administer bypassesrls? */ + bool admconnlimit; /* allowed to administer connlimit? */ + bool admpassword; /* allowed to administer password? */ + bool admvaliduntil; /* allowed to administer validuntil? */ int32 rolconnlimit; /* max connections allowed (-1=no limit) */It's cosmetic, but the space between rolbypassrls and adminherit is
maybe not needed, and I'd put rolconnlimit first (even though it
has a different type).Oh, totally agree. I had that blank there during development because
the "rol..." and "adm..." all started to blur together.
The way the adm* privs are now somewhere in the middle of the rol*
privs also looks weird for the end-user and there does not seems to be
some greater scheme behind it:
postgres=# SELECT * FROM pg_authid WHERE rolname = 'admin' \gx
-[ RECORD 1 ]--+------
oid | 16385
rolname | admin
rolsuper | f
rolinherit | t
rolcreaterole | t
rolcreatedb | f
rolcanlogin | t
rolreplication | f
rolbypassrls | f
adminherit | t
admcreaterole | t
admcreatedb | t
admcanlogin | t
admreplication | f
admbypassrls | f
admconnlimit | t
admpassword | t
admvaliduntil | t
rolconnlimit | -1
rolpassword |
rolvaliduntil |
Michael
--
Michael Banck
Teamleiter PostgreSQL-Team
Projektleiter
Tel.: +49 2166 9901-171
E-Mail: michael.banck@credativ.de
credativ GmbH, HRB Mönchengladbach 12080
USt-ID-Nummer: DE204566209
Trompeterallee 108, 41189 Mönchengladbach
Geschäftsführung: Dr. Michael Meskes, Geoff Richardson, Peter Lilley
Unser Umgang mit personenbezogenen Daten unterliegt
folgenden Bestimmungen: https://www.credativ.de/datenschutz
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 25, 2022, at 12:44 PM, Stephen Frost <sfrost@snowman.net> wrote:
I agree that CREATEROLE is overpowered and that the goal of this should
be to provide a way for roles to be created and dropped that doesn't
give the user who has that power everything that CREATEROLE currently
does.I'm attaching a patch that attempts to fix CREATEROLE without any connection to role ownership.
Alright.
The point I was making is that the concept of role ownership
isn't intrinsically linked to that and is, therefore, as you say, gravy.I agree, they aren't intrinsically linked, though the solution to one might interact in some ways with the solution to the other.
Sure.
That isn't to say that I'm entirely against the role ownership idea but
I'd want it to be focused on the goal of providing ways of creating and
dropping users and otherwise performing that kind of administration and
that doesn't require the specific change to make owners be members of
all roles they own and automatically have all privileges of those roles
all the time.The attached WIP patch attempts to solve most of the CREATEROLE problems but not the problem of which role who can drop which other role. That will likely require an ownership concept.
Yeah, we do need to have a way to determine who is allowed to drop
roles and role ownership seems like it's one possible approach to that.
The main idea here is that having CREATEROLE doesn't give you ADMIN on roles, nor on role attributes. For role attributes, the syntax has been extended. An excerpt from the patch's regression test illustrates some of that concept:
-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;
Right, this was one of the approaches that I was thinking could work for
managing role attributes and it's very similar to roles and the admin
option for them. As I suggested at least once, another possible
approach could be to have login users not be able to create roles but
for them to be able to SET ROLE to a role which is able to create roles,
and then, using your prior method, only allow the attributes which that
role has to be able to be given to other roles. That essentially makes
a role be a proxy for the per-attribute admin options. There's pros and
cons for each approach and so I'm curious as to which you feel is the
better approach? I get the feeling that you're more inclined to go with
the approach of having an admin option for each role attribute (having
written this WIP patch) but I'm not sure if that is because you
contempltaed both and felt this was better for some reason or more
because I wasn't explaining the other approach very well, or if there
was some other reason.
-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"
I would say not just privileged roles, but any roles that the user
doesn't have admin rights on.
Whether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied hinges on whether the role is given CREATEROLE. That hackery is necessary to preserve backwards compatibility. If we don't care about compatibility, I could change the patch to make "WITHOUT ADMIN OPTION" implied for all attributes when not specified.
Given the relative size of the changes we're talking about regarding
CREATEROLE, I don't really think we need to stress about backwards
compatibility too much.
Thanks,
Stephen
On Jan 31, 2022, at 12:43 AM, Michael Banck <michael.banck@credativ.de> wrote:
Ok, sure. I think this topic is hugely important and as I read the
patch anyway, I added some comments, but yeah, we need to figure out
the fundamentals first.
Right.
Perhaps some background on this patch series will help. The patch versions before v8 were creating an owner-owned relationship between the creator and the createe, and a lot of privileges were dependent on that ownership. Stephen objected that we were creating parallel tracks on which the privilege system was running; things like belonging to a role or having admin on a role were partially conflated with owning a role. He also objected that the pre-v8 patch sets allowed a creator role with the CREATEROLE privilege to give away any privilege the creator had, rather than needing to have GRANT or ADMIN option on the privilege being given.
The v8-WIP patch is not a complete replacement for the pre-v8 patches. It's just a balloon I'm floating to try out candidate solutions to some of Stephen's objections. In the long run, I want the solution to Stephen's objections to not create problems for anybody who liked the way the pre-v8 patches worked (Robert, Andrew, and to some extent me.)
In this WIP patch, for a creator to give *anything* away to a createe, the creator must have GRANT or ADMIN on the thing being given. That includes attributes like BYPASSRLS, CREATEDB, LOGIN, etc., and also ADMIN on any role the createe is granted into.
I tried to structure things for backwards compatibility, considering which things roles with CREATEROLE could give away historically. It turns out they can give away most everything, but not SUPERUSER, BYPASSRLS, or REPLICATION. So I structured the default privileges for CREATEROLE to match. But I'm uncertain that design is any good, and your comments below suggest that you find it pretty hard to use.
Part of the problem with trying to be backwards compatible is that we must break compatibility anyway, to address the problem that historically having CREATEROLE meant you effectively had ADMIN on all non-superuser roles. That's got to change. So in part I'm asking pgsql-hackers if partial backwards compatibility is worth the bother.
If we don't go with backwards compatibility, then CREATEROLE would only allow you to create a new role, but not to give that role LOGIN, nor CREATEDB, etc. You'd need to also have admin option on those things. To create a role that can give those things away, you'd need to run something like:
CREATE ROLE michael
CREATEROLE WITH ADMIN OPTION -- can further give away "createrole"
CREATEDB WITH ADMIN OPTION -- can further give away "createdb"
LOGIN WITH ADMIN OPTION -- can further give away "login"
NOREPLICATION WITHOUT ADMIN OPTION -- this would be implied anyway
NOBYPASSRLS WITHOUT ADMIN OPTION -- this would be implied anyway
CONNECTION LIMIT WITH ADMIN OPTION -- can specify connection limits
PASSWORD WITH ADMIN OPTION -- can specify passwords
VALID UNTIL WITH ADMIN OPTION -- can specify expiration
(I'm on the fence about the phrase "WITH ADMIN OPTION" vs. the phrase "WITH GRANT OPTION".)
Even then, when "michael" creates new roles, if he wants to be able to further administer those roles, he needs to remember to give himself ADMIN membership in that role at creation time. After the role is created, if he doesn't have ADMIN, he can't give it to himself. So, at create time, he needs to remember to do this:
SET ROLE michael;
CREATE ROLE mark ADMIN michael;
But that's still a bit strange, because "ADMIN michael" means that michael can grant other roles membership in "mark", not that michael can, for example, change mark's password. If we don't want CREATEROLE to imply that you can mess around with arbitrary roles (rather than only roles that you created or have been transferred control over) then we need the concept of role ownership. This patch doesn't go that far, so for now, only superusers can do those things. Assuming some form of this patch is acceptable, the v9 series will resurrect some of the pre-v7 logic for role ownership and say that the owner can do those things.
One thing I noticed (and which will likely make DBAs grumpy) is that it
seems being able to create users (as opposed to non-login roles/groups)
depends on when you get the CREATEROLE attribute (on role creation or
later), viz:postgres=# CREATE USER admin CREATEROLE;
CREATE ROLE
postgres=# SET ROLE admin;
SET
postgres=> CREATE USER testuser; -- this works
CREATE ROLE
postgres=> RESET ROLE;
RESET
postgres=# CREATE USER admin2;
CREATE ROLE
postgres=# ALTER ROLE admin2 CREATEROLE; -- we get CREATEROLE after the fact
ALTER ROLE
postgres=# SET ROLE admin2;
SET
postgres=> CREATE USER testuser2; -- bam
ERROR: must have grant option on LOGIN privilege to create login users
postgres=# SELECT rolname, admcreaterole, admcanlogin FROM
pg_authid
WHERE rolname LIKE 'admin%';
rolname | admcreaterole | admcanlogin
---------+---------------+-------------
admin | t | t
admin2 | f | f
(2 rows)Is that intentional? If it is, I think it would be nice if this
could be
changed, unless I'm missing some serious security concerns or so.It's intentional, but part of what I wanted review comments about.
The issue is that historically:CREATE USER michael CREATEROLE
meant that you could go on to do things like create users with LOGIN
privilege. I could take that away, which would be a backwards
compatibility break, or I can do the weird thing this patch does. Or
I could have yourALTER ROLE admin2 CREATEROLE;
also grant the other privileges like LOGIN unless you explicitly say
otherwise with a bunch of explicit WITHOUT ADMIN OPTION clauses.
Finding out which of those this is preferred was a big part of why I
put this up for review. Thanks for calling it out in under 24 hours!Ok, so what I would have needed to do in the above in order to have
"admin2" and "admin" be the same as far as creating login users is (I
believe):ALTER ROLE admin2 CREATEROLE LOGIN WITH ADMIN OPTION;
Yes, those it's more likely admin2 would have been created with these privileges to begin with, if the creator intended admin2 to do such things.
I think if possible, it would be nice to just have this part as default
if possible. I.e. CREATEROLE and HASLOGIN are historically so much
intertwined that I think the above should be implicit (again, if that
is possible); I don't care and/or haven't made up my mind about any of
the other options so far...
Possibily. But then, if you really wanted to grant someone CREATEROLE but not anything else, you'd need to remember which other things are implicit, and explicitly disavow them, like:
ALTER ROLE admin2 CREATEROLE (WITHOUT this, WITHOUT that, WITHOUT the other)
and I think that mostly stinks.
Ok, so now that I had another look, I see we are going down Pandora's
box: For any of the other things a role admin would like to do (change
password, change conn limit), one would have to go with this weird
disconnect between CREATE USER admin CREATEROLE and ALTER USER admin2
CREATEROLE [massive list of WITH ADMIN OPTION], and then I'm not sure
where we stop.
I agree. That's a good argument for just breaking backward compatibility.
By the way, is there now even a way to add admpassword to a role after
it got created?postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
ERROR: must have admin on password to change password attribute
postgres=> RESET ROLE;
RESET
postgres=# ALTER ROLE admin2 PASSWORD WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
UPDATE pg_authid SET admpassword = 't' WHERE rolname = 'admin2';
UPDATE 1
postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
postgres=>
I don't really have this worked out yet. That's mostly because I'm planning to fix it with role ownership, but perhaps there is a better way?
However, the next thing is:
postgres=# SET ROLE admin;
SET
postgres=> CREATE GROUP testgroup;
CREATE ROLE
postgres=> GRANT testgroup TO test;
ERROR: must have admin option on role "testgroup"First off, what does "admin option" mean on a role?
From the docs for "CREATE ROLE", https://www.postgresql.org/docs/14/sql-createrole.html
The ADMIN clause is like ROLE, but the named roles are added to the new role WITH ADMIN OPTION, giving them the right to grant membership in this role to others.
I then tried this:
postgres=# CREATE USER admin3 CREATEROLE WITH ADMIN OPTION;
CREATE ROLE
postgres=# SET ROLE admin3;
SET
postgres=> CREATE USER test3;
CREATE ROLE
postgres=> CREATE GROUP testgroup3;
CREATE ROLE
postgres=> GRANT testgroup3 TO test3;
ERROR: must have admin option on role "testgroup3"So I created both user and group, I have the CREATEROLE priv (with or
without admin option), but I still can't assign the group. Is that
(tracking who created a role and letting the creator do more thing) the
part that got chopped away in your last patch in order to find a common
ground?
You need ADMIN on the role, not on CREATEROLE. To add members to a target role, you must have ADMIN on that target role. To create new roles with CREATEROLE privilege, you must have ADMIN on the CREATEROLE privilege.
Is there now any way non-Superusers can assign groups to other users?
Yes, by having ADMIN on those groups.
I
feel this (next to creating users/groups) is the primary thing those
CREATEROLE admins are supposed to do/where doing up to now.
Right. In the past, having CREATEROLE implied having ADMIN on every role. I'm intentionally breaking that.
The way the adm* privs are now somewhere in the middle of the rol*
privs also looks weird for the end-user and there does not seems to be
some greater scheme behind it:
Because they are not variable length nor nullable, they must come before such fields (namely, rolpassword and rolvaliduntil). They don't really need to come before rolconnlimit, but I liked the idea of packing twelve booleans together, since with "bool" typedef'd to unsigned char, that's twelve contiguous bytes, starting after oid (4 bytes) and rolname (64 bytes) and likely fitting nicely without padding bytes on at least some platforms. If I split them on either side of rolconnlimit (which is 4 bytes), there'd be seven bools before it and five bools after, which wouldn't pack nicely.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Jan 31, 2022, at 8:53 AM, Stephen Frost <sfrost@snowman.net> wrote:
Yeah, we do need to have a way to determine who is allowed to drop
roles and role ownership seems like it's one possible approach to that.
Which other ways are on the table? Having ADMIN on a role doesn't allow you to do that, but maybe it could? What else?
The main idea here is that having CREATEROLE doesn't give you ADMIN on roles, nor on role attributes. For role attributes, the syntax has been extended. An excerpt from the patch's regression test illustrates some of that concept:
-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;Right, this was one of the approaches that I was thinking could work for
managing role attributes and it's very similar to roles and the admin
option for them. As I suggested at least once, another possible
approach could be to have login users not be able to create roles but
for them to be able to SET ROLE to a role which is able to create roles,
and then, using your prior method, only allow the attributes which that
role has to be able to be given to other roles.
I'm not sure how that works. If I have a group named "administrators" which as multiple attributes like BYPASSRLS and such, and user "stephen" is a member of "administrators", then stephen can not only give away bypassrls to new users but also has it himself. How is that an improvement? (I mean this as a question, not as criticism.)
That essentially makes
a role be a proxy for the per-attribute admin options. There's pros and
cons for each approach and so I'm curious as to which you feel is the
better approach? I get the feeling that you're more inclined to go with
the approach of having an admin option for each role attribute (having
written this WIP patch) but I'm not sure if that is because you
contempltaed both and felt this was better for some reason or more
because I wasn't explaining the other approach very well, or if there
was some other reason.
I need more explanation of the other option you are contemplating. My apologies if I'm being thick-headed.
-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"I would say not just privileged roles, but any roles that the user
doesn't have admin rights on.
Yes, that's how it works. But this portion of the test is only checking the interaction between CREATEROLE and built-in privileged roles, hence the comment.
Whether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied hinges on whether the role is given CREATEROLE. That hackery is necessary to preserve backwards compatibility. If we don't care about compatibility, I could change the patch to make "WITHOUT ADMIN OPTION" implied for all attributes when not specified.
Given the relative size of the changes we're talking about regarding
CREATEROLE, I don't really think we need to stress about backwards
compatibility too much.
Yeah, I'm leaning pretty strongly that way, too.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 31, 2022, at 8:53 AM, Stephen Frost <sfrost@snowman.net> wrote:
Yeah, we do need to have a way to determine who is allowed to drop
roles and role ownership seems like it's one possible approach to that.Which other ways are on the table? Having ADMIN on a role doesn't allow you to do that, but maybe it could? What else?
Supporting that through ADMIN is one option, another would be a
'DROPROLE' attribute, though we'd want a way to curtail that from being
able to be used for just any role and that does lead down a path similar
to ownership or just generally the concept that some roles have certain
rights over certain other roles (whether you create them or not...).
I do think there's a lot of value in being able to segregate certain
rights- consider that you may want a role that's able to create other
roles, perhaps grant them into some set of roles, can lock those roles
(prevent them from logging in, maybe do a password reset, something like
that), but which *isn't* able to drop those roles (and all their
objects) as that's dangerous and mistakes can certainly happen, or be
able to become that role because the creating role simply doesn't have
any need to be able to do that (or desire to in many cases, as we
discussed in the landlord-vs-tenant sub-thread).
Naturally, you'd want *some* role to be able to drop that role (and one
that doesn't have full superuser access) but that might be a role that's
not able to create new roles or take over accounts.
Separation of concerns and powers and all of that is what we want to be
going for here, more generically, which is why I was opposed to the
blanket "owners have all rights of all roles they own" implementation.
That approach doesn't support the ability to have a relatively
unprivileged role that's able to create other roles, which seems like a
pretty important use-case for us to be considering.
The terminology seems to also be driving us in a certain direction and I
don't know that it's necessarily a good one. That is- the term 'owner'
implies certain things and maybe that's where some of the objection to
my argument that owners shouldn't necessarily have all rights of the
roles they 'own' comes from (ok- I'll also put out there for general
consideration that since we're talking about roles, and login roles are
generally associated with people, that maybe 'owner' isn't a great term
to use for this anyway ...). I feel like the 'owner' concept came from
the way we have table owners and function owners and database owners
today rather than from a starting point of what do we wish to
specifically enable.
Perhaps instead of starting from the 'owner' concept, we start from the
question about the kinds of things we want roles to be able to do and
perhaps that will help inform the terminology.
- Create new roles
- Drop an existing role
- Drop objects which belong to a role
- Lock existing roles
- Change/reset the PW of existing roles
- Give roles to other roles
- Revoke access to some roles from other roles
- Give select role attributes to a role
- Revoke role attributes from a role
- Traditional role-based access control (group memberships, SET ROLE)
Certain of the above are already covered by the existing role membership
system and with the admin option, though there's definitely an argument
to be made as to if that is as capable as we'd like it to be (there's no
way to, today at least, GRANT *just* the admin option, for example, and
maybe that's something that it would actually be sensible to support).
Perhaps there is a need to have a user who has all of the above
capabilities and maybe that would be an 'owner' or 'manager', but as I
tried to illustrate above, there's definitely use-cases for giving
a role only some of the above capabilities rather than all of them
together at once.
The main idea here is that having CREATEROLE doesn't give you ADMIN on roles, nor on role attributes. For role attributes, the syntax has been extended. An excerpt from the patch's regression test illustrates some of that concept:
-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;Right, this was one of the approaches that I was thinking could work for
managing role attributes and it's very similar to roles and the admin
option for them. As I suggested at least once, another possible
approach could be to have login users not be able to create roles but
for them to be able to SET ROLE to a role which is able to create roles,
and then, using your prior method, only allow the attributes which that
role has to be able to be given to other roles.I'm not sure how that works. If I have a group named "administrators" which as multiple attributes like BYPASSRLS and such, and user "stephen" is a member of "administrators", then stephen can not only give away bypassrls to new users but also has it himself. How is that an improvement? (I mean this as a question, not as criticism.)
That's not how role attributes work though- "stephen" only has the
'bypassrls' role attribute after a 'set role administrators'. This has
been one of the issues with role attributes in general as there's no way
to change that (unlike the 'inherit' option for roles themselves) but in
this particular case it might be to our advantage.
That essentially makes
a role be a proxy for the per-attribute admin options. There's pros and
cons for each approach and so I'm curious as to which you feel is the
better approach? I get the feeling that you're more inclined to go with
the approach of having an admin option for each role attribute (having
written this WIP patch) but I'm not sure if that is because you
contempltaed both and felt this was better for some reason or more
because I wasn't explaining the other approach very well, or if there
was some other reason.I need more explanation of the other option you are contemplating. My apologies if I'm being thick-headed.
Hopefully the above helps. Note that in order to not allow the
"stephen" role simply alter itself to have the bypassrls role attribute,
we'd need to consider roles to not have 'ownership' (or whatever) over
themselves, which leads into the prior complaint I made around roles
having 'admin' rights on themselves which I generally don't feel is
correct either.
-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"I would say not just privileged roles, but any roles that the user
doesn't have admin rights on.Yes, that's how it works. But this portion of the test is only checking the interaction between CREATEROLE and built-in privileged roles, hence the comment.
But.. predefined roles aren't actually different in this regard from any
other role, so I disagree that such a test of explicitly predefined
roles makes sense..?
Whether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied hinges on whether the role is given CREATEROLE. That hackery is necessary to preserve backwards compatibility. If we don't care about compatibility, I could change the patch to make "WITHOUT ADMIN OPTION" implied for all attributes when not specified.
Given the relative size of the changes we're talking about regarding
CREATEROLE, I don't really think we need to stress about backwards
compatibility too much.Yeah, I'm leaning pretty strongly that way, too.
Great.
Thanks,
Stephen
On Mon, Jan 31, 2022 at 1:50 PM Stephen Frost <sfrost@snowman.net> wrote:
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 31, 2022, at 8:53 AM, Stephen Frost <sfrost@snowman.net> wrote:
Yeah, we do need to have a way to determine who is allowed to drop
roles and role ownership seems like it's one possible approach to that.Which other ways are on the table? Having ADMIN on a role doesn't allow you to do that, but maybe it could? What else?
Supporting that through ADMIN is one option, another would be a
'DROPROLE' attribute, though we'd want a way to curtail that from being
able to be used for just any role and that does lead down a path similar
to ownership or just generally the concept that some roles have certain
rights over certain other roles (whether you create them or not...).I do think there's a lot of value in being able to segregate certain
rights- consider that you may want a role that's able to create other
roles, perhaps grant them into some set of roles, can lock those roles
(prevent them from logging in, maybe do a password reset, something like
that), but which *isn't* able to drop those roles (and all their
objects) as that's dangerous and mistakes can certainly happen, or be
able to become that role because the creating role simply doesn't have
any need to be able to do that (or desire to in many cases, as we
discussed in the landlord-vs-tenant sub-thread).
This is precisely the use case I am trying to accomplish with this
patchset, roughly:
- An automated bot that creates users and adds them to the employees role
- Bot cannot access any employee (or other roles) table data
- Bot cannot become any employee
- Bot can disable the login of any employee
Yes there are attack surfaces around the fringes of login, etc but
those can be mitigated with certificate authentication. My pg_hba
would require any role in the employees role to use cert auth.
This would adequately mitigate many threats while greatly enhancing
user management.
Naturally, you'd want *some* role to be able to drop that role (and one
that doesn't have full superuser access) but that might be a role that's
not able to create new roles or take over accounts.
I suspect some kind of web backend to handle manual user pruning. I
don't expect Bot to automatically drop users because mistakes can
happen, and disabling the login ability seems like an adequate
tradeoff.
Separation of concerns and powers and all of that is what we want to be
going for here, more generically, which is why I was opposed to the
blanket "owners have all rights of all roles they own" implementation.
That approach doesn't support the ability to have a relatively
unprivileged role that's able to create other roles, which seems like a
pretty important use-case for us to be considering.
Agreed.
Show quoted text
The terminology seems to also be driving us in a certain direction and I
don't know that it's necessarily a good one. That is- the term 'owner'
implies certain things and maybe that's where some of the objection to
my argument that owners shouldn't necessarily have all rights of the
roles they 'own' comes from (ok- I'll also put out there for general
consideration that since we're talking about roles, and login roles are
generally associated with people, that maybe 'owner' isn't a great term
to use for this anyway ...). I feel like the 'owner' concept came from
the way we have table owners and function owners and database owners
today rather than from a starting point of what do we wish to
specifically enable.Perhaps instead of starting from the 'owner' concept, we start from the
question about the kinds of things we want roles to be able to do and
perhaps that will help inform the terminology.- Create new roles
- Drop an existing role
- Drop objects which belong to a role
- Lock existing roles
- Change/reset the PW of existing roles
- Give roles to other roles
- Revoke access to some roles from other roles
- Give select role attributes to a role
- Revoke role attributes from a role
- Traditional role-based access control (group memberships, SET ROLE)Certain of the above are already covered by the existing role membership
system and with the admin option, though there's definitely an argument
to be made as to if that is as capable as we'd like it to be (there's no
way to, today at least, GRANT *just* the admin option, for example, and
maybe that's something that it would actually be sensible to support).Perhaps there is a need to have a user who has all of the above
capabilities and maybe that would be an 'owner' or 'manager', but as I
tried to illustrate above, there's definitely use-cases for giving
a role only some of the above capabilities rather than all of them
together at once.The main idea here is that having CREATEROLE doesn't give you ADMIN on roles, nor on role attributes. For role attributes, the syntax has been extended. An excerpt from the patch's regression test illustrates some of that concept:
-- ok, superuser can create a role that can create login replication users, but
-- cannot itself login, nor perform replication
CREATE ROLE regress_role_repladmin
CREATEROLE WITHOUT ADMIN OPTION -- can create roles, but cannot give it away
NOCREATEDB WITHOUT ADMIN OPTION -- cannot create db, nor give it away
NOLOGIN WITH ADMIN OPTION -- cannot log in, but can give it away
NOREPLICATION WITH ADMIN OPTION -- cannot replicate, but can give it away
NOBYPASSRLS WITHOUT ADMIN OPTION; -- cannot bypassrls, nor give it away-- ok, superuser can create a role with CREATEROLE but restrict give-aways
CREATE ROLE regress_role_minoradmin
NOSUPERUSER -- WITHOUT ADMIN OPTION is implied
CREATEROLE WITHOUT ADMIN OPTION
NOCREATEDB WITHOUT ADMIN OPTION
NOLOGIN WITHOUT ADMIN OPTION
NOREPLICATION -- WITHOUT ADMIN OPTION is implied
NOBYPASSRLS -- WITHOUT ADMIN OPTION is implied
NOINHERIT WITHOUT ADMIN OPTION
CONNECTION LIMIT NONE WITHOUT ADMIN OPTION
VALID ALWAYS WITHOUT ADMIN OPTION
PASSWORD NULL WITHOUT ADMIN OPTION;Right, this was one of the approaches that I was thinking could work for
managing role attributes and it's very similar to roles and the admin
option for them. As I suggested at least once, another possible
approach could be to have login users not be able to create roles but
for them to be able to SET ROLE to a role which is able to create roles,
and then, using your prior method, only allow the attributes which that
role has to be able to be given to other roles.I'm not sure how that works. If I have a group named "administrators" which as multiple attributes like BYPASSRLS and such, and user "stephen" is a member of "administrators", then stephen can not only give away bypassrls to new users but also has it himself. How is that an improvement? (I mean this as a question, not as criticism.)
That's not how role attributes work though- "stephen" only has the
'bypassrls' role attribute after a 'set role administrators'. This has
been one of the issues with role attributes in general as there's no way
to change that (unlike the 'inherit' option for roles themselves) but in
this particular case it might be to our advantage.That essentially makes
a role be a proxy for the per-attribute admin options. There's pros and
cons for each approach and so I'm curious as to which you feel is the
better approach? I get the feeling that you're more inclined to go with
the approach of having an admin option for each role attribute (having
written this WIP patch) but I'm not sure if that is because you
contempltaed both and felt this was better for some reason or more
because I wasn't explaining the other approach very well, or if there
was some other reason.I need more explanation of the other option you are contemplating. My apologies if I'm being thick-headed.
Hopefully the above helps. Note that in order to not allow the
"stephen" role simply alter itself to have the bypassrls role attribute,
we'd need to consider roles to not have 'ownership' (or whatever) over
themselves, which leads into the prior complaint I made around roles
having 'admin' rights on themselves which I generally don't feel is
correct either.-- fail, having CREATEROLE is not enough to create roles in privileged roles
SET SESSION AUTHORIZATION regress_role_minoradmin;
CREATE ROLE regress_nosuch_read_all_data IN ROLE pg_read_all_data;
ERROR: must have admin option on role "pg_read_all_data"I would say not just privileged roles, but any roles that the user
doesn't have admin rights on.Yes, that's how it works. But this portion of the test is only checking the interaction between CREATEROLE and built-in privileged roles, hence the comment.
But.. predefined roles aren't actually different in this regard from any
other role, so I disagree that such a test of explicitly predefined
roles makes sense..?Whether "WITH ADMIN OPTION" or "WITHOUT ADMIN OPTION" is implied hinges on whether the role is given CREATEROLE. That hackery is necessary to preserve backwards compatibility. If we don't care about compatibility, I could change the patch to make "WITHOUT ADMIN OPTION" implied for all attributes when not specified.
Given the relative size of the changes we're talking about regarding
CREATEROLE, I don't really think we need to stress about backwards
compatibility too much.Yeah, I'm leaning pretty strongly that way, too.
Great.
Thanks,
Stephen
Hi,
Am Montag, dem 31.01.2022 um 09:18 -0800 schrieb Mark Dilger:
On Jan 31, 2022, at 12:43 AM, Michael Banck <
michael.banck@credativ.de> wrote:
Ok, sure. I think this topic is hugely important and as I read the
patch anyway, I added some comments, but yeah, we need to figure out
the fundamentals first.
Right.
Perhaps some background on this patch series will help.
[...]
Thanks a lot!
If we don't go with backwards compatibility, then CREATEROLE would only
allow you to create a new role, but not to give that role LOGIN, nor
CREATEDB, etc. You'd need to also have admin option on those things.
To create a role that can give those things away, you'd need to run
something like:
CREATE ROLE michael
CREATEROLE WITH ADMIN OPTION -- can further give away
"createrole"
CREATEDB WITH ADMIN OPTION -- can further give away
"createdb"
LOGIN WITH ADMIN OPTION -- can further give away "login"
NOREPLICATION WITHOUT ADMIN OPTION -- this would be implied
anyway
NOBYPASSRLS WITHOUT ADMIN OPTION -- this would be implied anyway
CONNECTION LIMIT WITH ADMIN OPTION -- can specify connection
limits
PASSWORD WITH ADMIN OPTION -- can specify passwords
VALID UNTIL WITH ADMIN OPTION -- can specify expiration
Those last three don't work for me:
postgres=# CREATE ROLE admin3 VALID UNTIL WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
postgres=# CREATE ROLE admin3 PASSWORD WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
postgres=# CREATE ROLE admin3 CONNECTION LIMIT WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
(I'm on the fence about the phrase "WITH ADMIN OPTION" vs. the phrase
"WITH GRANT OPTION".)Even then, when "michael" creates new roles, if he wants to be able
to further administer those roles, he needs to remember to give
himself ADMIN membership in that role at creation time. After the
role is created, if he doesn't have ADMIN, he can't give it to
himself. So, at create time, he needs to remember to do this:SET ROLE michael;
CREATE ROLE mark ADMIN michael;
What would happen if ADMIN was implicit if michael is a non-superuser
and there's no ADMIN in the CREATE ROLE statement? It would be
backwards-compatible, one could still let somebody else be ADMIN, but
ISTM a CREATEROLE role could no longer admin a role already existing
previously/it didn't create/got assigned admin for (e.g. the predefined
roles).
I.e. (responding what you wrote much further below), the CREATEROLE
role would no longer be ADMIN for all roles, just automatically for the
ones it created.
But that's still a bit strange, because "ADMIN michael" means that
michael can grant other roles membership in "mark", not that michael
can, for example, change mark's password.
Yeah, changing a password is one of the important tasks of a delegated
role admin, if no superusers are around.
If we don't want CREATEROLE to imply that you can mess around with
arbitrary roles (rather than only roles that you created or have been
transferred control over) then we need the concept of role
ownership. This patch doesn't go that far, so for now, only
superusers can do those things. Assuming some form of this patch is
acceptable, the v9 series will resurrect some of the pre-v7 logic for
role ownership and say that the owner can do those things.
Ok, so what I would have needed to do in the above in order to have
"admin2" and "admin" be the same as far as creating login users is (I
believe):ALTER ROLE admin2 CREATEROLE LOGIN WITH ADMIN OPTION;
Yes, those it's more likely admin2 would have been created with these
privileges to begin with, if the creator intended admin2 to do such
things.
Right, maybe people just have to adjust to the new way. It still feels
strange that whatever you do at role creation time is more meaningful
than when altering a role.
By the way, is there now even a way to add admpassword to a role
after it got created?postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
ERROR: must have admin on password to change password attribute
postgres=> RESET ROLE;
RESET
postgres=# ALTER ROLE admin2 PASSWORD WITH ADMIN OPTION;
ERROR: syntax error at or near "WITH"
UPDATE pg_authid SET admpassword = 't' WHERE rolname = 'admin2';
UPDATE 1
postgres=# SET ROLE admin2;
SET
postgres=> \password test
Enter new password for user "test":
Enter it again:
postgres=>I don't really have this worked out yet. That's mostly because I'm
planning to fix it with role ownership, but perhaps there is a better
way?
Well see above, maybe the patch is just broken/unfinished with respect
to PASSWORD and the others? It works for REPLICATION e.g.:
postgres=# ALTER ROLE admin2 REPLICATION WITH ADMIN OPTION;
ALTER ROLE
However, the next thing is:
postgres=# SET ROLE admin;
SET
postgres=> CREATE GROUP testgroup;
CREATE ROLE
postgres=> GRANT testgroup TO test;
ERROR: must have admin option on role "testgroup"First off, what does "admin option" mean on a role?
From the docs for "CREATE ROLE",
https://www.postgresql.org/docs/14/sql-createrole.htmlThe ADMIN clause is like ROLE, but the named roles are added to the
new role WITH ADMIN OPTION, giving them the right to grant membership
in this role to others.
Hrm, I see; I guess I never paid attention to that part so far. The
CREATEROLE thing or SUPERUSER was all I ever needed so far.
And with that I guess I should really bow out of this thread and start
reading from the beginning.
I then tried this:
postgres=# CREATE USER admin3 CREATEROLE WITH ADMIN OPTION;
CREATE ROLE
postgres=# SET ROLE admin3;
SET
postgres=> CREATE USER test3;
CREATE ROLE
postgres=> CREATE GROUP testgroup3;
CREATE ROLE
postgres=> GRANT testgroup3 TO test3;
ERROR: must have admin option on role "testgroup3"So I created both user and group, I have the CREATEROLE priv (with or
without admin option), but I still can't assign the group. Is that
(tracking who created a role and letting the creator do more thing) the
part that got chopped away in your last patch in order to find a common
ground?You need ADMIN on the role, not on CREATEROLE. To add members to a
target role, you must have ADMIN on that target role. To create new
roles with CREATEROLE privilege, you must have ADMIN on the
CREATEROLE privilege.
Right ok. Maybe it's just me, but I feel a lot of people will need to
learn a lot more than they'd like to know about the ADMIN thing after
this patch goes in.
I
feel this (next to creating users/groups) is the primary thing those
CREATEROLE admins are supposed to do/where doing up to now.Right. In the past, having CREATEROLE implied having ADMIN on every
role. I'm intentionally breaking that.
Right; I commented on that above.
The way the adm* privs are now somewhere in the middle of the rol*
privs also looks weird for the end-user and there does not seems to be
some greater scheme behind it:Because they are not variable length nor nullable, they must come
before such fields (namely, rolpassword and rolvaliduntil). They
don't really need to come before rolconnlimit, but I liked the idea
of packing twelve booleans together, since with "bool" typedef'd to
unsigned char, that's twelve contiguous bytes, starting after oid (4
bytes) and rolname (64 bytes) and likely fitting nicely without
padding bytes on at least some platforms. If I split them on either
side of rolconnlimit (which is 4 bytes), there'd be seven bools
before it and five bools after, which wouldn't pack nicely.
Hrm ok, but it's a user-visible column ordering, so I'm wondering
whether that should trump efficiency here.
Michael
--
Michael Banck
Teamleiter PostgreSQL-Team
Projektleiter
Tel.: +49 2166 9901-171
E-Mail: michael.banck@credativ.de
credativ GmbH, HRB Mönchengladbach 12080
USt-ID-Nummer: DE204566209
Trompeterallee 108, 41189 Mönchengladbach
Geschäftsführung: Dr. Michael Meskes, Geoff Richardson, Peter Lilley
Unser Umgang mit personenbezogenen Daten unterliegt
folgenden Bestimmungen: https://www.credativ.de/datenschutz
On Jan 31, 2022, at 10:50 AM, Stephen Frost <sfrost@snowman.net> wrote:
Supporting that through ADMIN is one option, another would be a
'DROPROLE' attribute, though we'd want a way to curtail that from being
able to be used for just any role and that does lead down a path similar
to ownership or just generally the concept that some roles have certain
rights over certain other roles (whether you create them or not...).
I've been operating under the assumption that I have a lot more freedom to create new features than to change how existing features behave, for two reasons: backwards compatibility and sql-spec compliance.
Changing how having ADMIN on a role works seems problematic for both those reasons. My family got me socks for Christmas, not what I actually wanted, a copy of the SQL-spec. So I'm somewhat guessing here. But I believe we'd have problems if we "fixed" the part where a role can revoke ADMIN from others on themselves. Whatever we have, whether we call it "ownership", it can't be something a role can unilaterally revoke.
As for a 'DROPROLE' attribute, I don't think that gets us anywhere. You don't seem to think so, either. So that leaves us with "ownership", perhaps by another word? I only chose that word because it's what we use elsewhere, but if we want to call it "managementship" and "manager" or whatever, that's fine. I'm not to the point of debating the terminology just yet. I'm still trying to get the behavior nailed down.
I do think there's a lot of value in being able to segregate certain
rights- consider that you may want a role that's able to create other
roles, perhaps grant them into some set of roles, can lock those roles
(prevent them from logging in, maybe do a password reset, something like
that), but which *isn't* able to drop those roles (and all their
objects) as that's dangerous and mistakes can certainly happen, or be
able to become that role because the creating role simply doesn't have
any need to be able to do that (or desire to in many cases, as we
discussed in the landlord-vs-tenant sub-thread).
I'm totally on the same page. Your argument upthread about wanting any malfeasance on the part of a service provider showing up in the audit logs was compelling. Even for those things the "owner"/"manager" has the rights to do, we might want to make them choose to do it explicitly and not merely do it by accident.
Naturally, you'd want *some* role to be able to drop that role (and one
that doesn't have full superuser access) but that might be a role that's
not able to create new roles or take over accounts.
I think it's important to go beyond the idea of a role attribute here. It's not that role "bob" can drop roles. It's that "bob" can drop *specific* roles, and for that, there has to be some kind of dependency tracked between "bob" and those other roles. I'm calling that "ownership". I think that language isn't just arbitrary, but actually helpful (technically, not politically) because REASSIGN OWNED should treat this kind of relationship exactly the same as it treats ownership of schemas, tables, functions, etc.
Separation of concerns and powers and all of that is what we want to be
going for here, more generically, which is why I was opposed to the
blanket "owners have all rights of all roles they own" implementation.
I'm hoping to bring back, in v9, the idea of ownership/managership. The real sticking point here is that we (Robert, Andrew, I, and possibly others) want to be able to drop in a non-superuser-creator-role into existing systems that use superuser for role management. We'd like it to be as transparent a switch as possible.
With a superuser creating a role, that superuser can come back and muck with the role afterward, and the role can't revoke the superuser's right to do so. It's not enough that a non-superuser-creator-role (henceforth, "manager") can grant itself ADMIN on the created role. It also needs to be able to set passwords, transfer object ownerships to/from the role, grant the role into other roles or other roles into it, etc. All of that has to be sandboxed such that the "manager" can't touch stuff outside the manager's sandbox, but within the sandbox, it shouldn't make any practical difference that the manager isn't actually a superuser.
I think what I had in v7 was almost right. I'm hoping that we just need to adjust things like the idea that managers always have implicit membership in and ADMIN on roles they manage. I think that needs to be optional, and the audit logs could show if the manager granted themselves such things, as it might violate policy and be a red flag in the audit log.
That approach doesn't support the ability to have a relatively
unprivileged role that's able to create other roles, which seems like a
pretty important use-case for us to be considering.
I think we have that ability. It's just that the creator role isn't "relatively unprivileged" vis-a-vis the created role. But that could be handled by creating the role and then transferring the ownership to some other role, or specifying in the CREATE ROLE command that the creator doesn't want those privileges, etc. That requires some tinkering with the design, though, because the permission to perform the ownership transfer to that other role would need to be circumscribed to not give away other privileges, like the right to become that other role, or the specification that the creator disavows certain privileges over the created role might need to be something the creator could get back by force with some subsequent GRANT command, or ...?
The terminology seems to also be driving us in a certain direction and I
don't know that it's necessarily a good one. That is- the term 'owner'
implies certain things and maybe that's where some of the objection to
my argument that owners shouldn't necessarily have all rights of the
roles they 'own' comes from
I think it does follow pretty closely the concept of ownership of objects, though. So closely, in fact, that I don't really see any daylight between the two concepts.
(ok- I'll also put out there for general
consideration that since we're talking about roles, and login roles are
generally associated with people, that maybe 'owner' isn't a great term
to use for this anyway ...).
Technically, we're talking about roles within computers owning other roles within computers, not about people owning people. We already have a command called REASSIGN OWNED, and if we don't call this ownership, then that command gets really squirrelly. Does it also reassign "managed"?
On the other hand, I'm not looking to create offense, so if this language seems unacceptable, perhaps you could propose something else?
I feel like the 'owner' concept came from
the way we have table owners and function owners and database owners
today rather than from a starting point of what do we wish to
specifically enable.
Let's compare this to the idea of owning a table. Can the owner of a table revoke SELECT from themselves? Yes, they can. They can also give it back to themselves:
CREATE ROLE michael;
SET ROLE michael;
CREATE TABLE michael_table (i INTEGER);
REVOKE SELECT ON michael_table FROM PUBLIC, michael;
SELECT * FROM michael_table;
ERROR: permission denied for table michael_table
GRANT SELECT ON michael_table TO michael;
SELECT * FROM michael_table;
i
---
(0 rows)
So I'm curious if we can have the same idea for ADMIN of a role? The owner can revoke the role from themselves, and they can also grant it back. Would that be acceptable?
Perhaps instead of starting from the 'owner' concept, we start from the
question about the kinds of things we want roles to be able to do and
perhaps that will help inform the terminology.- Create new roles
- Drop an existing role
- Drop objects which belong to a role
- Lock existing roles
- Change/reset the PW of existing roles
- Give roles to other roles
- Revoke access to some roles from other roles
- Give select role attributes to a role
- Revoke role attributes from a role
- Traditional role-based access control (group memberships, SET ROLE)
I agree we want the ability to do these things, and not as a single CREATEROLE privilege, but separable. The pre-v8 patch was separating only one who the role owner was, but v8 is attempting to separate these further, and I think that's the right way to go.
Certain of the above are already covered by the existing role membership
system and with the admin option, though there's definitely an argument
to be made as to if that is as capable as we'd like it to be (there's no
way to, today at least, GRANT *just* the admin option, for example, and
maybe that's something that it would actually be sensible to support).
I think the ADMIN stuff *would* be the way to go, but for it's weird self-administration feature. That to me seems to kill the idea. What do you think?
Perhaps there is a need to have a user who has all of the above
capabilities and maybe that would be an 'owner' or 'manager', but as I
tried to illustrate above, there's definitely use-cases for giving
a role only some of the above capabilities rather than all of them
together at once.
I'm using the terms "owner"/"manager" without regard for whether they have all those abilities or just some of them. However, I think these terms don't apply for just the traditional ADMIN option on the role. In that case, calling it "ownership" or "managership" is inappropriate.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 1/31/22 12:18, Mark Dilger wrote:
On Jan 31, 2022, at 12:43 AM, Michael Banck <michael.banck@credativ.de> wrote:
Ok, sure. I think this topic is hugely important and as I read the
patch anyway, I added some comments, but yeah, we need to figure out
the fundamentals first.Right.
Perhaps some background on this patch series will help. The patch versions before v8 were creating an owner-owned relationship between the creator and the createe, and a lot of privileges were dependent on that ownership. Stephen objected that we were creating parallel tracks on which the privilege system was running; things like belonging to a role or having admin on a role were partially conflated with owning a role. He also objected that the pre-v8 patch sets allowed a creator role with the CREATEROLE privilege to give away any privilege the creator had, rather than needing to have GRANT or ADMIN option on the privilege being given.
The v8-WIP patch is not a complete replacement for the pre-v8 patches. It's just a balloon I'm floating to try out candidate solutions to some of Stephen's objections. In the long run, I want the solution to Stephen's objections to not create problems for anybody who liked the way the pre-v8 patches worked (Robert, Andrew, and to some extent me.)
In this WIP patch, for a creator to give *anything* away to a createe, the creator must have GRANT or ADMIN on the thing being given. That includes attributes like BYPASSRLS, CREATEDB, LOGIN, etc., and also ADMIN on any role the createe is granted into.
I tried to structure things for backwards compatibility, considering which things roles with CREATEROLE could give away historically. It turns out they can give away most everything, but not SUPERUSER, BYPASSRLS, or REPLICATION. So I structured the default privileges for CREATEROLE to match. But I'm uncertain that design is any good, and your comments below suggest that you find it pretty hard to use.
Part of the problem with trying to be backwards compatible is that we must break compatibility anyway, to address the problem that historically having CREATEROLE meant you effectively had ADMIN on all non-superuser roles. That's got to change. So in part I'm asking pgsql-hackers if partial backwards compatibility is worth the bother.
If we don't go with backwards compatibility, then CREATEROLE would only allow you to create a new role, but not to give that role LOGIN, nor CREATEDB, etc. You'd need to also have admin option on those things. To create a role that can give those things away, you'd need to run something like:
CREATE ROLE michael
CREATEROLE WITH ADMIN OPTION -- can further give away "createrole"
CREATEDB WITH ADMIN OPTION -- can further give away "createdb"
LOGIN WITH ADMIN OPTION -- can further give away "login"
NOREPLICATION WITHOUT ADMIN OPTION -- this would be implied anyway
NOBYPASSRLS WITHOUT ADMIN OPTION -- this would be implied anyway
CONNECTION LIMIT WITH ADMIN OPTION -- can specify connection limits
PASSWORD WITH ADMIN OPTION -- can specify passwords
VALID UNTIL WITH ADMIN OPTION -- can specify expiration(I'm on the fence about the phrase "WITH ADMIN OPTION" vs. the phrase "WITH GRANT OPTION".)
Even then, when "michael" creates new roles, if he wants to be able to further administer those roles, he needs to remember to give himself ADMIN membership in that role at creation time. After the role is created, if he doesn't have ADMIN, he can't give it to himself. So, at create time, he needs to remember to do this:
SET ROLE michael;
CREATE ROLE mark ADMIN michael;But that's still a bit strange, because "ADMIN michael" means that michael can grant other roles membership in "mark", not that michael can, for example, change mark's password. If we don't want CREATEROLE to imply that you can mess around with arbitrary roles (rather than only roles that you created or have been transferred control over) then we need the concept of role ownership. This patch doesn't go that far, so for now, only superusers can do those things. Assuming some form of this patch is acceptable, the v9 series will resurrect some of the pre-v7 logic for role ownership and say that the owner can do those things.
This seems complicated. Maybe the previous proposal was too simple, but
simplicity has some virtues. It seemed to me that more complex rules
could possibly have been implemented for those who really needed them by
using SECURITY DEFINER functions. The whole 'NOFOO WITH ADMIN OPTION'
thing seems to me a bit like a POLA violation. Nevertheless I can
probably live with it as long as it's *really* well documented. Even so
I suspect it would be too complex for many, and they will just continue
to use superusers to create and manage roles if possible.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Feb 1, 2022, at 1:10 PM, Andrew Dunstan <andrew@dunslane.net> wrote:
The whole 'NOFOO WITH ADMIN OPTION'
thing seems to me a bit like a POLA violation. Nevertheless I can
probably live with it as long as it's *really* well documented. Even so
I suspect it would be too complex for many, and they will just continue
to use superusers to create and manage roles if possible.
I agree with the sentiment, but it might help to distinguish between surprising behavior vs. surprising grammar.
In existing postgresql releases, having CREATEROLE means you can give away most attributes, including ones you yourself don't have (createdb, login). So we already have the concept of NOFOO WITH ADMIN OPTION, we just don't call it that. In pre-v8 patches on this thread, I got rid of that; you *must* have the attribute to give it away. But maybe that was too restrictive, and we need a way to specify, attribute by attribute, how this works. Is this just a problem of surprising grammar? Is it surprising behavior? If the latter, I'm inclined to give up this WIP as having been a bad move. If the former, I'll try to propose some less objectionable grammar.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On 2/1/22 17:27, Mark Dilger wrote:
On Feb 1, 2022, at 1:10 PM, Andrew Dunstan <andrew@dunslane.net> wrote:
The whole 'NOFOO WITH ADMIN OPTION'
thing seems to me a bit like a POLA violation. Nevertheless I can
probably live with it as long as it's *really* well documented. Even so
I suspect it would be too complex for many, and they will just continue
to use superusers to create and manage roles if possible.I agree with the sentiment, but it might help to distinguish between surprising behavior vs. surprising grammar.
In existing postgresql releases, having CREATEROLE means you can give away most attributes, including ones you yourself don't have (createdb, login). So we already have the concept of NOFOO WITH ADMIN OPTION, we just don't call it that. In pre-v8 patches on this thread, I got rid of that; you *must* have the attribute to give it away. But maybe that was too restrictive, and we need a way to specify, attribute by attribute, how this works. Is this just a problem of surprising grammar? Is it surprising behavior? If the latter, I'm inclined to give up this WIP as having been a bad move. If the former, I'll try to propose some less objectionable grammar.
Certainly the grammar would need to be better. But I'm not sure any
grammar that expresses what is supported here is not going to be
confusing, because the underlying scheme seems complex. But I'm
persuadable. I'd like to hear from others on the subject.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
On Tue, Feb 1, 2022 at 6:38 PM Andrew Dunstan <andrew@dunslane.net> wrote:
In existing postgresql releases, having CREATEROLE means you can give away most attributes, including ones you yourself don't have (createdb, login). So we already have the concept of NOFOO WITH ADMIN OPTION, we just don't call it that. In pre-v8 patches on this thread, I got rid of that; you *must* have the attribute to give it away. But maybe that was too restrictive, and we need a way to specify, attribute by attribute, how this works. Is this just a problem of surprising grammar? Is it surprising behavior? If the latter, I'm inclined to give up this WIP as having been a bad move. If the former, I'll try to propose some less objectionable grammar.
Certainly the grammar would need to be better. But I'm not sure any
grammar that expresses what is supported here is not going to be
confusing, because the underlying scheme seems complex. But I'm
persuadable. I'd like to hear from others on the subject.
Well, we've been moving more and more in the direction of using
predefined roles to manage access. The things that are basically
Boolean flags on the role are mostly legacy stuff. So my tentative
opinion (and I'm susceptible to being persuaded that I'm wrong here)
is that putting a lot of work into fleshing out that infrastructure
does not necessarily make a ton of sense. Are we ever going to add
even one more flag that works that way?
Also, any account that can create roles is a pretty high-privilege
account. Maybe it's superuser, or maybe not, but it's certainly
powerful. In my opinion, that makes fine distinctions here less
important. Is there really an argument for saying "well, we're going
to let you bypass RLS, but we're not going to let you give that
privilege to others"? It seems contrived to think of restricting a
role that is powerful enough to create whole new accounts in such a
way. I'm not saying that someone couldn't have a use case for it, but
I think it'd be a pretty thin use case.
In short, I think it makes tons of sense to say that CREATEROLE lets
you give to others those role flags which you have, but not the ones
you lack. However, to me, it feels like overengineering to distinguish
between things you have and can give away, things you have and can't
give away, and things you don't even have.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Jan 31, 2022, at 10:50 AM, Stephen Frost <sfrost@snowman.net> wrote:
Supporting that through ADMIN is one option, another would be a
'DROPROLE' attribute, though we'd want a way to curtail that from being
able to be used for just any role and that does lead down a path similar
to ownership or just generally the concept that some roles have certain
rights over certain other roles (whether you create them or not...).I've been operating under the assumption that I have a lot more freedom to create new features than to change how existing features behave, for two reasons: backwards compatibility and sql-spec compliance.
I agree that those are concerns that need to be considered, though I'm
more concerned about the SQL compliance and less about backwards
compatibility in this case. For one thing, I'm afraid that we're not as
compliant as we really should be and that should really drive us to make
change here anyway, to get closer to what the spec calls for.
Changing how having ADMIN on a role works seems problematic for both those reasons. My family got me socks for Christmas, not what I actually wanted, a copy of the SQL-spec. So I'm somewhat guessing here. But I believe we'd have problems if we "fixed" the part where a role can revoke ADMIN from others on themselves. Whatever we have, whether we call it "ownership", it can't be something a role can unilaterally revoke.
As for a 'DROPROLE' attribute, I don't think that gets us anywhere. You don't seem to think so, either. So that leaves us with "ownership", perhaps by another word? I only chose that word because it's what we use elsewhere, but if we want to call it "managementship" and "manager" or whatever, that's fine. I'm not to the point of debating the terminology just yet. I'm still trying to get the behavior nailed down.
Yeah, didn't mean to imply that those were great ideas or that I was
particularly advocating for them, but just to bring up some other ideas
to try and get more thought going into this.
I do think there's a lot of value in being able to segregate certain
rights- consider that you may want a role that's able to create other
roles, perhaps grant them into some set of roles, can lock those roles
(prevent them from logging in, maybe do a password reset, something like
that), but which *isn't* able to drop those roles (and all their
objects) as that's dangerous and mistakes can certainly happen, or be
able to become that role because the creating role simply doesn't have
any need to be able to do that (or desire to in many cases, as we
discussed in the landlord-vs-tenant sub-thread).I'm totally on the same page. Your argument upthread about wanting any malfeasance on the part of a service provider showing up in the audit logs was compelling. Even for those things the "owner"/"manager" has the rights to do, we might want to make them choose to do it explicitly and not merely do it by accident.
Glad to hear that.
Naturally, you'd want *some* role to be able to drop that role (and one
that doesn't have full superuser access) but that might be a role that's
not able to create new roles or take over accounts.I think it's important to go beyond the idea of a role attribute here. It's not that role "bob" can drop roles. It's that "bob" can drop *specific* roles, and for that, there has to be some kind of dependency tracked between "bob" and those other roles. I'm calling that "ownership". I think that language isn't just arbitrary, but actually helpful (technically, not politically) because REASSIGN OWNED should treat this kind of relationship exactly the same as it treats ownership of schemas, tables, functions, etc.
I agree that role attributes isn't a good approach and that we should be
moving away from it.
I'm less sure that the existance of REASSIGN OWNED for schemas and
tables and such should be the driver for what this capability of one
role being able to drop another role needs to be called.
Separation of concerns and powers and all of that is what we want to be
going for here, more generically, which is why I was opposed to the
blanket "owners have all rights of all roles they own" implementation.I'm hoping to bring back, in v9, the idea of ownership/managership. The real sticking point here is that we (Robert, Andrew, I, and possibly others) want to be able to drop in a non-superuser-creator-role into existing systems that use superuser for role management. We'd like it to be as transparent a switch as possible.
That description itself really makes me wonder about the sense of what
was proposed. Specifically "existing systems that use superuser for
role management" doesn't make me picture a system where this manager
role has any need to run SELECT statements against the tables created by
the role that it created- SELECT'ing data from tables isn't in the
purview of 'role management' (and before someone complains that pg_dump
is part of this, I wouldn't call running pg_dump role management but
rather data export or, used very loosely, 'backup'). To the end, I push
back with: what exactly is this existing superuser doing that's role
management? The specific use-case, not just 'role management'. What
Joshua outlined was a reasonably defined use-case and that's what I'm
trying to get at here.
With a superuser creating a role, that superuser can come back and muck with the role afterward, and the role can't revoke the superuser's right to do so. It's not enough that a non-superuser-creator-role (henceforth, "manager") can grant itself ADMIN on the created role. It also needs to be able to set passwords, transfer object ownerships to/from the role, grant the role into other roles or other roles into it, etc. All of that has to be sandboxed such that the "manager" can't touch stuff outside the manager's sandbox, but within the sandbox, it shouldn't make any practical difference that the manager isn't actually a superuser.
I appreciate that there needs to be a role who has certain rights over
other roles and that those rights can't be revoked by the role. The
right to grant ADMIN on a create role is, itself, a right and what I was
suggesting is that it could be one that the created role isn't able to
revoke. I disagree that the only possible role that could create some
other role must necessarily be able to be essentially superuser when it
comes to that created role. I pointed out exactly the use-case where
that isn't the case and nothing here has said anything to refute the
existance of that use-case but seems to instead just focus on this idea
that we must have a 'mini superuser'.
I think what I had in v7 was almost right. I'm hoping that we just need to adjust things like the idea that managers always have implicit membership in and ADMIN on roles they manage. I think that needs to be optional, and the audit logs could show if the manager granted themselves such things, as it might violate policy and be a red flag in the audit log.
I'd like to also move in a direction where implicit membership in and
ADMIN rights on the role is optional and potentially not even something
that the creating role is able to grant themselves- though *some* role
would need that ability and, ideally, it would be one that can be
granted out individually without being a full superuser.
That approach doesn't support the ability to have a relatively
unprivileged role that's able to create other roles, which seems like a
pretty important use-case for us to be considering.I think we have that ability. It's just that the creator role isn't "relatively unprivileged" vis-a-vis the created role. But that could be handled by creating the role and then transferring the ownership to some other role, or specifying in the CREATE ROLE command that the creator doesn't want those privileges, etc. That requires some tinkering with the design, though, because the permission to perform the ownership transfer to that other role would need to be circumscribed to not give away other privileges, like the right to become that other role, or the specification that the creator disavows certain privileges over the created role might need to be something the creator could get back by force with some subsequent GRANT command, or ...?
If it's not relatively unprivileged regarding the created role then it's
not the ability which I outlined and therefore doesn't solve the
described use-case. I don't really feel that making it possible for the
creating role to give up those rights actually solves for the attack
vector that is someone gaining access to the creating role's access,
which is what we're talking about trying to address by having a separate
role whose only ability is to create roles which it then isn't able to
become or otherwise impact.
The terminology seems to also be driving us in a certain direction and I
don't know that it's necessarily a good one. That is- the term 'owner'
implies certain things and maybe that's where some of the objection to
my argument that owners shouldn't necessarily have all rights of the
roles they 'own' comes fromI think it does follow pretty closely the concept of ownership of objects, though. So closely, in fact, that I don't really see any daylight between the two concepts.
Except that at least in the case we're contemplating, it's not desired
for the creator of the role to have absolute authority over the created
role. That's a pretty big difference between roles and objects. That
we aren't seeing the distinction here is part of what I'm getting at
with the above paragraph.
(ok- I'll also put out there for general
consideration that since we're talking about roles, and login roles are
generally associated with people, that maybe 'owner' isn't a great term
to use for this anyway ...).Technically, we're talking about roles within computers owning other roles within computers, not about people owning people. We already have a command called REASSIGN OWNED, and if we don't call this ownership, then that command gets really squirrelly. Does it also reassign "managed"?
Technically we were talking about PostgreSQL clusters that are just data
files and processes within computers when it came to primaries and
replicas, but other terms were used previously and we generally agreed
that we should probably move away from those terms. Today REASSIGN
OWNED only talks about tables and views and other things which are
quite distinct from individuals.
On the other hand, I'm not looking to create offense, so if this language seems unacceptable, perhaps you could propose something else?
Manager might be one, but as I try to get at below, what I'm thinking
about is a set of privileges that roles have and there isn't a concept
of "owner" or "manager" but rather "role X has S, T, V privileges on
roles A, B, C". Conceptually perhaps we can consider a role that has
ALL privileges over another role to be that role's 'manager' or 'owner'
but we don't really even need to go into that once we've broken down the
privileges.
I feel like the 'owner' concept came from
the way we have table owners and function owners and database owners
today rather than from a starting point of what do we wish to
specifically enable.Let's compare this to the idea of owning a table. Can the owner of a table revoke SELECT from themselves? Yes, they can. They can also give it back to themselves:
CREATE ROLE michael;
SET ROLE michael;
CREATE TABLE michael_table (i INTEGER);
REVOKE SELECT ON michael_table FROM PUBLIC, michael;
SELECT * FROM michael_table;
ERROR: permission denied for table michael_table
GRANT SELECT ON michael_table TO michael;
SELECT * FROM michael_table;
i
---
(0 rows)So I'm curious if we can have the same idea for ADMIN of a role? The owner can revoke the role from themselves, and they can also grant it back. Would that be acceptable?
That might be acceptable for the ADMIN privilege of a role itself though
I'm not sure if that's really all that distinct from ADMIN.
Perhaps instead of starting from the 'owner' concept, we start from the
question about the kinds of things we want roles to be able to do and
perhaps that will help inform the terminology.- Create new roles
- Drop an existing role
- Drop objects which belong to a role
- Lock existing roles
- Change/reset the PW of existing roles
- Give roles to other roles
- Revoke access to some roles from other roles
- Give select role attributes to a role
- Revoke role attributes from a role
- Traditional role-based access control (group memberships, SET ROLE)I agree we want the ability to do these things, and not as a single CREATEROLE privilege, but separable. The pre-v8 patch was separating only one who the role owner was, but v8 is attempting to separate these further, and I think that's the right way to go.
Right, these should be separable, and I don't mean just the role
attributes but rather the above as distinct privileges. Whereby a role
could have the right to create other roles but *not* have the right to
drop roles (either the one they created, or perhaps any others, or maybe
even to have some distinct set of roles that they're able to drop that's
different from the roles they created), as an example.
Certain of the above are already covered by the existing role membership
system and with the admin option, though there's definitely an argument
to be made as to if that is as capable as we'd like it to be (there's no
way to, today at least, GRANT *just* the admin option, for example, and
maybe that's something that it would actually be sensible to support).I think the ADMIN stuff *would* be the way to go, but for it's weird self-administration feature. That to me seems to kill the idea. What do you think?
I don't think the self-administration stuff that we have for role ADMIN
rights is actually sensible and, while I know it's a backwards
compatibility break, it's something we should fix.
Perhaps there is a need to have a user who has all of the above
capabilities and maybe that would be an 'owner' or 'manager', but as I
tried to illustrate above, there's definitely use-cases for giving
a role only some of the above capabilities rather than all of them
together at once.I'm using the terms "owner"/"manager" without regard for whether they have all those abilities or just some of them. However, I think these terms don't apply for just the traditional ADMIN option on the role. In that case, calling it "ownership" or "managership" is inappropriate.
I don't think it's sensible to have one term that means "all" and then
use that same term to also mean "only some". That strikes me as
confusing and I don't know that we need to even have an explicit name
for the role that has 'all' of the rights or that we need to provide a
name for one that only has 'some' of them- they're just roles that have
certain privileges. The question that we need to solve is how to give
users the ability to choose what roles have which of the privileges that
we've outlined above and agreed should be separable.
THanks,
Stephen
On Feb 2, 2022, at 11:52 AM, Stephen Frost <sfrost@snowman.net> wrote:
The question that we need to solve is how to give
users the ability to choose what roles have which of the privileges that
we've outlined above and agreed should be separable.
Ok, there are really two different things going on here, and the conversation keeps conflating them. Maybe I'm wrong, but I think the conflation of these things is the primary problem preventing us from finishing up the design.
Thing 1: The superuser needs to be able to create roles who can create other roles. Let's call them "creators". Not every organization will want the same level of privilege to be given to a creator, or even that all creators have equal levels of privilege. So when the superuser creates a creator, the superuser needs to be able to configure what exactly what that creator can do. This includes which attributes the creator can give to new roles. It *might* include whether the creator maintains a dependency link with the created role, called "ownership" or somesuch. It *might* include whether the creator can create roles into which the creator is granted membership/administership. But there really isn't any reason that these things should be all-or-nothing. Maybe one creator maintains a dependency link with created roles, and that dependency link entails some privileges. Maybe other creators do not maintain such a link. It seems like superuser can define a creator in many different ways, as long as we nail down what those ways are, and what they mean.
Thing 2: The creator needs to be able to specify which attributes and role memberships are set up with for roles the creator creates. To the extent that the creator has been granted the privilege to create yet more creators, this recurses to Thing 1. But not all creators will have that ability.
I think the conversation gets off topic and disagreement abounds when Thing 1 is assumed to be hardcoded, leaving just the details of Thing 2 to be discussed.
It's perfectly reasonable (in my mind) that Robert, acting as superuser, may want to create a creator who acts like a superuser over the sandbox, while at the same time Stephen, acting as superuser, may want to create a creator who acts as a low privileged bot that only adds and removes roles, but cannot read their tables, SET ROLE to them, etc.
I don't see any reason that Robert and Stephen can't both get what they want. We just have to make Thing 1 flexible enough.
Do you agree at least with this much? If so, I think we can hammer out what to do about Thing 1 and get something committed in time for postgres 15. If not, then I'm probably going to stop working on this until next year, because at this point, we don't have enough time to finish.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Wed, Feb 2, 2022 at 3:23 PM Mark Dilger <mark.dilger@enterprisedb.com> wrote:
It's perfectly reasonable (in my mind) that Robert, acting as superuser, may want to create a creator who acts like a superuser over the sandbox, while at the same time Stephen, acting as superuser, may want to create a creator who acts as a low privileged bot that only adds and removes roles, but cannot read their tables, SET ROLE to them, etc.
I don't see any reason that Robert and Stephen can't both get what they want. We just have to make Thing 1 flexible enough.
Hmm, that would be fine with me. I don't mind a bit if other people
get what they want, as long as I can get what I want, too! In fact,
I'd prefer it if other people also get what they want...
That having been said, I have some reservations if it involves tightly
coupling new features that we're trying to add to existing things that
may or may not be that well designed, like the role-level INHERIT
flag, or WITH ADMIN OPTION, or the not-properly maintained
pg_auth_members.grantor column, or even the SQL standard. I'm not
saying we should ignore any of those things and I don't think that we
should ... but at the same time, we can't whether the feature does
what people want it to do, either. If we do, this whole thing is
really a complete waste of time. If a patch achieves infinitely large
amounts of backward compatibility, standards compliance, and
conformity with existing design but doesn't do the right stuff, forget
it!
--
Robert Haas
EDB: http://www.enterprisedb.com
I'm chiming in a little late here, but as someone who worked on a
system to basically work around the lack of unprivileged CREATE ROLE
for a cloud provider (I worked on the Heroku Data team for several
years), I thought it might be useful to offer my perspective. This is,
of course, not the only use case, but maybe it's useful to have
something concrete. As a caveat, I don't know how current this still
is (I no longer work there, though the docs [1]https://devcenter.heroku.com/articles/heroku-postgresql-credentials seem to still describe
the same system), or if there are better ways to achieve the goals of
a service provider.
Broadly, the general use case is something like what Robert has
sketched out in his e-mails. Heroku took care of setting up the
database, archiving, replication, and any other system-level config.
It would then keep the superuser credentials private, create a
database, and a user that owned that database and had all the
permissions we could grant it without compromising the integrity of
the system. (We did not want customers to break their databases, both
to ensure a better user experience and to avoid getting paged.)
Initially, this meant customers got just the one database user because
of CREATE ROLE's limitations.
To work around that, at some point, we added an API that would CREATE
ROLE for you, accessible through a dashboard and the Heroku CLI. This
ran CREATE ROLE (or DROP ROLE) for you, but otherwise it largely let
you configure the resulting roles as you pleased (using the original
role we create for you). We wanted to avoid reinventing the wheel as
much as possible, and the customer database (including the role
configuration) was mostly a black box for us (we did manage some
predefined permissions configurations through our dashboard, but the
Postgres catalogs were the source of truth for that).
Thinking about how this would fit into a potential non-superuser
CREATE ROLE world, the sandbox superuser model discussed above covers
this pretty well, though I share some of Robert's concerns around how
this fits into existing systems.
Hope this is useful feedback. Thanks for working on this!
[1]: https://devcenter.heroku.com/articles/heroku-postgresql-credentials
On Mon, Jan 31, 2022 at 1:57 PM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:
This is precisely the use case I am trying to accomplish with this
patchset, roughly:- An automated bot that creates users and adds them to the employees role
- Bot cannot access any employee (or other roles) table data
- Bot cannot become any employee
- Bot can disable the login of any employeeYes there are attack surfaces around the fringes of login, etc but
those can be mitigated with certificate authentication. My pg_hba
would require any role in the employees role to use cert auth.This would adequately mitigate many threats while greatly enhancing
user management.
So, where do we go from here?
I've been thinking about this comment a bit. On the one hand, I have
some reservations about the phrase "the use case I am trying to
accomplish with this patchset," because in the end, this is not your
patch set. It's not reasonable to complain that a patch someone else
wrote doesn't solve your problem; of course everyone writes patches to
solve their own problems, or those of their employer, not other
people's problems. And that's as it should be, else we will have few
contributors. On the other hand, to the extent that this patch set
makes things worse for a reasonable use case which you have in mind,
that's an entirely legitimate complaint.
After a bit of testing, it seems to me that as things stand today,
things are nearly perfect for the use case that you have in mind. I
would be interested to know whether you agree. If I set up an account
and give it CREATEROLE, it can create users, and it can put them into
the employees role, but it can't SET ROLE to any of those accounts. It
can also ALTER ROLE ... NOLOGIN on any of those accounts. The only gap
I see is that there are certain role-based flags which the CREATEROLE
account cannot set: SUPERUSER, BYPASSRLS, REPLICATION. You might
prefer a system where your bot account had the option to grant those
privileges also, and I think that's a reasonable thing to want.
However, I *also* think it's reasonable to want an account that can
create roles but can't give to those roles membership in roles that it
does not itself possess. Likewise, I think it's reasonable to want an
account that can only drop roles which that account itself created.
These kinds of requirements stem from a different use case than what
you are talking about here, but they seem like fine things to want,
and as far as I know we have pretty broad agreement that they are
reasonable. It seems extremely difficult to make a convincing argument
that this is not a thing which anyone should want to block:
rhaas=> create role bob role pg_execute_server_program;
CREATE ROLE
Honestly, that seems like a big yikes from here. How is it OK to block
"create role bob superuser" yet allow that command? I'm inclined to
think that's just broken. Even if the role were pg_write_all_data
rather than pg_execute_server_program, it's still a heck of a lot of
power to be handing out, and I don't see how anyone could make a
serious argument that we shouldn't have an option to restrict that.
Let me separate the two features that I just mentioned and talk about
them individually:
1. Don't allow a CREATEROLE user to give out membership in groups
which that user does not possess. Leaving aside the details of any
previously-proposed patches and just speaking theoretically, how can
this be implemented? I can think of a few ideas. We could (1A) just
change CREATEROLE to work that way, but IIUC that would break the use
case you outline here, so I guess that's off the table unless I am
misunderstanding the situation. We could also (1B) add a second role
attribute with a different name, like, err, CREATEROLEWEAKLY, that
behaves in that way, leaving the existing one untouched. But we could
also take it a lot further, because someone might want to let an
account hand out a set of privileges which corresponds neither to the
privileges of that account nor to the full set of available
privileges. That leads to another idea: (1C) implement an in-database
system that lets you specify which privileges an account has, and,
separately, which ones it can assign to others. I am skeptical of that
idea because it seems really, really complicated, not only from an
implementation standpoint but even just from a user-experience
standpoint. Suppose user 'userbot' has rights to grant a suitable set
of groups to the new users that it creates -- but then someone creates
a new group. Should that also be added to the things 'userbot' can
grant or not? What if we have 'userbot1' through 'userbot6' and each
of them can grant a different set of roles? I wouldn't mind (1D)
providing a hook that allows the system administrator to install a
loadable module that can enforce any rules it likes, but it seems way
too complicated to me to expose all of this configuration as SQL,
especially because for what I want to do, either (1A) or (1B) is
adequate, and (1B) is a LOT simpler than (1C). It also caters to what
I believe to be a common thing to want, without prejudice to the
possibility that other people want other things.
Joshua, what is your opinion on this point?
2. Only allow a CREATEROLE user to drop users which that account
created, and not just any role that isn't a superuser. Again leaving
aside previous proposals, this cannot be implemented without providing
some means by which we know which CREATEROLE user created which other
user. I believe there are a variety of words we could use to describe
that linkage, and I don't deeply care which ones we pick, although I
have my own preferences. We could speak of the CREATEROLE user being
the owner, manager, or administrator of the created role. We could
speak of a new kind of object, a TENANT, of which the CREATEROLE user
is the administrator and to which the created user is linked. I
proposed this previously and it's still my favorite idea. There are no
doubt other options as well. But it's axiomatic that we cannot
restrict the rights of a CREATEROLE user to drop other roles to a
subset of roles without having some way to define which subset is at
issue.
Now, my motivation for wanting this feature is pretty simple: I want
to have something that feels like a superuser but isn't a full
superuser, and can't interfere with accounts set up by the service
provider, but can do whatever they want to the other ones. But I think
this is potentially useful in the userbot case that you (Joshua)
mention as well, because it seems like it could be pretty desirable to
have a certain list of users which the userbot can't remove, just for
safety, either to limit the damage if somebody gets into that account,
or just to keep the bot from going nuts and doing something it
shouldn't in the event of a programming error. Now, if you DON'T care
about the userbot being able to access this functionality, that's fine
with me, because then there's nothing left to do but argue about what
to call the linkage between the CREATEROLE user and the created user.
Your userbot need not participate in whatever system we decide on, and
things are no worse for that use case than they are today.
But if you DO want the userbot to be able to access that
functionality, then things are more complicated, because now the
linkage has to be special-purpose. In that scenario, we can't say that
the right of a CREATEROLE user to drop a certain other role implies
having the privileges of that other role, because in your use case,
you don't want that, whereas in mine, I do. What makes this
particularly ugly is that we can't, as things currently stand, use a
role as the grouping mechanism, because of the fact that a role can
revoke membership in itself from some other role. It will not do for
roles to remove themselves from the set of roles that the CREATEROLE
user can drop. If we changed that behavior, then perhaps we could just
define a way to say that role X can drop roles if they are members of
group G. In my tenant scenario, G would be granted to X, and in your
userbot scenario, it wouldn't. Everybody wins, except for any people
who like the ability of roles to revoke themselves from any group
whatsoever.
So that leads to these questions: (2A) Do you care about restricting
which roles the userbot can drop? (2B) If yes, do you endorse
restricting the ability of roles to revoke themselves from other
roles?
I think that we don't have any great problems here, at least as far as
this very specific issue is concerned, if either the answer to (2A) is
no or the answer to (2B) is yes. However, if the answer to (2A) is yes
and the answer to (2B) is no, there are difficulties. Evidently in
that case we need some new kind of thing that behaves mostly likes a
group of roles but isn't actually a group of roles -- and that thing
needs to prohibit self-revocation. Given what I've written above, you
may be able to guess my preferred solution: let's call it a TENANT.
Then, my pseudo-super-user can have permission to (i) create roles in
that tenant, (ii) drop roles in that tenant, and (iii) assume the
privileges of roles in that tenant -- and your userbot can have
privileges to do (i) and (ii) but not (iii). All we need do is add a
roltenant column to pg_authid and find three bits someplace
corresponding to (i)-(iii), and we are home.
Thoughts?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Feb 17, 2022 at 12:40 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Jan 31, 2022 at 1:57 PM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:This is precisely the use case I am trying to accomplish with this
patchset, roughly:- An automated bot that creates users and adds them to the employees role
- Bot cannot access any employee (or other roles) table data
- Bot cannot become any employee
- Bot can disable the login of any employeeYes there are attack surfaces around the fringes of login, etc but
those can be mitigated with certificate authentication. My pg_hba
would require any role in the employees role to use cert auth.This would adequately mitigate many threats while greatly enhancing
user management.So, where do we go from here?
I've been thinking about this comment a bit. On the one hand, I have
some reservations about the phrase "the use case I am trying to
accomplish with this patchset," because in the end, this is not your
patch set. It's not reasonable to complain that a patch someone else
wrote doesn't solve your problem; of course everyone writes patches to
solve their own problems, or those of their employer, not other
people's problems. And that's as it should be, else we will have few
contributors. On the other hand, to the extent that this patch set
makes things worse for a reasonable use case which you have in mind,
that's an entirely legitimate complaint.
Yes, absolutely. It is my understanding that generally a community
consensus is attempted, I was throwing my (and Crunchy's) use case out
there as a possible goal, and I have spent time reviewing and testing
the patch, so I think that is fair. Obviously I am not in the position
to stipulate hard requirements.
After a bit of testing, it seems to me that as things stand today,
things are nearly perfect for the use case that you have in mind. I
would be interested to know whether you agree. If I set up an account
and give it CREATEROLE, it can create users, and it can put them into
the employees role, but it can't SET ROLE to any of those accounts. It
can also ALTER ROLE ... NOLOGIN on any of those accounts. The only gap
I see is that there are certain role-based flags which the CREATEROLE
account cannot set: SUPERUSER, BYPASSRLS, REPLICATION. You might
prefer a system where your bot account had the option to grant those
privileges also, and I think that's a reasonable thing to want.
I believe the only issue in the existing patchset was that membership
was required in employees was required for the Bot, but I can apply
the current patchset and test it out more in a bit.
However, I *also* think it's reasonable to want an account that can
create roles but can't give to those roles membership in roles that it
does not itself possess. Likewise, I think it's reasonable to want an
account that can only drop roles which that account itself created.
These kinds of requirements stem from a different use case than what
you are talking about here, but they seem like fine things to want,
and as far as I know we have pretty broad agreement that they are
reasonable. It seems extremely difficult to make a convincing argument
that this is not a thing which anyone should want to block:rhaas=> create role bob role pg_execute_server_program;
CREATE ROLEHonestly, that seems like a big yikes from here. How is it OK to block
"create role bob superuser" yet allow that command? I'm inclined to
think that's just broken. Even if the role were pg_write_all_data
rather than pg_execute_server_program, it's still a heck of a lot of
power to be handing out, and I don't see how anyone could make a
serious argument that we shouldn't have an option to restrict that.
Yes, agreed 100%. To be clear, I do not want Bot in the above use case
to be able to add any role other than employees to new roles it
creates. So we are in complete agreement there, the only difference is
that I do not want Bot to be able to become those roles (or use any
access granted via those roles), it's only job is to manage roles, not
look at data.
Let me separate the two features that I just mentioned and talk about
them individually:1. Don't allow a CREATEROLE user to give out membership in groups
which that user does not possess. Leaving aside the details of any
previously-proposed patches and just speaking theoretically, how can
this be implemented? I can think of a few ideas. We could (1A) just
change CREATEROLE to work that way, but IIUC that would break the use
case you outline here, so I guess that's off the table unless I am
misunderstanding the situation. We could also (1B) add a second role
attribute with a different name, like, err, CREATEROLEWEAKLY, that
behaves in that way, leaving the existing one untouched. But we could
also take it a lot further, because someone might want to let an
account hand out a set of privileges which corresponds neither to the
privileges of that account nor to the full set of available
privileges. That leads to another idea: (1C) implement an in-database
system that lets you specify which privileges an account has, and,
separately, which ones it can assign to others. I am skeptical of that
idea because it seems really, really complicated, not only from an
implementation standpoint but even just from a user-experience
standpoint. Suppose user 'userbot' has rights to grant a suitable set
of groups to the new users that it creates -- but then someone creates
a new group. Should that also be added to the things 'userbot' can
grant or not? What if we have 'userbot1' through 'userbot6' and each
of them can grant a different set of roles? I wouldn't mind (1D)
providing a hook that allows the system administrator to install a
loadable module that can enforce any rules it likes, but it seems way
too complicated to me to expose all of this configuration as SQL,
especially because for what I want to do, either (1A) or (1B) is
adequate, and (1B) is a LOT simpler than (1C). It also caters to what
I believe to be a common thing to want, without prejudice to the
possibility that other people want other things.
if 1A worked for admins, or members I think it may work (i.e., Bot is
admin of employees but not a member of employees and therefore can
manage employees but not become them or read their tables)
For example, today this works (in master):
postgres=# CREATE USER creator password 'a';
CREATE ROLE
postgres=# CREATE ROLE employees ADMIN creator NOLOGIN;
CREATE ROLE
as creator:
postgres=> CREATE USER joshua IN ROLE employees PASSWORD 'a';
ERROR: permission denied to create role
as superuser:
postgres=# CREATE USER joshua LOGIN PASSWORD 'a';
CREATE ROLE
as creator:
postgres=> GRANT employees TO joshua;
GRANT ROLE
postgres=> SET ROLE joshua;
ERROR: permission denied to set role "joshua"
postgres=> SET ROLE employees;
SET
So ADMIN of a role can add membership, but not create, and
unfortunately can SET ROLE to employees.
Can ADMIN mean "can create and drop roles with membership of this role
but not implicitly be a member of the role"?
I think Stephen was advocating for this but wanted to look at the SQL
spec to see if it conflicts.
The current (v8) patch conflates membership and admin:
postgres=# CREATE USER user_creator CREATEROLE WITHOUT ADMIN OPTION
PASSWORD 'a';
CREATE ROLE
postgres=# CREATE ROLE employees ADMIN user_creator NOLOGIN;
CREATE ROLE
(Note I never GRANTED employees to user_creator):
postgres=# \du
List of roles
Role name | Attributes
| Member of
--------------+------------------------------------------------------------+-------------
employees | Cannot login | {}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
user_creator | Create role
| {employees}
postgres=# REVOKE employees FROM user_creator;
REVOKE ROLE
as user_creator:
postgres=> CREATE USER joshua2 IN ROLE employees;
ERROR: must have admin option on role "employees"
This seems non-intuitive to me, employees was never granted, but after
being revoked the admin option is gone.
Joshua, what is your opinion on this point?
2. Only allow a CREATEROLE user to drop users which that account
created, and not just any role that isn't a superuser. Again leaving
aside previous proposals, this cannot be implemented without providing
some means by which we know which CREATEROLE user created which other
user. I believe there are a variety of words we could use to describe
that linkage, and I don't deeply care which ones we pick, although I
have my own preferences. We could speak of the CREATEROLE user being
the owner, manager, or administrator of the created role. We could
speak of a new kind of object, a TENANT, of which the CREATEROLE user
is the administrator and to which the created user is linked. I
proposed this previously and it's still my favorite idea. There are no
doubt other options as well. But it's axiomatic that we cannot
restrict the rights of a CREATEROLE user to drop other roles to a
subset of roles without having some way to define which subset is at
issue.Now, my motivation for wanting this feature is pretty simple: I want
to have something that feels like a superuser but isn't a full
superuser, and can't interfere with accounts set up by the service
provider, but can do whatever they want to the other ones. But I think
this is potentially useful in the userbot case that you (Joshua)
mention as well, because it seems like it could be pretty desirable to
have a certain list of users which the userbot can't remove, just for
safety, either to limit the damage if somebody gets into that account,
or just to keep the bot from going nuts and doing something it
shouldn't in the event of a programming error. Now, if you DON'T care
about the userbot being able to access this functionality, that's fine
with me, because then there's nothing left to do but argue about what
to call the linkage between the CREATEROLE user and the created user.
Your userbot need not participate in whatever system we decide on, and
things are no worse for that use case than they are today.
Not being able to drop roles that weren't created or managed by the
Bot is good. Being able to specify exactly what roles the Bot can drop
is ideal, we may want no automated drops whatsoever (just automated
disabling, to constrain possible damage).
But if you DO want the userbot to be able to access that
functionality, then things are more complicated, because now the
linkage has to be special-purpose. In that scenario, we can't say that
the right of a CREATEROLE user to drop a certain other role implies
having the privileges of that other role, because in your use case,
you don't want that, whereas in mine, I do. What makes this
particularly ugly is that we can't, as things currently stand, use a
role as the grouping mechanism, because of the fact that a role can
revoke membership in itself from some other role. It will not do for
roles to remove themselves from the set of roles that the CREATEROLE
user can drop. If we changed that behavior, then perhaps we could just
define a way to say that role X can drop roles if they are members of
group G. In my tenant scenario, G would be granted to X, and in your
userbot scenario, it wouldn't. Everybody wins, except for any people
who like the ability of roles to revoke themselves from any group
whatsoever.So that leads to these questions: (2A) Do you care about restricting
which roles the userbot can drop? (2B) If yes, do you endorse
restricting the ability of roles to revoke themselves from other
roles?
2A, yes
2B, yes, and IIUC this already exists:
postgres=> select current_user;
current_user
--------------
joshua
(1 row)
postgres=> REVOKE employees FROM joshua;
ERROR: must have admin option on role "employees"
I think that we don't have any great problems here, at least as far as
this very specific issue is concerned, if either the answer to (2A) is
no or the answer to (2B) is yes. However, if the answer to (2A) is yes
and the answer to (2B) is no, there are difficulties. Evidently in
that case we need some new kind of thing that behaves mostly likes a
group of roles but isn't actually a group of roles -- and that thing
needs to prohibit self-revocation. Given what I've written above, you
may be able to guess my preferred solution: let's call it a TENANT.
Then, my pseudo-super-user can have permission to (i) create roles in
that tenant, (ii) drop roles in that tenant, and (iii) assume the
privileges of roles in that tenant -- and your userbot can have
privileges to do (i) and (ii) but not (iii). All we need do is add a
roltenant column to pg_authid and find three bits someplace
corresponding to (i)-(iii), and we are home.
I believe this works.
Show quoted text
Thoughts?
--
Robert Haas
EDB: http://www.enterprisedb.com
[ Been away, catching up on email. ]
On Tue, Feb 22, 2022 at 10:54 AM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:
Yes, absolutely. It is my understanding that generally a community
consensus is attempted, I was throwing my (and Crunchy's) use case out
there as a possible goal, and I have spent time reviewing and testing
the patch, so I think that is fair. Obviously I am not in the position
to stipulate hard requirements.
I agree with all of that -- and thanks for writing back.
if 1A worked for admins, or members I think it may work (i.e., Bot is
admin of employees but not a member of employees and therefore can
manage employees but not become them or read their tables)For example, today this works (in master):
postgres=# CREATE USER creator password 'a';
CREATE ROLE
postgres=# CREATE ROLE employees ADMIN creator NOLOGIN;
CREATE ROLEas creator:
postgres=> CREATE USER joshua IN ROLE employees PASSWORD 'a';
ERROR: permission denied to create roleas superuser:
postgres=# CREATE USER joshua LOGIN PASSWORD 'a';
CREATE ROLEas creator:
postgres=> GRANT employees TO joshua;
GRANT ROLE
postgres=> SET ROLE joshua;
ERROR: permission denied to set role "joshua"
postgres=> SET ROLE employees;
SETSo ADMIN of a role can add membership, but not create, and
unfortunately can SET ROLE to employees.Can ADMIN mean "can create and drop roles with membership of this role
but not implicitly be a member of the role"?
I foresee big problems trying to go in this direction. According to
the documentation, "the ADMIN clause is like ROLE, but the named roles
are added to the new role WITH ADMIN OPTION, giving them the right to
grant membership in this role to others." And for me, the name "WITH
ADMIN OPTION" is a huge red flag. You grant membership in a role, and
you may grant that membership with the admin option, or without the
admin option, but either way you are granting membership. And to me
that is just built into the phraseology. You may be able to buy the
car that you want with or without the all-wheel drive option, and you
may even be able to upgrade a car purchased without that option to
have it later, but you can't buy all-wheel drive in the abstract
without an association to some particular car. That's what it means
for it to be an option.
Now, I think there is a good argument to be made that in this case the
fact that the administration privileges are an option associated with
membership is artificial. I expect we can all agree that it is
conceptually easy to understand the idea of being able to administer a
role and the idea of having that role's privileges as two separate
concepts, neither dependent upon the other, and certainly the SQL
syntax could be written in a way that makes that very natural. But as
it is, what is the equivalent of GRANT employees TO bot WITH ADMIN
OPTION when you want to convey only administration rights and not
membership? GRANT employees TO bot WITH ADMIN OPTION BUT WITHOUT THE
UNDERLYING MEMBERSHIP TO WHICH ADMIN IS AN OPTION? Maybe that sounds
sarcastic, but to me it seems like a genuinely serious problem. People
construct a mental model of how stuff works based to a significant
degree on the structure of the syntax, and I really don't see an
obvious way of extending the grammar in a way that is actually going
to make sense to people.
The current (v8) patch conflates membership and admin:
postgres=# CREATE USER user_creator CREATEROLE WITHOUT ADMIN OPTION
PASSWORD 'a';
CREATE ROLE
postgres=# CREATE ROLE employees ADMIN user_creator NOLOGIN;
CREATE ROLE(Note I never GRANTED employees to user_creator):
I think you did, because even right now without the patch "ADMIN
whatever" is documented to mean membership with admin option.
So that leads to these questions: (2A) Do you care about restricting
which roles the userbot can drop? (2B) If yes, do you endorse
restricting the ability of roles to revoke themselves from other
roles?2A, yes
2B, yes, and IIUC this already exists:
postgres=> select current_user;
current_user
--------------
joshua
(1 row)postgres=> REVOKE employees FROM joshua;
ERROR: must have admin option on role "employees"
No, because as Stephen correctly points out, you've got that REVOKE
command backwards.
I think that we don't have any great problems here, at least as far as
this very specific issue is concerned, if either the answer to (2A) is
no or the answer to (2B) is yes. However, if the answer to (2A) is yes
and the answer to (2B) is no, there are difficulties. Evidently in
that case we need some new kind of thing that behaves mostly likes a
group of roles but isn't actually a group of roles -- and that thing
needs to prohibit self-revocation. Given what I've written above, you
may be able to guess my preferred solution: let's call it a TENANT.
Then, my pseudo-super-user can have permission to (i) create roles in
that tenant, (ii) drop roles in that tenant, and (iii) assume the
privileges of roles in that tenant -- and your userbot can have
privileges to do (i) and (ii) but not (iii). All we need do is add a
roltenant column to pg_authid and find three bits someplace
corresponding to (i)-(iii), and we are home.I believe this works.
Cool.
--
Robert Haas
EDB: http://www.enterprisedb.com
The cfbot is testing the last patch posted to this thread which is the
remove-self-own patch which was already committed. I gather that
there's still (at least one) patch under discussion.
Could I suggest reposting the last version of the main patch, perhaps
rebasing it. That way the cfbot would at least continue to test for
conflicts.
On Fri, Apr 1, 2022 at 10:46 AM Greg Stark <stark@mit.edu> wrote:
The cfbot is testing the last patch posted to this thread which is the
remove-self-own patch which was already committed. I gather that
there's still (at least one) patch under discussion.Could I suggest reposting the last version of the main patch, perhaps
rebasing it. That way the cfbot would at least continue to test for
conflicts.
We should move this patch to the next CF or maybe even mark it
returned with feedback. We're not going to get anything else done here
for v15, and I'm not sure whether what we do beyond that will take
this form or not.
--
Robert Haas
EDB: http://www.enterprisedb.com