From 3b3a5e1d6d3d24b063151975eb495cfdbcfc71a2 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Mon, 13 Feb 2017 15:42:29 -0500
Subject: [PATCH v2 5/5] Add CREATE SUBSCRIPTION privilege on databases

This new privilege allows the creation of subscriptions.  This was
previously only allowed for superusers.
---
 doc/src/sgml/logical-replication.sgml      |  3 ++-
 doc/src/sgml/ref/create_subscription.sgml  |  6 +++++
 doc/src/sgml/ref/grant.sgml                | 12 +++++++++-
 src/backend/catalog/aclchk.c               |  4 ++++
 src/backend/commands/subscriptioncmds.c    | 38 +++++++++++++++++++-----------
 src/backend/parser/gram.y                  |  6 +++++
 src/backend/utils/adt/acl.c                |  9 +++++++
 src/bin/pg_dump/dumputils.c                |  2 ++
 src/include/nodes/parsenodes.h             |  3 ++-
 src/include/utils/acl.h                    |  5 ++--
 src/test/regress/expected/subscription.out | 14 ++++++++++-
 src/test/regress/sql/subscription.sql      | 14 ++++++++++-
 12 files changed, 95 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 89b4335d00..e49f0ef05b 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -312,7 +312,8 @@ <title>Security</title>
   </para>
 
   <para>
-   To create a subscription, the user must be a superuser.
+   To create a subscription, the user must have the <literal>CREATE
+   SUBSCRIPTION</literal> privilege in the database.
   </para>
 
   <para>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 9bed26219c..94dec39228 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -136,6 +136,12 @@ <title>Parameters</title>
   <title>Notes</title>
 
   <para>
+   To create a subscription, the invoking user must have the
+   <literal>CREATE SUBSCRIPTION</> privilege for the current database.  (Of
+   course, superusers bypass this check.)
+  </para>
+
+  <para>
    See <xref linkend="streaming-replication-authentication"> for details on
    how to configure access control between the subscription and the
    publication instance.
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index e1336e6857..9ca0eadb5c 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -38,7 +38,7 @@
          | ALL SEQUENCES IN SCHEMA <replaceable class="PARAMETER">schema_name</replaceable> [, ...] }
     TO <replaceable class="PARAMETER">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
-GRANT { { CREATE | CONNECT | TEMPORARY | TEMP } [, ...] | ALL [ PRIVILEGES ] }
+GRANT { { CREATE | CONNECT | TEMPORARY | TEMP | CREATE SUBSCRIPTION } [, ...] | ALL [ PRIVILEGES ] }
     ON DATABASE <replaceable>database_name</replaceable> [, ...]
     TO <replaceable class="PARAMETER">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
@@ -300,6 +300,15 @@ <title>GRANT on Database Objects</title>
     </varlistentry>
 
     <varlistentry>
+     <term>CREATE SUBSCRIPTION</term>
+     <listitem>
+      <para>
+       For databases, allows new subscriptions to be created within the database.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
      <term>CONNECT</term>
      <listitem>
       <para>
@@ -542,6 +551,7 @@ <title>Notes</title>
             X -- EXECUTE
             U -- USAGE
             C -- CREATE
+            S -- CREATE SUBSCRIPTION
             c -- CONNECT
             T -- TEMPORARY
       arwdDxt -- ALL PRIVILEGES (for tables, varies for other objects)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index e029485c8e..4ab6e0b784 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3362,6 +3362,8 @@ string_to_privilege(const char *privname)
 		return ACL_CREATE_TEMP;
 	if (strcmp(privname, "connect") == 0)
 		return ACL_CONNECT;
+	if (strcmp(privname, "create subscription") == 0)
+		return ACL_CREATE_SUBSCRIPTION;
 	if (strcmp(privname, "rule") == 0)
 		return 0;				/* ignore old RULE privileges */
 	ereport(ERROR,
@@ -3399,6 +3401,8 @@ privilege_to_string(AclMode privilege)
 			return "TEMP";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_CREATE_SUBSCRIPTION:
+			return "CREATE SUBSCRIPTION";
 		default:
 			elog(ERROR, "unrecognized privilege: %d", (int) privilege);
 	}
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 0198e6d75b..c20de68c75 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -26,6 +26,7 @@
 #include "catalog/pg_type.h"
 #include "catalog/pg_subscription.h"
 
+#include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
 #include "commands/subscriptioncmds.h"
@@ -221,6 +222,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
 	char		originname[NAMEDATALEN];
 	bool		create_slot;
 	List	   *publications;
+	AclResult	aclresult;
 
 	/*
 	 * Parse and check options.
@@ -239,10 +241,11 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
 	if (create_slot)
 		PreventTransactionChain(isTopLevel, "CREATE SUBSCRIPTION ... CREATE SLOT");
 
-	if (!superuser())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 (errmsg("must be superuser to create subscriptions"))));
+	/* must have CREATE SUBSCRIPTION privilege on database */
+	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE_SUBSCRIPTION);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, ACL_KIND_DATABASE,
+					   get_database_name(MyDatabaseId));
 
 	rel = heap_open(SubscriptionRelationId, RowExclusiveLock);
 
@@ -609,17 +612,24 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
 	if (form->subowner == newOwnerId)
 		return;
 
-	if (!pg_subscription_ownercheck(HeapTupleGetOid(tup), GetUserId()))
-		aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_SUBSCRIPTION,
-					   NameStr(form->subname));
+	if (!superuser())
+	{
+		AclResult	aclresult;
 
-	/* New owner must be a superuser */
-	if (!superuser_arg(newOwnerId))
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-		  errmsg("permission denied to change owner of subscription \"%s\"",
-				 NameStr(form->subname)),
-			 errhint("The owner of an subscription must be a superuser.")));
+		/* Must be owner */
+		if (!pg_subscription_ownercheck(HeapTupleGetOid(tup), GetUserId()))
+			aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_SUBSCRIPTION,
+						   NameStr(form->subname));
+
+		/* Must be able to become new owner */
+		check_is_member_of_role(GetUserId(), newOwnerId);
+
+		/* New owner must have CREATE SUBSCRIPTION privilege on database */
+		aclresult = pg_database_aclcheck(MyDatabaseId, newOwnerId, ACL_CREATE_SUBSCRIPTION);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, ACL_KIND_DATABASE,
+						   get_database_name(MyDatabaseId));
+	}
 
 	form->subowner = newOwnerId;
 	CatalogTupleUpdate(rel, &tup->t_self, tup);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 66b957bde4..26f8d9bb0c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6682,6 +6682,12 @@ privilege:	SELECT opt_column_list
 				n->cols = $2;
 				$$ = n;
 			}
+		| CREATE ColId
+			{
+				AccessPriv *n = makeNode(AccessPriv);
+				n->priv_name = psprintf("create %s", $2);
+				$$ = n;
+			}
 		;
 
 
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 5298e3dfe3..e0a3f6e6dd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -316,6 +316,9 @@ aclparse(const char *s, AclItem *aip)
 			case ACL_CONNECT_CHR:
 				read = ACL_CONNECT;
 				break;
+			case ACL_CREATE_SUBSCRIPTION_CHR:
+				read = ACL_CREATE_SUBSCRIPTION;
+				break;
 			case 'R':			/* ignore old RULE privileges */
 				read = 0;
 				break;
@@ -1617,6 +1620,8 @@ convert_priv_string(text *priv_type_text)
 		return ACL_CREATE_TEMP;
 	if (pg_strcasecmp(priv_type, "CONNECT") == 0)
 		return ACL_CONNECT;
+	if (pg_strcasecmp(priv_type, "CREATE SUBSCRIPTION") == 0)
+		return ACL_CREATE_SUBSCRIPTION;
 	if (pg_strcasecmp(priv_type, "RULE") == 0)
 		return 0;				/* ignore old RULE privileges */
 
@@ -1713,6 +1718,8 @@ convert_aclright_to_string(int aclright)
 			return "TEMPORARY";
 		case ACL_CONNECT:
 			return "CONNECT";
+		case ACL_CREATE_SUBSCRIPTION:
+			return "CREATE SUBSCRIPTION";
 		default:
 			elog(ERROR, "unrecognized aclright: %d", aclright);
 			return NULL;
@@ -3048,6 +3055,8 @@ convert_database_priv_string(text *priv_type_text)
 		{"TEMP WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_CREATE_TEMP)},
 		{"CONNECT", ACL_CONNECT},
 		{"CONNECT WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_CONNECT)},
+		{"CREATE SUBSCRIPTION", ACL_CREATE_SUBSCRIPTION},
+		{"CREATE SUBSCRIPTION WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_CREATE_SUBSCRIPTION)},
 		{NULL, 0}
 	};
 
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 2e49d5d12a..c7f10b7bd1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -532,6 +532,8 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('c', "CONNECT");
 		CONVERT_PRIV('T', "TEMPORARY");
+		if (remoteVersion >= 100000)
+			CONVERT_PRIV('S', "CREATE SUBSCRIPTION");
 	}
 	else if (strcmp(type, "TABLESPACE") == 0)
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 276489e556..4ec77e53c2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -75,7 +75,8 @@ typedef uint32 AclMode;			/* a bitmask of privilege bits */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
-#define N_ACL_RIGHTS	12		/* 1 plus the last 1<<x */
+#define ACL_CREATE_SUBSCRIPTION	(1<<12)
+#define N_ACL_RIGHTS	13		/* 1 plus the last 1<<x */
 #define ACL_NO_RIGHTS	0
 /* Currently, SELECT ... FOR [KEY] UPDATE/SHARE requires UPDATE privileges */
 #define ACL_SELECT_FOR_UPDATE	ACL_UPDATE
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 5a5836fd4a..a9fdb3250c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -139,9 +139,10 @@ typedef ArrayType Acl;
 #define ACL_CREATE_CHR			'C'
 #define ACL_CREATE_TEMP_CHR		'T'
 #define ACL_CONNECT_CHR			'c'
+#define ACL_CREATE_SUBSCRIPTION_CHR 'S'
 
 /* string holding all privilege code chars, in order by bitmask position */
-#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTc"
+#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTcS"
 
 /*
  * Bitmasks defining "all rights" for each supported object type
@@ -149,7 +150,7 @@ typedef ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
-#define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
+#define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT|ACL_CREATE_SUBSCRIPTION)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
 #define ACL_ALL_RIGHTS_FUNCTION		(ACL_EXECUTE)
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 3471d88ca7..b5aa15336b 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -2,8 +2,9 @@
 -- SUBSCRIPTION
 --
 CREATE ROLE regress_subscription_user LOGIN SUPERUSER;
+CREATE ROLE regress_subscription_user2 LOGIN;
 CREATE ROLE regress_subscription_user_dummy LOGIN NOSUPERUSER;
-SET SESSION AUTHORIZATION 'regress_subscription_user';
+SET SESSION AUTHORIZATION regress_subscription_user;
 -- fail - no publications
 CREATE SUBSCRIPTION testsub CONNECTION 'foo';
 ERROR:  syntax error at or near ";"
@@ -90,6 +91,17 @@ COMMIT;
 BEGIN;
 DROP SUBSCRIPTION testsub NODROP SLOT;
 COMMIT;
+-- permissions
+set client_min_messages to error;
+SET SESSION AUTHORIZATION regress_subscription_user2;
+CREATE SUBSCRIPTION testsub CONNECTION 'dbname=doesnotexist' PUBLICATION testpub WITH (DISABLED, NOCREATE SLOT);  -- fail
+ERROR:  permission denied for database regression
+SET SESSION AUTHORIZATION regress_subscription_user;
+GRANT CREATE SUBSCRIPTION ON DATABASE regression TO regress_subscription_user2;
+SET SESSION AUTHORIZATION regress_subscription_user2;
+CREATE SUBSCRIPTION testsub2 CONNECTION 'dbname=doesnotexist' PUBLICATION testpub WITH (DISABLED, NOCREATE SLOT);  -- ok
+reset client_min_messages;
+DROP SUBSCRIPTION testsub2 NODROP SLOT;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_subscription_user;
 DROP ROLE regress_subscription_user_dummy;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 5c05b14f9e..4b19110abc 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -3,8 +3,9 @@
 --
 
 CREATE ROLE regress_subscription_user LOGIN SUPERUSER;
+CREATE ROLE regress_subscription_user2 LOGIN;
 CREATE ROLE regress_subscription_user_dummy LOGIN NOSUPERUSER;
-SET SESSION AUTHORIZATION 'regress_subscription_user';
+SET SESSION AUTHORIZATION regress_subscription_user;
 
 -- fail - no publications
 CREATE SUBSCRIPTION testsub CONNECTION 'foo';
@@ -65,6 +66,17 @@ CREATE SUBSCRIPTION testsub CONNECTION 'dbname=doesnotexist' PUBLICATION testpub
 DROP SUBSCRIPTION testsub NODROP SLOT;
 COMMIT;
 
+-- permissions
+set client_min_messages to error;
+SET SESSION AUTHORIZATION regress_subscription_user2;
+CREATE SUBSCRIPTION testsub CONNECTION 'dbname=doesnotexist' PUBLICATION testpub WITH (DISABLED, NOCREATE SLOT);  -- fail
+SET SESSION AUTHORIZATION regress_subscription_user;
+GRANT CREATE SUBSCRIPTION ON DATABASE regression TO regress_subscription_user2;
+SET SESSION AUTHORIZATION regress_subscription_user2;
+CREATE SUBSCRIPTION testsub2 CONNECTION 'dbname=doesnotexist' PUBLICATION testpub WITH (DISABLED, NOCREATE SLOT);  -- ok
+reset client_min_messages;
+DROP SUBSCRIPTION testsub2 NODROP SLOT;
+
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_subscription_user;
 DROP ROLE regress_subscription_user_dummy;
-- 
2.12.0

