summaryrefslogtreecommitdiffstats
path: root/src/test/subscription/t/027_nosuperuser.pl
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/subscription/t/027_nosuperuser.pl')
-rw-r--r--src/test/subscription/t/027_nosuperuser.pl319
1 files changed, 319 insertions, 0 deletions
diff --git a/src/test/subscription/t/027_nosuperuser.pl b/src/test/subscription/t/027_nosuperuser.pl
new file mode 100644
index 0000000..8614bf0
--- /dev/null
+++ b/src/test/subscription/t/027_nosuperuser.pl
@@ -0,0 +1,319 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Test that logical replication respects permissions
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+my ($node_publisher, $node_subscriber, $publisher_connstr, $result, $offset);
+$offset = 0;
+
+sub publish_insert
+{
+ my ($tbl, $new_i) = @_;
+ $node_publisher->safe_psql(
+ 'postgres', qq(
+ SET SESSION AUTHORIZATION regress_alice;
+ INSERT INTO $tbl (i) VALUES ($new_i);
+ ));
+}
+
+sub publish_update
+{
+ my ($tbl, $old_i, $new_i) = @_;
+ $node_publisher->safe_psql(
+ 'postgres', qq(
+ SET SESSION AUTHORIZATION regress_alice;
+ UPDATE $tbl SET i = $new_i WHERE i = $old_i;
+ ));
+}
+
+sub publish_delete
+{
+ my ($tbl, $old_i) = @_;
+ $node_publisher->safe_psql(
+ 'postgres', qq(
+ SET SESSION AUTHORIZATION regress_alice;
+ DELETE FROM $tbl WHERE i = $old_i;
+ ));
+}
+
+sub expect_replication
+{
+ my ($tbl, $cnt, $min, $max, $testname) = @_;
+ $node_publisher->wait_for_catchup('admin_sub');
+ $result = $node_subscriber->safe_psql(
+ 'postgres', qq(
+ SELECT COUNT(i), MIN(i), MAX(i) FROM $tbl));
+ is($result, "$cnt|$min|$max", $testname);
+}
+
+sub expect_failure
+{
+ my ($tbl, $cnt, $min, $max, $re, $testname) = @_;
+ $offset = $node_subscriber->wait_for_log($re, $offset);
+ $result = $node_subscriber->safe_psql(
+ 'postgres', qq(
+ SELECT COUNT(i), MIN(i), MAX(i) FROM $tbl));
+ is($result, "$cnt|$min|$max", $testname);
+}
+
+sub revoke_superuser
+{
+ my ($role) = @_;
+ $node_subscriber->safe_psql(
+ 'postgres', qq(
+ ALTER ROLE $role NOSUPERUSER));
+}
+
+sub grant_superuser
+{
+ my ($role) = @_;
+ $node_subscriber->safe_psql(
+ 'postgres', qq(
+ ALTER ROLE $role SUPERUSER));
+}
+
+sub revoke_bypassrls
+{
+ my ($role) = @_;
+ $node_subscriber->safe_psql(
+ 'postgres', qq(
+ ALTER ROLE $role NOBYPASSRLS));
+}
+
+sub grant_bypassrls
+{
+ my ($role) = @_;
+ $node_subscriber->safe_psql(
+ 'postgres', qq(
+ ALTER ROLE $role BYPASSRLS));
+}
+
+# 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.
+#
+$node_publisher = PostgreSQL::Test::Cluster->new('publisher');
+$node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
+$node_publisher->init(allows_streaming => 'logical');
+$node_subscriber->init;
+$node_publisher->start;
+$node_subscriber->start;
+$publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+my %remainder_a = (
+ publisher => 0,
+ subscriber => 1);
+my %remainder_b = (
+ publisher => 1,
+ subscriber => 0);
+
+for my $node ($node_publisher, $node_subscriber)
+{
+ my $remainder_a = $remainder_a{ $node->name };
+ my $remainder_b = $remainder_b{ $node->name };
+ $node->safe_psql(
+ 'postgres', qq(
+ CREATE ROLE regress_admin SUPERUSER LOGIN;
+ CREATE ROLE regress_alice NOSUPERUSER LOGIN;
+ GRANT CREATE ON DATABASE postgres TO regress_alice;
+ SET SESSION AUTHORIZATION regress_alice;
+ CREATE SCHEMA alice;
+ GRANT USAGE ON SCHEMA alice TO regress_admin;
+
+ CREATE TABLE alice.unpartitioned (i INTEGER);
+ ALTER TABLE alice.unpartitioned REPLICA IDENTITY FULL;
+ GRANT SELECT ON TABLE alice.unpartitioned TO regress_admin;
+
+ CREATE TABLE alice.hashpart (i INTEGER) PARTITION BY HASH (i);
+ ALTER TABLE alice.hashpart REPLICA IDENTITY FULL;
+ GRANT SELECT ON TABLE alice.hashpart TO regress_admin;
+ CREATE TABLE alice.hashpart_a PARTITION OF alice.hashpart
+ FOR VALUES WITH (MODULUS 2, REMAINDER $remainder_a);
+ ALTER TABLE alice.hashpart_a REPLICA IDENTITY FULL;
+ CREATE TABLE alice.hashpart_b PARTITION OF alice.hashpart
+ FOR VALUES WITH (MODULUS 2, REMAINDER $remainder_b);
+ ALTER TABLE alice.hashpart_b REPLICA IDENTITY FULL;
+ ));
+}
+$node_publisher->safe_psql(
+ 'postgres', qq(
+SET SESSION AUTHORIZATION regress_alice;
+
+CREATE PUBLICATION alice
+ FOR TABLE alice.unpartitioned, alice.hashpart
+ WITH (publish_via_partition_root = true);
+));
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+SET SESSION AUTHORIZATION regress_admin;
+CREATE SUBSCRIPTION admin_sub CONNECTION '$publisher_connstr' PUBLICATION alice;
+));
+
+# Wait for initial sync to finish
+$node_subscriber->wait_for_subscription_sync($node_publisher, 'admin_sub');
+
+# Verify that "regress_admin" can replicate into the tables
+#
+publish_insert("alice.unpartitioned", 1);
+publish_insert("alice.unpartitioned", 3);
+publish_insert("alice.unpartitioned", 5);
+publish_update("alice.unpartitioned", 1 => 7);
+publish_delete("alice.unpartitioned", 3);
+expect_replication("alice.unpartitioned", 2, 5, 7,
+ "superuser admin replicates into unpartitioned");
+
+# Revoke and restore superuser privilege for "regress_admin",
+# verifying that replication fails while superuser privilege is
+# missing, but works again and catches up once superuser is restored.
+#
+revoke_superuser("regress_admin");
+publish_update("alice.unpartitioned", 5 => 9);
+expect_failure(
+ "alice.unpartitioned",
+ 2,
+ 5,
+ 7,
+ qr/ERROR: ( [A-Z0-9]+:)? permission denied for table unpartitioned/msi,
+ "non-superuser admin fails to replicate update");
+grant_superuser("regress_admin");
+expect_replication("alice.unpartitioned", 2, 7, 9,
+ "admin with restored superuser privilege replicates update");
+
+# Grant INSERT, UPDATE, DELETE privileges on the target tables to
+# "regress_admin" so that superuser privileges are not necessary for
+# replication.
+#
+# Note that UPDATE and DELETE also require SELECT privileges, which
+# will be granted in subsequent test.
+#
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ALTER ROLE regress_admin NOSUPERUSER;
+SET SESSION AUTHORIZATION regress_alice;
+GRANT INSERT,UPDATE,DELETE ON
+ alice.unpartitioned,
+ alice.hashpart, alice.hashpart_a, alice.hashpart_b
+ TO regress_admin;
+REVOKE SELECT ON alice.unpartitioned FROM regress_admin;
+));
+
+publish_insert("alice.unpartitioned", 11);
+expect_replication("alice.unpartitioned", 3, 7, 11,
+ "nosuperuser admin with INSERT privileges can replicate into unpartitioned"
+);
+
+publish_update("alice.unpartitioned", 7 => 13);
+expect_failure(
+ "alice.unpartitioned",
+ 3,
+ 7,
+ 11,
+ qr/ERROR: ( [A-Z0-9]+:)? permission denied for table unpartitioned/msi,
+ "non-superuser admin without SELECT privileges fails to replicate update"
+);
+
+# Now grant SELECT
+#
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+SET SESSION AUTHORIZATION regress_alice;
+GRANT SELECT ON
+ alice.unpartitioned,
+ alice.hashpart, alice.hashpart_a, alice.hashpart_b
+ TO regress_admin;
+));
+
+publish_delete("alice.unpartitioned", 9);
+expect_replication("alice.unpartitioned", 2, 11, 13,
+ "nosuperuser admin with all table privileges can replicate into unpartitioned"
+);
+
+# Test partitioning
+#
+publish_insert("alice.hashpart", 101);
+publish_insert("alice.hashpart", 102);
+publish_insert("alice.hashpart", 103);
+publish_update("alice.hashpart", 102 => 120);
+publish_delete("alice.hashpart", 101);
+expect_replication("alice.hashpart", 2, 103, 120,
+ "nosuperuser admin with all table privileges can replicate into hashpart"
+);
+
+
+# Enable RLS on the target table and check that "regress_admin" can
+# only replicate into it when superuser or bypassrls.
+#
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+SET SESSION AUTHORIZATION regress_alice;
+ALTER TABLE alice.unpartitioned ENABLE ROW LEVEL SECURITY;
+));
+
+revoke_superuser("regress_admin");
+publish_insert("alice.unpartitioned", 15);
+expect_failure(
+ "alice.unpartitioned",
+ 2,
+ 11,
+ 13,
+ qr/ERROR: ( [A-Z0-9]+:)? user "regress_admin" cannot replicate into relation with row-level security enabled: "unpartitioned\w*"/msi,
+ "non-superuser admin fails to replicate insert into rls enabled table");
+grant_superuser("regress_admin");
+expect_replication("alice.unpartitioned", 3, 11, 15,
+ "admin with restored superuser privilege replicates insert into rls enabled unpartitioned"
+);
+
+revoke_superuser("regress_admin");
+publish_update("alice.unpartitioned", 11 => 17);
+expect_failure(
+ "alice.unpartitioned",
+ 3,
+ 11,
+ 15,
+ qr/ERROR: ( [A-Z0-9]+:)? user "regress_admin" cannot replicate into relation with row-level security enabled: "unpartitioned\w*"/msi,
+ "non-superuser admin fails to replicate update into rls enabled unpartitioned"
+);
+
+grant_bypassrls("regress_admin");
+expect_replication("alice.unpartitioned", 3, 13, 17,
+ "admin with bypassrls replicates update into rls enabled unpartitioned");
+
+revoke_bypassrls("regress_admin");
+publish_delete("alice.unpartitioned", 13);
+expect_failure(
+ "alice.unpartitioned",
+ 3,
+ 13,
+ 17,
+ qr/ERROR: ( [A-Z0-9]+:)? user "regress_admin" cannot replicate into relation with row-level security enabled: "unpartitioned\w*"/msi,
+ "non-superuser admin without bypassrls fails to replicate delete into rls enabled unpartitioned"
+);
+grant_bypassrls("regress_admin");
+expect_replication("alice.unpartitioned", 2, 15, 17,
+ "admin with bypassrls replicates delete into rls enabled unpartitioned");
+grant_superuser("regress_admin");
+
+# Alter the subscription owner to "regress_alice". She has neither superuser
+# nor bypassrls, but as the table owner should be able to replicate.
+#
+$node_subscriber->safe_psql(
+ 'postgres', qq(
+ALTER SUBSCRIPTION admin_sub DISABLE;
+ALTER ROLE regress_alice SUPERUSER;
+ALTER SUBSCRIPTION admin_sub OWNER TO regress_alice;
+ALTER ROLE regress_alice NOSUPERUSER;
+ALTER SUBSCRIPTION admin_sub ENABLE;
+));
+
+publish_insert("alice.unpartitioned", 23);
+publish_update("alice.unpartitioned", 15 => 25);
+publish_delete("alice.unpartitioned", 17);
+expect_replication("alice.unpartitioned", 2, 23, 25,
+ "nosuperuser nobypassrls table owner can replicate delete into unpartitioned despite rls"
+);
+
+done_testing();