From dd96a267f96e21ea390c5de46058c3158a5feef8 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Wed, 2 Nov 2016 12:00:00 -0400
Subject: [PATCH v2 3/5] Add USAGE privilege for publications

Add pg_publication.pubacl column and associated infrastructure so that
publications can have privileges.  USAGE privilege on the publication is
now required for a connecting subscription to be able to use it.
Previously, any connecting user could use any publication, which was not
unreasonable because that user needs to have the REPLICATION attribute,
which is pretty powerful anyway, but we might want to move away from
that, and this gives finer control.
---
 doc/src/sgml/catalogs.sgml                  |  12 ++
 doc/src/sgml/func.sgml                      |  27 ++++
 doc/src/sgml/logical-replication.sgml       |   5 +
 doc/src/sgml/ref/grant.sgml                 |   8 ++
 doc/src/sgml/ref/revoke.sgml                |   6 +
 src/backend/catalog/aclchk.c                | 213 ++++++++++++++++++++++++++++
 src/backend/commands/event_trigger.c        |   1 +
 src/backend/commands/publicationcmds.c      |   2 +
 src/backend/parser/gram.y                   |   8 ++
 src/backend/replication/pgoutput/pgoutput.c |   8 ++
 src/backend/utils/adt/acl.c                 | 200 ++++++++++++++++++++++++++
 src/bin/pg_dump/dumputils.c                 |   2 +
 src/bin/pg_dump/pg_dump.c                   |  60 ++++++--
 src/bin/pg_dump/pg_dump.h                   |   4 +
 src/bin/psql/describe.c                     |   8 +-
 src/bin/psql/tab-complete.c                 |   1 +
 src/include/catalog/catversion.h            |   2 +-
 src/include/catalog/pg_proc.h               |  13 ++
 src/include/catalog/pg_publication.h        |   6 +-
 src/include/nodes/parsenodes.h              |   1 +
 src/include/utils/acl.h                     |   4 +
 src/test/regress/expected/publication.out   |  33 +++--
 src/test/regress/sql/publication.sql        |   4 +
 src/test/subscription/t/003_privileges.pl   |  72 ++++++++++
 24 files changed, 675 insertions(+), 25 deletions(-)
 create mode 100644 src/test/subscription/t/003_privileges.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 2c2da2ad8a..5f5fdc31c0 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5378,6 +5378,18 @@ <title><structname>pg_publication</structname> Columns</title>
       <entry>If true, <command>DELETE</command> operations are replicated for
        tables in the publication.</entry>
      </row>
+
+     <row>
+      <entry><structfield>pubacl</structfield></entry>
+      <entry><type>aclitem[]</type></entry>
+      <entry></entry>
+      <entry>
+       Access privileges; see
+       <xref linkend="sql-grant"> and
+       <xref linkend="sql-revoke">
+       for details
+      </entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index a521912317..6cb4cff199 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -16173,6 +16173,21 @@ <title>Access Privilege Inquiry Functions</title>
        <entry>does current user have privilege for language</entry>
       </row>
       <row>
+       <entry><literal><function>has_publication_privilege</function>(<parameter>user</parameter>,
+                                  <parameter>publication</parameter>,
+                                  <parameter>privilege</parameter>)</literal>
+       </entry>
+       <entry><type>boolean</type></entry>
+       <entry>does user have privilege for publication</entry>
+      </row>
+      <row>
+       <entry><literal><function>has_publication_privilege</function>(<parameter>publication</parameter>,
+                                  <parameter>privilege</parameter>)</literal>
+       </entry>
+       <entry><type>boolean</type></entry>
+       <entry>does current user have privilege for publication</entry>
+      </row>
+      <row>
        <entry><literal><function>has_schema_privilege</function>(<parameter>user</parameter>,
                                   <parameter>schema</parameter>,
                                   <parameter>privilege</parameter>)</literal>
@@ -16306,6 +16321,9 @@ <title>Access Privilege Inquiry Functions</title>
     <primary>has_language_privilege</primary>
    </indexterm>
    <indexterm>
+    <primary>has_publication_privilege</primary>
+   </indexterm>
+   <indexterm>
     <primary>has_schema_privilege</primary>
    </indexterm>
    <indexterm>
@@ -16450,6 +16468,15 @@ <title>Access Privilege Inquiry Functions</title>
    </para>
 
    <para>
+    <function>has_publication_privilege</function> checks whether a user
+    can access a publication in a particular way.
+    Its argument possibilities
+    are analogous to <function>has_table_privilege</function>.
+    The desired access privilege type must evaluate to
+    <literal>USAGE</literal>.
+   </para>
+
+   <para>
     <function>has_schema_privilege</function> checks whether a user
     can access a schema in a particular way.
     Its argument possibilities
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 6da39d25e3..4c8d454c9e 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -316,6 +316,11 @@ <title>Security</title>
   </para>
 
   <para>
+   To use a publication, the remote user of a subscription connection must
+   have the <literal>USAGE</literal> privilege on the publication.
+  </para>
+
+  <para>
    The subscription apply process will run in the local database with the
    privileges of a superuser.
   </para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 9fb4c2fd7e..e1336e6857 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -67,6 +67,10 @@
     ON LARGE OBJECT <replaceable class="PARAMETER">loid</replaceable> [, ...]
     TO <replaceable class="PARAMETER">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON PUBLICATION <replaceable>publication_name</replaceable> [, ...]
+    TO <replaceable class="PARAMETER">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+
 GRANT { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] }
     ON SCHEMA <replaceable>schema_name</replaceable> [, ...]
     TO <replaceable class="PARAMETER">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -368,6 +372,10 @@ <title>GRANT on Database Objects</title>
        tables using the server, and also to create, alter, or drop their own
        user's user mappings associated with that server.
       </para>
+      <para>
+       For publications, this privilege allows a subscription to use the
+       publication.
+      </para>
      </listitem>
     </varlistentry>
 
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index ce532543f0..ba1a4e73a8 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -88,6 +88,12 @@
     [ CASCADE | RESTRICT ]
 
 REVOKE [ GRANT OPTION FOR ]
+    { USAGE | ALL [ PRIVILEGES ] }
+    ON PUBLICATION <replaceable>publication_name</replaceable> [, ...]
+    FROM { [ GROUP ] <replaceable class="PARAMETER">role_name</replaceable> | PUBLIC } [, ...]
+    [ CASCADE | RESTRICT ]
+
+REVOKE [ GRANT OPTION FOR ]
     { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] }
     ON SCHEMA <replaceable>schema_name</replaceable> [, ...]
     FROM { [ GROUP ] <replaceable class="PARAMETER">role_name</replaceable> | PUBLIC } [, ...]
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index be86d76a59..e029485c8e 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -109,6 +109,7 @@ static void ExecGrant_Function(InternalGrant *grantStmt);
 static void ExecGrant_Language(InternalGrant *grantStmt);
 static void ExecGrant_Largeobject(InternalGrant *grantStmt);
 static void ExecGrant_Namespace(InternalGrant *grantStmt);
+static void ExecGrant_Publication(InternalGrant *grantStmt);
 static void ExecGrant_Tablespace(InternalGrant *grantStmt);
 static void ExecGrant_Type(InternalGrant *grantStmt);
 
@@ -283,6 +284,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case ACL_KIND_TYPE:
 			whole_mask = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case ACL_KIND_PUBLICATION:
+			whole_mask = ACL_ALL_RIGHTS_PUBLICATION;
+			break;
 		default:
 			elog(ERROR, "unrecognized object kind: %d", objkind);
 			/* not reached, but keep compiler quiet */
@@ -497,6 +501,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_FOREIGN_SERVER;
 			errormsg = gettext_noop("invalid privilege type %s for foreign server");
 			break;
+		case ACL_OBJECT_PUBLICATION:
+			all_privileges = ACL_ALL_RIGHTS_PUBLICATION;
+			errormsg = gettext_noop("invalid privilege type %s for publication");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -594,6 +602,9 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case ACL_OBJECT_NAMESPACE:
 			ExecGrant_Namespace(istmt);
 			break;
+		case ACL_OBJECT_PUBLICATION:
+			ExecGrant_Publication(istmt);
+			break;
 		case ACL_OBJECT_TABLESPACE:
 			ExecGrant_Tablespace(istmt);
 			break;
@@ -736,6 +747,15 @@ objectNamesToOids(GrantObjectType objtype, List *objnames)
 				objects = lappend_oid(objects, srvid);
 			}
 			break;
+		case ACL_OBJECT_PUBLICATION:
+			foreach(cell, objnames)
+			{
+				char	   *pubname = strVal(lfirst(cell));
+				Oid			pubid = get_publication_oid(pubname, false);
+
+				objects = lappend_oid(objects, pubid);
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) objtype);
@@ -2936,6 +2956,126 @@ ExecGrant_Namespace(InternalGrant *istmt)
 }
 
 static void
+ExecGrant_Publication(InternalGrant *istmt)
+{
+	Relation	relation;
+	ListCell   *cell;
+
+	if (istmt->all_privs && istmt->privileges == ACL_NO_RIGHTS)
+		istmt->privileges = ACL_ALL_RIGHTS_PUBLICATION;
+
+	relation = heap_open(PublicationRelationId, RowExclusiveLock);
+
+	foreach(cell, istmt->objects)
+	{
+		Oid			pubId = lfirst_oid(cell);
+		Form_pg_publication pg_publication_tuple;
+		Datum		aclDatum;
+		bool		isNull;
+		AclMode		avail_goptions;
+		AclMode		this_privileges;
+		Acl		   *old_acl;
+		Acl		   *new_acl;
+		Oid			grantorId;
+		Oid			ownerId;
+		HeapTuple	newtuple;
+		Datum		values[Natts_pg_publication];
+		bool		nulls[Natts_pg_publication];
+		bool		replaces[Natts_pg_publication];
+		int			noldmembers;
+		int			nnewmembers;
+		Oid		   *oldmembers;
+		Oid		   *newmembers;
+		HeapTuple	tuple;
+
+		/* Search syscache for pg_publication */
+		tuple = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubId));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for publication %u", pubId);
+
+		pg_publication_tuple = (Form_pg_publication) GETSTRUCT(tuple);
+
+		/*
+		 * Get owner ID and working copy of existing ACL. If there's no ACL,
+		 * substitute the proper default.
+		 */
+		ownerId = pg_publication_tuple->pubowner;
+		aclDatum = heap_getattr(tuple, Anum_pg_publication_pubacl,
+								RelationGetDescr(relation), &isNull);
+		if (isNull)
+		{
+			old_acl = acldefault(ACL_OBJECT_PUBLICATION, ownerId);
+			/* There are no old member roles according to the catalogs */
+			noldmembers = 0;
+			oldmembers = NULL;
+		}
+		else
+		{
+			old_acl = DatumGetAclPCopy(aclDatum);
+			/* Get the roles mentioned in the existing ACL */
+			noldmembers = aclmembers(old_acl, &oldmembers);
+		}
+
+		/* Determine ID to do the grant as, and available grant options */
+		select_best_grantor(GetUserId(), istmt->privileges,
+							old_acl, ownerId,
+							&grantorId, &avail_goptions);
+
+		/*
+		 * Restrict the privileges to what we can actually grant, and emit the
+		 * standards-mandated warning and error messages.
+		 */
+		this_privileges =
+			restrict_and_check_grant(istmt->is_grant, avail_goptions,
+									 istmt->all_privs, istmt->privileges,
+									 pubId, grantorId, ACL_KIND_PUBLICATION,
+									 NameStr(pg_publication_tuple->pubname),
+									 0, NULL);
+
+		/*
+		 * Generate new ACL.
+		 */
+		new_acl = merge_acl_with_grant(old_acl, istmt->is_grant,
+									   istmt->grant_option, istmt->behavior,
+									   istmt->grantees, this_privileges,
+									   grantorId, ownerId);
+
+		/*
+		 * We need the members of both old and new ACLs so we can correct the
+		 * shared dependency information.
+		 */
+		nnewmembers = aclmembers(new_acl, &newmembers);
+
+		/* finished building new ACL value, now insert it */
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, false, sizeof(nulls));
+		MemSet(replaces, false, sizeof(replaces));
+
+		replaces[Anum_pg_publication_pubacl - 1] = true;
+		values[Anum_pg_publication_pubacl - 1] = PointerGetDatum(new_acl);
+
+		newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values,
+									 nulls, replaces);
+
+		CatalogTupleUpdate(relation, &newtuple->t_self, newtuple);
+
+		/* Update the shared dependency ACL info */
+		updateAclDependencies(PublicationRelationId, pubId, 0,
+							  ownerId,
+							  noldmembers, oldmembers,
+							  nnewmembers, newmembers);
+
+		ReleaseSysCache(tuple);
+		pfree(new_acl);
+
+		/* prevent error when processing duplicate objects */
+		CommandCounterIncrement();
+	}
+
+	heap_close(relation, RowExclusiveLock);
+}
+
+static void
 ExecGrant_Tablespace(InternalGrant *istmt)
 {
 	Relation	relation;
@@ -4192,6 +4332,67 @@ pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 }
 
 /*
+ * Exported routine for examining a user's privileges for a publication.
+ */
+AclMode
+pg_publication_aclmask(Oid pub_oid, Oid roleid,
+					   AclMode mask, AclMaskHow how)
+{
+	AclMode		result;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+	Acl		   *acl;
+	Oid			ownerId;
+
+	Form_pg_publication pubForm;
+
+	/* Bypass permission checks for superusers */
+	if (superuser_arg(roleid))
+		return mask;
+
+	/*
+	 * Must get the publication's tuple from pg_publication
+	 */
+	tuple = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pub_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("publication with OID %u does not exist",
+						pub_oid)));
+	pubForm = (Form_pg_publication) GETSTRUCT(tuple);
+
+	/*
+	 * Normal case: get the publication's ACL from pg_publication
+	 */
+	ownerId = pubForm->pubowner;
+
+	aclDatum = SysCacheGetAttr(PUBLICATIONOID, tuple,
+							   Anum_pg_publication_pubacl, &isNull);
+	if (isNull)
+	{
+		/* No ACL, so build default ACL */
+		acl = acldefault(ACL_OBJECT_PUBLICATION, ownerId);
+		aclDatum = (Datum) 0;
+	}
+	else
+	{
+		/* detoast rel's ACL if necessary */
+		acl = DatumGetAclP(aclDatum);
+	}
+
+	result = aclmask(acl, roleid, ownerId, mask, how);
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
  * Exported routine for examining a user's privileges for a type.
  */
 AclMode
@@ -4502,6 +4703,18 @@ pg_foreign_server_aclcheck(Oid srv_oid, Oid roleid, AclMode mode)
 }
 
 /*
+ * Exported routine for checking a user's access privileges to a publication
+ */
+AclResult
+pg_publication_aclcheck(Oid pub_oid, Oid roleid, AclMode mode)
+{
+	if (pg_publication_aclmask(pub_oid, roleid, mode, ACLMASK_ANY) != 0)
+		return ACLCHECK_OK;
+	else
+		return ACLCHECK_NO_PRIV;
+}
+
+/*
  * Exported routine for checking a user's access privileges to a type
  */
 AclResult
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 346b347ae1..7438cfa59a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1199,6 +1199,7 @@ EventTriggerSupportsGrantObjectType(GrantObjectType objtype)
 		case ACL_OBJECT_LANGUAGE:
 		case ACL_OBJECT_LARGEOBJECT:
 		case ACL_OBJECT_NAMESPACE:
+		case ACL_OBJECT_PUBLICATION:
 		case ACL_OBJECT_TYPE:
 			return true;
 		default:
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index d69e39dc5b..6d1f392348 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -212,6 +212,8 @@ CreatePublication(CreatePublicationStmt *stmt)
 	values[Anum_pg_publication_pubdelete - 1] =
 		BoolGetDatum(publish_delete);
 
+	nulls[Anum_pg_publication_pubacl - 1] = true;
+
 	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
 	/* Insert tuple into catalog. */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6316688a88..66b957bde4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6769,6 +6769,14 @@ privilege_target:
 					n->objs = $3;
 					$$ = n;
 				}
+			| PUBLICATION name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = ACL_OBJECT_PUBLICATION;
+					n->objs = $2;
+					$$ = n;
+				}
 			| SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 0ceb4be375..54fb8fa185 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -14,6 +14,8 @@
 
 #include "catalog/pg_publication.h"
 
+#include "miscadmin.h"
+
 #include "replication/logical.h"
 #include "replication/logicalproto.h"
 #include "replication/origin.h"
@@ -398,6 +400,12 @@ LoadPublications(List *pubnames)
 	{
 		char		   *pubname = (char *) lfirst(lc);
 		Publication	   *pub = GetPublicationByName(pubname, false);
+		AclResult		aclresult;
+
+		aclresult = pg_publication_aclcheck(pub->oid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, ACL_KIND_PUBLICATION,
+						   get_publication_name(pub->oid));
 
 		result = lappend(result, pub);
 	}
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 35bdfc9a46..5298e3dfe3 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -106,6 +106,8 @@ static Oid	convert_function_name(text *functionname);
 static AclMode convert_function_priv_string(text *priv_type_text);
 static Oid	convert_language_name(text *languagename);
 static AclMode convert_language_priv_string(text *priv_type_text);
+static Oid	convert_publication_name(text *publicationname);
+static AclMode convert_publication_priv_string(text *priv_type_text);
 static Oid	convert_schema_name(text *schemaname);
 static AclMode convert_schema_priv_string(text *priv_type_text);
 static Oid	convert_server_name(text *servername);
@@ -791,6 +793,10 @@ acldefault(GrantObjectType objtype, Oid ownerId)
 			world_default = ACL_USAGE;
 			owner_default = ACL_ALL_RIGHTS_TYPE;
 			break;
+		case ACL_OBJECT_PUBLICATION:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_PUBLICATION;
+			break;
 		default:
 			elog(ERROR, "unrecognized objtype: %d", (int) objtype);
 			world_default = ACL_NO_RIGHTS;		/* keep compiler quiet */
@@ -883,6 +889,9 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 'S':
 			objtype = ACL_OBJECT_FOREIGN_SERVER;
 			break;
+		case 'p':
+			objtype = ACL_OBJECT_PUBLICATION;
+			break;
 		case 'T':
 			objtype = ACL_OBJECT_TYPE;
 			break;
@@ -3621,6 +3630,197 @@ convert_language_priv_string(text *priv_type_text)
 
 
 /*
+ * has_publication_privilege variants
+ *		These are all named "has_publication_privilege" at the SQL level.
+ *		They take various combinations of publication name, publication OID,
+ *		user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not, or NULL if object doesn't exist.
+ */
+
+/*
+ * has_publication_privilege_name_name
+ *		Check user privileges on a publication given
+ *		name username, text publicationname, and text priv name.
+ */
+Datum
+has_publication_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *publicationname = PG_GETARG_TEXT_P(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(2);
+	Oid			roleid;
+	Oid			publicationoid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	publicationoid = convert_publication_name(publicationname);
+	mode = convert_publication_priv_string(priv_type_text);
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_publication_privilege_name
+ *		Check user privileges on a publication given
+ *		text publicationname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_publication_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *publicationname = PG_GETARG_TEXT_P(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(1);
+	Oid			roleid;
+	Oid			publicationoid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	publicationoid = convert_publication_name(publicationname);
+	mode = convert_publication_priv_string(priv_type_text);
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_publication_privilege_name_id
+ *		Check user privileges on a publication given
+ *		name usename, publication oid, and text priv name.
+ */
+Datum
+has_publication_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			publicationoid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_publication_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(PUBLICATIONOID, ObjectIdGetDatum(publicationoid)))
+		PG_RETURN_NULL();
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_publication_privilege_id
+ *		Check user privileges on a publication given
+ *		publication oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_publication_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			publicationoid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_publication_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(PUBLICATIONOID, ObjectIdGetDatum(publicationoid)))
+		PG_RETURN_NULL();
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_publication_privilege_id_name
+ *		Check user privileges on a publication given
+ *		roleid, text publicationname, and text priv name.
+ */
+Datum
+has_publication_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *publicationname = PG_GETARG_TEXT_P(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(2);
+	Oid			publicationoid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	publicationoid = convert_publication_name(publicationname);
+	mode = convert_publication_priv_string(priv_type_text);
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_publication_privilege_id_id
+ *		Check user privileges on a publication given
+ *		roleid, publication oid, and text priv name.
+ */
+Datum
+has_publication_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			publicationoid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_P(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_publication_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(PUBLICATIONOID, ObjectIdGetDatum(publicationoid)))
+		PG_RETURN_NULL();
+
+	aclresult = pg_publication_aclcheck(publicationoid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_publication_privilege family.
+ */
+
+/*
+ * Given a publication name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_publication_name(text *publicationname)
+{
+	char	   *pubname = text_to_cstring(publicationname);
+
+	return get_publication_oid(pubname, false);
+}
+
+/*
+ * convert_publication_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_publication_priv_string(text *priv_type_text)
+{
+	static const priv_map publication_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, publication_priv_map);
+}
+
+
+/*
  * has_schema_privilege variants
  *		These are all named "has_schema_privilege" at the SQL level.
  *		They take various combinations of schema name, schema OID,
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index b41f2b9125..2e49d5d12a 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -520,6 +520,8 @@ do { \
 		CONVERT_PRIV('X', "EXECUTE");
 	else if (strcmp(type, "LANGUAGE") == 0)
 		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "PUBLICATION") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "SCHEMA") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e67171dccb..7d31d46cbd 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3349,7 +3349,11 @@ dumpPolicy(Archive *fout, PolicyInfo *polinfo)
 void
 getPublications(Archive *fout)
 {
-	PQExpBuffer query;
+	PQExpBuffer acl_subquery = createPQExpBuffer();
+	PQExpBuffer racl_subquery = createPQExpBuffer();
+	PQExpBuffer initacl_subquery = createPQExpBuffer();
+	PQExpBuffer initracl_subquery = createPQExpBuffer();
+	PQExpBuffer query = createPQExpBuffer();
 	PGresult   *res;
 	PublicationInfo *pubinfo;
 	int			i_tableoid;
@@ -3360,24 +3364,46 @@ getPublications(Archive *fout)
 	int			i_pubinsert;
 	int			i_pubupdate;
 	int			i_pubdelete;
+	int			i_pubacl;
+	int			i_rpubacl;
+	int			i_initpubacl;
+	int			i_initrpubacl;
 	int			i,
 				ntups;
 
+
 	if (fout->remoteVersion < 100000)
 		return;
 
-	query = createPQExpBuffer();
-
-	resetPQExpBuffer(query);
+	buildACLQueries(acl_subquery, racl_subquery, initacl_subquery,
+					initracl_subquery, "p.pubacl", "p.pubowner", "'p'",
+					fout->dopt->binary_upgrade);
 
 	/* Get the publications. */
 	appendPQExpBuffer(query,
 					  "SELECT p.tableoid, p.oid, p.pubname, "
+					  "%s AS pubacl, "
+					  "%s AS rpubacl, "
+					  "%s AS initpubacl, "
+					  "%s AS initrpubacl, "
 					  "(%s p.pubowner) AS rolname, "
 					  "p.puballtables, p.pubinsert, p.pubupdate, p.pubdelete "
-					  "FROM pg_catalog.pg_publication p",
+					  "FROM pg_catalog.pg_publication p "
+					  "LEFT JOIN pg_init_privs pip ON "
+					  "(p.oid = pip.objoid "
+					  "AND pip.classoid = 'pg_publication'::regclass "
+					  "AND pip.objsubid = 0) ",
+					  acl_subquery->data,
+					  racl_subquery->data,
+					  initacl_subquery->data,
+					  initracl_subquery->data,
 					  username_subquery);
 
+	destroyPQExpBuffer(acl_subquery);
+	destroyPQExpBuffer(racl_subquery);
+	destroyPQExpBuffer(initacl_subquery);
+	destroyPQExpBuffer(initracl_subquery);
+
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
 	ntups = PQntuples(res);
@@ -3390,6 +3416,10 @@ getPublications(Archive *fout)
 	i_pubinsert = PQfnumber(res, "pubinsert");
 	i_pubupdate = PQfnumber(res, "pubupdate");
 	i_pubdelete = PQfnumber(res, "pubdelete");
+	i_pubacl = PQfnumber(res, "pubacl");
+	i_rpubacl = PQfnumber(res, "rpubacl");
+	i_initpubacl = PQfnumber(res, "initpubacl");
+	i_initrpubacl = PQfnumber(res, "initrpubacl");
 
 	pubinfo = pg_malloc(ntups * sizeof(PublicationInfo));
 
@@ -3410,6 +3440,10 @@ getPublications(Archive *fout)
 			(strcmp(PQgetvalue(res, i, i_pubupdate), "t") == 0);
 		pubinfo[i].pubdelete =
 			(strcmp(PQgetvalue(res, i, i_pubdelete), "t") == 0);
+		pubinfo[i].pubacl = pg_strdup(PQgetvalue(res, i, i_pubacl));
+		pubinfo[i].rpubacl = pg_strdup(PQgetvalue(res, i, i_rpubacl));
+		pubinfo[i].initpubacl = pg_strdup(PQgetvalue(res, i, i_initpubacl));
+		pubinfo[i].initrpubacl = pg_strdup(PQgetvalue(res, i, i_initrpubacl));
 
 		if (strlen(pubinfo[i].rolname) == 0)
 			write_msg(NULL, "WARNING: owner of publication \"%s\" appears to be invalid\n",
@@ -3430,6 +3464,7 @@ dumpPublication(Archive *fout, PublicationInfo *pubinfo)
 	DumpOptions *dopt = fout->dopt;
 	PQExpBuffer delq;
 	PQExpBuffer query;
+	char	   *qpubname;
 
 	if (dopt->dataOnly)
 		return;
@@ -3437,11 +3472,11 @@ dumpPublication(Archive *fout, PublicationInfo *pubinfo)
 	delq = createPQExpBuffer();
 	query = createPQExpBuffer();
 
-	appendPQExpBuffer(delq, "DROP PUBLICATION %s;\n",
-					  fmtId(pubinfo->dobj.name));
+	qpubname = pg_strdup(fmtId(pubinfo->dobj.name));
 
-	appendPQExpBuffer(query, "CREATE PUBLICATION %s",
-					  fmtId(pubinfo->dobj.name));
+	appendPQExpBuffer(delq, "DROP PUBLICATION %s;\n", qpubname);
+
+	appendPQExpBuffer(query, "CREATE PUBLICATION %s", qpubname);
 
 	if (pubinfo->puballtables)
 		appendPQExpBufferStr(query, " FOR ALL TABLES");
@@ -3474,6 +3509,13 @@ dumpPublication(Archive *fout, PublicationInfo *pubinfo)
 				 NULL, 0,
 				 NULL, NULL);
 
+	if (pubinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, pubinfo->dobj.catId, pubinfo->dobj.dumpId, "PUBLICATION",
+				qpubname, NULL, pubinfo->dobj.name,
+				NULL, pubinfo->rolname, pubinfo->pubacl, pubinfo->rpubacl,
+				pubinfo->initpubacl, pubinfo->initrpubacl);
+
+	free(qpubname);
 	destroyPQExpBuffer(delq);
 	destroyPQExpBuffer(query);
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index a466527ec6..533ac24ebf 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -583,6 +583,10 @@ typedef struct _PublicationInfo
 	bool		pubinsert;
 	bool		pubupdate;
 	bool		pubdelete;
+	char	   *pubacl;
+	char	   *rpubacl;
+	char	   *initpubacl;
+	char	   *initrpubacl;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 61a3e2a848..6c697ce448 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5003,7 +5003,9 @@ describePublications(const char *pattern)
 
 	printfPQExpBuffer(&buf,
 					  "SELECT oid, pubname, puballtables, pubinsert,\n"
-					  "  pubupdate, pubdelete\n"
+					  "  pubupdate, pubdelete, ");
+	printACLColumn(&buf, "pubacl");
+	appendPQExpBuffer(&buf,
 					  "FROM pg_catalog.pg_publication\n");
 
 	processSQLNamePattern(pset.db, &buf, pattern, false, false,
@@ -5022,7 +5024,7 @@ describePublications(const char *pattern)
 	for (i = 0; i < PQntuples(res); i++)
 	{
 		const char	align = 'l';
-		int			ncols = 3;
+		int			ncols = 4;
 		int			nrows = 1;
 		int			tables = 0;
 		PGresult   *tabres;
@@ -5041,10 +5043,12 @@ describePublications(const char *pattern)
 		printTableAddHeader(&cont, gettext_noop("Inserts"), true, align);
 		printTableAddHeader(&cont, gettext_noop("Updates"), true, align);
 		printTableAddHeader(&cont, gettext_noop("Deletes"), true, align);
+		printTableAddHeader(&cont, gettext_noop("Access privileges"), true, align);
 
 		printTableAddCell(&cont, PQgetvalue(res, i, 3), false, false);
 		printTableAddCell(&cont, PQgetvalue(res, i, 4), false, false);
 		printTableAddCell(&cont, PQgetvalue(res, i, 5), false, false);
+		printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
 
 		if (puballtables)
 			printfPQExpBuffer(&buf,
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e8458e939e..84633559c0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -2800,6 +2800,7 @@ psql_completion(const char *text, int start, int end)
 								   " UNION SELECT 'FUNCTION'"
 								   " UNION SELECT 'LANGUAGE'"
 								   " UNION SELECT 'LARGE OBJECT'"
+								   " UNION SELECT 'PUBLICATION'"
 								   " UNION SELECT 'SCHEMA'"
 								   " UNION SELECT 'SEQUENCE'"
 								   " UNION SELECT 'TABLE'"
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index b24e3953a1..eb2cb8d862 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	201703151
+#define CATALOG_VERSION_NO	201703155
 
 #endif
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 3d5d866071..990ce2bf33 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -3611,6 +3611,19 @@ DESCR("current user privilege on language by language name");
 DATA(insert OID = 2267 (  has_language_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 2 0 16 "26 25" _null_ _null_ _null_ _null_ _null_ has_language_privilege_id _null_ _null_ _null_ ));
 DESCR("current user privilege on language by language oid");
 
+DATA(insert OID = 4001 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "19 25 25" _null_ _null_ _null_ _null_ _null_	has_publication_privilege_name_name _null_ _null_ _null_ ));
+DESCR("user privilege on publication by username, publication name");
+DATA(insert OID = 4002 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "19 26 25" _null_ _null_ _null_ _null_ _null_	has_publication_privilege_name_id _null_ _null_ _null_ ));
+DESCR("user privilege on publication by username, publication oid");
+DATA(insert OID = 4003 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "26 25 25" _null_ _null_ _null_ _null_ _null_	has_publication_privilege_id_name _null_ _null_ _null_ ));
+DESCR("user privilege on publication by user oid, publication name");
+DATA(insert OID = 4004 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "26 26 25" _null_ _null_ _null_ _null_ _null_	has_publication_privilege_id_id _null_ _null_ _null_ ));
+DESCR("user privilege on publication by user oid, publication oid");
+DATA(insert OID = 4005 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 2 0 16 "25 25" _null_ _null_ _null_ _null_ _null_ has_publication_privilege_name _null_ _null_ _null_ ));
+DESCR("current user privilege on publication by publication name");
+DATA(insert OID = 4006 (  has_publication_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 2 0 16 "26 25" _null_ _null_ _null_ _null_ _null_ has_publication_privilege_id _null_ _null_ _null_ ));
+DESCR("current user privilege on publication by publication oid");
+
 DATA(insert OID = 2268 (  has_schema_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "19 25 25" _null_ _null_ _null_ _null_ _null_	has_schema_privilege_name_name _null_ _null_ _null_ ));
 DESCR("user privilege on schema by username, schema name");
 DATA(insert OID = 2269 (  has_schema_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s s 3 0 16 "19 26 25" _null_ _null_ _null_ _null_ _null_	has_schema_privilege_name_id _null_ _null_ _null_ ));
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index f3c4f3932b..bce81c373f 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -49,6 +49,9 @@ CATALOG(pg_publication,6104)
 	/* true if deletes are published */
 	bool		pubdelete;
 
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		pubacl[1];		/* access permissions */
+#endif
 } FormData_pg_publication;
 
 /* ----------------
@@ -63,13 +66,14 @@ typedef FormData_pg_publication *Form_pg_publication;
  * ----------------
  */
 
-#define Natts_pg_publication				6
+#define Natts_pg_publication				7
 #define Anum_pg_publication_pubname			1
 #define Anum_pg_publication_pubowner		2
 #define Anum_pg_publication_puballtables	3
 #define Anum_pg_publication_pubinsert		4
 #define Anum_pg_publication_pubupdate		5
 #define Anum_pg_publication_pubdelete		6
+#define Anum_pg_publication_pubacl			7
 
 typedef struct PublicationActions
 {
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d576523f6a..276489e556 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1782,6 +1782,7 @@ typedef enum GrantObjectType
 	ACL_OBJECT_LANGUAGE,		/* procedural language */
 	ACL_OBJECT_LARGEOBJECT,		/* largeobject */
 	ACL_OBJECT_NAMESPACE,		/* namespace */
+	ACL_OBJECT_PUBLICATION,		/* publication */
 	ACL_OBJECT_TABLESPACE,		/* tablespace */
 	ACL_OBJECT_TYPE				/* type */
 } GrantObjectType;
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 0d118525c9..5a5836fd4a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -158,6 +158,7 @@ typedef ArrayType Acl;
 #define ACL_ALL_RIGHTS_NAMESPACE	(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_PUBLICATION	(ACL_USAGE)
 
 /* operation codes for pg_*_aclmask */
 typedef enum
@@ -273,6 +274,8 @@ extern AclMode pg_foreign_data_wrapper_aclmask(Oid fdw_oid, Oid roleid,
 								AclMode mask, AclMaskHow how);
 extern AclMode pg_foreign_server_aclmask(Oid srv_oid, Oid roleid,
 						  AclMode mask, AclMaskHow how);
+extern AclMode pg_publication_aclmask(Oid pub_oid, Oid roleid,
+				AclMode mask, AclMaskHow how);
 extern AclMode pg_type_aclmask(Oid type_oid, Oid roleid,
 				AclMode mask, AclMaskHow how);
 
@@ -290,6 +293,7 @@ extern AclResult pg_namespace_aclcheck(Oid nsp_oid, Oid roleid, AclMode mode);
 extern AclResult pg_tablespace_aclcheck(Oid spc_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_data_wrapper_aclcheck(Oid fdw_oid, Oid roleid, AclMode mode);
 extern AclResult pg_foreign_server_aclcheck(Oid srv_oid, Oid roleid, AclMode mode);
+extern AclResult pg_publication_aclcheck(Oid pub_oid, Oid roleid, AclMode mode);
 extern AclResult pg_type_aclcheck(Oid type_oid, Oid roleid, AclMode mode);
 
 extern void aclcheck_error(AclResult aclerr, AclObjectKind objectkind,
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 5a7c0edf7d..f72bb1b2d3 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -76,10 +76,10 @@ ERROR:  relation "testpub_tbl1" is already member of publication "testpub_fortbl
 CREATE PUBLICATION testpub_fortbl FOR TABLE testpub_tbl1;
 ERROR:  publication "testpub_fortbl" already exists
 \dRp+ testpub_fortbl
- Publication testpub_fortbl
- Inserts | Updates | Deletes 
----------+---------+---------
- t       | t       | t
+           Publication testpub_fortbl
+ Inserts | Updates | Deletes | Access privileges 
+---------+---------+---------+-------------------
+ t       | t       | t       | 
 Tables:
     "pub_test.testpub_nopk"
     "public.testpub_tbl1"
@@ -117,10 +117,10 @@ Publications:
     "testpub_fortbl"
 
 \dRp+ testpub_default
- Publication testpub_default
- Inserts | Updates | Deletes 
----------+---------+---------
- t       | t       | t
+           Publication testpub_default
+ Inserts | Updates | Deletes | Access privileges 
+---------+---------+---------+-------------------
+ t       | t       | t       | 
 Tables:
     "pub_test.testpub_nopk"
     "public.testpub_tbl1"
@@ -161,10 +161,10 @@ REVOKE CREATE ON DATABASE regression FROM regress_publication_user2;
 DROP VIEW testpub_view;
 DROP TABLE testpub_tbl1;
 \dRp+ testpub_default
- Publication testpub_default
- Inserts | Updates | Deletes 
----------+---------+---------
- t       | t       | t
+           Publication testpub_default
+ Inserts | Updates | Deletes | Access privileges 
+---------+---------+---------+-------------------
+ t       | t       | t       | 
 (1 row)
 
 -- fail - must be owner of publication
@@ -190,6 +190,15 @@ ALTER PUBLICATION testpub_default OWNER TO regress_publication_user2;
  testpub_default | regress_publication_user2 | t       | t       | t
 (1 row)
 
+GRANT USAGE ON PUBLICATION testpub_default TO regress_publication_user;
+\dRp+ testpub_default
+                             Publication testpub_default
+ Inserts | Updates | Deletes |                   Access privileges                   
+---------+---------+---------+-------------------------------------------------------
+ t       | t       | t       | regress_publication_user2=U/regress_publication_user2+
+         |         |         | regress_publication_user=U/regress_publication_user2
+(1 row)
+
 DROP PUBLICATION testpub_default;
 DROP PUBLICATION testpib_ins_trunct;
 DROP PUBLICATION testpub_fortbl;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index cff9931a77..f237c3faa9 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -112,6 +112,10 @@ CREATE PUBLICATION testpub2;  -- ok
 
 \dRp testpub_default
 
+GRANT USAGE ON PUBLICATION testpub_default TO regress_publication_user;
+
+\dRp+ testpub_default
+
 DROP PUBLICATION testpub_default;
 DROP PUBLICATION testpib_ins_trunct;
 DROP PUBLICATION testpub_fortbl;
diff --git a/src/test/subscription/t/003_privileges.pl b/src/test/subscription/t/003_privileges.pl
new file mode 100644
index 0000000000..86fd866e28
--- /dev/null
+++ b/src/test/subscription/t/003_privileges.pl
@@ -0,0 +1,72 @@
+# Tests of privileges for logical replication
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 2;
+
+my $node_publisher = get_new_node('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+my $node_subscriber = get_new_node('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$node_publisher->safe_psql('postgres', qq(
+CREATE USER test_user1;
+CREATE USER test_user2 REPLICATION;
+GRANT CREATE ON DATABASE postgres TO test_user1;
+
+SET ROLE test_user1;
+CREATE TABLE test1 (a int PRIMARY KEY, b text);
+CREATE PUBLICATION mypub1 FOR TABLE test1;
+));
+
+my $appname = 'tap_sub';
+
+$node_subscriber->safe_psql('postgres', qq(
+CREATE TABLE test1 (a int PRIMARY KEY, b text);
+CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr user=test_user2 application_name=$appname' PUBLICATION mypub1;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+SET ROLE test_user1;
+INSERT INTO test1 VALUES (1, 'one');
+));
+
+my $log = TestLib::slurp_file($node_publisher->logfile);
+like($log, qr/permission denied for publication mypub1/, "permission denied on publication");
+
+$node_publisher->safe_psql('postgres', qq(
+SET ROLE test_user1;
+GRANT USAGE ON PUBLICATION mypub1 TO test_user2;
+));
+
+# drop and recreate subscription so it sees the newly granted
+# privileges
+$node_subscriber->safe_psql('postgres', qq(
+DROP SUBSCRIPTION mysub1;
+CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr user=test_user2 application_name=$appname' PUBLICATION mypub1;
+));
+
+$node_publisher->safe_psql('postgres', qq(
+SET ROLE test_user1;
+INSERT INTO test1 VALUES (2, 'two');
+));
+
+my $caughtup_query =
+	"SELECT pg_current_wal_location() <= replay_location FROM pg_stat_replication WHERE application_name = '$appname';";
+$node_publisher->poll_query_until('postgres', $caughtup_query)
+	or die "Timed out while waiting for subscriber to catch up";
+
+my $result = $node_subscriber->safe_psql('postgres', qq(
+SELECT a, b FROM test1;
+));
+
+is($result, '2|two', 'replication catches up after privileges granted');
+
+$node_subscriber->stop;
+$node_publisher->stop;
-- 
2.12.0

