From dacee128d165fb1194ec3f1eef612ce410e986c5 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 2 Nov 2016 12:00:00 -0400 Subject: [PATCH 3/6] 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 | 34 ++--- src/test/regress/sql/publication.sql | 2 + src/test/subscription/t/003_privileges.pl | 72 ++++++++++ 24 files changed, 670 insertions(+), 29 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 96cb9185c2..4537fec8eb 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -5366,6 +5366,18 @@ <structname>pg_publication</structname> Columns If true, DELETE operations are replicated for tables in the publication. + + + pubacl + aclitem[] + + + Access privileges; see + and + + for details + + diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index d7738b18b7..81320b1941 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -15859,6 +15859,21 @@ Access Privilege Inquiry Functions does current user have privilege for language + has_publication_privilege(user, + publication, + privilege) + + boolean + does user have privilege for publication + + + has_publication_privilege(publication, + privilege) + + boolean + does current user have privilege for publication + + has_schema_privilege(user, schema, privilege) @@ -15992,6 +16007,9 @@ Access Privilege Inquiry Functions has_language_privilege + has_publication_privilege + + has_schema_privilege @@ -16136,6 +16154,15 @@ Access Privilege Inquiry Functions + has_publication_privilege checks whether a user + can access a publication in a particular way. + Its argument possibilities + are analogous to has_table_privilege. + The desired access privilege type must evaluate to + USAGE. + + + has_schema_privilege 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 4889f3e591..b6636d76b1 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -319,6 +319,11 @@ Security + To use a publication, the remote user of a subscription connection must + have the USAGE privilege on the publication. + + + The subscription apply process will run in the local database with the privileges of a superuser. diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 6b0fbb1ff4..996d183561 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -67,6 +67,10 @@ ON LARGE OBJECT loid [, ...] TO role_specification [, ...] [ WITH GRANT OPTION ] +GRANT { USAGE | ALL [ PRIVILEGES ] } + ON PUBLICATION publication_name [, ...] + TO role_specification [, ...] [ WITH GRANT OPTION ] + GRANT { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] } ON SCHEMA schema_name [, ...] TO role_specification [, ...] [ WITH GRANT OPTION ] @@ -378,6 +382,10 @@ GRANT on Database Objects tables using the server, and also to create, alter, or drop their own user's user mappings associated with that server. + + For publications, this privilege allows a subscription to use the + publication. + diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index fc00129620..6ba532dff8 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 publication_name [, ...] + FROM { [ GROUP ] role_name | PUBLIC } [, ...] + [ CASCADE | RESTRICT ] + +REVOKE [ GRANT OPTION FOR ] { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] } ON SCHEMA schema_name [, ...] FROM { [ GROUP ] role_name | PUBLIC } [, ...] diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index d8579e6a55..20ab6d5b0d 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; @@ -737,6 +748,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); @@ -2937,6 +2957,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; @@ -4197,6 +4337,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 @@ -4507,6 +4708,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 258f3d7ae5..e06b4b7bdb 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 07cc81ee76..2f05264e84 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -6864,6 +6864,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 079f9352fe..e84c8d80c7 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -107,6 +107,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); @@ -795,6 +797,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 */ @@ -887,6 +893,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; @@ -3648,6 +3657,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 75bb7f2c2c..8b6d70f2cb 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -522,6 +522,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 7364a12c25..2e5c9b068d 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -3327,7 +3327,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; @@ -3338,24 +3342,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); @@ -3368,6 +3394,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)); @@ -3388,6 +3418,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", @@ -3408,6 +3442,7 @@ dumpPublication(Archive *fout, PublicationInfo *pubinfo) DumpOptions *dopt = fout->dopt; PQExpBuffer delq; PQExpBuffer query; + char *qpubname; if (dopt->dataOnly) return; @@ -3415,11 +3450,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"); @@ -3452,6 +3487,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 e2e4cbcc08..7011da08fa 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -4966,7 +4966,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, @@ -4985,7 +4987,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; @@ -5004,10 +5006,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 59519f068a..1cb06eeee2 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -2705,6 +2705,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 5f42bde136..42cc94f47c 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201702101 +#define CATALOG_VERSION_NO 201702135 #endif diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index bb7053a942..a1922e03d6 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -3578,6 +3578,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 e0e94dd06b..840e443009 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1744,6 +1744,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 a34d8126b7..6d91656a37 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -159,6 +159,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 @@ -274,6 +275,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); @@ -291,6 +294,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 e63612e0d5..f82c539cf9 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -75,10 +75,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" @@ -116,10 +116,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" @@ -160,18 +160,20 @@ 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) ALTER PUBLICATION testpub_default OWNER TO regress_publication_user2; +GRANT USAGE ON PUBLICATION testpub_default TO regress_publication_user; \dRp+ testpub_default - Publication testpub_default - Inserts | Updates | Deletes ----------+---------+--------- - t | t | t + 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; diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 7b322bb7d9..6f83f66207 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -97,6 +97,8 @@ CREATE PUBLICATION testpub2; -- ok ALTER PUBLICATION testpub_default OWNER TO regress_publication_user2; +GRANT USAGE ON PUBLICATION testpub_default TO regress_publication_user; + \dRp+ testpub_default DROP PUBLICATION testpub_default; 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.11.1