diff --git a/contrib/test_decoding/expected/permissions.out b/contrib/test_decoding/expected/permissions.out index 8d100646ce6..6e94d7106bd 100644 --- a/contrib/test_decoding/expected/permissions.out +++ b/contrib/test_decoding/expected/permissions.out @@ -29,6 +29,17 @@ SELECT pg_drop_replication_slot('regression_slot'); (1 row) RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET output_plugin_libraries; -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/expected/repack.out b/contrib/test_decoding/expected/repack.out index c4ff41be690..5ddc63238c5 100644 --- a/contrib/test_decoding/expected/repack.out +++ b/contrib/test_decoding/expected/repack.out @@ -106,7 +106,6 @@ CREATE TABLE repack_plugin (a int); SELECT * FROM pg_create_logical_replication_slot('s_repack', 'pgrepack'); ERROR: unsupported use of logical decoding plugin "pgrepack" DETAIL: This plugin can only be used by REPACK (CONCURRENTLY). -CONTEXT: slot "s_repack", output plugin "pgrepack", in the startup callback INSERT INTO repack_plugin VALUES (1); SELECT * FROM pg_logical_slot_get_binary_changes('s_repack', NULL, NULL); ERROR: replication slot "s_repack" does not exist diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out index 7de03c79f6f..cf0136455b0 100644 --- a/contrib/test_decoding/expected/slot.out +++ b/contrib/test_decoding/expected/slot.out @@ -30,8 +30,11 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'tes init (1 row) +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); ERROR: could not access file "nonexistent": No such file or directory +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset \c - diff --git a/contrib/test_decoding/sql/permissions.sql b/contrib/test_decoding/sql/permissions.sql index 94db936aee2..b3d8b1a8e57 100644 --- a/contrib/test_decoding/sql/permissions.sql +++ b/contrib/test_decoding/sql/permissions.sql @@ -15,6 +15,14 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc SELECT pg_drop_replication_slot('regression_slot'); RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET output_plugin_libraries; + -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql index 580e3ae3bef..50a950341fc 100644 --- a/contrib/test_decoding/sql/slot.sql +++ b/contrib/test_decoding/sql/slot.sql @@ -9,7 +9,10 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_p', 'test SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'test_decoding', true); +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 1352ae85c10..88ed724b639 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4858,6 +4858,63 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows + + output_plugin_libraries (string) + + output_plugin_libraries configuration parameter + + + + + Lists the libraries installed in + that are also trusted for use as logical output plugins by replication + clients. Any logical decoding + or replication requests for + other libraries will be refused. All users are subject to this + restriction. The default is 'pgoutput, test_decoding', + which are the two logical output plugins included in the standard + PostgreSQL distribution. + + + The format is a comma-separated list of library names, where each name + is interpreted as for the LOAD + command (but logical decoding clients must specify a plugin name that + exactly matches an entry in the list, without + variations in case or path structure). Whitespace between entries is + ignored; surround a library name with double quotes if you need to + include whitespace or commas in the name. + + + It is the responsibility of the server administrator to ensure that + libraries added to this list do not unintentionally give additional + privileges to non-superusers when they are loaded into the server. + + + + When updating the server from a version that does not have the + output_plugin_libraries parameter, the following + query can help construct the list of plugins that are required by all + persistent logical replication slots: + +SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; + + Review the list carefully for safety before adjusting + output_plugin_libraries. + + + The above query can only display plugins which were successfully + added to replication slots at some point in the past. Newly refused + requests will appear in the logs with a message similar to + +ERROR: library "..." may not be used as an output plugin +DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. + + + + + + wal_keep_size (integer) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index efbed7b3bcb..938c939e97d 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2559,6 +2559,15 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER and it must have the LOGIN attribute. + + The name of the output plugin used by the replication connection must be + included in the server's . (For + subscriptions, the plugin name that is used is pgoutput.) + Superusers may modify the trusted list per-connection, by including + options=-coutput_plugin_libraries=... in the connection + string. + + In order to be able to copy the initial table data or synchronize sequences, the role used for the replication connection must have the @@ -2800,7 +2809,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER The output plugins referenced by the slots in the old cluster must be - installed in the new PostgreSQL executable directory. + installed in the new PostgreSQL executable directory. They must also be + included in the new cluster's ; + see that parameter's documentation for safety information. diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index c30d40a8641..98e5f1dd8f9 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -31,6 +31,7 @@ #include "access/xact.h" #include "access/xlog_internal.h" #include "access/xlogutils.h" +#include "commands/repack.h" #include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" @@ -42,9 +43,11 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/guc.h" #include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" +#include "utils/varlena.h" /* data for errcontext callback */ typedef struct LogicalErrorCallbackState @@ -54,6 +57,9 @@ typedef struct LogicalErrorCallbackState XLogRecPtr report_location; } LogicalErrorCallbackState; +/* GUC variables */ +char *output_plugin_libraries_string; + /* wrappers around output plugin callbacks */ static void output_plugin_error_callback(void *arg); static void startup_cb_wrapper(LogicalDecodingContext *ctx, OutputPluginOptions *opt, @@ -144,6 +150,7 @@ StartupDecodingContext(List *output_plugin_options, bool need_full_snapshot, bool fast_forward, bool in_create, + bool for_repack, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, LogicalOutputPluginWriterWrite do_write, @@ -170,7 +177,86 @@ StartupDecodingContext(List *output_plugin_options, * now. */ if (!fast_forward) - LoadOutputPlugin(&ctx->callbacks, NameStr(slot->data.plugin)); + { + /* + * Before loading this library, make sure it's been blessed for + * logical decoding. + */ + const char *plugin = NameStr(slot->data.plugin); + bool plugin_allowed = false; + + if (strcmp(plugin, "pgrepack") == 0) + { + /* + * REPACK is a special case -- print a more helpful error message + * here if we're not being called in the correct context. + */ + if (!for_repack || !AmRepackWorker()) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unsupported use of logical decoding plugin \"%s\"", + "pgrepack"), + errdetail("This plugin can only be used by %s.", + "REPACK (CONCURRENTLY)")); + + plugin_allowed = true; + } + else if (output_plugin_libraries_string && + output_plugin_libraries_string[0]) + { + /* Check this plugin against output_plugin_libraries. */ + char *rawstring; + List *elemlist = NIL; + + /* Need a modifiable copy */ + rawstring = pstrdup(output_plugin_libraries_string); + + if (!SplitGUCList(rawstring, ',', &elemlist)) + { + /* syntax error in list */ + ereport(LOG, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid list syntax in parameter \"%s\"", + "output_plugin_libraries"))); + + list_free(elemlist); + elemlist = NIL; + } + + foreach_ptr(char, allowed, elemlist) + { + if (strcmp(allowed, plugin) == 0) + { + plugin_allowed = true; + break; + } + } + + list_free(elemlist); + pfree(rawstring); + } + + if (!plugin_allowed) + { + /* + * Use the same error message as check_restricted_library_name(), + * but provide additional context for the DBA in the logs. (The + * HINT will be sent to the client, but that's not a secret.) + */ + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("library \"%s\" may not be used as an output plugin", + plugin), + /*- translator: The second %s is the value of the output_plugin_libraries GUC, which may contain whitespace, commas, and double-quotes (") */ + errdetail_log("The configuration parameter \"%s\" (currently '%s') does not name this library as a trusted output plugin.", + "output_plugin_libraries", + output_plugin_libraries_string), + errhint("If it is safe for all REPLICATION users to use this library as an output plugin, add it to \"%s\" and reload the server configuration.", + "output_plugin_libraries")); + } + + LoadOutputPlugin(&ctx->callbacks, plugin); + } /* * Now that the slot's xmin has been set, we can announce ourselves as a @@ -434,7 +520,7 @@ CreateInitDecodingContext(const char *plugin, ReplicationSlotSave(); ctx = StartupDecodingContext(NIL, restart_lsn, xmin_horizon, - need_full_snapshot, false, true, + need_full_snapshot, false, true, for_repack, xl_routine, prepare_write, do_write, update_progress); @@ -569,7 +655,8 @@ CreateDecodingContext(XLogRecPtr start_lsn, ctx = StartupDecodingContext(output_plugin_options, start_lsn, InvalidTransactionId, false, - fast_forward, false, xl_routine, prepare_write, + fast_forward, false, false, + xl_routine, prepare_write, do_write, update_progress); /* call output plugin initialization callback */ @@ -720,7 +807,9 @@ OutputPluginUpdateProgress(struct LogicalDecodingContext *ctx, /* * Load the output plugin, lookup its output plugin init function, and check - * that it provides the required callbacks. + * that it provides the required callbacks. The caller must have checked that + * the current user has the necessary privileges to load the given plugin; + * standard LOAD restrictions are not applied here. */ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugin) diff --git a/src/backend/replication/pgrepack/pgrepack.c b/src/backend/replication/pgrepack/pgrepack.c index 5c5095bde4e..a1cbc8db68a 100644 --- a/src/backend/replication/pgrepack/pgrepack.c +++ b/src/backend/replication/pgrepack/pgrepack.c @@ -54,12 +54,10 @@ repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, RepackDecodingState *dstate; if (!AmRepackWorker()) - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("unsupported use of logical decoding plugin \"%s\"", - "pgrepack"), - errdetail("This plugin can only be used by %s.", - "REPACK (CONCURRENTLY)")); + { + /* StartupDecodingContext() should have caught this case already */ + elog(FATAL, "unexpected pgrepack startup outside of repack worker"); + } /* Initial setup of our private state */ Assert(CurrentMemoryContext == ctx->context); diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 2cd115deaee..15f9261dab6 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2308,6 +2308,15 @@ ifdef => 'DEBUG_BOUNDED_SORT', }, +{ name => 'output_plugin_libraries', type => 'string', context => 'PGC_SUSET', group => 'REPLICATION_SENDING', + short_desc => 'Lists libraries that may be named as logical decoding output plugins.', + long_desc => 'Users with REPLICATION privileges may only use plugins in this list when creating logical replication slots.', + # Note that src/bin/pg_upgrade/check.c assumes GUC_LIST_QUOTE here. + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'output_plugin_libraries_string', + boot_val => '"pgoutput, test_decoding"', +}, + { name => 'parallel_leader_participation', type => 'bool', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', short_desc => 'Controls whether Gather and Gather Merge also run subplans.', long_desc => 'Should gather nodes also run subplans or just gather tuples?', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 290ccbc543e..7fd83081462 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -72,6 +72,7 @@ #include "postmaster/syslogger.h" #include "postmaster/walsummarizer.h" #include "postmaster/walwriter.h" +#include "replication/logical.h" #include "replication/logicallauncher.h" #include "replication/slot.h" #include "replication/slotsync.h" diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 23dd27957be..f61bd6ad4de 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -863,6 +863,7 @@ #dynamic_library_path = '$libdir' #extension_control_path = '$system' #gin_fuzzy_search_limit = 0 +#output_plugin_libraries = 'pgoutput, test_decoding' # approved plugins for logical decoding #------------------------------------------------------------------------------ diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 0d9f817875d..a08afaf8de5 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -734,6 +734,7 @@ variable_is_guc_list_quote(const char *name) { if (pg_strcasecmp(name, "local_preload_libraries") == 0 || pg_strcasecmp(name, "oauth_validator_libraries") == 0 || + pg_strcasecmp(name, "output_plugin_libraries") == 0 || pg_strcasecmp(name, "search_path") == 0 || pg_strcasecmp(name, "session_preload_libraries") == 0 || pg_strcasecmp(name, "shared_preload_libraries") == 0 || diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 184379c52af..e298e5658da 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -2210,6 +2210,7 @@ check_new_cluster_replication_slots(void) int nslots_on_new; int rdt_slot_on_new; int max_replication_slots; + char *output_plugin_libraries; char *wal_level; int i_nslots_on_new; int i_rdt_slot_on_new; @@ -2269,10 +2270,10 @@ check_new_cluster_replication_slots(void) PQclear(res); res = executeQueryOrDie(conn, "SELECT setting FROM pg_settings " - "WHERE name IN ('wal_level', 'max_replication_slots') " + "WHERE name IN ('wal_level', 'output_plugin_libraries', 'max_replication_slots') " "ORDER BY name DESC;"); - if (PQntuples(res) != 2) + if (PQntuples(res) != 3) pg_fatal("could not determine parameter settings on new cluster"); wal_level = PQgetvalue(res, 0, 0); @@ -2282,7 +2283,85 @@ check_new_cluster_replication_slots(void) pg_fatal("\"wal_level\" must be \"replica\" or \"logical\" but is set to \"%s\"", wal_level); - max_replication_slots = atoi(PQgetvalue(res, 1, 0)); + output_plugin_libraries = PQgetvalue(res, 1, 0); + + /* + * Make sure the output_plugin_libraries setting covers all plugins needed + * by any migrated slots. + */ + if (nslots_on_old > 0) + { + char *guc_copy = pg_strdup(output_plugin_libraries); + char **allowed_plugins; + char output_path[MAXPGPATH]; + FILE *script = NULL; + + if (!SplitGUCList(guc_copy, ',', &allowed_plugins)) + { + /* + * Should not happen. (Frontend and backend GUC_LIST_QUOTE parsing + * have to remain compatible for pg_dump at minimum.) + */ + pg_fatal("could not parse \"output_plugin_libraries\" setting '%s'", + output_plugin_libraries); + } + + snprintf(output_path, sizeof(output_path), "%s/%s", + log_opts.basedir, + "disallowed_output_plugins.txt"); + + for (int dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++) + { + LogicalSlotInfoArr *slot_arr = &old_cluster.dbarr.dbs[dbnum].slot_arr; + + for (int slotnum = 0; slotnum < slot_arr->nslots; slotnum++) + { + LogicalSlotInfo *slot = &slot_arr->slots[slotnum]; + bool allowed = false; + + /* + * We expect the output_plugin_libraries length to be small in + * practice; O(n*m) shouldn't be a problem here. + */ + for (char **p = allowed_plugins; *p; p++) + { + if (strcmp(slot->plugin, *p) == 0) + { + allowed = true; + break; + } + } + + if (!allowed) + { + if (script == NULL && + (script = fopen_priv(output_path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", output_path); + + fprintf(script, "The slot \"%s\" uses plugin \"%s\"\n", + slot->slotname, slot->plugin); + } + } + } + + if (script) + { + fclose(script); + + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains logical replication slots with plugins\n" + "that are not allowed by the new cluster's output_plugin_libraries\n" + "setting. You can add trusted plugins to output_plugin_libraries\n" + "and/or remove affected slots, and then restart the upgrade.\n" + "A list of the problematic slots is in the file:\n" + " %s", output_path); + } + + pg_free(allowed_plugins); + pg_free(guc_copy); + } + + max_replication_slots = atoi(PQgetvalue(res, 2, 0)); if (old_cluster.sub_retain_dead_tuples && nslots_on_old + 1 > max_replication_slots) diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl index 05918aad935..01ab82402ae 100644 --- a/src/bin/pg_upgrade/t/003_logical_slots.pl +++ b/src/bin/pg_upgrade/t/003_logical_slots.pl @@ -62,7 +62,7 @@ chdir ${PostgreSQL::Test::Utils::tmp_check}; $oldpub->start; $oldpub->safe_psql( 'postgres', qq[ - SELECT pg_create_logical_replication_slot('test_slot1', 'test_decoding'); + SELECT pg_create_logical_replication_slot('test_slot1', 'pgoutput'); SELECT pg_create_logical_replication_slot('test_slot2', 'test_decoding'); SELECT pg_create_logical_replication_slot('test_slot3', 'test_decoding'); ]); @@ -90,6 +90,52 @@ ok(-d $newpub->data_dir . "/pg_upgrade_output.d", # old cluster. Both slots will be used for subsequent tests. $newpub->append_conf('postgresql.conf', "max_replication_slots = 3"); +# ------------------------------ +# TEST: Confirm pg_upgrade fails when slot plugins are prohibited by the new cluster + +$newpub->append_conf('postgresql.conf', + "output_plugin_libraries = 'pgoutput'"); + +command_checks_all( + [@pg_upgrade_cmd], + 1, + [ + qr/Your installation contains logical replication slots with plugins/, + qr/that are not allowed by the new cluster's output_plugin_libraries/, + ], + [qr//], + 'run of pg_upgrade where the old cluster has untrusted output plugins'); + +my $slots_filename; + +# Find a txt file that contains a list of logical replication slots that cannot +# be upgraded. We cannot predict the file's path because the output directory +# contains a milliseconds timestamp. File::Find::find must be used. +find( + sub { + if ($File::Find::name =~ m/disallowed_output_plugins\.txt/) + { + $slots_filename = $File::Find::name; + } + }, + $newpub->data_dir . "/pg_upgrade_output.d"); + +# Check the report. +my $content = slurp_file($slots_filename); +like( + $content, + qr/The slot "test_slot2" uses plugin "test_decoding"/m, + 'the previous test failed due to prohibited plugins'); +like( + $content, + qr/The slot "test_slot3" uses plugin "test_decoding"/m, + 'the previous test failed due to prohibited plugins'); +unlike($content, qr/test_slot1/m, 'allowed plugin is not reported'); + +# Fix things for the next tests. +$newpub->append_conf('postgresql.conf', + "output_plugin_libraries = 'pgoutput, test_decoding'"); + # ------------------------------ # TEST: Confirm pg_upgrade fails when the slot still has unconsumed WAL records @@ -127,11 +173,6 @@ command_checks_all( ); # Verify the reason why the logical replication slot cannot be upgraded -my $slots_filename; - -# Find a txt file that contains a list of logical replication slots that cannot -# be upgraded. We cannot predict the file's path because the output directory -# contains a milliseconds timestamp. File::Find::find must be used. find( sub { if ($File::Find::name =~ m/invalid_logical_slots\.txt/) diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index 6e0b7628001..abc88c49ade 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -154,6 +154,9 @@ extern XLogRecPtr LogicalReplicationSlotCheckPendingWal(XLogRecPtr end_of_wal, extern XLogRecPtr LogicalSlotAdvanceAndCheckSnapState(XLogRecPtr moveto, bool *found_consistent_snapshot); +/* GUCs */ +extern PGDLLIMPORT char *output_plugin_libraries_string; + /* * This macro determines the log level for messages about starting logical diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 075c52f98fd..335efd86bca 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -477,13 +477,13 @@ $result = is( $result, qq(2|f 3|t), 'check replicated update on subscriber'); -# Test create and immediate drop of replication slot via replication commands -# (this exposed a memory-management bug in v18) my $publisher_host = $node_publisher->host; my $publisher_port = $node_publisher->port; my $connstr_db = "host=$publisher_host port=$publisher_port replication=database dbname=postgres"; +# Test create and immediate drop of replication slot via replication commands +# (this exposed a memory-management bug in v18) is( $node_publisher->psql( 'postgres', qq[ @@ -495,6 +495,27 @@ is( $node_publisher->psql( 0, 'create and immediate drop of replication slot'); +# REPLICATION users should not be able to bypass LOAD restrictions. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE USER repluser REPLICATION; +)); + +my ($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', + qq[ + SET ROLE repluser; + CREATE_REPLICATION_SLOT fail_slot LOGICAL regress;', + ], + timeout => $PostgreSQL::Test::Utils::timeout_default, + extra_params => [ '-d', $connstr_db ]); + +is($ret, 3, 'loading unblessed output plugin fails'); +like( + $stderr, + qr/ERROR: library "regress" may not be used as an output plugin/, + 'loading unblessed output plugin fails: stderr'); + $node_publisher->stop('fast'); $node_subscriber->stop('fast'); @@ -592,7 +613,7 @@ $node_publisher->safe_psql( CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub WITH (connect=false); )); -my ($ret, $stdout, $stderr) = +($ret, $stdout, $stderr) = $node_publisher->psql('postgres', q{DROP SUBSCRIPTION regress_sub1}); isnt($ret, 0, "replication slot does not exist: exit code not 0");