From 6d3d297598f03fa54ba0fe65976ef93be3d00821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 11 Aug 2025 11:24:43 +0200 Subject: [PATCH 1/3] rabbit_plugins: Add `list/0` to get the list of plugins ... without having to pass a plugins path. [Why] It's painful to have to get the plugins path, then pass it to `list/1` every time. It's also more difficult to discover how to use `rabbit_plugins` to get that list of plugins. --- deps/rabbit/src/rabbit_plugins.erl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/deps/rabbit/src/rabbit_plugins.erl b/deps/rabbit/src/rabbit_plugins.erl index d4d0ccd4c5..99f6a8b2a0 100644 --- a/deps/rabbit/src/rabbit_plugins.erl +++ b/deps/rabbit/src/rabbit_plugins.erl @@ -8,7 +8,7 @@ -module(rabbit_plugins). -include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("kernel/include/logger.hrl"). --export([setup/0, active/0, read_enabled/1, list/1, list/2, dependencies/3, running_plugins/0]). +-export([setup/0, active/0, read_enabled/1, list/0, list/1, list/2, dependencies/3, running_plugins/0]). -export([ensure/1]). -export([validate_plugins/1, format_invalid_plugins/1]). -export([is_strictly_plugin/1, strictly_plugins/2, strictly_plugins/1]). @@ -130,7 +130,7 @@ setup() -> -spec active() -> [plugin_name()]. active() -> - InstalledPlugins = plugin_names(list(plugins_dir())), + InstalledPlugins = plugin_names(list()), [App || {App, _, _} <- rabbit_misc:which_applications(), lists:member(App, InstalledPlugins)]. @@ -157,6 +157,13 @@ is_enabled_on_node(Name, Node) -> _Class:_Reason:_Stacktrace -> false end. +-spec list() -> [#plugin{}]. +%% @doc Get the list of plugins from the configured plugin path. + +list() -> + PluginsPath = plugins_dir(), + list(PluginsPath). + %% @doc Get the list of plugins which are ready to be enabled. -spec list(string()) -> [#plugin{}]. @@ -228,7 +235,7 @@ strictly_plugins(Plugins, AllPlugins) -> -spec strictly_plugins([plugin_name()]) -> [plugin_name()]. strictly_plugins(Plugins) -> - AllPlugins = list(plugins_dir()), + AllPlugins = list(), lists:filter( fun(Name) -> is_strictly_plugin(lists:keyfind(Name, #plugin.name, AllPlugins)) @@ -283,7 +290,7 @@ running_plugins() -> prepare_plugins(Enabled) -> ExpandDir = plugins_expand_dir(), - AllPlugins = list(plugins_dir()), + AllPlugins = list(), Wanted = dependencies(false, Enabled, AllPlugins), WantedPlugins = lookup_plugins(Wanted, AllPlugins), {ValidPlugins, Problems} = validate_plugins(WantedPlugins), From a8bef770a5b5550b2e5b97ddf62a987a71ea4e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 11 Aug 2025 11:27:27 +0200 Subject: [PATCH 2/3] rabbit_plugins: Add `which_plugin/1` to query which plugin provides a module [Why] This will be used in a later commit to find the auth backend plugin that provides a configured auth backend module. [How] We go through the list of available plugins, regardless if they are enabled or not, then look up the given module in the list of modules associated with each plugin's application. --- deps/rabbit/src/rabbit_plugins.erl | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/deps/rabbit/src/rabbit_plugins.erl b/deps/rabbit/src/rabbit_plugins.erl index 99f6a8b2a0..36892a301a 100644 --- a/deps/rabbit/src/rabbit_plugins.erl +++ b/deps/rabbit/src/rabbit_plugins.erl @@ -14,6 +14,7 @@ -export([is_strictly_plugin/1, strictly_plugins/2, strictly_plugins/1]). -export([plugins_dir/0, plugin_names/1, plugins_expand_dir/0, enabled_plugins_file/0]). -export([is_enabled/1, is_enabled_on_node/2]). +-export([which_plugin/1]). % Export for testing purpose. -export([is_version_supported/2, validate_plugins/2]). @@ -286,6 +287,56 @@ running_plugins() -> ActivePlugins = active(), {ok, [{App, Vsn} || {App, _ , Vsn} <- rabbit_misc:which_applications(), lists:member(App, ActivePlugins)]}. +-spec which_plugin(Module) -> Ret when + Module :: module(), + Ret :: {ok, PluginName} | {error, Reason}, + PluginName :: atom(), + Reason :: no_provider. +%% @doc Returns the name of the plugin that provides the given module. +%% +%% If no plugin provides the module, `{error, no_provider}' is returned. +%% +%% The returned plugin might not be enabled, thus using the given module might +%% not work until the plugin is enabled. +%% +%% @returns An `{ok, PluginName}' tuple with the name of the plugin providing +%% the module, or `{error, no_provider}'. + +which_plugin(Module) -> + Plugins = list(), + which_plugin(Plugins, Module). + +which_plugin([#plugin{name = Name} | Rest], Module) -> + %% Get the list of modules belonging to this plugin. + ModulesKey = case application:get_key(Name, modules) of + {ok, _} = Ret -> + Ret; + undefined -> + %% The plugin application might not be loaded. Load + %% it temporarily and try again. + case application:load(Name) of + ok -> + Ret = application:get_key(Name, modules), + _ = application:unload(Name), + Ret; + {error, _Reason} -> + undefined + end + end, + case ModulesKey of + {ok, Modules} -> + case lists:member(Module, Modules) of + true -> + {ok, Name}; + false -> + which_plugin(Rest, Module) + end; + undefined -> + which_plugin(Rest, Module) + end; +which_plugin([], _Module) -> + {error, no_provider}. + %%---------------------------------------------------------------------------- prepare_plugins(Enabled) -> From 23588b665a3d1e922f0e626fc5644b35e0ef736a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Mon, 11 Aug 2025 11:33:25 +0200 Subject: [PATCH 3/3] rabbit_access_control: Check configured auth backends are enabled at boot time [Why] If a user configures an auth backend module, but doesn't enabled the plugin that provides it, it will get a crash and a stacktrace when authentication is performed. The error is not helpful to understand what the problem is. [How] We add a boot step that go through the configured auth backends and query the core of RabbitMQ and the plugins. If an auth backend is provided by a plugin, the plugin must be enabled to consider the auth backend to be valid. In the end, at least one auth backend must be valid, otherwise the boot is aborted. If only some of the configured auth backends were filtered out, but there are still some valid auth backends, we store the filtered list in the application environment variable so that authentication/authorization doesn't try to use them later. We also report invalid auth backends in the logs: * Info message for a single invalid auth backend: [info] <0.213.0> The `rabbit_auth_backend_ldap` auth backend module is configured. However, the `rabbitmq_auth_backend_ldap` plugin must be enabled in order to use this auth backend. Until then it will be skipped during authentication/authorization * Warning message when some auth backends were filtered out: [warning] <0.213.0> Some configured backends were dropped because their corresponding plugins are disabled. Please look at the info messages above to learn which plugin(s) should be enabled. Here is the list of auth backends kept after filering: [warning] <0.213.0> [rabbit_auth_backend_internal] * Error message when no auth backends are valid: [error] <0.213.0> None of the configured auth backends are usable because their corresponding plugins were not enabled. Please look at the info messages above to learn which plugin(s) should be enabled. V2: In fact, `rabbit_plugins:is_enabled/1` indicates if a plugin is running, not if it is enabled... The new check runs as a boot step and thus is executed before plugins are started. Therefore we can't use this API. Instead, we use `rabbit_plugins:enabled_plugins/0' which lists explicitly enabled plugins. The drawback is that in the auth backend is enabled implicitly because it is a dependency of another explicitly enabled plugin, the check will still consider it is disabled and thus abort the boot. Fixes #13783. --- deps/rabbit/src/rabbit.erl | 8 + deps/rabbit/src/rabbit_access_control.erl | 136 +++++++ deps/rabbit/src/rabbit_plugins.erl | 2 +- .../test/rabbit_access_control_SUITE.erl | 360 +++++++++++++++++- .../my_auth_plugin/Makefile | 15 + .../my_auth_plugin/src/my_auth_plugin.erl | 36 ++ 6 files changed, 544 insertions(+), 13 deletions(-) create mode 100644 deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/Makefile create mode 100644 deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/src/my_auth_plugin.erl diff --git a/deps/rabbit/src/rabbit.erl b/deps/rabbit/src/rabbit.erl index 6224c70127..2bca266ca7 100644 --- a/deps/rabbit/src/rabbit.erl +++ b/deps/rabbit/src/rabbit.erl @@ -52,6 +52,14 @@ {requires, pre_boot}, {enables, external_infrastructure}]}). +-rabbit_boot_step({auth_backend_plugins_check, + [{description, "check configured auth plugins are enabled"}, + {mfa, {rabbit_access_control, + ensure_auth_backends_are_enabled, + []}}, + {requires, pre_boot}, + {enables, external_infrastructure}]}). + %% rabbit_alarm currently starts memory and disk space monitors -rabbit_boot_step({rabbit_alarm, [{description, "alarm handler"}, diff --git a/deps/rabbit/src/rabbit_access_control.erl b/deps/rabbit/src/rabbit_access_control.erl index 3fb09726b2..b0b2ecf0f8 100644 --- a/deps/rabbit/src/rabbit_access_control.erl +++ b/deps/rabbit/src/rabbit_access_control.erl @@ -10,6 +10,7 @@ -include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("kernel/include/logger.hrl"). +-export([ensure_auth_backends_are_enabled/0]). -export([check_user_pass_login/2, check_user_login/2, check_user_login/3, check_user_loopback/2, check_vhost_access/4, check_resource_access/4, check_topic_access/4, check_user_id/2]). @@ -18,6 +19,141 @@ %%---------------------------------------------------------------------------- +-spec ensure_auth_backends_are_enabled() -> Ret when + Ret :: ok | {error, Reason}, + Reason :: string(). + +ensure_auth_backends_are_enabled() -> + {ok, AuthBackends} = application:get_env(rabbit, auth_backends), + ValidAuthBackends = filter_valid_auth_backend_configuration( + AuthBackends, []), + case ValidAuthBackends of + AuthBackends -> + ok; + [_ | _] -> + %% Some auth backend modules were filtered out because their + %% corresponding plugin is either unavailable or disabled. We + %% update the application environment variable so that + %% authentication and authorization do not try to use them. + ?LOG_WARNING( + "Some configured backends were dropped because their " + "corresponding plugins are disabled. Please look at the " + "info messages above to learn which plugin(s) should be " + "enabled. Here is the list of auth backends kept after " + "filering:~n~p", [ValidAuthBackends]), + ok = application:set_env(rabbit, auth_backends, ValidAuthBackends), + ok; + [] -> + %% None of the auth backend modules are usable. Log an error and + %% abort the boot of RabbitMQ. + ?LOG_ERROR( + "None of the configured auth backends are usable because " + "their corresponding plugins were not enabled. Please look " + "at the info messages above to learn which plugin(s) should " + "be enabled."), + {error, + "Authentication/authorization backends require plugins to be " + "enabled; see logs for details"} + end. + +filter_valid_auth_backend_configuration( + [Mod | Rest], ValidAuthBackends) + when is_atom(Mod) -> + case is_auth_backend_module_enabled(Mod) of + true -> + ValidAuthBackends1 = [Mod | ValidAuthBackends], + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends1); + false -> + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends) + end; +filter_valid_auth_backend_configuration( + [{ModN, ModZ} = Mod | Rest], ValidAuthBackends) + when is_atom(ModN) andalso is_atom(ModZ) -> + %% Both auth backend modules must be usable to keep the entire pair. + IsModNEnabled = is_auth_backend_module_enabled(ModN), + IsModZEnabled = is_auth_backend_module_enabled(ModZ), + case IsModNEnabled andalso IsModZEnabled of + true -> + ValidAuthBackends1 = [Mod | ValidAuthBackends], + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends1); + false -> + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends) + end; +filter_valid_auth_backend_configuration( + [{ModN, ModZs} | Rest], ValidAuthBackends) + when is_atom(ModN) andalso is_list(ModZs) -> + %% The authentication backend module and at least on of the authorization + %% backend module must be usable to keep the entire pair. + %% + %% The list of authorization backend modules may be shorter than the + %% configured one after the filtering. + IsModNEnabled = is_auth_backend_module_enabled(ModN), + EnabledModZs = lists:filter(fun is_auth_backend_module_enabled/1, ModZs), + case IsModNEnabled andalso EnabledModZs =/= [] of + true -> + Mod1 = {ModN, EnabledModZs}, + ValidAuthBackends1 = [Mod1 | ValidAuthBackends], + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends1); + false -> + filter_valid_auth_backend_configuration(Rest, ValidAuthBackends) + end; +filter_valid_auth_backend_configuration([], ValidAuthBackends) -> + lists:reverse(ValidAuthBackends). + +is_auth_backend_module_enabled(Mod) when is_atom(Mod) -> + %% We check if the module is provided by the core of RabbitMQ or a plugin, + %% and if that plugin is enabled. + {ok, Modules} = application:get_key(rabbit, modules), + case lists:member(Mod, Modules) of + true -> + true; + false -> + %% The module is not provided by RabbitMQ core. Let's query + %% plugins then. + case rabbit_plugins:which_plugin(Mod) of + {ok, PluginName} -> + %% FIXME: The definition of an "enabled plugin" in + %% `rabbit_plugins' varies from funtion to function. + %% Sometimes, it means the "rabbitmq-plugin enable + %% " was executed, sometimes it means the plugin + %% is running. + %% + %% This function is a boot step and is executed before + %% plugin are started. Therefore, we can't rely on + %% `rabbit_plugins:is_enabled/1' because it uses the + %% latter definition of "the plugin is running, regardless + %% of if it is enabled or not". + %% + %% Therefore, we use `rabbit_plugins:enabled_plugins/0' + %% which lists explicitly enabled plugins. Unfortunately, + %% it won't include the implicitly enabled plugins (i.e, + %% plugins that are dependencies of explicitly enabled + %% plugins). + EnabledPlugins = rabbit_plugins:enabled_plugins(), + case lists:member(PluginName, EnabledPlugins) of + true -> + true; + false -> + ?LOG_INFO( + "The `~ts` auth backend module is configured. " + "However, the `~ts` plugin must be enabled in " + "order to use this auth backend. Until then " + "it will be skipped during " + "authentication/authorization", + [Mod, PluginName]), + false + end; + {error, no_provider} -> + ?LOG_INFO( + "The `~ts` auth backend module is configured. " + "However, no plugins available provide this " + "module. Until then it will be skipped during " + "authentication/authorization", + [Mod]), + false + end + end. + -spec check_user_pass_login (rabbit_types:username(), rabbit_types:password()) -> {'ok', rabbit_types:user()} | diff --git a/deps/rabbit/src/rabbit_plugins.erl b/deps/rabbit/src/rabbit_plugins.erl index 36892a301a..067c6e54db 100644 --- a/deps/rabbit/src/rabbit_plugins.erl +++ b/deps/rabbit/src/rabbit_plugins.erl @@ -13,7 +13,7 @@ -export([validate_plugins/1, format_invalid_plugins/1]). -export([is_strictly_plugin/1, strictly_plugins/2, strictly_plugins/1]). -export([plugins_dir/0, plugin_names/1, plugins_expand_dir/0, enabled_plugins_file/0]). --export([is_enabled/1, is_enabled_on_node/2]). +-export([is_enabled/1, is_enabled_on_node/2, enabled_plugins/0]). -export([which_plugin/1]). % Export for testing purpose. diff --git a/deps/rabbit/test/rabbit_access_control_SUITE.erl b/deps/rabbit/test/rabbit_access_control_SUITE.erl index a2e4660ffa..2911769830 100644 --- a/deps/rabbit/test/rabbit_access_control_SUITE.erl +++ b/deps/rabbit/test/rabbit_access_control_SUITE.erl @@ -2,34 +2,54 @@ %% License, v. 2.0. If a copy of the MPL was not distributed with this %% file, You can obtain one at https://mozilla.org/MPL/2.0/. %% -%% Copyright (c) 2024 Broadcom. All Rights Reserved. +%% Copyright (c) 2024-2025 Broadcom. All Rights Reserved. %% The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. %% -module(rabbit_access_control_SUITE). --compile(export_all). - -include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_assert.hrl"). + -include_lib("rabbit_common/include/rabbit.hrl"). +-export([all/0, + groups/0, + init_per_suite/1, + end_per_suite/1, + init_per_group/2, + end_per_group/2, + init_per_testcase/2, + end_per_testcase/2, + + expiry_timestamp/1, + with_enabled_plugin/1, + with_enabled_plugin_plus_internal/1, + with_missing_plugin/1, + with_missing_plugin_plus_internal/1, + with_disabled_plugin/1, + with_disabled_plugin_plus_internal/1 + ]). + %%%=================================================================== %%% Common Test callbacks %%%=================================================================== all() -> - [{group, tests}]. - -%% replicate eunit like test resolution -all_tests() -> - [F - || {F, _} <- ?MODULE:module_info(functions), - re:run(atom_to_list(F), "_test$") /= nomatch]. + [{group, unit_tests}, + {group, integration_tests}]. groups() -> - [{tests, [], all_tests()}]. + [{unit_tests, [], [expiry_timestamp]}, + {integration_tests, [], [with_enabled_plugin, + with_enabled_plugin_plus_internal, + with_missing_plugin, + with_missing_plugin_plus_internal, + with_disabled_plugin, + with_disabled_plugin_plus_internal]}]. init_per_suite(Config) -> Config. @@ -37,20 +57,74 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. +init_per_group(integration_tests, Config) -> + rabbit_ct_helpers:log_environment(), + rabbit_ct_helpers:run_setup_steps(Config); init_per_group(_Group, Config) -> Config. +end_per_group(integration_tests, Config) -> + rabbit_ct_helpers:run_teardown_steps(Config); end_per_group(_Group, _Config) -> ok. +init_per_testcase(Testcase, Config) + when Testcase =:= with_missing_plugin orelse + Testcase =:= with_missing_plugin_plus_internal -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + do_init_per_testcase(Testcase, Config, []); +init_per_testcase(Testcase, Config) + when Testcase =:= with_enabled_plugin orelse + Testcase =:= with_enabled_plugin_plus_internal orelse + Testcase =:= with_disabled_plugin orelse + Testcase =:= with_disabled_plugin_plus_internal -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + do_init_per_testcase(Testcase, Config, [fun prepare_my_plugin/1]); init_per_testcase(_TestCase, Config) -> Config. +do_init_per_testcase(Testcase, Config, PrepSteps) -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + TestNumber = rabbit_ct_helpers:testcase_number(Config, ?MODULE, Testcase), + ClusterSize = 1, + Config1 = rabbit_ct_helpers:set_config( + Config, + [{rmq_nodename_suffix, Testcase}, + {rmq_nodes_count, ClusterSize}, + {tcp_ports_base, {skip_n_nodes, TestNumber * ClusterSize}}, + {start_rmq_with_plugins_disabled, true}]), + Config2 = rabbit_ct_helpers:merge_app_env( + Config1, + {rabbit, + [{log, [{file, [{level, debug}]}]}]}), + Config3 = rabbit_ct_helpers:run_steps( + Config2, + PrepSteps ++ + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()), + Config3. + +end_per_testcase(Testcase, Config) + when Testcase =:= with_enabled_plugin orelse + Testcase =:= with_enabled_plugin_plus_internal orelse + Testcase =:= with_missing_plugin orelse + Testcase =:= with_missing_plugin_plus_internal orelse + Testcase =:= with_disabled_plugin orelse + Testcase =:= with_disabled_plugin_plus_internal -> + Config1 = rabbit_ct_helpers:run_steps( + Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()), + rabbit_ct_helpers:testcase_finished(Config1, Testcase); end_per_testcase(_TestCase, _Config) -> meck:unload(), ok. -expiry_timestamp_test(_) -> +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +expiry_timestamp(_) -> %% test rabbit_access_control:expiry_timestamp/1 returns the earliest expiry time Now = os:system_time(seconds), BeforeNow = Now - 60, @@ -102,3 +176,265 @@ expiry_timestamp_test(_) -> {rabbit_expiry_backend, unused}]}, ?assertEqual(Now, rabbit_access_control:expiry_timestamp(User7)), ok. + +with_enabled_plugin(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + ok = rabbit_ct_broker_helpers:enable_plugin(Config, Node, my_auth_plugin), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + rabbit_ct_broker_helpers:rpc( + Config, Node, os, unsetenv, ["RABBITMQ_ENABLED_PLUGINS"]), + rabbit_ct_broker_helpers:rpc( + Config, Node, os, unsetenv, ["LEAVE_PLUGINS_DISABLED"]), + + AuthBackends = [my_auth_plugin], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertEqual( + ok, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?assertMatch({error, {auth_failure, _}}, test_connection(Config, Node)), + + ok. + +with_enabled_plugin_plus_internal(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + ok = rabbit_ct_broker_helpers:enable_plugin(Config, Node, my_auth_plugin), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + rabbit_ct_broker_helpers:rpc( + Config, Node, os, unsetenv, ["RABBITMQ_ENABLED_PLUGINS"]), + rabbit_ct_broker_helpers:rpc( + Config, Node, os, unsetenv, ["LEAVE_PLUGINS_DISABLED"]), + + AuthBackends = [my_auth_plugin, rabbit_auth_backend_internal], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertEqual( + ok, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?assertEqual(ok, test_connection(Config, Node)), + + ok. + +with_missing_plugin(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + AuthBackends = [my_auth_plugin], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertThrow( + {error, + {rabbit, + {{error, + "Authentication/authorization backends require plugins to be " + "enabled; see logs for details"}, + _}}}, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?awaitMatch( + true, + check_log(Config, Node, "no plugins available provide this module"), + 30000), + + ok. + +with_missing_plugin_plus_internal(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + AuthBackends = [my_auth_plugin, rabbit_auth_backend_internal], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertEqual( + ok, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?awaitMatch( + true, + check_log(Config, Node, "no plugins available provide this module"), + 30000), + + ?assertEqual(ok, test_connection(Config, Node)), + + ok. + +with_disabled_plugin(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + AuthBackends = [my_auth_plugin], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertThrow( + {error, + {rabbit, + {{error, + "Authentication/authorization backends require plugins to be " + "enabled; see logs for details"}, + _}}}, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?awaitMatch( + true, + check_log( + Config, Node, + "the `my_auth_plugin` plugin must be enabled in order to use " + "this auth backend"), + 30000), + + ok. + +with_disabled_plugin_plus_internal(Config) -> + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:stop_broker(Config, Node), + + AuthBackends = [my_auth_plugin, rabbit_auth_backend_internal], + rabbit_ct_broker_helpers:rpc( + Config, Node, application, set_env, + [rabbit, auth_backends, AuthBackends, [{persistent, true}]]), + + ?assertEqual( + ok, + rabbit_ct_broker_helpers:start_broker(Config, Node)), + + ?awaitMatch( + true, + check_log( + Config, Node, + "the `my_auth_plugin` plugin must be enabled in order to use " + "this auth backend"), + 30000), + + ?assertEqual(ok, test_connection(Config, Node)), + + ok. + +%% ------------------------------------------------------------------- +%% Internal helpers. +%% ------------------------------------------------------------------- + +prepare_my_plugin(Config) -> + case os:getenv("RABBITMQ_RUN") of + false -> + build_my_plugin(Config); + _ -> + MyPluginDir = filename:dirname( + filename:dirname( + code:where_is_file("my_auth_plugin.app"))), + PluginsDir = filename:dirname(MyPluginDir), + rabbit_ct_helpers:set_config( + Config, [{rmq_plugins_dir, PluginsDir}]) + end. + +build_my_plugin(Config) -> + DataDir = filename:join( + filename:dirname(filename:dirname(?config(data_dir, Config))), + ?MODULE_STRING ++ "_data"), + PluginSrcDir = filename:join(DataDir, "my_auth_plugin"), + PluginsDir = filename:join(PluginSrcDir, "plugins"), + Config1 = rabbit_ct_helpers:set_config(Config, + [{rmq_plugins_dir, PluginsDir}]), + {MyPlugin, OtherPlugins} = list_my_plugin_plugins(PluginSrcDir), + case MyPlugin of + [] -> + DepsDir = ?config(erlang_mk_depsdir, Config), + Args = ["test-dist", + {"DEPS_DIR=~ts", [DepsDir]}, + %% We clear ALL_DEPS_DIRS to make sure they are + %% not recompiled when the plugin is built. `rabbit` + %% was previously compiled with -DTEST and if it is + %% recompiled because of this plugin, it will be + %% recompiled without -DTEST: the testsuite depends + %% on test code so we can't allow that. + %% + %% Note that we do not clear the DEPS variable: we need + %% it to be correct because it is used to generate + %% `my_auth_plugin.app` (and a RabbitMQ plugin must + %% depend on `rabbit`). + "ALL_DEPS_DIRS="], + case rabbit_ct_helpers:make(Config1, PluginSrcDir, Args) of + {ok, _} -> + {_, OtherPlugins1} = list_my_plugin_plugins(PluginSrcDir), + remove_other_plugins(PluginSrcDir, OtherPlugins1), + update_cli_path(Config1, PluginSrcDir); + {error, _} -> + {skip, + "Failed to compile the `my_auth_plugin` test plugin"} + end; + _ -> + remove_other_plugins(PluginSrcDir, OtherPlugins), + update_cli_path(Config1, PluginSrcDir) + end. + +update_cli_path(Config, PluginSrcDir) -> + SbinDir = filename:join(PluginSrcDir, "sbin"), + Rabbitmqctl = filename:join(SbinDir, "rabbitmqctl"), + RabbitmqPlugins = filename:join(SbinDir, "rabbitmq-plugins"), + RabbitmqQueues = filename:join(SbinDir, "rabbitmq-queues"), + case filelib:is_regular(Rabbitmqctl) of + true -> + ct:pal(?LOW_IMPORTANCE, + "Switching to CLI in e.g. ~ts", [Rabbitmqctl]), + rabbit_ct_helpers:set_config( + Config, + [{rabbitmqctl_cmd, Rabbitmqctl}, + {rabbitmq_plugins_cmd, RabbitmqPlugins}, + {rabbitmq_queues_cmd, RabbitmqQueues}]); + false -> + Config + end. + +list_my_plugin_plugins(PluginSrcDir) -> + Files = filelib:wildcard("plugins/*", PluginSrcDir), + lists:partition( + fun(Path) -> + Filename = filename:basename(Path), + re:run(Filename, "^my_auth_plugin-", [{capture, none}]) =:= match + end, Files). + +remove_other_plugins(PluginSrcDir, OtherPlugins) -> + ok = rabbit_file:recursive_delete( + [filename:join(PluginSrcDir, OtherPlugin) + || OtherPlugin <- OtherPlugins]). + +check_log(Config, Node, Msg) -> + LogLocations = rabbit_ct_broker_helpers:rpc( + Config, Node, + rabbit, log_locations, []), + lists:any( + fun(LogLocation) -> + check_log1(LogLocation, Msg) + end, LogLocations). + +check_log1(LogLocation, Msg) -> + case filelib:is_regular(LogLocation) of + true -> + {ok, Content} = file:read_file(LogLocation), + ReOpts = [{capture, none}, multiline, unicode], + match =:= re:run(Content, Msg, ReOpts); + false -> + false + end. + +test_connection(Config, Node) -> + case rabbit_ct_client_helpers:open_unmanaged_connection(Config, Node) of + Conn when is_pid(Conn) -> + ok = rabbit_ct_client_helpers:close_connection(Conn), + ok; + {error, _} = Error -> + Error + end. diff --git a/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/Makefile b/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/Makefile new file mode 100644 index 0000000000..acfa83e494 --- /dev/null +++ b/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/Makefile @@ -0,0 +1,15 @@ +PROJECT = my_auth_plugin +PROJECT_DESCRIPTION = Plugin to test access control +PROJECT_VERSION = 1.0.0 + +define PROJECT_APP_EXTRA_KEYS + {broker_version_requirements, []} +endef + +DEPS = rabbit_common rabbit + +DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk +DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk + +include ../../../../../rabbitmq-components.mk +include ../../../../../erlang.mk diff --git a/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/src/my_auth_plugin.erl b/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/src/my_auth_plugin.erl new file mode 100644 index 0000000000..2c663badf3 --- /dev/null +++ b/deps/rabbit/test/rabbit_access_control_SUITE_data/my_auth_plugin/src/my_auth_plugin.erl @@ -0,0 +1,36 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2025 Broadcom. All Rights Reserved. The term “Broadcom” +%% refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(my_auth_plugin). + +-behaviour(rabbit_authn_backend). +-behaviour(rabbit_authz_backend). + +-export([user_login_authentication/2, user_login_authorization/2, + check_vhost_access/3, check_resource_access/4, check_topic_access/4]). +-export([expiry_timestamp/1]). + +%% ------------------------------------------------------------------- +%% Implementation of rabbit_authn_backend. +%% ------------------------------------------------------------------- + +user_login_authentication(_, _) -> + {error, unknown_user}. + +%% ------------------------------------------------------------------- +%% Implementation of rabbit_authz_backend. +%% ------------------------------------------------------------------- + +user_login_authorization(_, _) -> + {error, unknown_user}. + +check_vhost_access(_AuthUser, _VHostPath, _AuthzData) -> true. +check_resource_access(_AuthUser, _Resource, _Permission, _Context) -> true. +check_topic_access(_AuthUser, _Resource, _Permission, _Context) -> true. + +expiry_timestamp(_AuthUser) -> never.