Skip to content

Commit a2ab9c0

Browse files
committed
Respect permissions within logical replication.
Prevent logical replication workers from performing insert, update, delete, truncate, or copy commands on tables unless the subscription owner has permission to do so. Prevent subscription owners from circumventing row-level security by forbidding replication into tables with row-level security policies which the subscription owner is subject to, without regard to whether the policy would ordinarily allow the INSERT, UPDATE, DELETE or TRUNCATE which is being replicated. This seems sufficient for now, as superusers, roles with bypassrls, and target table owners should still be able to replicate despite RLS policies. We can revisit the question of applying row-level security policies on a per-row basis if this restriction proves too severe in practice. Author: Mark Dilger Reviewed-by: Jeff Davis, Andrew Dunstan, Ronan Dunklau Discussion: https://postgr.es/m/9DFC88D3-1300-4DE8-ACBC-4CEF84399A53%40enterprisedb.com
1 parent d0d6226 commit a2ab9c0

File tree

6 files changed

+499
-8
lines changed

6 files changed

+499
-8
lines changed

doc/src/sgml/logical-replication.sgml

+28-8
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,27 @@
330330
will simply be skipped.
331331
</para>
332332

333+
<para>
334+
Logical replication operations are performed with the privileges of the role
335+
which owns the subscription. Permissions failures on target tables will
336+
cause replication conflicts, as will enabled
337+
<link linkend="ddl-rowsecurity">row-level security</link> on target tables
338+
that the subscription owner is subject to, without regard to whether any
339+
policy would ordinary reject the <command>INSERT</command>,
340+
<command>UPDATE</command>, <command>DELETE</command> or
341+
<command>TRUNCATE</command> which is being replicated. This restriction on
342+
row-level security may be lifted in a future version of
343+
<productname>PostgreSQL</productname>.
344+
</para>
345+
333346
<para>
334347
A conflict will produce an error and will stop the replication; it must be
335348
resolved manually by the user. Details about the conflict can be found in
336349
the subscriber's server log.
337350
</para>
338351

339352
<para>
340-
The resolution can be done either by changing data on the subscriber so
353+
The resolution can be done either by changing data or permissions on the subscriber so
341354
that it does not conflict with the incoming change or by skipping the
342355
transaction that conflicts with the existing data. The transaction can be
343356
skipped by calling the <link linkend="pg-replication-origin-advance">
@@ -530,9 +543,9 @@
530543

531544
<para>
532545
A user able to modify the schema of subscriber-side tables can execute
533-
arbitrary code as a superuser. Limit ownership
534-
and <literal>TRIGGER</literal> privilege on such tables to roles that
535-
superusers trust. Moreover, if untrusted users can create tables, use only
546+
arbitrary code as the role which owns any subscription which modifies those tables. Limit ownership
547+
and <literal>TRIGGER</literal> privilege on such tables to trusted roles.
548+
Moreover, if untrusted users can create tables, use only
536549
publications that list tables explicitly. That is to say, create a
537550
subscription <literal>FOR ALL TABLES</literal> or
538551
<literal>FOR ALL TABLES IN SCHEMA</literal> only when superusers trust
@@ -576,13 +589,20 @@
576589

577590
<para>
578591
The subscription apply process will run in the local database with the
579-
privileges of a superuser.
592+
privileges of the subscription owner.
593+
</para>
594+
595+
<para>
596+
On the publisher, privileges are only checked once at the start of a
597+
replication connection and are not re-checked as each change record is read.
580598
</para>
581599

582600
<para>
583-
Privileges are only checked once at the start of a replication connection.
584-
They are not re-checked as each change record is read from the publisher,
585-
nor are they re-checked for each change when applied.
601+
On the subscriber, the subscription owner's privileges are re-checked for
602+
each transaction when applied. If a worker is in the process of applying a
603+
transaction when the ownership of the subscription is changed by a
604+
concurrent transaction, the application of the current transaction will
605+
continue under the old owner's privileges.
586606
</para>
587607
</sect1>
588608

src/backend/commands/subscriptioncmds.c

+2
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,8 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
14811481

14821482
InvokeObjectPostAlterHook(SubscriptionRelationId,
14831483
form->oid, 0);
1484+
1485+
ApplyLauncherWakeupAtCommit();
14841486
}
14851487

14861488
/*

src/backend/replication/logical/tablesync.c

+28
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,11 @@
111111
#include "replication/origin.h"
112112
#include "storage/ipc.h"
113113
#include "storage/lmgr.h"
114+
#include "utils/acl.h"
114115
#include "utils/builtins.h"
115116
#include "utils/lsyscache.h"
116117
#include "utils/memutils.h"
118+
#include "utils/rls.h"
117119
#include "utils/snapmgr.h"
118120
#include "utils/syscache.h"
119121

@@ -924,6 +926,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
924926
char relstate;
925927
XLogRecPtr relstate_lsn;
926928
Relation rel;
929+
AclResult aclresult;
927930
WalRcvExecResult *res;
928931
char originname[NAMEDATALEN];
929932
RepOriginId originid;
@@ -1042,6 +1045,31 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
10421045
*/
10431046
rel = table_open(MyLogicalRepWorker->relid, RowExclusiveLock);
10441047

1048+
/*
1049+
* Check that our table sync worker has permission to insert into the
1050+
* target table.
1051+
*/
1052+
aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(),
1053+
ACL_INSERT);
1054+
if (aclresult != ACLCHECK_OK)
1055+
aclcheck_error(aclresult,
1056+
get_relkind_objtype(rel->rd_rel->relkind),
1057+
RelationGetRelationName(rel));
1058+
1059+
/*
1060+
* COPY FROM does not honor RLS policies. That is not a problem for
1061+
* subscriptions owned by roles with BYPASSRLS privilege (or superuser, who
1062+
* has it implicitly), but other roles should not be able to circumvent
1063+
* RLS. Disallow logical replication into RLS enabled relations for such
1064+
* roles.
1065+
*/
1066+
if (check_enable_rls(RelationGetRelid(rel), InvalidOid, false) == RLS_ENABLED)
1067+
ereport(ERROR,
1068+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1069+
errmsg("\"%s\" cannot replicate into relation with row-level security enabled: \"%s\"",
1070+
GetUserNameFromId(GetUserId(), true),
1071+
RelationGetRelationName(rel))));
1072+
10451073
/*
10461074
* Start a transaction in the remote node in REPEATABLE READ mode. This
10471075
* ensures that both the replication slot we create (see below) and the

src/backend/replication/logical/worker.c

+42
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
#include "storage/proc.h"
180180
#include "storage/procarray.h"
181181
#include "tcop/tcopprot.h"
182+
#include "utils/acl.h"
182183
#include "utils/builtins.h"
183184
#include "utils/catcache.h"
184185
#include "utils/dynahash.h"
@@ -189,6 +190,7 @@
189190
#include "utils/lsyscache.h"
190191
#include "utils/memutils.h"
191192
#include "utils/rel.h"
193+
#include "utils/rls.h"
192194
#include "utils/syscache.h"
193195
#include "utils/timeout.h"
194196

@@ -1530,6 +1532,38 @@ GetRelationIdentityOrPK(Relation rel)
15301532
return idxoid;
15311533
}
15321534

1535+
/*
1536+
* Check that we (the subscription owner) have sufficient privileges on the
1537+
* target relation to perform the given operation.
1538+
*/
1539+
static void
1540+
TargetPrivilegesCheck(Relation rel, AclMode mode)
1541+
{
1542+
Oid relid;
1543+
AclResult aclresult;
1544+
1545+
relid = RelationGetRelid(rel);
1546+
aclresult = pg_class_aclcheck(relid, GetUserId(), mode);
1547+
if (aclresult != ACLCHECK_OK)
1548+
aclcheck_error(aclresult,
1549+
get_relkind_objtype(rel->rd_rel->relkind),
1550+
get_rel_name(relid));
1551+
1552+
/*
1553+
* We lack the infrastructure to honor RLS policies. It might be possible
1554+
* to add such infrastructure here, but tablesync workers lack it, too, so
1555+
* we don't bother. RLS does not ordinarily apply to TRUNCATE commands,
1556+
* but it seems dangerous to replicate a TRUNCATE and then refuse to
1557+
* replicate subsequent INSERTs, so we forbid all commands the same.
1558+
*/
1559+
if (check_enable_rls(relid, InvalidOid, false) == RLS_ENABLED)
1560+
ereport(ERROR,
1561+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
1562+
errmsg("\"%s\" cannot replicate into relation with row-level security enabled: \"%s\"",
1563+
GetUserNameFromId(GetUserId(), true),
1564+
RelationGetRelationName(rel))));
1565+
}
1566+
15331567
/*
15341568
* Handle INSERT message.
15351569
*/
@@ -1613,6 +1647,7 @@ apply_handle_insert_internal(ApplyExecutionData *edata,
16131647
ExecOpenIndices(relinfo, false);
16141648

16151649
/* Do the insert. */
1650+
TargetPrivilegesCheck(relinfo->ri_RelationDesc, ACL_INSERT);
16161651
ExecSimpleRelationInsert(relinfo, estate, remoteslot);
16171652

16181653
/* Cleanup. */
@@ -1796,6 +1831,7 @@ apply_handle_update_internal(ApplyExecutionData *edata,
17961831
EvalPlanQualSetSlot(&epqstate, remoteslot);
17971832

17981833
/* Do the actual update. */
1834+
TargetPrivilegesCheck(relinfo->ri_RelationDesc, ACL_UPDATE);
17991835
ExecSimpleRelationUpdate(relinfo, estate, &epqstate, localslot,
18001836
remoteslot);
18011837
}
@@ -1917,6 +1953,7 @@ apply_handle_delete_internal(ApplyExecutionData *edata,
19171953
EvalPlanQualSetSlot(&epqstate, localslot);
19181954

19191955
/* Do the actual delete. */
1956+
TargetPrivilegesCheck(relinfo->ri_RelationDesc, ACL_DELETE);
19201957
ExecSimpleRelationDelete(relinfo, estate, &epqstate, localslot);
19211958
}
19221959
else
@@ -2110,6 +2147,8 @@ apply_handle_tuple_routing(ApplyExecutionData *edata,
21102147
ExecOpenIndices(partrelinfo, false);
21112148

21122149
EvalPlanQualSetSlot(&epqstate, remoteslot_part);
2150+
TargetPrivilegesCheck(partrelinfo->ri_RelationDesc,
2151+
ACL_UPDATE);
21132152
ExecSimpleRelationUpdate(partrelinfo, estate, &epqstate,
21142153
localslot, remoteslot_part);
21152154
ExecCloseIndices(partrelinfo);
@@ -2236,6 +2275,7 @@ apply_handle_truncate(StringInfo s)
22362275
}
22372276

22382277
remote_rels = lappend(remote_rels, rel);
2278+
TargetPrivilegesCheck(rel->localrel, ACL_TRUNCATE);
22392279
rels = lappend(rels, rel->localrel);
22402280
relids = lappend_oid(relids, rel->localreloid);
22412281
if (RelationIsLogicallyLogged(rel->localrel))
@@ -2273,6 +2313,7 @@ apply_handle_truncate(StringInfo s)
22732313
continue;
22742314
}
22752315

2316+
TargetPrivilegesCheck(childrel, ACL_TRUNCATE);
22762317
rels = lappend(rels, childrel);
22772318
part_rels = lappend(part_rels, childrel);
22782319
relids = lappend_oid(relids, childrelid);
@@ -2915,6 +2956,7 @@ maybe_reread_subscription(void)
29152956
strcmp(newsub->slotname, MySubscription->slotname) != 0 ||
29162957
newsub->binary != MySubscription->binary ||
29172958
newsub->stream != MySubscription->stream ||
2959+
newsub->owner != MySubscription->owner ||
29182960
!equal(newsub->publications, MySubscription->publications))
29192961
{
29202962
ereport(LOG,

src/test/perl/PostgreSQL/Test/Cluster.pm

+36
Original file line numberDiff line numberDiff line change
@@ -2599,6 +2599,42 @@ sub wait_for_slot_catchup
25992599

26002600
=pod
26012601
2602+
=item $node->wait_for_log(regexp, offset)
2603+
2604+
Waits for the contents of the server log file, starting at the given offset, to
2605+
match the supplied regular expression. Checks the entire log if no offset is
2606+
given. Times out after 180 seconds.
2607+
2608+
If successful, returns the length of the entire log file, in bytes.
2609+
2610+
=cut
2611+
2612+
sub wait_for_log
2613+
{
2614+
my ($self, $regexp, $offset) = @_;
2615+
$offset = 0 unless defined $offset;
2616+
2617+
my $max_attempts = 180 * 10;
2618+
my $attempts = 0;
2619+
2620+
while ($attempts < $max_attempts)
2621+
{
2622+
my $log = PostgreSQL::Test::Utils::slurp_file($self->logfile, $offset);
2623+
2624+
return $offset+length($log) if ($log =~ m/$regexp/);
2625+
2626+
# Wait 0.1 second before retrying.
2627+
usleep(100_000);
2628+
2629+
$attempts++;
2630+
}
2631+
2632+
# The logs didn't match within 180 seconds. Give up.
2633+
croak "timed out waiting for match: $regexp";
2634+
}
2635+
2636+
=pod
2637+
26022638
=item $node->query_hash($dbname, $query, @columns)
26032639
26042640
Execute $query on $dbname, replacing any appearance of the string __COLUMNS__

0 commit comments

Comments
 (0)