From 1b5e990732387b1c40fa61490bf5c9f68fa9a3ea Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Fri, 21 May 2021 08:58:22 -0700
Subject: [PATCH v3 1/5] Add default role for managing logical replication

Reducing the number of administrative tasks that require superuser
privileges by adding a new pg_logical_replication role and
delegating to it the authority to add, alter, or drop publications
and subscriptions on any table regardless of table ownership.
---
 src/backend/catalog/objectaddress.c        |  6 ++-
 src/backend/commands/publicationcmds.c     | 27 ++++++++++----
 src/backend/commands/subscriptioncmds.c    | 26 +++++++++----
 src/include/catalog/pg_authid.dat          |  5 +++
 src/test/regress/expected/privileges.out   | 43 +++++++++++++++++++++-
 src/test/regress/expected/subscription.out |  4 +-
 src/test/regress/sql/privileges.sql        | 42 ++++++++++++++++++++-
 7 files changed, 130 insertions(+), 23 deletions(-)

diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index d79c3cde7c..2347bc2a00 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2506,12 +2506,14 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			}
 			break;
 		case OBJECT_PUBLICATION:
-			if (!pg_publication_ownercheck(address.objectId, roleid))
+			if (!pg_publication_ownercheck(address.objectId, roleid) &&
+				!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal((Value *) object));
 			break;
 		case OBJECT_SUBSCRIPTION:
-			if (!pg_subscription_ownercheck(address.objectId, roleid))
+			if (!pg_subscription_ownercheck(address.objectId, roleid) &&
+				!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal((Value *) object));
 			break;
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 95c253c8e0..681fe8adf7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -24,6 +24,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/objectaddress.h"
 #include "catalog/partition.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_publication.h"
 #include "catalog/pg_publication_rel.h"
@@ -165,11 +166,15 @@ CreatePublication(CreatePublicationStmt *stmt)
 		aclcheck_error(aclresult, OBJECT_DATABASE,
 					   get_database_name(MyDatabaseId));
 
-	/* FOR ALL TABLES requires superuser */
-	if (stmt->for_all_tables && !superuser())
+	/*
+	 * FOR ALL TABLES requires superuser or membership in the
+	 * pg_logical_replication role
+	 */
+	if (stmt->for_all_tables && !superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to create FOR ALL TABLES publication")));
+				 errmsg("must be superuser or a member of the pg_logical_replication role to create FOR ALL TABLES publication")));
 
 	rel = table_open(PublicationRelationId, RowExclusiveLock);
 
@@ -454,7 +459,8 @@ AlterPublication(AlterPublicationStmt *stmt)
 	pubform = (Form_pg_publication) GETSTRUCT(tup);
 
 	/* must be owner */
-	if (!pg_publication_ownercheck(pubform->oid, GetUserId()))
+	if (!pg_publication_ownercheck(pubform->oid, GetUserId()) &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION,
 					   stmt->pubname);
 
@@ -615,8 +621,12 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 		Relation	rel = (Relation) lfirst(lc);
 		ObjectAddress obj;
 
-		/* Must be owner of the table or superuser. */
-		if (!pg_class_ownercheck(RelationGetRelid(rel), GetUserId()))
+		/*
+		 * Must be owner of the table or superuser or a member of the
+		 * pg_logical_replication role.
+		 */
+		if (!pg_class_ownercheck(RelationGetRelid(rel), GetUserId()) &&
+			!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 			aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind),
 						   RelationGetRelationName(rel));
 
@@ -679,7 +689,8 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 	if (form->pubowner == newOwnerId)
 		return;
 
-	if (!superuser())
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 	{
 		AclResult	aclresult;
 
@@ -702,7 +713,7 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 					 errmsg("permission denied to change owner of publication \"%s\"",
 							NameStr(form->pubname)),
-					 errhint("The owner of a FOR ALL TABLES publication must be a superuser.")));
+					 errhint("The owner of a FOR ALL TABLES publication must be a superuser or a member of the pg_logical_replication role.")));
 	}
 
 	form->pubowner = newOwnerId;
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 8aa6de1785..9b1399c593 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -23,6 +23,7 @@
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/objectaddress.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_type.h"
@@ -372,10 +373,11 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
 	if (create_slot)
 		PreventInTransactionBlock(isTopLevel, "CREATE SUBSCRIPTION ... WITH (create_slot = true)");
 
-	if (!superuser())
+	if (!superuser() &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to create subscriptions")));
+				 errmsg("must be superuser or a member of the pg_logical_replication role to create subscriptions")));
 
 	/*
 	 * If built with appropriate switch, whine when regression-testing
@@ -779,7 +781,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
 	subid = form->oid;
 
 	/* must be owner */
-	if (!pg_subscription_ownercheck(subid, GetUserId()))
+	if (!pg_subscription_ownercheck(subid, GetUserId()) &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
 					   stmt->subname);
 
@@ -1096,7 +1099,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel)
 	subid = form->oid;
 
 	/* must be owner */
-	if (!pg_subscription_ownercheck(subid, GetUserId()))
+	if (!pg_subscription_ownercheck(subid, GetUserId()) &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
 					   stmt->subname);
 
@@ -1380,17 +1384,23 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 	if (form->subowner == newOwnerId)
 		return;
 
-	if (!pg_subscription_ownercheck(form->oid, GetUserId()))
+	if (!pg_subscription_ownercheck(form->oid, GetUserId()) &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_LOGICAL_REPLICATION))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION,
 					   NameStr(form->subname));
 
-	/* New owner must be a superuser */
-	if (!superuser_arg(newOwnerId))
+	/*
+	 * New owner must be a superuser or a member of the pg_logical_replication
+	 * role
+	 */
+	if (!superuser_arg(newOwnerId) &&
+		!has_privs_of_role(newOwnerId, ROLE_PG_LOGICAL_REPLICATION))
+
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
 				 errmsg("permission denied to change owner of subscription \"%s\"",
 						NameStr(form->subname)),
-				 errhint("The owner of a subscription must be a superuser.")));
+				 errhint("The owner of a subscription must be a superuser or a member of the pg_logical_replication role.")));
 
 	form->subowner = newOwnerId;
 	CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat
index 3da68016b6..0048a5fab7 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -79,5 +79,10 @@
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
   rolpassword => '_null_', rolvaliduntil => '_null_' },
+{ oid => '9276', oid_symbol => 'ROLE_PG_LOGICAL_REPLICATION',
+  rolname => 'pg_logical_replication', rolsuper => 'f', rolinherit => 't',
+  rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+  rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+  rolpassword => '_null_', rolvaliduntil => '_null_' },
 
 ]
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 83cff902f3..2a2754266f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -13,6 +13,7 @@ DROP ROLE IF EXISTS regress_priv_user4;
 DROP ROLE IF EXISTS regress_priv_user5;
 DROP ROLE IF EXISTS regress_priv_user6;
 DROP ROLE IF EXISTS regress_priv_user7;
+DROP ROLE IF EXISTS regress_priv_user8;
 SELECT lo_unlink(oid) FROM pg_largeobject_metadata WHERE oid >= 1000 AND oid < 3000 ORDER BY oid;
  lo_unlink 
 -----------
@@ -29,6 +30,7 @@ CREATE USER regress_priv_user5;	-- duplicate
 ERROR:  role "regress_priv_user5" already exists
 CREATE USER regress_priv_user6;
 CREATE USER regress_priv_user7;
+CREATE USER regress_priv_user8;
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
 CREATE GROUP regress_priv_group1;
@@ -1829,6 +1831,42 @@ TABLE information_schema.enabled_roles;
 INSERT INTO datdba_only DEFAULT VALUES;
 ERROR:  permission denied for table datdba_only
 ROLLBACK;
+-- test pg_logical_replication
+\c -
+SET client_min_messages = 'ERROR';
+CREATE SUBSCRIPTION owner_subscription CONNECTION 'dbname=nosuchdb' PUBLICATION nosuchpub WITH (connect = false, enabled = false, slot_name = NONE, create_slot = false);
+CREATE PUBLICATION owner_publication FOR TABLE datdba_only;
+RESET client_min_messages;
+GRANT pg_logical_replication TO regress_priv_user8;
+GRANT CREATE ON DATABASE regression TO regress_priv_user8;
+SET SESSION AUTHORIZATION regress_priv_user8;
+SET client_min_messages = 'ERROR';
+CREATE SUBSCRIPTION user8_subscription CONNECTION 'dbname=nosuchdb' PUBLICATION nosuchpub WITH (connect = false, enabled = false, slot_name = NONE, create_slot = false);
+CREATE PUBLICATION user8_publication FOR TABLE datdba_only;
+RESET client_min_messages;
+ALTER PUBLICATION user8_publication ADD TABLE atest1;
+ALTER PUBLICATION user8_publication SET (publish = 'insert, delete');
+ALTER PUBLICATION user8_publication DROP TABLE datdba_only;
+DROP PUBLICATION user8_publication;
+ALTER SUBSCRIPTION user8_subscription SET PUBLICATION nosuchpub WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION user8_subscription ADD PUBLICATION nosuchpub2 WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION user8_subscription DROP PUBLICATION nosuchpub2 WITH (refresh = false);
+DROP SUBSCRIPTION user8_subscription;
+ALTER SUBSCRIPTION owner_subscription SET PUBLICATION nosuchpub WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION owner_subscription ADD PUBLICATION nosuchpub2 WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION owner_subscription OWNER TO regress_priv_user8;
+ALTER SUBSCRIPTION owner_subscription OWNER TO regress_priv_user8;
+ALTER SUBSCRIPTION owner_subscription DROP PUBLICATION nosuchpub2 WITH (refresh = false);
+DROP SUBSCRIPTION owner_subscription;
+ALTER PUBLICATION owner_publication ADD TABLE atest1;
+ALTER PUBLICATION owner_publication DROP TABLE datdba_only;
+ALTER PUBLICATION owner_publication SET TABLE atest2;
+ALTER PUBLICATION owner_publication SET (publish = 'insert, update, delete');
+ALTER PUBLICATION owner_publication OWNER TO regress_priv_user8;
+DROP PUBLICATION owner_publication;
+RESET SESSION AUTHORIZATION;
+REVOKE pg_logical_replication FROM regress_priv_user8;
+REVOKE CREATE ON DATABASE regression FROM regress_priv_user8;
 -- test default ACLs
 \c -
 CREATE SCHEMA testns;
@@ -2328,8 +2366,9 @@ DROP USER regress_priv_user4;
 DROP USER regress_priv_user5;
 DROP USER regress_priv_user6;
 DROP USER regress_priv_user7;
-DROP USER regress_priv_user8; -- does not exist
-ERROR:  role "regress_priv_user8" does not exist
+DROP USER regress_priv_user8;
+DROP USER regress_priv_user9; -- does not exist
+ERROR:  role "regress_priv_user9" does not exist
 -- permissions with LOCK TABLE
 CREATE USER regress_locktable_user;
 CREATE TABLE lock_table (a int);
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 09576c176b..18954a316e 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -43,7 +43,7 @@ ERROR:  subscription "regress_testsub" already exists
 -- fail - must be superuser
 SET SESSION AUTHORIZATION 'regress_subscription_user2';
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION foo WITH (connect = false);
-ERROR:  must be superuser to create subscriptions
+ERROR:  must be superuser or a member of the pg_logical_replication role to create subscriptions
 SET SESSION AUTHORIZATION 'regress_subscription_user';
 -- fail - invalid option combinations
 CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, copy_data = true);
@@ -137,7 +137,7 @@ ALTER SUBSCRIPTION regress_testsub_foo RENAME TO regress_testsub;
 -- fail - new owner must be superuser
 ALTER SUBSCRIPTION regress_testsub OWNER TO regress_subscription_user2;
 ERROR:  permission denied to change owner of subscription "regress_testsub"
-HINT:  The owner of a subscription must be a superuser.
+HINT:  The owner of a subscription must be a superuser or a member of the pg_logical_replication role.
 ALTER ROLE regress_subscription_user2 SUPERUSER;
 -- now it works
 ALTER SUBSCRIPTION regress_testsub OWNER TO regress_subscription_user2;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 3d1a1db987..cae248391c 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -17,6 +17,7 @@ DROP ROLE IF EXISTS regress_priv_user4;
 DROP ROLE IF EXISTS regress_priv_user5;
 DROP ROLE IF EXISTS regress_priv_user6;
 DROP ROLE IF EXISTS regress_priv_user7;
+DROP ROLE IF EXISTS regress_priv_user8;
 
 SELECT lo_unlink(oid) FROM pg_largeobject_metadata WHERE oid >= 1000 AND oid < 3000 ORDER BY oid;
 
@@ -32,6 +33,7 @@ CREATE USER regress_priv_user5;
 CREATE USER regress_priv_user5;	-- duplicate
 CREATE USER regress_priv_user6;
 CREATE USER regress_priv_user7;
+CREATE USER regress_priv_user8;
 
 GRANT pg_read_all_data TO regress_priv_user6;
 GRANT pg_write_all_data TO regress_priv_user7;
@@ -1092,6 +1094,43 @@ TABLE information_schema.enabled_roles;
 INSERT INTO datdba_only DEFAULT VALUES;
 ROLLBACK;
 
+-- test pg_logical_replication
+\c -
+SET client_min_messages = 'ERROR';
+CREATE SUBSCRIPTION owner_subscription CONNECTION 'dbname=nosuchdb' PUBLICATION nosuchpub WITH (connect = false, enabled = false, slot_name = NONE, create_slot = false);
+CREATE PUBLICATION owner_publication FOR TABLE datdba_only;
+RESET client_min_messages;
+GRANT pg_logical_replication TO regress_priv_user8;
+GRANT CREATE ON DATABASE regression TO regress_priv_user8;
+SET SESSION AUTHORIZATION regress_priv_user8;
+SET client_min_messages = 'ERROR';
+CREATE SUBSCRIPTION user8_subscription CONNECTION 'dbname=nosuchdb' PUBLICATION nosuchpub WITH (connect = false, enabled = false, slot_name = NONE, create_slot = false);
+CREATE PUBLICATION user8_publication FOR TABLE datdba_only;
+RESET client_min_messages;
+ALTER PUBLICATION user8_publication ADD TABLE atest1;
+ALTER PUBLICATION user8_publication SET (publish = 'insert, delete');
+ALTER PUBLICATION user8_publication DROP TABLE datdba_only;
+DROP PUBLICATION user8_publication;
+ALTER SUBSCRIPTION user8_subscription SET PUBLICATION nosuchpub WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION user8_subscription ADD PUBLICATION nosuchpub2 WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION user8_subscription DROP PUBLICATION nosuchpub2 WITH (refresh = false);
+DROP SUBSCRIPTION user8_subscription;
+ALTER SUBSCRIPTION owner_subscription SET PUBLICATION nosuchpub WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION owner_subscription ADD PUBLICATION nosuchpub2 WITH (copy_data = false, refresh = false);
+ALTER SUBSCRIPTION owner_subscription OWNER TO regress_priv_user8;
+ALTER SUBSCRIPTION owner_subscription OWNER TO regress_priv_user8;
+ALTER SUBSCRIPTION owner_subscription DROP PUBLICATION nosuchpub2 WITH (refresh = false);
+DROP SUBSCRIPTION owner_subscription;
+ALTER PUBLICATION owner_publication ADD TABLE atest1;
+ALTER PUBLICATION owner_publication DROP TABLE datdba_only;
+ALTER PUBLICATION owner_publication SET TABLE atest2;
+ALTER PUBLICATION owner_publication SET (publish = 'insert, update, delete');
+ALTER PUBLICATION owner_publication OWNER TO regress_priv_user8;
+DROP PUBLICATION owner_publication;
+RESET SESSION AUTHORIZATION;
+REVOKE pg_logical_replication FROM regress_priv_user8;
+REVOKE CREATE ON DATABASE regression FROM regress_priv_user8;
+
 -- test default ACLs
 \c -
 
@@ -1391,7 +1430,8 @@ DROP USER regress_priv_user4;
 DROP USER regress_priv_user5;
 DROP USER regress_priv_user6;
 DROP USER regress_priv_user7;
-DROP USER regress_priv_user8; -- does not exist
+DROP USER regress_priv_user8;
+DROP USER regress_priv_user9; -- does not exist
 
 
 -- permissions with LOCK TABLE
-- 
2.21.1 (Apple Git-122.3)

