diff --git a/deps/amqp10_client/src/amqp10_client.erl b/deps/amqp10_client/src/amqp10_client.erl index 68cac2622265..a5de8bc88aa7 100644 --- a/deps/amqp10_client/src/amqp10_client.erl +++ b/deps/amqp10_client/src/amqp10_client.erl @@ -106,9 +106,7 @@ open_connection(ConnectionConfig0) -> notify_when_opened => NotifyWhenOpened, notify_when_closed => NotifyWhenClosed }, - Sasl = maps:get(sasl, ConnectionConfig1), - ConnectionConfig2 = ConnectionConfig1#{sasl => amqp10_client_connection:encrypt_sasl(Sasl)}, - ConnectionConfig = merge_default_tls_options(ConnectionConfig2), + ConnectionConfig = merge_default_tls_options(ConnectionConfig1), amqp10_client_connection:open(ConnectionConfig). %% @doc Closes a connection. diff --git a/deps/amqp10_client/src/amqp10_client_connection.erl b/deps/amqp10_client/src/amqp10_client_connection.erl index 4a9c738eac98..80c75f986a66 100644 --- a/deps/amqp10_client/src/amqp10_client_connection.erl +++ b/deps/amqp10_client/src/amqp10_client_connection.erl @@ -22,9 +22,7 @@ socket_ready/2, protocol_header_received/5, begin_session/1, - heartbeat/1, - encrypt_sasl/1, - decrypt_sasl/1]). + heartbeat/1]). %% gen_statem callbacks -export([init/1, @@ -52,7 +50,8 @@ -type address() :: inet:socket_address() | inet:hostname(). -type encrypted_sasl() :: {plaintext, binary()} | {encrypted, binary()}. --type decrypted_sasl() :: none | anon | {plain, User :: binary(), Pwd :: binary()}. +-type decrypted_sasl() :: none | anon | external | {plain, User :: binary(), Pwd :: binary()}. +-type sasl() :: encrypted_sasl() | decrypted_sasl(). -type connection_config() :: #{container_id => binary(), % AMQP container id @@ -72,9 +71,7 @@ % set to a negative value to allow a sender to "overshoot" the flow % control by this margin transfer_limit_margin => 0 | neg_integer(), - %% These credentials_obfuscation-wrapped values have the type of - %% decrypted_sasl/0 - sasl => encrypted_sasl() | decrypted_sasl(), + sasl => sasl(), properties => amqp10_client_types:properties() }. @@ -92,16 +89,15 @@ }). -export_type([connection_config/0, - amqp10_socket/0, - encrypted_sasl/0, - decrypted_sasl/0]). + amqp10_socket/0]). %% ------------------------------------------------------------------- %% Public API. %% ------------------------------------------------------------------- -spec open(connection_config()) -> supervisor:startchild_ret(). -open(Config) -> +open(Config0) -> + Config = maps:update_with(sasl, fun maybe_encrypt_sasl/1, Config0), %% Start the supervision tree dedicated to that connection. It %% starts at least a connection process (the PID we want to return) %% and a reader process (responsible for opening and reading the @@ -127,17 +123,23 @@ open(Config) -> close(Pid, Reason) -> gen_statem:cast(Pid, {close, Reason}). --spec encrypt_sasl(decrypted_sasl()) -> encrypted_sasl(). -encrypt_sasl(none) -> - credentials_obfuscation:encrypt(none); -encrypt_sasl(DecryptedSasl) -> - credentials_obfuscation:encrypt(term_to_binary(DecryptedSasl)). - --spec decrypt_sasl(encrypted_sasl()) -> decrypted_sasl(). -decrypt_sasl(none) -> - credentials_obfuscation:decrypt(none); -decrypt_sasl(EncryptedSasl) -> - binary_to_term(credentials_obfuscation:decrypt(EncryptedSasl)). +-spec maybe_encrypt_sasl(decrypted_sasl()) -> sasl(). +maybe_encrypt_sasl(Sasl) + when Sasl =:= none orelse + Sasl =:= anon orelse + Sasl =:= external -> + Sasl; +maybe_encrypt_sasl(Plain = {plain, _User, _Passwd}) -> + credentials_obfuscation:encrypt(term_to_binary(Plain)). + +-spec maybe_decrypt_sasl(sasl()) -> decrypted_sasl(). +maybe_decrypt_sasl(Sasl) + when Sasl =:= none orelse + Sasl =:= anon orelse + Sasl =:= external -> + Sasl; +maybe_decrypt_sasl(Encrypted) -> + binary_to_term(credentials_obfuscation:decrypt(Encrypted)). %% ------------------------------------------------------------------- %% Private API. @@ -207,13 +209,11 @@ sasl_hdr_sent({call, From}, begin_session, {keep_state, State1}. sasl_hdr_rcvds(_EvtType, #'v1_0.sasl_mechanisms'{ - sasl_server_mechanisms = {array, symbol, Mechs}}, - State = #state{config = #{sasl := EncryptedSasl}}) -> - DecryptedSasl = decrypt_sasl(EncryptedSasl), - SaslBin = {symbol, decrypted_sasl_to_bin(DecryptedSasl)}, - case lists:any(fun(S) when S =:= SaslBin -> true; - (_) -> false - end, Mechs) of + sasl_server_mechanisms = {array, symbol, AvailableMechs}}, + State = #state{config = #{sasl := Sasl}}) -> + DecryptedSasl = maybe_decrypt_sasl(Sasl), + OurMech = {symbol, decrypted_sasl_to_mechanism(DecryptedSasl)}, + case lists:member(OurMech, AvailableMechs) of true -> ok = send_sasl_init(State, DecryptedSasl), {next_state, sasl_init_sent, State}; @@ -454,6 +454,15 @@ send_close(#state{socket = Socket}, _Reason) -> send_sasl_init(State, anon) -> Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"ANONYMOUS">>}}, send(Frame, 1, State); +send_sasl_init(State, external) -> + Frame = #'v1_0.sasl_init'{ + mechanism = {symbol, <<"EXTERNAL">>}, + %% "This response is empty when the client is requesting to act + %% as the identity the server associated with its authentication + %% credentials." + %% https://datatracker.ietf.org/doc/html/rfc4422#appendix-A.1 + initial_response = {binary, <<>>}}, + send(Frame, 1, State); send_sasl_init(State, {plain, User, Pass}) -> Response = <<0:8, User/binary, 0:8, Pass/binary>>, Frame = #'v1_0.sasl_init'{mechanism = {symbol, <<"PLAIN">>}, @@ -546,9 +555,12 @@ translate_err(#'v1_0.error'{condition = Cond, description = Desc}) -> amqp10_event(Evt) -> {amqp10_event, {connection, self(), Evt}}. -decrypted_sasl_to_bin({plain, _, _}) -> <<"PLAIN">>; -decrypted_sasl_to_bin(anon) -> <<"ANONYMOUS">>; -decrypted_sasl_to_bin(none) -> <<"ANONYMOUS">>. +decrypted_sasl_to_mechanism(anon) -> + <<"ANONYMOUS">>; +decrypted_sasl_to_mechanism(external) -> + <<"EXTERNAL">>; +decrypted_sasl_to_mechanism({plain, _, _}) -> + <<"PLAIN">>. config_defaults() -> #{sasl => none, diff --git a/deps/rabbitmq_auth_mechanism_ssl/BUILD.bazel b/deps/rabbitmq_auth_mechanism_ssl/BUILD.bazel index 778774f9e63b..6127cccd64ec 100644 --- a/deps/rabbitmq_auth_mechanism_ssl/BUILD.bazel +++ b/deps/rabbitmq_auth_mechanism_ssl/BUILD.bazel @@ -1,17 +1,22 @@ +load("@rules_erlang//:eunit2.bzl", "eunit") load("@rules_erlang//:xref2.bzl", "xref") load("@rules_erlang//:dialyze.bzl", "dialyze", "plt") +load("//:rabbitmq_home.bzl", "rabbitmq_home") +load("//:rabbitmq_run.bzl", "rabbitmq_run") load( "//:rabbitmq.bzl", "BROKER_VERSION_REQUIREMENTS_ANY", "RABBITMQ_DIALYZER_OPTS", "assert_suites", "rabbitmq_app", + "rabbitmq_integration_suite", ) load( ":app.bzl", "all_beam_files", "all_srcs", "all_test_beam_files", + "test_suite_beam_files", ) APP_NAME = "rabbitmq_auth_mechanism_ssl" @@ -26,7 +31,7 @@ APP_ENV = """[ all_beam_files(name = "all_beam_files") -all_test_beam_files() +all_test_beam_files(name = "all_test_beam_files") all_srcs(name = "all_srcs") @@ -70,6 +75,28 @@ dialyze( target = ":erlang_app", ) +rabbitmq_home( + name = "broker-for-tests-home", + testonly = True, + plugins = [ + ":test_erlang_app", + ], +) + +rabbitmq_run( + name = "rabbitmq-for-tests-run", + testonly = True, + home = ":broker-for-tests-home", +) + +rabbitmq_integration_suite( + name = "system_SUITE", + shard_count = 1, + runtime_deps = [ + "//deps/amqp10_client:erlang_app", + ], +) + assert_suites() alias( @@ -77,3 +104,10 @@ alias( actual = ":erlang_app", visibility = ["//visibility:public"], ) + +test_suite_beam_files(name = "test_suite_beam_files") + +eunit( + name = "eunit", + target = ":test_erlang_app", +) diff --git a/deps/rabbitmq_auth_mechanism_ssl/Makefile b/deps/rabbitmq_auth_mechanism_ssl/Makefile index 9b540fdaf716..f6705d7c3a6a 100644 --- a/deps/rabbitmq_auth_mechanism_ssl/Makefile +++ b/deps/rabbitmq_auth_mechanism_ssl/Makefile @@ -14,6 +14,7 @@ endef LOCAL_DEPS = public_key DEPS = rabbit_common rabbit +TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp10_client DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk diff --git a/deps/rabbitmq_auth_mechanism_ssl/app.bzl b/deps/rabbitmq_auth_mechanism_ssl/app.bzl index 6a95279a2cff..335857be922e 100644 --- a/deps/rabbitmq_auth_mechanism_ssl/app.bzl +++ b/deps/rabbitmq_auth_mechanism_ssl/app.bzl @@ -75,4 +75,11 @@ def all_test_beam_files(name = "all_test_beam_files"): ) def test_suite_beam_files(name = "test_suite_beam_files"): - pass + erlang_bytecode( + name = "system_SUITE_beam_files", + testonly = True, + srcs = ["test/system_SUITE.erl"], + outs = ["test/system_SUITE.beam"], + app_name = "rabbitmq_auth_mechanism_ssl", + erlc_opts = "//:test_erlc_opts", + ) diff --git a/deps/rabbitmq_auth_mechanism_ssl/src/rabbit_auth_mechanism_ssl.erl b/deps/rabbitmq_auth_mechanism_ssl/src/rabbit_auth_mechanism_ssl.erl index 6fc78d9bdeb3..11a7e79ee700 100644 --- a/deps/rabbitmq_auth_mechanism_ssl/src/rabbit_auth_mechanism_ssl.erl +++ b/deps/rabbitmq_auth_mechanism_ssl/src/rabbit_auth_mechanism_ssl.erl @@ -23,7 +23,9 @@ {cleanup, {rabbit_registry, unregister, [auth_mechanism, <<"EXTERNAL">>]}}]}). --record(state, {username = undefined}). +-record(state, { + username = undefined :: undefined | rabbit_types:username() | {refused, none, string(), [term()]} + }). description() -> [{description, <<"TLS peer verification-based authentication plugin. Used in combination with the EXTERNAL SASL mechanism.">>}]. diff --git a/deps/rabbitmq_auth_mechanism_ssl/test/system_SUITE.erl b/deps/rabbitmq_auth_mechanism_ssl/test/system_SUITE.erl new file mode 100644 index 000000000000..b5f1a5696110 --- /dev/null +++ b/deps/rabbitmq_auth_mechanism_ssl/test/system_SUITE.erl @@ -0,0 +1,104 @@ +%% 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-2023 VMware, Inc. or its affiliates. All rights reserved. + +-module(system_SUITE). + +-compile([export_all, + nowarn_export_all]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [{group, tests}]. + +groups() -> + [ + {tests, [shuffle], + [amqp] + } + ]. + +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(amqp10_client), + rabbit_ct_helpers:log_environment(), + Config. + +end_per_suite(Config) -> + Config. + +init_per_group(_Group, Config0) -> + %% Command `deps/rabbitmq_ct_helpers/tools/tls-certs$ make` + %% will put our hostname as common name in the client cert. + Config1 = rabbit_ct_helpers:merge_app_env( + Config0, + {rabbit, + [ + {auth_mechanisms, ['EXTERNAL']}, + {ssl_cert_login_from, common_name} + ]}), + Config = rabbit_ct_helpers:run_setup_steps( + Config1, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()), + {ok, UserString} = inet:gethostname(), + User = unicode:characters_to_binary(UserString), + ok = rabbit_ct_broker_helpers:add_user(Config, User), + Vhost = <<"test vhost">>, + ok = rabbit_ct_broker_helpers:add_vhost(Config, Vhost), + [{test_vhost, Vhost}, + {test_user, User}] ++ Config. + +end_per_group(_Group, Config) -> + ok = rabbit_ct_broker_helpers:delete_user(Config, ?config(test_user, Config)), + ok = rabbit_ct_broker_helpers:delete_vhost(Config, ?config(test_vhost, Config)), + rabbit_ct_helpers:run_teardown_steps( + Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_testcase(Testcase, Config) -> + ok = set_permissions(Config, <<>>, <<>>, <<"^some vhost permission">>), + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + ok = clear_permissions(Config), + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +amqp(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), + CACertFile = ?config(rmq_certsdir, Config) ++ "/testca/cacert.pem", + CertFile = ?config(rmq_certsdir, Config) ++ "/client/cert.pem", + KeyFile = ?config(rmq_certsdir, Config) ++ "/client/key.pem", + OpnConf = #{address => Host, + port => Port, + container_id => atom_to_binary(?FUNCTION_NAME), + hostname => <<"vhost:", Vhost/binary>>, + sasl => external, + tls_opts => {secure_port, [{cacertfile, CACertFile}, + {certfile, CertFile}, + {keyfile, KeyFile}]} + }, + {ok, Connection} = amqp10_client:open_connection(OpnConf), + receive {amqp10_event, {connection, Connection, opened}} -> ok + after 5000 -> ct:fail(missing_opened) + end, + ok = amqp10_client:close_connection(Connection). + +set_permissions(Config, ConfigurePerm, WritePerm, ReadPerm) -> + ok = rabbit_ct_broker_helpers:set_permissions(Config, + ?config(test_user, Config), + ?config(test_vhost, Config), + ConfigurePerm, + WritePerm, + ReadPerm). + +clear_permissions(Config) -> + User = ?config(test_user, Config), + Vhost = ?config(test_vhost, Config), + ok = rabbit_ct_broker_helpers:clear_permissions(Config, User, Vhost).