Honor run_as_owner option in tablesync worker.
authorMasahiko Sawada <[email protected]>
Fri, 9 Jun 2023 01:43:03 +0000 (10:43 +0900)
committerMasahiko Sawada <[email protected]>
Fri, 9 Jun 2023 01:43:03 +0000 (10:43 +0900)
Commit 482675987 introduced "run_as_owner" subscription option so that
subscription runs with either the permissions of the subscription
owner or the permission of the table owner. However, tablesync workers
did not use this option for the initial data copy.

With this change, tablesync workers run with appropriate permissions
based on "run_as_owner" option.

Ajin Cherian, with changes and regression tests added by me.

Reported-By: Amit Kapila
Author: Ajin Cherian, Masahiko Sawada
Reviewed-by: Ajin Cherian, Amit Kapila
Discussion: https://postgr.es/m/CAA4eK1L=qzRHPEn+qeMoKQGFBzqGoLBzt_ov0A89iFFiut+ppA@mail.gmail.com

src/backend/replication/logical/tablesync.c
src/test/subscription/t/033_run_as_table_owner.pl

index c56d42dcd2c2c3aec1f8a74e5c33c70fa955d104..abae8d44dfc64d8d84228d774ede255ef78a698f 100644 (file)
 #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. */
index 0aa8a093efcd5e1d3dd2d0fdb1ec45cd4ec8eb2a..2d0e0e78e3ef5e11a849b173e542567d256430f6 100644 (file)
@@ -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();