diff --git a/deps/rabbitmq_auth_backend_ldap/Makefile b/deps/rabbitmq_auth_backend_ldap/Makefile index 2224ea369a..987add9a87 100644 --- a/deps/rabbitmq_auth_backend_ldap/Makefile +++ b/deps/rabbitmq_auth_backend_ldap/Makefile @@ -35,7 +35,7 @@ define PROJECT_APP_EXTRA_KEYS endef LOCAL_DEPS = eldap public_key -DEPS = rabbit_common rabbit +DEPS = rabbit_common rabbit rabbitmq_management TEST_DEPS = ct_helper rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client dep_ct_helper = git https://github.com/extend/ct_helper.git master diff --git a/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap_mgmt.erl b/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap_mgmt.erl new file mode 100644 index 0000000000..13e11350fa --- /dev/null +++ b/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap_mgmt.erl @@ -0,0 +1,267 @@ +%% 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-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(rabbit_auth_backend_ldap_mgmt). + +-behaviour(rabbit_mgmt_extension). + +-export([dispatcher/0, web_ui/0]). + +-export([init/2, + content_types_accepted/2, + allowed_methods/2, + resource_exists/2, + is_authorized/2, + accept_content/2]). + + +-include_lib("kernel/include/logger.hrl"). +-include_lib("rabbitmq_web_dispatch/include/rabbitmq_web_dispatch_records.hrl"). + +dispatcher() -> [{"/ldap/validate/simple-bind", ?MODULE, []}]. + +web_ui() -> []. + +%%-------------------------------------------------------------------- + +init(Req, _Opts) -> + {cowboy_rest, rabbit_mgmt_cors:set_headers(Req, ?MODULE), #context{}}. + +content_types_accepted(ReqData, Context) -> + {[{'*', accept_content}], ReqData, Context}. + +allowed_methods(ReqData, Context) -> + {[<<"PUT">>, <<"OPTIONS">>], ReqData, Context}. + +resource_exists(ReqData, Context) -> + {true, ReqData, Context}. + +is_authorized(ReqData, Context) -> + rabbit_mgmt_util:is_authorized(ReqData, Context). + +accept_content(ReqData0, Context) -> + F = fun (_Values, BodyMap, ReqData1) -> + try + Port = safe_parse_int(maps:get(port, BodyMap, 389), "port"), + UseSsl = safe_parse_bool(maps:get(use_ssl, BodyMap, false), "use_ssl"), + UseStartTls = safe_parse_bool(maps:get(use_starttls, BodyMap, false), "use_starttls"), + Servers = maps:get(servers, BodyMap, []), + UserDN = maps:get(user_dn, BodyMap, <<"">>), + Password = maps:get(password, BodyMap, <<"">>), + Options0 = [ + {port, Port}, + {timeout, 5000} + ], + {ok, Options1} = maybe_add_ssl_options(Options0, UseSsl, BodyMap), + case eldap:open(Servers, Options1) of + {ok, LDAP} -> + Result = case maybe_starttls(LDAP, UseStartTls, BodyMap) of + ok -> + case eldap:simple_bind(LDAP, UserDN, Password) of + ok -> + {true, ReqData1, Context}; + {error, invalidCredentials} -> + rabbit_mgmt_util:unprocessable_entity("invalid LDAP credentials: " + "authentication failure", + ReqData1, Context); + {error, unwillingToPerform} -> + rabbit_mgmt_util:unprocessable_entity("invalid LDAP credentials: " + "authentication failure", + ReqData1, Context); + {error, invalidDNSyntax} -> + rabbit_mgmt_util:unprocessable_entity("invalid LDAP credentials: " + "DN syntax invalid / too long", + ReqData1, Context); + {error, E} -> + Reason = unicode_format(E), + rabbit_mgmt_util:unprocessable_entity(Reason, ReqData1, Context) + end; + {error, tls_already_started} -> + rabbit_mgmt_util:unprocessable_entity("TLS configuration error: " + "cannot use StartTLS on an SSL connection " + "(use_ssl and use_starttls cannot both be true)", + ReqData1, Context); + Error -> + Reason = unicode_format(Error), + rabbit_mgmt_util:unprocessable_entity(Reason, ReqData1, Context) + end, + eldap:close(LDAP), + Result; + {error, E} -> + Reason = unicode_format("LDAP connection failed: ~tp " + "(servers: ~tp, user_dn: ~ts, password: ~s)", + [E, Servers, UserDN, format_password_for_logging(Password)]), + rabbit_mgmt_util:bad_request(Reason, ReqData1, Context) + end + catch throw:{bad_request, ErrMsg} -> + rabbit_mgmt_util:bad_request(ErrMsg, ReqData1, Context) + end + end, + rabbit_mgmt_util:with_decode([], ReqData0, Context, F). + +%%-------------------------------------------------------------------- + +maybe_starttls(_LDAP, false, _BodyMap) -> + ok; +maybe_starttls(LDAP, true, BodyMap) -> + {ok, TlsOptions} = tls_options(BodyMap), + eldap:start_tls(LDAP, TlsOptions, 5000). + +maybe_add_ssl_options(Options0, false, _BodyMap) -> + {ok, Options0}; +maybe_add_ssl_options(Options0, true, BodyMap) -> + case maps:is_key(ssl_options, BodyMap) of + false -> + {ok, Options0}; + true -> + Options1 = [{ssl, true} | Options0], + {ok, TlsOptions} = tls_options(BodyMap), + Options2 = [{sslopts, TlsOptions} | Options1], + {ok, Options2} + end. + +tls_options(BodyMap) when is_map_key(ssl_options, BodyMap) -> + SslOptionsMap = maps:get(ssl_options, BodyMap), + case is_map(SslOptionsMap) of + false -> + throw({bad_request, "ssl_options must be a map/object"}); + true -> + ok + end, + CaCertfile = maps:get(<<"cacertfile">>, SslOptionsMap, undefined), + CaCertPemData = maps:get(<<"cacert_pem_data">>, SslOptionsMap, undefined), + TlsOpts0 = case {CaCertfile, CaCertPemData} of + {undefined, undefined} -> + [{cacerts, public_key:cacerts_get()}]; + _ -> + [] + end, + %% NB: for some reason the "cacertfile" key isn't turned into an atom + TlsOpts1 = case CaCertfile of + undefined -> + TlsOpts0; + CaCertfile -> + [{cacertfile, CaCertfile} | TlsOpts0] + end, + TlsOpts2 = case CaCertPemData of + undefined -> + TlsOpts1; + CaCertPems when is_list(CaCertPems) -> + F0 = fun (P) -> + try + case public_key:pem_decode(P) of + [{'Certificate', CaCertDerEncoded, not_encrypted}] -> + {true, CaCertDerEncoded}; + [] -> + throw({bad_request, "invalid PEM data in cacert_pem_data: " + "no valid certificates found"}); + _Unexpected -> + throw({bad_request, "unexpected cacert_pem_data passed to " + "/ldap/validate/simple-bind ssl_options.cacerts"}) + end + catch + error:Reason -> + throw({bad_request, unicode_format("invalid PEM data in cacert_pem_data: ~tp", [Reason])}) + end + end, + CaCertsDerEncoded = lists:filtermap(F0, CaCertPems), + [{cacerts, CaCertsDerEncoded} | TlsOpts1]; + _ -> + TlsOpts1 + end, + TlsOpts3 = case maps:get(<<"verify">>, SslOptionsMap, undefined) of + undefined -> + TlsOpts2; + Verify -> + try + VerifyStr = unicode:characters_to_list(Verify), + [{verify, list_to_existing_atom(VerifyStr)} | TlsOpts2] + catch + error:badarg -> + throw({bad_request, "invalid verify option passed to " + "/ldap/validate/simple-bind ssl_options.verify"}) + end + end, + TlsOpts4 = case maps:get(<<"server_name_indication">>, SslOptionsMap, disable) of + disable -> + TlsOpts3; + SniValue -> + try + SniStr = unicode:characters_to_list(SniValue), + [{server_name_indication, SniStr} | TlsOpts3] + catch + error:badarg -> + throw({bad_request, "invalid server_name_indication: expected string"}); + error:_ -> + throw({bad_request, "invalid server_name_indication: expected string"}) + end + end, + TlsOpts5 = case maps:get(<<"depth">>, SslOptionsMap, undefined) of + undefined -> + TlsOpts4; + DepthValue -> + Depth = safe_parse_int(DepthValue, "ssl_options.depth"), + [{depth, Depth} | TlsOpts4] + end, + TlsOpts6 = case maps:get(<<"versions">>, SslOptionsMap, undefined) of + undefined -> + TlsOpts5; + VersionStrs when is_list(VersionStrs) -> + F1 = fun (VStr) -> + try + {true, list_to_existing_atom(VStr)} + catch error:badarg -> + throw({bad_request, "invalid TLS version passed to " + "/ldap/validate/simple-bind ssl_options.versions"}) + end + end, + Versions = lists:filtermap(F1, VersionStrs), + [{versions, Versions} | TlsOpts5] + end, + TlsOpts7 = case maps:get(<<"ssl_hostname_verification">>, SslOptionsMap, undefined) of + undefined -> + TlsOpts6; + "wildcard" -> + [{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]} | TlsOpts6]; + _ -> + throw({bad_request, "invalid value passed to " + "/ldap/validate/simple-bind ssl_options.ssl_hostname_verification"}) + end, + {ok, TlsOpts7}; +tls_options(_BodyMap) -> + {ok, []}. + +unicode_format(Arg) -> + rabbit_data_coercion:to_utf8_binary(io_lib:format("~tp", [Arg])). + +unicode_format(Format, Args) -> + rabbit_data_coercion:to_utf8_binary(io_lib:format(Format, Args)). + +format_password_for_logging(<<>>) -> + "[empty]"; +format_password_for_logging(Password) -> + io_lib:format("[~p characters]", [string:length(Password)]). + +safe_parse_int(Value, FieldName) -> + try + rabbit_mgmt_util:parse_int(Value) + catch + throw:{error, {not_integer, BadValue}} -> + Msg = unicode_format("invalid value for ~s: expected integer, got ~tp", + [FieldName, BadValue]), + throw({bad_request, Msg}) + end. + +safe_parse_bool(Value, FieldName) -> + try + rabbit_mgmt_util:parse_bool(Value) + catch + throw:{error, {not_boolean, BadValue}} -> + Msg = unicode_format("invalid value for ~s: expected boolean, got ~tp", + [FieldName, BadValue]), + throw({bad_request, Msg}) + end. diff --git a/deps/rabbitmq_auth_backend_ldap/test/system_SUITE.erl b/deps/rabbitmq_auth_backend_ldap/test/system_SUITE.erl index 0a571ede5e..9ac4dd104d 100644 --- a/deps/rabbitmq_auth_backend_ldap/test/system_SUITE.erl +++ b/deps/rabbitmq_auth_backend_ldap/test/system_SUITE.erl @@ -11,6 +11,9 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + +-import(rabbit_mgmt_test_util, [http_put/4]). -define(ALICE_NAME, "Alice"). -define(BOB_NAME, "Bob"). @@ -97,6 +100,7 @@ all() -> groups() -> Tests = [ + validate_ldap_configuration_via_api, purge_connection, ldap_only, ldap_and_internal, @@ -158,10 +162,23 @@ end_per_group(_, Config) -> init_slapd(Config) -> DataDir = ?config(data_dir, Config), PrivDir = ?config(priv_dir, Config), + CertsDir = ?config(rmq_certsdir, Config), + CaCertfile = filename:join([CertsDir, "testca", "cacert.pem"]), + ServerCertfile = filename:join([CertsDir, "server", "cert.pem"]), + ServerKeyfile = filename:join([CertsDir, "server", "key.pem"]), TcpPort = 25389, + TlsPort = 25689, SlapdDir = filename:join([PrivDir, "openldap"]), InitSlapd = filename:join([DataDir, "init-slapd.sh"]), - Cmd = [InitSlapd, SlapdDir, {"~b", [TcpPort]}], + Cmd = [ + InitSlapd, + SlapdDir, + {"~b", [TcpPort]}, + {"~b", [TlsPort]}, + CaCertfile, + ServerCertfile, + ServerKeyfile + ], case rabbit_ct_helpers:exec(Cmd) of {ok, Stdout} -> {match, [SlapdPid]} = re:run( @@ -174,7 +191,8 @@ init_slapd(Config) -> [SlapdPid, TcpPort]), rabbit_ct_helpers:set_config(Config, [{slapd_pid, SlapdPid}, - {ldap_port, TcpPort}]); + {ldap_port, TcpPort}, + {ldap_tls_port, TlsPort}]); _ -> _ = rabbit_ct_helpers:exec(["pkill", "-INT", "slapd"]), {skip, "Failed to initialize slapd(8)"} @@ -206,6 +224,10 @@ end_internal(Config) -> ok = control_action(Config, delete_user, [?BOB_NAME]), ok = control_action(Config, delete_user, [?PETER_NAME]). + +init_per_testcase(validate_ldap_configuration_via_api = Testcase, Config) -> + _ = application:start(inets), + rabbit_ct_helpers:testcase_started(Config, Testcase); init_per_testcase(Testcase, Config) when Testcase == ldap_and_internal; Testcase == internal_followed_ldap_and_internal -> @@ -229,6 +251,9 @@ init_per_testcase(Testcase, Config) init_per_testcase(Testcase, Config) -> rabbit_ct_helpers:testcase_started(Config, Testcase). +end_per_testcase(validate_ldap_configuration_via_api = Testcase, Config) -> + _ = application:stop(inets), + rabbit_ct_helpers:testcase_finished(Config, Testcase); end_per_testcase(Testcase, Config) when Testcase == ldap_and_internal; Testcase == internal_followed_ldap_and_internal -> @@ -270,6 +295,434 @@ end_per_testcase(Testcase, Config) -> %% Testsuite cases %% ------------------------------------------------------------------- +validate_ldap_configuration_via_api(Config) -> + CertsDir = ?config(rmq_certsdir, Config), + CaCertfile = filename:join([CertsDir, "testca", "cacert.pem"]), + + %% {user_dn_pattern, "cn=${username},ou=People,dc=rabbitmq,dc=com"}, + UserDNFmt = "cn=~ts,ou=People,dc=rabbitmq,dc=com", + AliceUserDN = rabbit_data_coercion:to_utf8_binary(io_lib:format(UserDNFmt, [?ALICE_NAME])), + InvalidUserDN = rabbit_data_coercion:to_utf8_binary(io_lib:format(UserDNFmt, ["NOBODY"])), + Password = rabbit_data_coercion:to_utf8_binary("password"), + + LdapPort = ?config(ldap_port, Config), + LdapTlsPort = ?config(ldap_tls_port, Config), + + %% NB: bad resource name + http_put(Config, "/ldap/validate/bad-bind-name", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + }, ?METHOD_NOT_ALLOWED), + %% Invalid JSON should return 400 Bad Request + rabbit_mgmt_test_util:http_put_raw(Config, "/ldap/validate/simple-bind", + "{invalid json", ?BAD_REQUEST), + + %% HTTP Method coverage tests + %% GET method - should return 405 Method Not Allowed + ?assertMatch({ok, {{_, ?METHOD_NOT_ALLOWED, _}, _Headers, _ResBody}}, + rabbit_mgmt_test_util:req(Config, 0, get, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")])), + + %% HEAD method - should return 405 Method Not Allowed (same as GET) + ?assertMatch({ok, {{_, ?METHOD_NOT_ALLOWED, _}, _Headers, _ResBody}}, + rabbit_mgmt_test_util:req(Config, 0, head, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")])), + + %% POST method - should return 405 Method Not Allowed + ?assertMatch({ok, {{_, ?METHOD_NOT_ALLOWED, _}, _Headers, _ResBody}}, + rabbit_mgmt_test_util:req(Config, 0, post, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], "{}")), + + %% DELETE method - should return 405 Method Not Allowed + ?assertMatch({ok, {{_, ?METHOD_NOT_ALLOWED, _}, _Headers, _ResBody}}, + rabbit_mgmt_test_util:req(Config, 0, delete, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")])), + + %% OPTIONS method - should return 200 with Allow header showing only PUT, OPTIONS + {ok, {{_, OptionsCode, _}, OptionsHeaders, _OptionsResBody}} = + rabbit_mgmt_test_util:req(Config, 0, options, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")]), + ?assertEqual(?OK, OptionsCode), + AllowHeader = proplists:get_value("allow", OptionsHeaders), + ?assert(string:str(string:to_upper(AllowHeader), "PUT") > 0), + ?assert(string:str(string:to_upper(AllowHeader), "OPTIONS") > 0), + %% Should NOT contain GET or HEAD + ?assertEqual(0, string:str(string:to_upper(AllowHeader), "GET")), + ?assertEqual(0, string:str(string:to_upper(AllowHeader), "HEAD")), + + %% Missing required fields tests + %% Empty servers array - connection failure (400) + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => [], + 'port' => LdapPort + }, ?BAD_REQUEST), + + %% Missing servers field entirely - defaults to [], same as above (400) + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'port' => LdapPort + }, ?BAD_REQUEST), + + %% Missing user_dn field entirely - empty DN fails credential validation (422) + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + }, ?UNPROCESSABLE_ENTITY), + + %% Missing password field entirely - empty password fails credential validation (422) + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'servers' => ["localhost"], + 'port' => LdapPort + }, ?UNPROCESSABLE_ENTITY), + + %% Invalid field values tests + %% Invalid port - string instead of number + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => "not_a_number" + }, ?BAD_REQUEST), + + %% Invalid port - negative number + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => -1 + }, ?BAD_REQUEST), + + %% Invalid boolean - string instead of boolean + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort, + 'use_ssl' => "maybe" + }, ?BAD_REQUEST), + + %% Invalid servers - non-list value + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => "not_a_list", + 'port' => LdapPort + }, ?BAD_REQUEST), + + %% Network/Infrastructure scenarios + %% Non-existent server + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["nonexistent.example.com"], + 'port' => LdapPort + }, ?BAD_REQUEST), + + %% Invalid hostname format + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["not..a..valid..hostname"], + 'port' => LdapPort + }, ?BAD_REQUEST), + + %% Edge case credentials tests + %% Empty password - should be 422 (credential validation failure) + {ok, {{_, 422, _}, _Headers1, EmptyPasswordBody}} = + rabbit_mgmt_test_util:req(Config, 0, put, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], + rabbit_mgmt_test_util:format_for_upload(#{ + 'user_dn' => AliceUserDN, + 'password' => "", + 'servers' => ["localhost"], + 'port' => LdapPort + })), + EmptyPasswordJson = rabbit_json:decode(EmptyPasswordBody), + ?assertEqual(<<"unprocessable_entity">>, maps:get(<<"error">>, EmptyPasswordJson)), + ?assertEqual(<<"anonymous_auth">>, maps:get(<<"reason">>, EmptyPasswordJson)), + + %% Empty user DN - should be 422 (credential validation failure) + {ok, {{_, 422, _}, _Headers2, EmptyUserDnBody}} = + rabbit_mgmt_test_util:req(Config, 0, put, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], + rabbit_mgmt_test_util:format_for_upload(#{ + 'user_dn' => "", + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + })), + EmptyUserDnJson = rabbit_json:decode(EmptyUserDnBody), + ?assertEqual(<<"unprocessable_entity">>, maps:get(<<"error">>, EmptyUserDnJson)), + ?assertEqual(<<"anonymous_auth">>, maps:get(<<"reason">>, EmptyUserDnJson)), + + %% Very long user DN (test size limits) + {ok, {{_, 422, _}, _Headers3, LongUserDnBody}} = + rabbit_mgmt_test_util:req(Config, 0, put, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], + rabbit_mgmt_test_util:format_for_upload(#{ + 'user_dn' => binary:copy(<<"x">>, 10000), + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + })), + LongUserDnJson = rabbit_json:decode(LongUserDnBody), + ?assertEqual(<<"unprocessable_entity">>, maps:get(<<"error">>, LongUserDnJson)), + ?assertEqual(<<"invalid LDAP credentials: DN syntax invalid / too long">>, + maps:get(<<"reason">>, LongUserDnJson)), + + %% Very long password (test size limits) + {ok, {{_, 422, _}, _Headers4, LongPasswordBody}} = + rabbit_mgmt_test_util:req(Config, 0, put, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], + rabbit_mgmt_test_util:format_for_upload(#{ + 'user_dn' => AliceUserDN, + 'password' => binary:copy(<<"x">>, 10000), + 'servers' => ["localhost"], + 'port' => LdapPort + })), + LongPasswordJson = rabbit_json:decode(LongPasswordBody), + ?assertEqual(<<"unprocessable_entity">>, maps:get(<<"error">>, LongPasswordJson)), + ?assertEqual(<<"invalid LDAP credentials: authentication failure">>, + maps:get(<<"reason">>, LongPasswordJson)), + + %% SSL/TLS Edge Cases + %% Both use_ssl and use_starttls set to true - TLS configuration error + {ok, {{_, 422, _}, _Headers5, BothTlsBody}} = + rabbit_mgmt_test_util:req(Config, 0, put, "/ldap/validate/simple-bind", + [rabbit_mgmt_test_util:auth_header("guest", "guest")], + rabbit_mgmt_test_util:format_for_upload(#{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'use_starttls' => true, + 'ssl_options' => #{ + 'cacertfile' => CaCertfile + } + })), + BothTlsJson = rabbit_json:decode(BothTlsBody), + ?assertEqual(<<"unprocessable_entity">>, maps:get(<<"error">>, BothTlsJson)), + ?assertEqual(<<"TLS configuration error: cannot use StartTLS on an SSL connection (use_ssl and use_starttls cannot both be true)">>, + maps:get(<<"reason">>, BothTlsJson)), + + %% Invalid certificate file path + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'cacertfile' => "/nonexistent/path/cert.pem" + } + }, ?BAD_REQUEST), + + %% Invalid PEM data - should now return 400 Bad Request + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'cacert_pem_data' => ["not-valid-pem-data"] + } + }, ?BAD_REQUEST), + + %% Invalid SSL options structure - not a map + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => "not_a_map" + }, ?BAD_REQUEST), + + %% Invalid TLS versions + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'versions' => ["invalid_version", "tlsv1.2"], + 'cacertfile' => CaCertfile + } + }, ?BAD_REQUEST), + + %% Invalid verify option + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'verify' => "invalid_verify_option", + 'cacertfile' => CaCertfile + } + }, ?BAD_REQUEST), + + %% Invalid depth value - string instead of integer + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'depth' => "not_a_number", + 'cacertfile' => CaCertfile + } + }, ?BAD_REQUEST), + + %% Invalid server_name_indication - integer instead of string + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'server_name_indication' => 123, + 'cacertfile' => CaCertfile + } + }, ?BAD_REQUEST), + + %% Invalid server_name_indication - boolean instead of string + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'server_name_indication' => true, + 'cacertfile' => CaCertfile + } + }, ?BAD_REQUEST), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + }, ?NO_CONTENT), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => InvalidUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort + }, ?UNPROCESSABLE_ENTITY), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'cacertfile' => CaCertfile + } + }, ?NO_CONTENT), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'server_name_indication' => "localhost", + 'cacertfile' => CaCertfile + } + }, ?NO_CONTENT), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapPort, + 'use_ssl' => false, + 'use_starttls' => true, + 'ssl_options' => #{ + 'server_name_indication' => "localhost", + 'cacertfile' => CaCertfile + } + }, ?NO_CONTENT), + {ok, CaCertfileContent} = file:read_file(CaCertfile), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'versions' => ["tlsv1.2", "tlsv1.3"], + 'depth' => 8, + 'verify' => "verify_peer", + 'cacert_pem_data' => [CaCertfileContent] + } + }, ?NO_CONTENT), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'versions' => ["tlsfoobar", "tlsv1.3"], + 'depth' => 8, + 'verify' => "verify_peer", + 'cacert_pem_data' => [CaCertfileContent, CaCertfileContent] + } + }, ?BAD_REQUEST), + http_put(Config, "/ldap/validate/simple-bind", + #{ + 'user_dn' => AliceUserDN, + 'password' => Password, + 'servers' => ["localhost"], + 'port' => LdapTlsPort, + 'use_ssl' => true, + 'ssl_options' => #{ + 'verify' => "verify_peer", + 'cacertfile' => CaCertfile, + 'ssl_hostname_verification' => "wildcard" + } + }, ?NO_CONTENT). + purge_connection(Config) -> {ok, _} = rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_ldap, diff --git a/deps/rabbitmq_auth_backend_ldap/test/system_SUITE_data/init-slapd.sh b/deps/rabbitmq_auth_backend_ldap/test/system_SUITE_data/init-slapd.sh index 2a9f9d3d48..43b9aa503e 100755 --- a/deps/rabbitmq_auth_backend_ldap/test/system_SUITE_data/init-slapd.sh +++ b/deps/rabbitmq_auth_backend_ldap/test/system_SUITE_data/init-slapd.sh @@ -1,13 +1,18 @@ #!/bin/sh # vim:sw=4:et: -set -ex +set -eux readonly slapd_data_dir="$1" readonly tcp_port="$2" +readonly tls_port="$3" +readonly cacertfile="$4" +readonly server_certfile="$5" +readonly server_keyfile="$6" readonly pidfile="$slapd_data_dir/slapd.pid" -readonly uri="ldap://localhost:$tcp_port" +readonly tcp_uri="ldap://localhost:$tcp_port" +readonly tls_uri="ldaps://localhost:$tls_port" readonly binddn="cn=config" readonly passwd=secret @@ -68,6 +73,10 @@ loglevel 7 database config rootdn "$binddn" rootpw $passwd + +TLSCACertificateFile $cacertfile +TLSCertificateFile $server_certfile +TLSCertificateKeyFile $server_keyfile EOF cat "$conf_file" @@ -79,7 +88,7 @@ mkdir -p "$conf_dir" "$slapd" \ -f "$conf_file" \ -F "$conf_dir" \ - -h "$uri" + -h "$tcp_uri $tls_uri" readonly auth="-x -D $binddn -w $passwd" @@ -87,7 +96,7 @@ readonly auth="-x -D $binddn -w $passwd" # shellcheck disable=SC2034 for seconds in 1 2 3 4 5 6 7 8 9 10; do # shellcheck disable=SC2086 - ldapsearch $auth -H "$uri" -LLL -b cn=config dn && break; + ldapsearch $auth -H "$tcp_uri" -LLL -b cn=config dn && break; sleep 1 done @@ -106,22 +115,22 @@ mkdir -p "$example_data_dir" # shellcheck disable=SC2086 sed -E -e "s,^olcDbDirectory:.*,olcDbDirectory: $example_data_dir," \ < "$example_ldif_dir/global.ldif" | \ - ldapadd $auth -H "$uri" + ldapadd $auth -H "$tcp_uri" # We remove the module path from the example LDIF as it was already # configured. # shellcheck disable=SC2086 sed -E -e "s,^olcModulePath:.*,olcModulePath: $modulepath," \ < "$example_ldif_dir/memberof_init.ldif" | \ - ldapadd $auth -H "$uri" + ldapadd $auth -H "$tcp_uri" # shellcheck disable=SC2086 -ldapmodify $auth -H "$uri" -f "$example_ldif_dir/refint_1.ldif" +ldapmodify $auth -H "$tcp_uri" -f "$example_ldif_dir/refint_1.ldif" # shellcheck disable=SC2086 -ldapadd $auth -H "$uri" -f "$example_ldif_dir/refint_2.ldif" +ldapadd $auth -H "$tcp_uri" -f "$example_ldif_dir/refint_2.ldif" # shellcheck disable=SC2086 -ldapsearch $auth -H "$uri" -LLL -b cn=config dn +ldapsearch $auth -H "$tcp_uri" -LLL -b cn=config dn echo SLAPD_PID="$(cat "$pidfile")" diff --git a/deps/rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl b/deps/rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl index 857cc89467..88565b0781 100644 --- a/deps/rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl +++ b/deps/rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl @@ -8,6 +8,7 @@ -define(BAD_REQUEST, 400). -define(NOT_AUTHORISED, 401). -define(METHOD_NOT_ALLOWED, 405). +-define(UNPROCESSABLE_ENTITY, 422). %%-define(NOT_FOUND, 404). Defined for AMQP by amqp_client.hrl (as 404) %% httpc seems to get racy when using HTTP 1.1 -define(HTTPC_OPTS, [{version, "HTTP/1.0"}, {autoredirect, false}]). diff --git a/deps/rabbitmq_management/src/rabbit_mgmt_util.erl b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl index 54fef24144..7448b0cc74 100644 --- a/deps/rabbitmq_management/src/rabbit_mgmt_util.erl +++ b/deps/rabbitmq_management/src/rabbit_mgmt_util.erl @@ -18,8 +18,9 @@ is_authorized_vhost_visible_for_monitoring/2, is_authorized_global_parameters/2]). -export([user/1]). --export([bad_request/3, service_unavailable/3, bad_request_exception/4, +-export([bad_request/3, service_unavailable/3, not_authorised/3, bad_request_exception/4, internal_server_error/3, internal_server_error/4, precondition_failed/3, + unprocessable_entity/3, id/2, parse_bool/1, parse_int/1, redirect_to_home/3]). -export([with_decode/4, not_found/3]). -export([with_channel/4, with_channel/5]). @@ -675,10 +676,12 @@ a2b(B) -> B. bad_request(Reason, ReqData, Context) -> halt_response(400, bad_request, Reason, ReqData, Context). +unprocessable_entity(Reason, ReqData, Context) -> + halt_response(422, unprocessable_entity, Reason, ReqData, Context). + service_unavailable(Reason, ReqData, Context) -> halt_response(503, service_unavailable, Reason, ReqData, Context). - not_authorised(Reason, ReqData, Context) -> rabbit_web_dispatch_access_control:not_authorised(Reason, ReqData, Context).