Add SASL mechanism ANONYMOUS

## 1. Introduce new SASL mechanism ANONYMOUS

 ### What?
Introduce a new `rabbit_auth_mechanism` implementation for SASL
mechanism ANONYMOUS called `rabbit_auth_mechanism_anonymous`.

 ### Why?
As described in AMQP section 5.3.3.1, ANONYMOUS should be used when the
client doesn't need to authenticate.

Introducing a new `rabbit_auth_mechanism` consolidates and simplifies how anonymous
logins work across all RabbitMQ protocols that support SASL. This commit
therefore allows AMQP 0.9.1, AMQP 1.0, stream clients to connect out of
the box to RabbitMQ without providing any username or password.

Today's AMQP 0.9.1 and stream protocol client libs hard code RabbitMQ default credentials
`guest:guest` for example done in:
* 0215e85643/src/main/java/com/rabbitmq/client/ConnectionFactory.java (L58-L61)
* ddb7a2f068/uri.go (L31-L32)

Hard coding RabbitMQ specific default credentials in dozens of different
client libraries is an anti-pattern in my opinion.
Furthermore, there are various AMQP 1.0 and MQTT client libraries which
we do not control or maintain and which still should work out of the box
when a user is getting started with RabbitMQ (that is without
providing `guest:guest` credentials).

 ### How?
The old RabbitMQ 3.13 AMQP 1.0 plugin `default_user`
[configuration](146b4862d8/deps/rabbitmq_amqp1_0/Makefile (L6))
is replaced with the following two new `rabbit` configurations:
```
{anonymous_login_user, <<"guest">>},
{anonymous_login_pass, <<"guest">>},
```
We call it `anonymous_login_user` because this user will be used for
anonymous logins. The subsequent commit uses the same setting for
anonymous logins in MQTT. Hence, this user is orthogonal to the protocol
used when the client connects.

Setting `anonymous_login_pass` could have been left out.
This commit decides to include it because our documentation has so far
recommended:
> It is highly recommended to pre-configure a new user with a generated username and password or delete the guest user
> or at least change its password to reasonably secure generated value that won't be known to the public.

By having the new module `rabbit_auth_mechanism_anonymous` internally
authenticate with `anonymous_login_pass` instead of blindly allowing
access without any password, we protect operators that relied on the
sentence:
> or at least change its password to reasonably secure generated value that won't be known to the public

To ease the getting started experience, since RabbitMQ already deploys a
guest user with full access to the default virtual host `/`, this commit
also allows SASL mechanism ANONYMOUS in `rabbit` setting `auth_mechanisms`.

In production, operators should disable SASL mechanism ANONYMOUS by
setting `anonymous_login_user` to `none` (or by removing ANONYMOUS from
the `auth_mechanisms` setting. This will be documented separately.
Even if operators forget or don't read the docs, this new ANONYMOUS
mechanism won't do any harm because it relies on the default user name
`guest` and password `guest`, which is recommended against in
production, and who by default can only connect from the local host.

 ## 2. Require SASL security layer in AMQP 1.0

 ### What?
An AMQP 1.0 client must use the SASL security layer.

 ### Why?
This is in line with the mandatory usage of SASL in AMQP 0.9.1 and
RabbitMQ stream protocol.
Since (presumably) any AMQP 1.0 client knows how to authenticate with a
username and password using SASL mechanism PLAIN, any AMQP 1.0 client
also (presumably) implements the trivial SASL mechanism ANONYMOUS.

Skipping SASL is not recommended in production anyway.
By requiring SASL, configuration for operators becomes easier.
Following the principle of least surprise, when an an operator
configures `auth_mechanisms` to exclude `ANONYMOUS`, anonymous logins
will be prohibited in SASL and also by disallowing skipping the SASL
layer.

 ### How?
This commit implements AMQP 1.0 figure 2.13.

A follow-up commit needs to be pushed to `v3.13.x` which will use SASL
mechanism `anon` instead of `none` in the Erlang AMQP 1.0 client
such that AMQP 1.0 shovels running on 3.13 can connect to 4.0 RabbitMQ nodes.
This commit is contained in:
David Ansari 2024-08-14 12:19:17 +02:00
parent cabe873348
commit d46f07c0a4
18 changed files with 362 additions and 282 deletions

View File

@ -429,8 +429,8 @@ parse_result(Map) ->
throw(plain_sasl_missing_userinfo);
_ ->
case UserInfo of
[] -> none;
undefined -> none;
[] -> anon;
undefined -> anon;
U -> parse_usertoken(U)
end
end,
@ -456,11 +456,6 @@ parse_result(Map) ->
Ret0#{tls_opts => {secure_port, TlsOpts}}
end.
parse_usertoken(undefined) ->
none;
parse_usertoken("") ->
none;
parse_usertoken(U) ->
[User, Pass] = string:tokens(U, ":"),
{plain,
@ -532,7 +527,7 @@ parse_uri_test_() ->
[?_assertEqual({ok, #{address => "my_host",
port => 9876,
hostname => <<"my_host">>,
sasl => none}}, parse_uri("amqp://my_host:9876")),
sasl => anon}}, parse_uri("amqp://my_host:9876")),
%% port defaults
?_assertMatch({ok, #{port := 5671}}, parse_uri("amqps://my_host")),
?_assertMatch({ok, #{port := 5672}}, parse_uri("amqp://my_host")),

View File

@ -103,8 +103,7 @@ stop_amqp10_client_app(Config) ->
%% -------------------------------------------------------------------
init_per_group(rabbitmq, Config0) ->
Config = rabbit_ct_helpers:set_config(Config0,
{sasl, {plain, <<"guest">>, <<"guest">>}}),
Config = rabbit_ct_helpers:set_config(Config0, {sasl, anon}),
Config1 = rabbit_ct_helpers:merge_app_env(Config,
[{rabbit,
[{max_message_size, 134217728}]}]),
@ -115,7 +114,7 @@ init_per_group(rabbitmq_strict, Config0) ->
{sasl, {plain, <<"guest">>, <<"guest">>}}),
Config1 = rabbit_ct_helpers:merge_app_env(Config,
[{rabbit,
[{amqp1_0_default_user, none},
[{anonymous_login_user, none},
{max_message_size, 134217728}]}]),
rabbit_ct_helpers:run_steps(Config1, rabbit_ct_broker_helpers:setup_steps());

View File

@ -58,8 +58,6 @@ _APP_ENV = """[
{default_user_tags, [administrator]},
{default_vhost, <<"/">>},
{default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
{amqp1_0_default_user, <<"guest">>},
{amqp1_0_default_vhost, <<"/">>},
{loopback_users, [<<"guest">>]},
{password_hashing_module, rabbit_password_hashing_sha256},
{server_properties, []},
@ -67,7 +65,9 @@ _APP_ENV = """[
{collect_statistics_interval, 5000},
{mnesia_table_loading_retry_timeout, 30000},
{mnesia_table_loading_retry_limit, 10},
{auth_mechanisms, ['PLAIN', 'AMQPLAIN']},
{anonymous_login_user, <<"guest">>},
{anonymous_login_pass, <<"guest">>},
{auth_mechanisms, ['PLAIN', 'AMQPLAIN', 'ANONYMOUS']},
{auth_backends, [rabbit_auth_backend_internal]},
{delegate_count, 16},
{trace_vhosts, []},

View File

@ -38,8 +38,6 @@ define PROJECT_ENV
{default_user_tags, [administrator]},
{default_vhost, <<"/">>},
{default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
{amqp1_0_default_user, <<"guest">>},
{amqp1_0_default_vhost, <<"/">>},
{loopback_users, [<<"guest">>]},
{password_hashing_module, rabbit_password_hashing_sha256},
{server_properties, []},
@ -47,7 +45,12 @@ define PROJECT_ENV
{collect_statistics_interval, 5000},
{mnesia_table_loading_retry_timeout, 30000},
{mnesia_table_loading_retry_limit, 10},
{auth_mechanisms, ['PLAIN', 'AMQPLAIN']},
%% The identity to act as for anonymous logins.
{anonymous_login_user, <<"guest">>},
{anonymous_login_pass, <<"guest">>},
%% "The server mechanisms are ordered in decreasing level of preference."
%% AMQP §5.3.3.1
{auth_mechanisms, ['PLAIN', 'AMQPLAIN', 'ANONYMOUS']},
{auth_backends, [rabbit_auth_backend_internal]},
{delegate_count, 16},
{trace_vhosts, []},

3
deps/rabbit/app.bzl vendored
View File

@ -58,6 +58,7 @@ def all_beam_files(name = "all_beam_files"):
"src/rabbit_amqqueue_sup_sup.erl",
"src/rabbit_auth_backend_internal.erl",
"src/rabbit_auth_mechanism_amqplain.erl",
"src/rabbit_auth_mechanism_anonymous.erl",
"src/rabbit_auth_mechanism_cr_demo.erl",
"src/rabbit_auth_mechanism_plain.erl",
"src/rabbit_autoheal.erl",
@ -313,6 +314,7 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/rabbit_amqqueue_sup_sup.erl",
"src/rabbit_auth_backend_internal.erl",
"src/rabbit_auth_mechanism_amqplain.erl",
"src/rabbit_auth_mechanism_anonymous.erl",
"src/rabbit_auth_mechanism_cr_demo.erl",
"src/rabbit_auth_mechanism_plain.erl",
"src/rabbit_autoheal.erl",
@ -586,6 +588,7 @@ def all_srcs(name = "all_srcs"):
"src/rabbit_amqqueue_sup_sup.erl",
"src/rabbit_auth_backend_internal.erl",
"src/rabbit_auth_mechanism_amqplain.erl",
"src/rabbit_auth_mechanism_anonymous.erl",
"src/rabbit_auth_mechanism_cr_demo.erl",
"src/rabbit_auth_mechanism_plain.erl",
"src/rabbit_autoheal.erl",

View File

@ -444,13 +444,12 @@ end}.
%% ===========================================================================
%% Choose the available SASL mechanism(s) to expose.
%% The two default (built in) mechanisms are 'PLAIN' and
%% 'AMQPLAIN'. Additional mechanisms can be added via
%% plugins.
%% The three default (built in) mechanisms are 'PLAIN', 'AMQPLAIN' and 'ANONYMOUS'.
%% Additional mechanisms can be added via plugins.
%%
%% See https://www.rabbitmq.com/authentication.html for more details.
%%
%% {auth_mechanisms, ['PLAIN', 'AMQPLAIN']},
%% {auth_mechanisms, ['PLAIN', 'AMQPLAIN', 'ANONYMOUS']},
{mapping, "auth_mechanisms.$name", "rabbit.auth_mechanisms", [
{datatype, atom}]}.
@ -735,6 +734,30 @@ end}.
end
end}.
%% Connections that skip SASL layer or use SASL mechanism ANONYMOUS will use this identity.
%% Setting this to a username will allow (anonymous) clients to connect and act as this
%% given user. For production environments, set this value to 'none'.
{mapping, "anonymous_login_user", "rabbit.anonymous_login_user",
[{datatype, [{enum, [none]}, string]}]}.
{translation, "rabbit.anonymous_login_user",
fun(Conf) ->
case cuttlefish:conf_get("anonymous_login_user", Conf) of
none -> none;
User -> list_to_binary(User)
end
end}.
{mapping, "anonymous_login_pass", "rabbit.anonymous_login_pass", [
{datatype, [tagged_binary, binary]}
]}.
{translation, "rabbit.anonymous_login_pass",
fun(Conf) ->
rabbit_cuttlefish:optionally_tagged_binary("anonymous_login_pass", Conf)
end}.
%%
%% Default Policies
%% ====================
@ -2649,32 +2672,6 @@ end}.
end
}.
% ===============================
% AMQP 1.0
% ===============================
%% Connections that skip SASL layer or use SASL mechanism ANONYMOUS will connect as this account.
%% Setting this to a username will allow clients to connect without authenticating.
%% For production environments, set this value to 'none'.
{mapping, "amqp1_0.default_user", "rabbit.amqp1_0_default_user",
[{datatype, [{enum, [none]}, string]}]}.
{mapping, "amqp1_0.default_vhost", "rabbit.amqp1_0_default_vhost",
[{datatype, string}]}.
{translation, "rabbit.amqp1_0_default_user",
fun(Conf) ->
case cuttlefish:conf_get("amqp1_0.default_user", Conf) of
none -> none;
User -> list_to_binary(User)
end
end}.
{translation , "rabbit.amqp1_0_default_vhost",
fun(Conf) ->
list_to_binary(cuttlefish:conf_get("amqp1_0.default_vhost", Conf))
end}.
{mapping, "stream.replication.port_range.min", "osiris.port_range", [
{datatype, [integer]},
{validators, ["non_zero_positive_integer"]}

View File

@ -11,7 +11,7 @@
-include_lib("amqp10_common/include/amqp10_types.hrl").
-include("rabbit_amqp.hrl").
-export([init/2,
-export([init/1,
info/2,
mainloop/2]).
@ -45,12 +45,12 @@
%% client port
peer_port :: inet:port_number(),
connected_at :: integer(),
user :: rabbit_types:option(rabbit_types:user()),
user :: unauthenticated | rabbit_types:user(),
timeout :: non_neg_integer(),
incoming_max_frame_size :: pos_integer(),
outgoing_max_frame_size :: unlimited | pos_integer(),
channel_max :: non_neg_integer(),
auth_mechanism :: none | anonymous | {binary(), module()},
auth_mechanism :: none | {binary(), module()},
auth_state :: term(),
properties :: undefined | {map, list(tuple())}
}).
@ -65,7 +65,9 @@
sock :: rabbit_net:socket(),
proxy_socket :: undefined | {rabbit_proxy_socket, any(), any()},
connection :: #v1_connection{},
connection_state :: pre_init | starting | waiting_amqp0100 | securing | running | closing | closed,
connection_state :: received_amqp3100 | waiting_sasl_init | securing |
waiting_amqp0100 | waiting_open | running |
closing | closed,
callback :: handshake |
{frame_header, protocol()} |
{frame_body, protocol(), DataOffset :: pos_integer(), channel_number()},
@ -92,7 +94,7 @@ unpack_from_0_9_1(
callback = handshake,
recv_len = RecvLen,
pending_recv = PendingRecv,
connection_state = pre_init,
connection_state = received_amqp3100,
heartbeater = none,
helper_sup = SupPid,
buf = Buf,
@ -108,7 +110,7 @@ unpack_from_0_9_1(
port = Port,
peer_port = PeerPort,
connected_at = ConnectedAt,
user = none,
user = unauthenticated,
timeout = HandshakeTimeout,
incoming_max_frame_size = ?INITIAL_MAX_FRAME_SIZE,
outgoing_max_frame_size = ?INITIAL_MAX_FRAME_SIZE,
@ -148,15 +150,19 @@ recvloop(Deb, State = #v1{sock = Sock, recv_len = RecvLen, buf_len = BufLen})
{error, Reason} ->
throw({inet_error, Reason})
end;
recvloop(Deb, State = #v1{recv_len = RecvLen, buf = Buf, buf_len = BufLen}) ->
recvloop(Deb, State0 = #v1{callback = Callback,
recv_len = RecvLen,
buf = Buf,
buf_len = BufLen}) ->
Bin = case Buf of
[B] -> B;
_ -> list_to_binary(lists:reverse(Buf))
end,
{Data, Rest} = split_binary(Bin, RecvLen),
recvloop(Deb, handle_input(State#v1.callback, Data,
State#v1{buf = [Rest],
buf_len = BufLen - RecvLen})).
State1 = State0#v1{buf = [Rest],
buf_len = BufLen - RecvLen},
State = handle_input(Callback, Data, State1),
recvloop(Deb, State).
-spec mainloop([sys:dbg_opt()], state()) ->
no_return() | ok.
@ -240,7 +246,8 @@ handle_other(Other, _State) ->
exit({unexpected_message, Other}).
switch_callback(State, Callback, Length) ->
State#v1{callback = Callback, recv_len = Length}.
State#v1{callback = Callback,
recv_len = Length}.
terminate(Reason, State)
when ?IS_RUNNING(State) ->
@ -387,9 +394,12 @@ handle_connection_frame(
idle_time_out = IdleTimeout,
hostname = Hostname,
properties = Properties},
#v1{connection_state = starting,
connection = Connection = #v1_connection{name = ConnectionName,
user = User = #user{username = Username}},
#v1{connection_state = waiting_open,
connection = Connection = #v1_connection{
name = ConnectionName,
user = User = #user{username = Username},
auth_mechanism = {Mechanism, _Mod}
},
helper_sup = HelperSupPid,
sock = Sock} = State0) ->
logger:update_process_metadata(#{amqp_container => ContainerId}),
@ -404,9 +414,9 @@ handle_connection_frame(
rabbit_core_metrics:auth_attempt_succeeded(<<>>, Username, amqp10),
notify_auth(user_authentication_success, Username, State0),
rabbit_log_connection:info(
"Connection from AMQP 1.0 container '~ts': user '~ts' "
"authenticated and granted access to vhost '~ts'",
[ContainerId, Username, Vhost]),
"Connection from AMQP 1.0 container '~ts': user '~ts' authenticated "
"using SASL mechanism ~s and granted access to vhost '~ts'",
[ContainerId, Username, Mechanism, Vhost]),
OutgoingMaxFrameSize = case ClientMaxFrame of
undefined ->
@ -539,50 +549,53 @@ handle_session_frame(Channel, Body, #v1{tracked_channels = Channels} = State) ->
end
end.
%% TODO: write a proper ANONYMOUS plugin and unify with STOMP
handle_sasl_frame(#'v1_0.sasl_init'{mechanism = {symbol, <<"ANONYMOUS">>},
hostname = _Hostname},
#v1{connection_state = starting,
connection = Connection,
sock = Sock} = State0) ->
case default_user() of
none ->
silent_close_delay(),
Outcome = #'v1_0.sasl_outcome'{code = ?V_1_0_SASL_CODE_SYS_PERM},
ok = send_on_channel0(Sock, Outcome, rabbit_amqp_sasl),
throw(banned_unauthenticated_connection);
_ ->
%% We only need to send the frame, again start_connection
%% will set up the default user.
Outcome = #'v1_0.sasl_outcome'{code = ?V_1_0_SASL_CODE_OK},
ok = send_on_channel0(Sock, Outcome, rabbit_amqp_sasl),
State = State0#v1{connection_state = waiting_amqp0100,
connection = Connection#v1_connection{auth_mechanism = anonymous}},
switch_callback(State, handshake, 8)
end;
handle_sasl_frame(#'v1_0.sasl_init'{mechanism = {symbol, Mechanism},
initial_response = {binary, Response},
hostname = _Hostname},
State0 = #v1{connection_state = starting,
connection = Connection,
sock = Sock}) ->
handle_sasl_frame(#'v1_0.sasl_init'{mechanism = {symbol, Mechanism},
initial_response = Response,
hostname = _},
State0 = #v1{connection_state = waiting_sasl_init,
connection = Connection,
sock = Sock}) ->
ResponseBin = case Response of
undefined -> <<>>;
{binary, Bin} -> Bin
end,
AuthMechanism = auth_mechanism_to_module(Mechanism, Sock),
State = State0#v1{connection =
Connection#v1_connection{
auth_mechanism = {Mechanism, AuthMechanism},
auth_state = AuthMechanism:init(Sock)},
connection_state = securing},
auth_phase_1_0(Response, State);
AuthState = AuthMechanism:init(Sock),
State = State0#v1{
connection = Connection#v1_connection{
auth_mechanism = {Mechanism, AuthMechanism},
auth_state = AuthState},
connection_state = securing},
auth_phase(ResponseBin, State);
handle_sasl_frame(#'v1_0.sasl_response'{response = {binary, Response}},
State = #v1{connection_state = securing}) ->
auth_phase_1_0(Response, State);
auth_phase(Response, State);
handle_sasl_frame(Performative, State) ->
throw({unexpected_1_0_sasl_frame, Performative, State}).
handle_input(handshake, <<"AMQP", 0, 1, 0, 0>>,
#v1{connection_state = waiting_amqp0100} = State) ->
start_connection(amqp, State);
handle_input(handshake, <<"AMQP",0,1,0,0>>,
#v1{connection_state = waiting_amqp0100,
sock = Sock,
connection = Connection = #v1_connection{user = #user{}},
helper_sup = HelperSup
} = State0) ->
%% Client already got successfully authenticated by SASL.
send_handshake(Sock, <<"AMQP",0,1,0,0>>),
ChildSpec = #{id => session_sup,
start => {rabbit_amqp_session_sup, start_link, [self()]},
restart => transient,
significant => true,
shutdown => infinity,
type => supervisor},
{ok, SessionSupPid} = supervisor:start_child(HelperSup, ChildSpec),
State = State0#v1{
session_sup = SessionSupPid,
%% "After establishing or accepting a TCP connection and sending
%% the protocol header, each peer MUST send an open frame before
%% sending any other frames." [2.4.1]
connection_state = waiting_open,
connection = Connection#v1_connection{timeout = ?NORMAL_TIMEOUT}},
switch_callback(State, {frame_header, amqp}, 8);
handle_input({frame_header, Mode},
Header = <<Size:32, DOff:8, Type:8, Channel:16>>,
State) when DOff >= 2 ->
@ -618,75 +631,27 @@ handle_input({frame_body, Mode, DOff, Channel},
handle_input(Callback, Data, _State) ->
throw({bad_input, Callback, Data}).
-spec init(protocol(), tuple()) -> no_return().
init(Mode, PackedState) ->
-spec init(tuple()) -> no_return().
init(PackedState) ->
{ok, HandshakeTimeout} = application:get_env(rabbit, handshake_timeout),
{parent, Parent} = erlang:process_info(self(), parent),
ok = rabbit_connection_sup:remove_connection_helper_sup(Parent, helper_sup_amqp_091),
State0 = unpack_from_0_9_1(PackedState, Parent, HandshakeTimeout),
State = start_connection(Mode, State0),
State = advertise_sasl_mechanism(State0),
%% By invoking recvloop here we become 1.0.
recvloop(sys:debug_options([]), State).
start_connection(Mode = sasl, State = #v1{sock = Sock}) ->
advertise_sasl_mechanism(State0 = #v1{connection_state = received_amqp3100,
connection = Connection,
sock = Sock}) ->
send_handshake(Sock, <<"AMQP",3,1,0,0>>),
%% "The server mechanisms are ordered in decreasing level of preference." [5.3.3.1]
Ms0 = [{symbol, atom_to_binary(M)} || M <- auth_mechanisms(Sock)],
Ms1 = case default_user() of
none -> Ms0;
_ -> Ms0 ++ [{symbol, <<"ANONYMOUS">>}]
end,
Ms2 = {array, symbol, Ms1},
Ms = #'v1_0.sasl_mechanisms'{sasl_server_mechanisms = Ms2},
Ms1 = {array, symbol, Ms0},
Ms = #'v1_0.sasl_mechanisms'{sasl_server_mechanisms = Ms1},
ok = send_on_channel0(Sock, Ms, rabbit_amqp_sasl),
start_connection0(Mode, State);
start_connection(Mode = amqp,
State = #v1{sock = Sock,
connection = C = #v1_connection{user = User}}) ->
case User of
none ->
%% Client either skipped SASL layer or used SASL mechansim ANONYMOUS.
case default_user() of
none ->
send_handshake(Sock, <<"AMQP",3,1,0,0>>),
throw(banned_unauthenticated_connection);
NoAuthUsername ->
case rabbit_access_control:check_user_login(NoAuthUsername, []) of
{ok, NoAuthUser} ->
State1 = State#v1{connection = C#v1_connection{user = NoAuthUser}},
send_handshake(Sock, <<"AMQP",0,1,0,0>>),
start_connection0(Mode, State1);
{refused, _, _, _} ->
send_handshake(Sock, <<"AMQP",3,1,0,0>>),
throw(amqp1_0_default_user_missing)
end
end;
#user{} ->
%% Client already got successfully authenticated by SASL.
send_handshake(Sock, <<"AMQP",0,1,0,0>>),
start_connection0(Mode, State)
end.
start_connection0(Mode, State0 = #v1{connection = Connection,
helper_sup = HelperSup}) ->
SessionSup = case Mode of
sasl ->
undefined;
amqp ->
ChildSpec = #{id => session_sup,
start => {rabbit_amqp_session_sup, start_link, [self()]},
restart => transient,
significant => true,
shutdown => infinity,
type => supervisor},
{ok, Pid} = supervisor:start_child(HelperSup, ChildSpec),
Pid
end,
State = State0#v1{session_sup = SessionSup,
connection_state = starting,
State = State0#v1{connection_state = waiting_sasl_init,
connection = Connection#v1_connection{timeout = ?NORMAL_TIMEOUT}},
switch_callback(State, {frame_header, Mode}, 8).
switch_callback(State, {frame_header, sasl}, 8).
send_handshake(Sock, Handshake) ->
ok = inet_op(fun () -> rabbit_net:send(Sock, Handshake) end).
@ -715,18 +680,25 @@ auth_mechanism_to_module(TypeBin, Sock) ->
end
end.
%% Returns mechanisms ordered in decreasing level of preference (as configured).
auth_mechanisms(Sock) ->
{ok, Configured} = application:get_env(rabbit, auth_mechanisms),
[Name || {Name, Module} <- rabbit_registry:lookup_all(auth_mechanism),
Module:should_offer(Sock), lists:member(Name, Configured)].
{ok, ConfiguredMechs} = application:get_env(rabbit, auth_mechanisms),
RegisteredMechs = rabbit_registry:lookup_all(auth_mechanism),
lists:filter(
fun(Mech) ->
case proplists:lookup(Mech, RegisteredMechs) of
{Mech, Mod} ->
Mod:should_offer(Sock);
none ->
false
end
end, ConfiguredMechs).
%% Begin 1-0
auth_phase_1_0(Response,
State = #v1{sock = Sock,
connection = Connection =
#v1_connection{auth_mechanism = {Name, AuthMechanism},
auth_state = AuthState}}) ->
auth_phase(
Response,
State = #v1{sock = Sock,
connection = Conn = #v1_connection{auth_mechanism = {Name, AuthMechanism},
auth_state = AuthState}}) ->
case AuthMechanism:handle_response(Response, AuthState) of
{refused, Username, Msg, Args} ->
%% We don't trust the client at this point - force them to wait
@ -745,16 +717,16 @@ auth_phase_1_0(Response,
{challenge, Challenge, AuthState1} ->
Secure = #'v1_0.sasl_challenge'{challenge = {binary, Challenge}},
ok = send_on_channel0(Sock, Secure, rabbit_amqp_sasl),
State#v1{connection = Connection#v1_connection{auth_state = AuthState1}};
State#v1{connection = Conn#v1_connection{auth_state = AuthState1}};
{ok, User} ->
Outcome = #'v1_0.sasl_outcome'{code = ?V_1_0_SASL_CODE_OK},
ok = send_on_channel0(Sock, Outcome, rabbit_amqp_sasl),
State1 = State#v1{connection_state = waiting_amqp0100,
connection = Connection#v1_connection{user = User}},
connection = Conn#v1_connection{user = User,
auth_state = none}},
switch_callback(State1, handshake, 8)
end.
auth_fail(Username, State) ->
rabbit_core_metrics:auth_attempt_failed(<<>>, Username, amqp10),
notify_auth(user_authentication_failure, Username, State).
@ -822,8 +794,7 @@ send_to_new_session(
vhost({utf8, <<"vhost:", VHost/binary>>}) ->
VHost;
vhost(_) ->
application:get_env(rabbit, amqp1_0_default_vhost,
application:get_env(rabbit, default_vhost, <<"/">>)).
application:get_env(rabbit, default_vhost, <<"/">>).
check_user_loopback(#v1{connection = #v1_connection{user = #user{username = Username}},
sock = Socket} = State) ->
@ -917,15 +888,6 @@ ensure_credential_expiry_timer(User) ->
end
end.
-spec default_user() -> none | rabbit_types:username().
default_user() ->
case application:get_env(rabbit, amqp1_0_default_user) of
{ok, none} ->
none;
{ok, Username} when is_binary(Username) ->
Username
end.
%% We don't trust the client at this point - force them to wait
%% for a bit so they can't DOS us with repeated failed logins etc.
silent_close_delay() ->
@ -978,12 +940,11 @@ i(frame_max, #v1{connection = #v1_connection{outgoing_max_frame_size = Val}}) ->
end;
i(timeout, #v1{connection = #v1_connection{timeout = Millis}}) ->
Millis div 1000;
i(user,
#v1{connection = #v1_connection{user = #user{username = Val}}}) ->
Val;
i(user,
#v1{connection = #v1_connection{user = none}}) ->
'';
i(user, #v1{connection = #v1_connection{user = User}}) ->
case User of
#user{username = Val} -> Val;
unauthenticated -> ''
end;
i(state, S) ->
i(connection_state, S);
i(connection_state, #v1{connection_state = Val}) ->

View File

@ -0,0 +1,53 @@
%% 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) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(rabbit_auth_mechanism_anonymous).
-behaviour(rabbit_auth_mechanism).
-export([description/0, should_offer/1, init/1, handle_response/2]).
-define(STATE, []).
-rabbit_boot_step(
{?MODULE,
[{description, "auth mechanism anonymous"},
{mfa, {rabbit_registry, register, [auth_mechanism, <<"ANONYMOUS">>, ?MODULE]}},
{requires, rabbit_registry},
{enables, kernel_ready}]}).
description() ->
[{description, <<"SASL ANONYMOUS authentication mechanism">>}].
should_offer(_Sock) ->
case credentials() of
{ok, _, _} ->
true;
error ->
false
end.
init(_Sock) ->
?STATE.
handle_response(_Response, ?STATE) ->
{ok, User, Pass} = credentials(),
rabbit_access_control:check_user_pass_login(User, Pass).
-spec credentials() ->
{ok, rabbit_types:username(), rabbit_types:password()} | error.
credentials() ->
case application:get_env(rabbit, anonymous_login_user) of
{ok, User} when is_binary(User) ->
case application:get_env(rabbit, anonymous_login_pass) of
{ok, Pass} when is_binary(Pass) ->
{ok, User, Pass};
_ ->
error
end;
_ ->
error
end.

View File

@ -39,11 +39,15 @@ handle_response(Response, _State) ->
extract_user_pass(Response) ->
case extract_elem(Response) of
{ok, User, Response1} -> case extract_elem(Response1) of
{ok, Pass, <<>>} -> {ok, User, Pass};
_ -> error
end;
error -> error
{ok, User, Response1} ->
case extract_elem(Response1) of
{ok, Pass, <<>>} ->
{ok, User, Pass};
_ ->
error
end;
error ->
error
end.
extract_elem(<<0:8, Rest/binary>>) ->

View File

@ -60,6 +60,8 @@
%% from connection storms and DoS.
-define(SILENT_CLOSE_DELAY, 3).
-define(CHANNEL_MIN, 1).
%% AMQP 1.0 §5.3
-define(PROTOCOL_ID_SASL, 3).
%%--------------------------------------------------------------------------
@ -432,6 +434,12 @@ log_connection_exception(Severity, Name, {handshake_error, tuning, _Channel,
log_connection_exception_with_severity(Severity,
"closing AMQP connection ~tp (~ts):~nfailed to negotiate connection parameters: ~ts",
[self(), Name, Explanation]);
log_connection_exception(Severity, Name, {sasl_required, ProtocolId}) ->
log_connection_exception_with_severity(
Severity,
"closing AMQP 1.0 connection (~ts): RabbitMQ requires SASL "
"security layer (expected protocol ID 3, but client sent protocol ID ~b)",
[Name, ProtocolId]);
%% old exception structure
log_connection_exception(Severity, Name, connection_closed_abruptly) ->
log_connection_exception_with_severity(Severity,
@ -1086,8 +1094,11 @@ handle_input(Callback, Data, _State) ->
throw({bad_input, Callback, Data}).
%% AMQP 1.0 §2.2
version_negotiation({Id, 1, 0, 0}, State) ->
become_10(Id, State);
version_negotiation({?PROTOCOL_ID_SASL, 1, 0, 0}, State) ->
become_10(State);
version_negotiation({ProtocolId, 1, 0, 0}, #v1{sock = Sock}) ->
%% AMQP 1.0 figure 2.13: We require SASL security layer.
refuse_connection(Sock, {sasl_required, ProtocolId});
version_negotiation({0, 0, 9, 1}, State) ->
start_091_connection({0, 9, 1}, rabbit_framing_amqp_0_9_1, State);
version_negotiation({1, 1, 0, 9}, State) ->
@ -1126,14 +1137,13 @@ start_091_connection({ProtocolMajor, ProtocolMinor, _ProtocolRevision},
-spec refuse_connection(rabbit_net:socket(), any()) -> no_return().
refuse_connection(Sock, Exception) ->
refuse_connection(Sock, Exception, {0, 1, 0, 0}).
refuse_connection(Sock, Exception, {?PROTOCOL_ID_SASL, 1, 0, 0}).
-spec refuse_connection(_, _, _) -> no_return().
refuse_connection(Sock, Exception, {A, B, C, D}) ->
ok = inet_op(fun () -> rabbit_net:send(Sock, <<"AMQP",A,B,C,D>>) end),
throw(Exception).
ensure_stats_timer(State = #v1{connection_state = running}) ->
rabbit_event:ensure_stats_timer(State, #v1.stats_timer, emit_stats);
ensure_stats_timer(State) ->
@ -1626,21 +1636,12 @@ emit_stats(State) ->
State1 = rabbit_event:reset_stats_timer(State, #v1.stats_timer),
ensure_stats_timer(State1).
%% 1.0 stub
-spec become_10(non_neg_integer(), #v1{}) -> no_return().
become_10(Id, State = #v1{sock = Sock}) ->
Mode = case Id of
0 -> amqp;
3 -> sasl;
_ -> refuse_connection(
Sock, {unsupported_amqp1_0_protocol_id, Id},
{3, 1, 0, 0})
end,
F = fun (_Deb, Buf, BufLen, State0) ->
{rabbit_amqp_reader, init,
[Mode, pack_for_1_0(Buf, BufLen, State0)]}
end,
State#v1{connection_state = {become, F}}.
become_10(State) ->
Fun = fun(_Deb, Buf, BufLen, State0) ->
{rabbit_amqp_reader, init,
[pack_for_1_0(Buf, BufLen, State0)]}
end,
State#v1{connection_state = {become, Fun}}.
pack_for_1_0(Buf, BufLen, #v1{sock = Sock,
recv_len = RecvLen,

View File

@ -58,11 +58,10 @@ groups() ->
%% authn
authn_failure_event,
sasl_anonymous_success,
sasl_none_success,
sasl_plain_success,
sasl_anonymous_failure,
sasl_none_failure,
sasl_plain_failure,
sasl_none_failure,
vhost_absent,
%% limits
@ -609,10 +608,6 @@ sasl_anonymous_success(Config) ->
Mechanism = anon,
ok = sasl_success(Mechanism, Config).
sasl_none_success(Config) ->
Mechanism = none,
ok = sasl_success(Mechanism, Config).
sasl_plain_success(Config) ->
Mechanism = {plain, <<"guest">>, <<"guest">>},
ok = sasl_success(Mechanism, Config).
@ -627,38 +622,40 @@ sasl_success(Mechanism, Config) ->
ok = amqp10_client:close_connection(Connection).
sasl_anonymous_failure(Config) ->
Mechanism = anon,
?assertEqual(
{sasl_not_supported, Mechanism},
sasl_failure(Mechanism, Config)
).
sasl_none_failure(Config) ->
Mechanism = none,
sasl_failure(Mechanism, Config).
sasl_plain_failure(Config) ->
Mechanism = {plain, <<"guest">>, <<"wrong password">>},
?assertEqual(
sasl_auth_failure,
sasl_failure(Mechanism, Config)
).
sasl_failure(Mechanism, Config) ->
App = rabbit,
Par = amqp1_0_default_user,
Par = anonymous_login_user,
{ok, Default} = rpc(Config, application, get_env, [App, Par]),
%% Prohibit anonymous login.
ok = rpc(Config, application, set_env, [App, Par, none]),
Mechanism = anon,
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := Mechanism},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
Reason = receive {amqp10_event, {connection, Connection, {closed, Reason0}}} -> Reason0
after 5000 -> ct:fail(missing_closed)
end,
receive {amqp10_event, {connection, Connection, {closed, Reason}}} ->
?assertEqual({sasl_not_supported, Mechanism}, Reason)
after 5000 -> ct:fail(missing_closed)
end,
ok = rpc(Config, application, set_env, [App, Par, Default]),
Reason.
ok = rpc(Config, application, set_env, [App, Par, Default]).
sasl_plain_failure(Config) ->
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := {plain, <<"guest">>, <<"wrong password">>}},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, {closed, Reason}}} ->
?assertEqual(sasl_auth_failure, Reason)
after 5000 -> ct:fail(missing_closed)
end.
%% Skipping SASL is disallowed in RabbitMQ.
sasl_none_failure(Config) ->
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := none},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, {closed, _Reason}}} -> ok
after 5000 -> ct:fail(missing_closed)
end.
vhost_absent(Config) ->
OpnConf = connection_config(Config, <<"this vhost does not exist">>),

View File

@ -48,8 +48,13 @@ module AmqpClient =
let s = Session c
{ Conn = c; Session = s }
let connectWithOpen uri opn =
let c = Connection(Address uri, null, opn, null)
let connectAnon uri =
let c = Connection(Address uri, SaslProfile.Anonymous, null, null)
let s = Session c
{ Conn = c; Session = s }
let connectAnonWithOpen uri opn =
let c = Connection(Address uri, SaslProfile.Anonymous, opn, null)
let s = Session c
{ Conn = c; Session = s }
@ -114,7 +119,7 @@ module Test =
]
let testOutcome uri (attach: Attach) (cond: string) =
use ac = connect uri
use ac = connectAnon uri
let trySet (mre: AutoResetEvent) =
try mre.Set() |> ignore with _ -> ()
@ -135,7 +140,7 @@ module Test =
let no_routes_is_released uri =
// tests that a message sent to an exchange that resolves no routes for the
// binding key returns the Released outcome, rather than Accepted
use ac = connect uri
use ac = connectAnon uri
let address = "/exchanges/no_routes_is_released"
let sender = SenderLink(ac.Session, "released-sender", address)
let trySet (mre: AutoResetEvent) =
@ -160,7 +165,7 @@ module Test =
()
let roundtrip uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/roundtrip"
for body in sampleTypes do
let corr = "correlation"
@ -175,7 +180,7 @@ module Test =
()
let streams uri =
use c = connect uri
use c = connectAnon uri
let name = "streams-test"
let address = "/queues/streams"
let sender = SenderLink(c.Session, name + "-sender" , address)
@ -216,7 +221,7 @@ module Test =
open RabbitMQ.Client
let roundtrip_to_amqp_091 uri =
use c = connect uri
use c = connectAnon uri
let q = "roundtrip_to_amqp_091"
let target = "/queues/roundtrip_to_amqp_091"
let corr = "correlation"
@ -282,7 +287,7 @@ module Test =
let opn = Open(ContainerId = Guid.NewGuid().ToString(),
HostName = addr.Host, ChannelMax = 256us,
MaxFrameSize = frameSize)
use c = connectWithOpen uri opn
use c = connectAnonWithOpen uri opn
let sender, receiver = senderReceiver c "test" "/queues/fragmentation"
let m = new Message(String.replicate size "a")
sender.Send m
@ -290,7 +295,7 @@ module Test =
assertEqual (m.Body) (m'.Body)
let message_annotations uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/message_annotations"
let ann = MessageAnnotations()
let k1 = Symbol "key1"
@ -309,7 +314,7 @@ module Test =
assertTrue (m.MessageAnnotations.[k2] = m'.MessageAnnotations.[k2])
let footer uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/footer"
let footer = Footer()
let k1 = Symbol "key1"
@ -327,7 +332,7 @@ module Test =
assertTrue (m.Footer.[k2] = m'.Footer.[k2])
let data_types uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/data_types"
let aSeq = amqpSequence sampleTypes
(new Message(aSeq)) |> sender.Send
@ -337,7 +342,7 @@ module Test =
List.exists ((=) a) sampleTypes |> assertTrue
let reject uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/reject"
new Message "testing reject" |> sender.Send
let m = receiver.Receive()
@ -345,7 +350,7 @@ module Test =
assertEqual null (receiver.Receive(TimeSpan.FromMilliseconds 100.))
let redelivery uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/redelivery"
new Message "testing redelivery" |> sender.Send
let m = receiver.Receive()
@ -363,7 +368,7 @@ module Test =
session.Close()
let released uri =
use c = connect uri
use c = connectAnon uri
let sender, receiver = senderReceiver c "test" "/queues/released"
new Message "testing released" |> sender.Send
let m = receiver.Receive()
@ -392,7 +397,7 @@ module Test =
"/queues/autodel_q", "/queues/autodel_q", ""] do
let rnd = Random()
use c = connect uri
use c = connectAnon uri
let sender = SenderLink(c.Session, "test-sender", target)
let receiver = ReceiverLink(c.Session, "test-receiver", source)
receiver.SetCredit(100, true)
@ -411,7 +416,7 @@ module Test =
for dest, cond in
["/exchanges/missing", "amqp:not-found"
"/fruit/orange", "amqp:invalid-field"] do
use ac = connect uri
use ac = connectAnon uri
let trySet (mre: AutoResetEvent) =
try mre.Set() |> ignore with _ -> ()

View File

@ -220,6 +220,8 @@ ssl_options.fail_if_no_peer_cert = true",
{default_user_settings,
"default_user = guest
default_pass = guest
anonymous_login_user = guest
anonymous_login_pass = guest
default_user_tags.administrator = true
default_permissions.configure = .*
default_permissions.read = .*
@ -227,9 +229,16 @@ default_permissions.write = .*",
[{rabbit,
[{default_user,<<"guest">>},
{default_pass,<<"guest">>},
{anonymous_login_user,<<"guest">>},
{anonymous_login_pass,<<"guest">>},
{default_user_tags,[administrator]},
{default_permissions,[<<".*">>,<<".*">>,<<".*">>]}]}],
[]},
{anonymous_login_user,
"anonymous_login_user = none",
[{rabbit,
[{anonymous_login_user, none}]}],
[]},
{cluster_formation,
"cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config
cluster_formation.classic_config.nodes.peer1 = rabbit@hostname1

View File

@ -282,31 +282,36 @@ version_negotiation(Config) ->
ok = rabbit_ct_broker_helpers:rpc(Config, ?MODULE, version_negotiation1, [Config]).
version_negotiation1(Config) ->
H = ?config(rmq_hostname, Config),
P = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
Hostname = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
[?assertEqual(<<"AMQP",0,1,0,0>>, version_negotiation2(H, P, Vsn)) ||
[?assertEqual(<<"AMQP",3,1,0,0>>,
version_negotiation2(Hostname, Port, Vsn)) ||
Vsn <- [<<"AMQP",0,1,0,0>>,
<<"AMQP",0,1,0,1>>,
<<"AMQP",0,1,1,0>>,
<<"AMQP",0,9,1,0>>,
<<"AMQP",0,0,8,0>>,
<<"XXXX",0,1,0,0>>,
<<"XXXX",0,0,9,1>>]],
[?assertEqual(<<"AMQP",3,1,0,0>>, version_negotiation2(H, P, Vsn)) ||
Vsn <- [<<"AMQP",1,1,0,0>>,
<<"AMQP",1,1,0,0>>,
<<"AMQP",2,1,0,0>>,
<<"AMQP",3,1,0,0>>,
<<"AMQP",3,1,0,1>>,
<<"AMQP",3,1,0,1>>,
<<"AMQP",4,1,0,0>>,
<<"AMQP",9,1,0,0>>]],
<<"AMQP",9,1,0,0>>,
<<"XXXX",0,1,0,0>>,
<<"XXXX",0,0,9,1>>
]],
[?assertEqual(<<"AMQP",0,0,9,1>>, version_negotiation2(H, P, Vsn)) ||
[?assertEqual(<<"AMQP",0,0,9,1>>,
version_negotiation2(Hostname, Port, Vsn)) ||
Vsn <- [<<"AMQP",0,0,9,2>>,
<<"AMQP",0,0,10,0>>,
<<"AMQP",0,0,10,1>>]],
ok.
version_negotiation2(H, P, Header) ->
{ok, C} = gen_tcp:connect(H, P, [binary, {active, false}]),
version_negotiation2(Hostname, Port, Header) ->
{ok, C} = gen_tcp:connect(Hostname, Port, [binary, {active, false}]),
ok = gen_tcp:send(C, Header),
{ok, ServerVersion} = gen_tcp:recv(C, 8, 100),
ok = gen_tcp:close(C),

View File

@ -13,12 +13,13 @@
-include_lib("eunit/include/eunit.hrl").
all() ->
[{group, tests}].
[{group, external_enforced}].
groups() ->
[
{tests, [shuffle],
[amqp]
{external_enforced, [shuffle],
[external_succeeds,
anonymous_fails]
}
].
@ -37,6 +38,7 @@ init_per_group(_Group, Config0) ->
Config0,
{rabbit,
[
%% Enforce EXTERNAL disallowing other mechanisms.
{auth_mechanisms, ['EXTERNAL']},
{ssl_cert_login_from, common_name}
]}),
@ -68,7 +70,7 @@ end_per_testcase(Testcase, Config) ->
ok = clear_permissions(Config),
rabbit_ct_helpers:testcase_finished(Config, Testcase).
amqp(Config) ->
external_succeeds(Config) ->
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls),
Host = ?config(rmq_hostname, Config),
Vhost = ?config(test_vhost, Config),
@ -90,6 +92,24 @@ amqp(Config) ->
end,
ok = amqp10_client:close_connection(Connection).
anonymous_fails(Config) ->
Mechansim = anon,
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl => Mechansim},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, {closed, Reason}}} ->
?assertEqual({sasl_not_supported, Mechansim}, Reason)
after 5000 -> ct:fail(missing_closed)
end.
connection_config(Config, Vhost) ->
Host = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
#{address => Host,
port => Port,
container_id => <<"my container">>,
hostname => <<"vhost:", Vhost/binary>>}.
set_permissions(Config, ConfigurePerm, WritePerm, ReadPerm) ->
ok = rabbit_ct_broker_helpers:set_permissions(Config,
?config(test_user, Config),

View File

@ -72,13 +72,25 @@ end_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_finished(Config, Testcase).
old_to_new_on_old(Config) ->
ok = shovel(?OLD, ?NEW, ?OLD, Config).
case rabbit_ct_helpers:is_mixed_versions() of
true ->
{skip, "TODO: Unskip when lower version is >= 3.13.7 "
"because AMQP 1.0 client must use SASL when connecting to 4.0"};
false ->
ok = shovel(?OLD, ?NEW, ?OLD, Config)
end.
old_to_new_on_new(Config) ->
ok = shovel(?OLD, ?NEW, ?NEW, Config).
new_to_old_on_old(Config) ->
ok = shovel(?NEW, ?OLD, ?OLD, Config).
case rabbit_ct_helpers:is_mixed_versions() of
true ->
{skip, "TODO: Unskip when lower version is >= 3.13.7 "
"because AMQP 1.0 client must use SASL when connecting to 4.0"};
false ->
ok = shovel(?NEW, ?OLD, ?OLD, Config)
end.
new_to_old_on_new(Config) ->
ok = shovel(?NEW, ?OLD, ?NEW, Config).

View File

@ -63,7 +63,8 @@ groups() ->
offset_lag_calculation,
test_super_stream_duplicate_partitions,
authentication_error_should_close_with_delay,
unauthorized_vhost_access_should_close_with_delay
unauthorized_vhost_access_should_close_with_delay,
sasl_anonymous
]},
%% Run `test_global_counters` on its own so the global metrics are
%% initialised to 0 for each testcase
@ -249,6 +250,16 @@ test_stream(Config) ->
test_server(gen_tcp, Stream, Config),
ok.
sasl_anonymous(Config) ->
Port = get_port(gen_tcp, Config),
Opts = get_opts(gen_tcp),
{ok, S} = gen_tcp:connect("localhost", Port, Opts),
C0 = rabbit_stream_core:init(0),
C1 = test_peer_properties(gen_tcp, S, C0),
C2 = sasl_handshake(gen_tcp, S, C1),
C3 = test_anonymous_sasl_authenticate(gen_tcp, S, C2),
_C = tune(gen_tcp, S, C3).
test_update_secret(Config) ->
Transport = gen_tcp,
{S, C0} = connect_and_authenticate(Transport, Config),
@ -1150,17 +1161,20 @@ test_authenticate(Transport, S, C0, Username, Password) ->
sasl_handshake(Transport, S, C0) ->
SaslHandshakeFrame = request(sasl_handshake),
ok = Transport:send(S, SaslHandshakeFrame),
Plain = <<"PLAIN">>,
AmqPlain = <<"AMQPLAIN">>,
{Cmd, C1} = receive_commands(Transport, S, C0),
case Cmd of
{response, _, {sasl_handshake, ?RESPONSE_CODE_OK, Mechanisms}} ->
?assertEqual([AmqPlain, Plain], lists:sort(Mechanisms));
?assertEqual([<<"AMQPLAIN">>, <<"ANONYMOUS">>, <<"PLAIN">>],
lists:sort(Mechanisms));
_ ->
ct:fail("invalid cmd ~tp", [Cmd])
end,
C1.
test_anonymous_sasl_authenticate(Transport, S, C) ->
Res = sasl_authenticate(Transport, S, C, <<"ANONYMOUS">>, <<>>),
expect_successful_authentication(Res).
test_plain_sasl_authenticate(Transport, S, C1, Username) ->
test_plain_sasl_authenticate(Transport, S, C1, Username, Username).
@ -1175,6 +1189,7 @@ expect_successful_authentication({SaslAuth, C2} = _SaslReponse) ->
?assertEqual({response, 2, {sasl_authenticate, ?RESPONSE_CODE_OK}},
SaslAuth),
C2.
expect_unsuccessful_authentication({SaslAuth, C2} = _SaslReponse, ExpectedError) ->
?assertEqual({response, 2, {sasl_authenticate, ExpectedError}},
SaslAuth),

View File

@ -556,6 +556,7 @@ rabbit:
- rabbit_amqqueue_sup_sup
- rabbit_auth_backend_internal
- rabbit_auth_mechanism_amqplain
- rabbit_auth_mechanism_anonymous
- rabbit_auth_mechanism_cr_demo
- rabbit_auth_mechanism_plain
- rabbit_autoheal