Skip to content

Commit 60ae4d4

Browse files
authored
Support SASL mechanism EXTERNAL in Erlang AMQP 1.0 client (#11984)
* Support SASL mechanism EXTERNAL in Erlang AMQP 1.0 client * Move test to plugin rabbitmq_auth_mechanism_ssl In theory, there can be other plugin that offer SASL mechanism EXTERNAL. Therefore, instead of adding a test dependency from app rabbit to app rabbitmq_auth_mechanism_ssl, it's better to test this plugin specific functionality directly in the plugin itself.
1 parent e396c16 commit 60ae4d4

File tree

7 files changed

+196
-38
lines changed

7 files changed

+196
-38
lines changed

deps/amqp10_client/src/amqp10_client.erl

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ open_connection(ConnectionConfig0) ->
106106
notify_when_opened => NotifyWhenOpened,
107107
notify_when_closed => NotifyWhenClosed
108108
},
109-
Sasl = maps:get(sasl, ConnectionConfig1),
110-
ConnectionConfig2 = ConnectionConfig1#{sasl => amqp10_client_connection:encrypt_sasl(Sasl)},
111-
ConnectionConfig = merge_default_tls_options(ConnectionConfig2),
109+
ConnectionConfig = merge_default_tls_options(ConnectionConfig1),
112110
amqp10_client_connection:open(ConnectionConfig).
113111

114112
%% @doc Closes a connection.

deps/amqp10_client/src/amqp10_client_connection.erl

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
socket_ready/2,
2323
protocol_header_received/5,
2424
begin_session/1,
25-
heartbeat/1,
26-
encrypt_sasl/1,
27-
decrypt_sasl/1]).
25+
heartbeat/1]).
2826

2927
%% gen_statem callbacks
3028
-export([init/1,
@@ -52,7 +50,8 @@
5250
-type address() :: inet:socket_address() | inet:hostname().
5351

5452
-type encrypted_sasl() :: {plaintext, binary()} | {encrypted, binary()}.
55-
-type decrypted_sasl() :: none | anon | {plain, User :: binary(), Pwd :: binary()}.
53+
-type decrypted_sasl() :: none | anon | external | {plain, User :: binary(), Pwd :: binary()}.
54+
-type sasl() :: encrypted_sasl() | decrypted_sasl().
5655

5756
-type connection_config() ::
5857
#{container_id => binary(), % AMQP container id
@@ -72,9 +71,7 @@
7271
% set to a negative value to allow a sender to "overshoot" the flow
7372
% control by this margin
7473
transfer_limit_margin => 0 | neg_integer(),
75-
%% These credentials_obfuscation-wrapped values have the type of
76-
%% decrypted_sasl/0
77-
sasl => encrypted_sasl() | decrypted_sasl(),
74+
sasl => sasl(),
7875
properties => amqp10_client_types:properties()
7976
}.
8077

@@ -92,16 +89,15 @@
9289
}).
9390

9491
-export_type([connection_config/0,
95-
amqp10_socket/0,
96-
encrypted_sasl/0,
97-
decrypted_sasl/0]).
92+
amqp10_socket/0]).
9893

9994
%% -------------------------------------------------------------------
10095
%% Public API.
10196
%% -------------------------------------------------------------------
10297

10398
-spec open(connection_config()) -> supervisor:startchild_ret().
104-
open(Config) ->
99+
open(Config0) ->
100+
Config = maps:update_with(sasl, fun maybe_encrypt_sasl/1, Config0),
105101
%% Start the supervision tree dedicated to that connection. It
106102
%% starts at least a connection process (the PID we want to return)
107103
%% and a reader process (responsible for opening and reading the
@@ -127,17 +123,23 @@ open(Config) ->
127123
close(Pid, Reason) ->
128124
gen_statem:cast(Pid, {close, Reason}).
129125

130-
-spec encrypt_sasl(decrypted_sasl()) -> encrypted_sasl().
131-
encrypt_sasl(none) ->
132-
credentials_obfuscation:encrypt(none);
133-
encrypt_sasl(DecryptedSasl) ->
134-
credentials_obfuscation:encrypt(term_to_binary(DecryptedSasl)).
135-
136-
-spec decrypt_sasl(encrypted_sasl()) -> decrypted_sasl().
137-
decrypt_sasl(none) ->
138-
credentials_obfuscation:decrypt(none);
139-
decrypt_sasl(EncryptedSasl) ->
140-
binary_to_term(credentials_obfuscation:decrypt(EncryptedSasl)).
126+
-spec maybe_encrypt_sasl(decrypted_sasl()) -> sasl().
127+
maybe_encrypt_sasl(Sasl)
128+
when Sasl =:= none orelse
129+
Sasl =:= anon orelse
130+
Sasl =:= external ->
131+
Sasl;
132+
maybe_encrypt_sasl(Plain = {plain, _User, _Passwd}) ->
133+
credentials_obfuscation:encrypt(term_to_binary(Plain)).
134+
135+
-spec maybe_decrypt_sasl(sasl()) -> decrypted_sasl().
136+
maybe_decrypt_sasl(Sasl)
137+
when Sasl =:= none orelse
138+
Sasl =:= anon orelse
139+
Sasl =:= external ->
140+
Sasl;
141+
maybe_decrypt_sasl(Encrypted) ->
142+
binary_to_term(credentials_obfuscation:decrypt(Encrypted)).
141143

142144
%% -------------------------------------------------------------------
143145
%% Private API.
@@ -207,13 +209,11 @@ sasl_hdr_sent({call, From}, begin_session,
207209
{keep_state, State1}.
208210

209211
sasl_hdr_rcvds(_EvtType, #'v1_0.sasl_mechanisms'{
210-
sasl_server_mechanisms = {array, symbol, Mechs}},
211-
State = #state{config = #{sasl := EncryptedSasl}}) ->
212-
DecryptedSasl = decrypt_sasl(EncryptedSasl),
213-
SaslBin = {symbol, decrypted_sasl_to_bin(DecryptedSasl)},
214-
case lists:any(fun(S) when S =:= SaslBin -> true;
215-
(_) -> false
216-
end, Mechs) of
212+
sasl_server_mechanisms = {array, symbol, AvailableMechs}},
213+
State = #state{config = #{sasl := Sasl}}) ->
214+
DecryptedSasl = maybe_decrypt_sasl(Sasl),
215+
OurMech = {symbol, decrypted_sasl_to_mechanism(DecryptedSasl)},
216+
case lists:member(OurMech, AvailableMechs) of
217217
true ->
218218
ok = send_sasl_init(State, DecryptedSasl),
219219
{next_state, sasl_init_sent, State};
@@ -454,6 +454,15 @@ send_close(#state{socket = Socket}, _Reason) ->
454454
send_sasl_init(State, anon) ->
455455
Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"ANONYMOUS">>}},
456456
send(Frame, 1, State);
457+
send_sasl_init(State, external) ->
458+
Frame = #'v1_0.sasl_init'{
459+
mechanism = {symbol, <<"EXTERNAL">>},
460+
%% "This response is empty when the client is requesting to act
461+
%% as the identity the server associated with its authentication
462+
%% credentials."
463+
%% https://datatracker.ietf.org/doc/html/rfc4422#appendix-A.1
464+
initial_response = {binary, <<>>}},
465+
send(Frame, 1, State);
457466
send_sasl_init(State, {plain, User, Pass}) ->
458467
Response = <<0:8, User/binary, 0:8, Pass/binary>>,
459468
Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"PLAIN">>},
@@ -546,9 +555,12 @@ translate_err(#'v1_0.error'{condition = Cond, description = Desc}) ->
546555
amqp10_event(Evt) ->
547556
{amqp10_event, {connection, self(), Evt}}.
548557

549-
decrypted_sasl_to_bin({plain, _, _}) -> <<"PLAIN">>;
550-
decrypted_sasl_to_bin(anon) -> <<"ANONYMOUS">>;
551-
decrypted_sasl_to_bin(none) -> <<"ANONYMOUS">>.
558+
decrypted_sasl_to_mechanism(anon) ->
559+
<<"ANONYMOUS">>;
560+
decrypted_sasl_to_mechanism(external) ->
561+
<<"EXTERNAL">>;
562+
decrypted_sasl_to_mechanism({plain, _, _}) ->
563+
<<"PLAIN">>.
552564

553565
config_defaults() ->
554566
#{sasl => none,

deps/rabbitmq_auth_mechanism_ssl/BUILD.bazel

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1+
load("@rules_erlang//:eunit2.bzl", "eunit")
12
load("@rules_erlang//:xref2.bzl", "xref")
23
load("@rules_erlang//:dialyze.bzl", "dialyze", "plt")
4+
load("//:rabbitmq_home.bzl", "rabbitmq_home")
5+
load("//:rabbitmq_run.bzl", "rabbitmq_run")
36
load(
47
"//:rabbitmq.bzl",
58
"BROKER_VERSION_REQUIREMENTS_ANY",
69
"RABBITMQ_DIALYZER_OPTS",
710
"assert_suites",
811
"rabbitmq_app",
12+
"rabbitmq_integration_suite",
913
)
1014
load(
1115
":app.bzl",
1216
"all_beam_files",
1317
"all_srcs",
1418
"all_test_beam_files",
19+
"test_suite_beam_files",
1520
)
1621

1722
APP_NAME = "rabbitmq_auth_mechanism_ssl"
@@ -26,7 +31,7 @@ APP_ENV = """[
2631

2732
all_beam_files(name = "all_beam_files")
2833

29-
all_test_beam_files()
34+
all_test_beam_files(name = "all_test_beam_files")
3035

3136
all_srcs(name = "all_srcs")
3237

@@ -70,10 +75,39 @@ dialyze(
7075
target = ":erlang_app",
7176
)
7277

78+
rabbitmq_home(
79+
name = "broker-for-tests-home",
80+
testonly = True,
81+
plugins = [
82+
":test_erlang_app",
83+
],
84+
)
85+
86+
rabbitmq_run(
87+
name = "rabbitmq-for-tests-run",
88+
testonly = True,
89+
home = ":broker-for-tests-home",
90+
)
91+
92+
rabbitmq_integration_suite(
93+
name = "system_SUITE",
94+
shard_count = 1,
95+
runtime_deps = [
96+
"//deps/amqp10_client:erlang_app",
97+
],
98+
)
99+
73100
assert_suites()
74101

75102
alias(
76103
name = "rabbitmq_auth_mechanism_ssl",
77104
actual = ":erlang_app",
78105
visibility = ["//visibility:public"],
79106
)
107+
108+
test_suite_beam_files(name = "test_suite_beam_files")
109+
110+
eunit(
111+
name = "eunit",
112+
target = ":test_erlang_app",
113+
)

deps/rabbitmq_auth_mechanism_ssl/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ endef
1414

1515
LOCAL_DEPS = public_key
1616
DEPS = rabbit_common rabbit
17+
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp10_client
1718

1819
DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk
1920
DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk

deps/rabbitmq_auth_mechanism_ssl/app.bzl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,11 @@ def all_test_beam_files(name = "all_test_beam_files"):
7575
)
7676

7777
def test_suite_beam_files(name = "test_suite_beam_files"):
78-
pass
78+
erlang_bytecode(
79+
name = "system_SUITE_beam_files",
80+
testonly = True,
81+
srcs = ["test/system_SUITE.erl"],
82+
outs = ["test/system_SUITE.beam"],
83+
app_name = "rabbitmq_auth_mechanism_ssl",
84+
erlc_opts = "//:test_erlc_opts",
85+
)

deps/rabbitmq_auth_mechanism_ssl/src/rabbit_auth_mechanism_ssl.erl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
{cleanup, {rabbit_registry, unregister,
2424
[auth_mechanism, <<"EXTERNAL">>]}}]}).
2525

26-
-record(state, {username = undefined}).
26+
-record(state, {
27+
username = undefined :: undefined | rabbit_types:username() | {refused, none, string(), [term()]}
28+
}).
2729

2830
description() ->
2931
[{description, <<"TLS peer verification-based authentication plugin. Used in combination with the EXTERNAL SASL mechanism.">>}].
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
6+
7+
-module(system_SUITE).
8+
9+
-compile([export_all,
10+
nowarn_export_all]).
11+
12+
-include_lib("common_test/include/ct.hrl").
13+
-include_lib("eunit/include/eunit.hrl").
14+
15+
all() ->
16+
[{group, tests}].
17+
18+
groups() ->
19+
[
20+
{tests, [shuffle],
21+
[amqp]
22+
}
23+
].
24+
25+
init_per_suite(Config) ->
26+
{ok, _} = application:ensure_all_started(amqp10_client),
27+
rabbit_ct_helpers:log_environment(),
28+
Config.
29+
30+
end_per_suite(Config) ->
31+
Config.
32+
33+
init_per_group(_Group, Config0) ->
34+
%% Command `deps/rabbitmq_ct_helpers/tools/tls-certs$ make`
35+
%% will put our hostname as common name in the client cert.
36+
Config1 = rabbit_ct_helpers:merge_app_env(
37+
Config0,
38+
{rabbit,
39+
[
40+
{auth_mechanisms, ['EXTERNAL']},
41+
{ssl_cert_login_from, common_name}
42+
]}),
43+
Config = rabbit_ct_helpers:run_setup_steps(
44+
Config1,
45+
rabbit_ct_broker_helpers:setup_steps() ++
46+
rabbit_ct_client_helpers:setup_steps()),
47+
{ok, UserString} = inet:gethostname(),
48+
User = unicode:characters_to_binary(UserString),
49+
ok = rabbit_ct_broker_helpers:add_user(Config, User),
50+
Vhost = <<"test vhost">>,
51+
ok = rabbit_ct_broker_helpers:add_vhost(Config, Vhost),
52+
[{test_vhost, Vhost},
53+
{test_user, User}] ++ Config.
54+
55+
end_per_group(_Group, Config) ->
56+
ok = rabbit_ct_broker_helpers:delete_user(Config, ?config(test_user, Config)),
57+
ok = rabbit_ct_broker_helpers:delete_vhost(Config, ?config(test_vhost, Config)),
58+
rabbit_ct_helpers:run_teardown_steps(
59+
Config,
60+
rabbit_ct_client_helpers:teardown_steps() ++
61+
rabbit_ct_broker_helpers:teardown_steps()).
62+
63+
init_per_testcase(Testcase, Config) ->
64+
ok = set_permissions(Config, <<>>, <<>>, <<"^some vhost permission">>),
65+
rabbit_ct_helpers:testcase_started(Config, Testcase).
66+
67+
end_per_testcase(Testcase, Config) ->
68+
ok = clear_permissions(Config),
69+
rabbit_ct_helpers:testcase_finished(Config, Testcase).
70+
71+
amqp(Config) ->
72+
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls),
73+
Host = ?config(rmq_hostname, Config),
74+
Vhost = ?config(test_vhost, Config),
75+
CACertFile = ?config(rmq_certsdir, Config) ++ "/testca/cacert.pem",
76+
CertFile = ?config(rmq_certsdir, Config) ++ "/client/cert.pem",
77+
KeyFile = ?config(rmq_certsdir, Config) ++ "/client/key.pem",
78+
OpnConf = #{address => Host,
79+
port => Port,
80+
container_id => atom_to_binary(?FUNCTION_NAME),
81+
hostname => <<"vhost:", Vhost/binary>>,
82+
sasl => external,
83+
tls_opts => {secure_port, [{cacertfile, CACertFile},
84+
{certfile, CertFile},
85+
{keyfile, KeyFile}]}
86+
},
87+
{ok, Connection} = amqp10_client:open_connection(OpnConf),
88+
receive {amqp10_event, {connection, Connection, opened}} -> ok
89+
after 5000 -> ct:fail(missing_opened)
90+
end,
91+
ok = amqp10_client:close_connection(Connection).
92+
93+
set_permissions(Config, ConfigurePerm, WritePerm, ReadPerm) ->
94+
ok = rabbit_ct_broker_helpers:set_permissions(Config,
95+
?config(test_user, Config),
96+
?config(test_vhost, Config),
97+
ConfigurePerm,
98+
WritePerm,
99+
ReadPerm).
100+
101+
clear_permissions(Config) ->
102+
User = ?config(test_user, Config),
103+
Vhost = ?config(test_vhost, Config),
104+
ok = rabbit_ct_broker_helpers:clear_permissions(Config, User, Vhost).

0 commit comments

Comments
 (0)