From 89e8b74439f2834b76c5b8939f4c42b274036c40 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 4/6] 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 b6636d76b1..98411af0e6 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -315,7 +315,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 59e5ad00c8..830c612fad 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -131,6 +131,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 996d183561..f4f9afe7da 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 ]
 
@@ -310,6 +310,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>
@@ -553,6 +562,7 @@ <title>Notes</title>
             X -- EXECUTE
             U -- USAGE
             C -- CREATE
+            S -- CREATE SUBSCRIPTION
             c -- CONNECT
             T -- TEMPORARY
      arwdDxtp -- ALL PRIVILEGES (for tables, varies for other objects)
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 20ab6d5b0d..f38e4802bc 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3365,6 +3365,8 @@ string_to_privilege(const char *privname)
 		return ACL_CONNECT;
 	if (strcmp(privname, "publication") == 0)
 		return ACL_PUBLICATION;
+	if (strcmp(privname, "create subscription") == 0)
+		return ACL_CREATE_SUBSCRIPTION;
 	if (strcmp(privname, "rule") == 0)
 		return 0;				/* ignore old RULE privileges */
 	ereport(ERROR,
@@ -3404,6 +3406,8 @@ privilege_to_string(AclMode privilege)
 			return "CONNECT";
 		case ACL_PUBLICATION:
 			return "PUBLICATION";
+		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 ab21e64b48..ed2a294c64 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -25,6 +25,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"
@@ -220,11 +221,13 @@ CreateSubscription(CreateSubscriptionStmt *stmt)
 	char		originname[NAMEDATALEN];
 	bool		create_slot;
 	List	   *publications;
+	AclResult	aclresult;
 
-	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);
 
@@ -575,17 +578,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 2f05264e84..55a5d0a7c7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6777,6 +6777,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 e84c8d80c7..7f4513117a 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -320,6 +320,9 @@ aclparse(const char *s, AclItem *aip)
 			case ACL_PUBLICATION_CHR:
 				read = ACL_PUBLICATION;
 				break;
+			case ACL_CREATE_SUBSCRIPTION_CHR:
+				read = ACL_CREATE_SUBSCRIPTION;
+				break;
 			case 'R':			/* ignore old RULE privileges */
 				read = 0;
 				break;
@@ -1640,6 +1643,8 @@ convert_priv_string(text *priv_type_text)
 		return ACL_CONNECT;
 	if (pg_strcasecmp(priv_type, "PUBLICATION") == 0)
 		return ACL_PUBLICATION;
+	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 */
 
@@ -1738,6 +1743,8 @@ convert_aclright_to_string(int aclright)
 			return "CONNECT";
 		case ACL_PUBLICATION:
 			return "PUBLICATION";
+		case ACL_CREATE_SUBSCRIPTION:
+			return "CREATE SUBSCRIPTION";
 		default:
 			elog(ERROR, "unrecognized aclright: %d", aclright);
 			return NULL;
@@ -3075,6 +3082,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 8b6d70f2cb..00c6b1b411 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -534,6 +534,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 840e443009..488aec08ce 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -76,7 +76,8 @@ typedef uint32 AclMode;			/* a bitmask of privilege bits */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
 #define ACL_PUBLICATION	(1<<12)
-#define N_ACL_RIGHTS	13		/* 1 plus the last 1<<x */
+#define ACL_CREATE_SUBSCRIPTION	(1<<13)
+#define N_ACL_RIGHTS	14		/* 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 6d91656a37..fdd12767f0 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -140,9 +140,10 @@ typedef ArrayType Acl;
 #define ACL_CREATE_TEMP_CHR		'T'
 #define ACL_CONNECT_CHR			'c'
 #define ACL_PUBLICATION_CHR		'p'
+#define ACL_CREATE_SUBSCRIPTION_CHR 'S'
 
 /* string holding all privilege code chars, in order by bitmask position */
-#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTcp"
+#define ACL_ALL_RIGHTS_STR	"arwdDxtXUCTcpS"
 
 /*
  * Bitmasks defining "all rights" for each supported object type
@@ -150,7 +151,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|ACL_PUBLICATION)
 #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 2ccec98b15..7677fbb79f 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -2,7 +2,8 @@
 -- SUBSCRIPTION
 --
 CREATE ROLE regress_subscription_user LOGIN SUPERUSER;
-SET SESSION AUTHORIZATION 'regress_subscription_user';
+CREATE ROLE regress_subscription_user2 LOGIN;
+SET SESSION AUTHORIZATION regress_subscription_user;
 -- fail - no publications
 CREATE SUBSCRIPTION testsub CONNECTION 'foo';
 ERROR:  syntax error at or near ";"
@@ -62,5 +63,16 @@ ALTER SUBSCRIPTION testsub DISABLE;
 
 COMMIT;
 DROP SUBSCRIPTION testsub NODROP SLOT;
+-- 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;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 68c17d5cfd..30ad03e887 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -3,7 +3,8 @@
 --
 
 CREATE ROLE regress_subscription_user LOGIN SUPERUSER;
-SET SESSION AUTHORIZATION 'regress_subscription_user';
+CREATE ROLE regress_subscription_user2 LOGIN;
+SET SESSION AUTHORIZATION regress_subscription_user;
 
 -- fail - no publications
 CREATE SUBSCRIPTION testsub CONNECTION 'foo';
@@ -40,5 +41,16 @@ CREATE SUBSCRIPTION testsub CONNECTION 'dbname=doesnotexist' PUBLICATION testpub
 
 DROP SUBSCRIPTION testsub NODROP SLOT;
 
+-- 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;
-- 
2.11.1

