diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index c56d42dcd2c..abae8d44dfc 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -120,6 +120,7 @@ #include "utils/rls.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/usercontext.h" static bool table_states_valid = false; static List *table_states_not_ready = NIL; @@ -1252,7 +1253,9 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) WalRcvExecResult *res; char originname[NAMEDATALEN]; RepOriginId originid; + UserContext ucxt; bool must_use_password; + bool run_as_owner; /* Check the state of the table synchronization. */ StartTransactionCommand(); @@ -1374,31 +1377,6 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) */ rel = table_open(MyLogicalRepWorker->relid, RowExclusiveLock); - /* - * Check that our table sync worker has permission to insert into the - * target table. - */ - aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(), - ACL_INSERT); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, - get_relkind_objtype(rel->rd_rel->relkind), - RelationGetRelationName(rel)); - - /* - * COPY FROM does not honor RLS policies. That is not a problem for - * subscriptions owned by roles with BYPASSRLS privilege (or superuser, - * who has it implicitly), but other roles should not be able to - * circumvent RLS. Disallow logical replication into RLS enabled - * relations for such roles. - */ - if (check_enable_rls(RelationGetRelid(rel), InvalidOid, false) == RLS_ENABLED) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("user \"%s\" cannot replicate into relation with row-level security enabled: \"%s\"", - GetUserNameFromId(GetUserId(), true), - RelationGetRelationName(rel)))); - /* * Start a transaction in the remote node in REPEATABLE READ mode. This * ensures that both the replication slot we create (see below) and the @@ -1456,6 +1434,39 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) originname))); } + /* + * Make sure that the copy command runs as the table owner, unless + * the user has opted out of that behaviour. + */ + run_as_owner = MySubscription->runasowner; + if (!run_as_owner) + SwitchToUntrustedUser(rel->rd_rel->relowner, &ucxt); + + /* + * Check that our table sync worker has permission to insert into the + * target table. + */ + aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(), + ACL_INSERT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, + get_relkind_objtype(rel->rd_rel->relkind), + RelationGetRelationName(rel)); + + /* + * COPY FROM does not honor RLS policies. That is not a problem for + * subscriptions owned by roles with BYPASSRLS privilege (or superuser, + * who has it implicitly), but other roles should not be able to + * circumvent RLS. Disallow logical replication into RLS enabled + * relations for such roles. + */ + if (check_enable_rls(RelationGetRelid(rel), InvalidOid, false) == RLS_ENABLED) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("user \"%s\" cannot replicate into relation with row-level security enabled: \"%s\"", + GetUserNameFromId(GetUserId(), true), + RelationGetRelationName(rel)))); + /* Now do the initial data copy */ PushActiveSnapshot(GetTransactionSnapshot()); copy_table(rel); @@ -1469,6 +1480,9 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) res->err))); walrcv_clear_result(res); + if(!run_as_owner) + RestoreUserContext(&ucxt); + table_close(rel, NoLock); /* Make the copy visible. */ diff --git a/src/test/subscription/t/033_run_as_table_owner.pl b/src/test/subscription/t/033_run_as_table_owner.pl index 0aa8a093efc..2d0e0e78e3e 100644 --- a/src/test/subscription/t/033_run_as_table_owner.pl +++ b/src/test/subscription/t/033_run_as_table_owner.pl @@ -70,8 +70,8 @@ sub revoke_superuser # Create publisher and subscriber nodes with schemas owned and published by # "regress_alice" but subscribed and replicated by different role -# "regress_admin". For partitioned tables, layout the partitions differently -# on the publisher than on the subscriber. +# "regress_admin" and "regress_admin2". For partitioned tables, layout the +# partitions differently on the publisher than on the subscriber. # $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); @@ -86,6 +86,7 @@ for my $node ($node_publisher, $node_subscriber) $node->safe_psql( 'postgres', qq( CREATE ROLE regress_admin SUPERUSER LOGIN; + CREATE ROLE regress_admin2 SUPERUSER LOGIN; CREATE ROLE regress_alice NOSUPERUSER LOGIN; GRANT CREATE ON DATABASE postgres TO regress_alice; SET SESSION AUTHORIZATION regress_alice; @@ -192,4 +193,37 @@ GRANT regress_alice TO regress_admin WITH INHERIT TRUE, SET FALSE; expect_replication("alice.unpartitioned", 3, 7, 13, "with INHERIT but not SET ROLE can replicate"); +# Remove the subscrition and truncate the table for the initial data sync +# tests. +$node_subscriber->safe_psql( + 'postgres', qq( +DROP SUBSCRIPTION admin_sub; +TRUNCATE alice.unpartitioned; +)); + +# Create a new subscription "admin_sub" owned by regress_admin2. It's +# disabled so that we revoke superuser privilege after creation. +$node_subscriber->safe_psql( + 'postgres', qq( +SET SESSION AUTHORIZATION regress_admin2; +CREATE SUBSCRIPTION admin_sub CONNECTION '$publisher_connstr' PUBLICATION alice +WITH (run_as_owner = false, password_required = false, copy_data = true, enabled = false); +)); + +# Revoke superuser privilege for "regress_admin2", and give it the +# ability to SET ROLE. Then enable the subscription "admin_sub". +revoke_superuser("regress_admin2"); +$node_subscriber->safe_psql( + 'postgres', qq( +GRANT regress_alice TO regress_admin2 WITH INHERIT FALSE, SET TRUE; +ALTER SUBSCRIPTION admin_sub ENABLE; +)); + +# Because the initial data sync is working as the table owner, all +# data should be copied. +$node_subscriber->wait_for_subscription_sync($node_publisher, + 'admin_sub'); +expect_replication("alice.unpartitioned", 3, 7, 13, + "table owner can do the initial data copy"); + done_testing();