Skip to content

Commit 9127acb

Browse files
committed
Implement LDAP credentials validation via HTTP API
See discussion #14244 These changes will allow a user to make an HTTP API request to... ``` /api/ldap/validate/simple-bind ``` ...with an appropriate JSON body, and the plugin will attempt a connection to the specified LDAP server using the provided credentials. This allows validation that a connection can be made to an LDAP server from a RabbitMQ cluster environment. * Add code and tests for `eldap:simple_bind` validation. * Add support for testing TLS connections to OpenLDAP * Add support for validating TLS related configuration via `/ldap/validate/simple-bind` * Add support for various TLS options: * versions * depth * multiple CA cert pem data * Fall back to system certs if neither `cacertfile` nor `cacerts_pem_data` are provided to the `simple-bind` validation. * Add `ssl_hostname_verification` support.
1 parent a7827ba commit 9127acb

File tree

5 files changed

+358
-14
lines changed

5 files changed

+358
-14
lines changed

deps/rabbitmq_auth_backend_ldap/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ define PROJECT_APP_EXTRA_KEYS
3535
endef
3636

3737
LOCAL_DEPS = eldap public_key
38-
DEPS = rabbit_common rabbit
38+
DEPS = rabbit_common rabbit rabbitmq_management
3939
TEST_DEPS = ct_helper rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client
4040
dep_ct_helper = git https://github.com/extend/ct_helper.git master
4141

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
-module(rabbit_auth_backend_ldap_mgmt).
9+
10+
-behaviour(rabbit_mgmt_extension).
11+
12+
-export([dispatcher/0, web_ui/0]).
13+
14+
-export([init/2,
15+
content_types_accepted/2,
16+
allowed_methods/2,
17+
resource_exists/2,
18+
is_authorized/2,
19+
accept_content/2]).
20+
21+
22+
-include_lib("kernel/include/logger.hrl").
23+
-include_lib("rabbitmq_web_dispatch/include/rabbitmq_web_dispatch_records.hrl").
24+
25+
dispatcher() -> [{"/ldap/validate/simple-bind", ?MODULE, []}].
26+
27+
web_ui() -> [].
28+
29+
%%--------------------------------------------------------------------
30+
31+
init(Req, _Opts) ->
32+
{cowboy_rest, rabbit_mgmt_cors:set_headers(Req, ?MODULE), #context{}}.
33+
34+
content_types_accepted(ReqData, Context) ->
35+
{[{'*', accept_content}], ReqData, Context}.
36+
37+
allowed_methods(ReqData, Context) ->
38+
{[<<"HEAD">>, <<"PUT">>, <<"OPTIONS">>], ReqData, Context}.
39+
40+
resource_exists(ReqData, Context) ->
41+
{true, ReqData, Context}.
42+
43+
is_authorized(ReqData, Context) ->
44+
rabbit_mgmt_util:is_authorized(ReqData, Context).
45+
46+
accept_content(ReqData0, Context) ->
47+
F = fun (_Values, BodyMap, ReqData1) ->
48+
Port = rabbit_mgmt_util:parse_int(maps:get(port, BodyMap, 389)),
49+
UseSsl = rabbit_mgmt_util:parse_bool(maps:get(use_ssl, BodyMap, false)),
50+
UseStartTls = rabbit_mgmt_util:parse_bool(maps:get(use_starttls, BodyMap, false)),
51+
Servers = maps:get(servers, BodyMap, []),
52+
UserDN = maps:get(user_dn, BodyMap, <<"">>),
53+
Password = maps:get(password, BodyMap, <<"">>),
54+
Options0 = [
55+
{port, Port},
56+
{timeout, 5000}
57+
],
58+
try
59+
{ok, Options1} = maybe_add_ssl_options(Options0, UseSsl, BodyMap),
60+
case eldap:open(Servers, Options1) of
61+
{ok, LDAP} ->
62+
Result = case maybe_starttls(LDAP, UseStartTls, BodyMap) of
63+
ok ->
64+
case eldap:simple_bind(LDAP, UserDN, Password) of
65+
ok ->
66+
{true, ReqData1, Context};
67+
{error, invalidCredentials} ->
68+
rabbit_mgmt_util:not_authorised("invalid credentials", ReqData1, Context);
69+
{error, unwillingToPerform} ->
70+
rabbit_mgmt_util:not_authorised("invalid credentials", ReqData1, Context);
71+
{error, E} ->
72+
Reason = unicode_format(E),
73+
rabbit_mgmt_util:bad_request(Reason, ReqData1, Context)
74+
end;
75+
Error ->
76+
Reason = unicode_format(Error),
77+
rabbit_mgmt_util:bad_request(Reason, ReqData1, Context)
78+
end,
79+
eldap:close(LDAP),
80+
Result;
81+
{error, E} ->
82+
Reason = unicode_format(E),
83+
rabbit_mgmt_util:bad_request(Reason, ReqData1, Context)
84+
end
85+
catch throw:{bad_request, ErrMsg} ->
86+
rabbit_mgmt_util:bad_request(ErrMsg, ReqData1, Context)
87+
end
88+
end,
89+
rabbit_mgmt_util:with_decode([], ReqData0, Context, F).
90+
91+
%%--------------------------------------------------------------------
92+
93+
unicode_format(Arg) ->
94+
rabbit_data_coercion:to_utf8_binary(io_lib:format("~tp", [Arg])).
95+
96+
maybe_starttls(_LDAP, false, _BodyMap) ->
97+
ok;
98+
maybe_starttls(LDAP, true, BodyMap) ->
99+
{ok, TlsOptions} = tls_options(BodyMap),
100+
eldap:start_tls(LDAP, TlsOptions, 5000).
101+
102+
maybe_add_ssl_options(Options0, false, _BodyMap) ->
103+
{ok, Options0};
104+
maybe_add_ssl_options(Options0, true, BodyMap) ->
105+
case maps:is_key(ssl_options, BodyMap) of
106+
false ->
107+
{ok, Options0};
108+
true ->
109+
Options1 = [{ssl, true} | Options0],
110+
{ok, TlsOptions} = tls_options(BodyMap),
111+
Options2 = [{sslopts, TlsOptions} | Options1],
112+
{ok, Options2}
113+
end.
114+
115+
tls_options(BodyMap) when is_map_key(ssl_options, BodyMap) ->
116+
SslOptionsMap = maps:get(ssl_options, BodyMap),
117+
CaCertfile = maps:get(<<"cacertfile">>, SslOptionsMap, undefined),
118+
CaCertPemData = maps:get(<<"cacert_pem_data">>, SslOptionsMap, undefined),
119+
TlsOpts0 = case {CaCertfile, CaCertPemData} of
120+
{undefined, undefined} ->
121+
[{cacerts, public_key:cacerts_get()}];
122+
_ ->
123+
[]
124+
end,
125+
%% NB: for some reason the "cacertfile" key isn't turned into an atom
126+
TlsOpts1 = case CaCertfile of
127+
undefined ->
128+
TlsOpts0;
129+
CaCertfile ->
130+
[{cacertfile, CaCertfile} | TlsOpts0]
131+
end,
132+
TlsOpts2 = case CaCertPemData of
133+
undefined ->
134+
TlsOpts1;
135+
CaCertPems when is_list(CaCertPems) ->
136+
F0 = fun (P) ->
137+
case public_key:pem_decode(P) of
138+
[{'Certificate', CaCertDerEncoded, not_encrypted}] ->
139+
{true, CaCertDerEncoded};
140+
_Unexpected ->
141+
throw({bad_request, "unexpected cacert_pem_data passed to "
142+
"/ldap/validate/simple-bind ssl_options.cacerts"})
143+
end
144+
end,
145+
CaCertsDerEncoded = lists:filtermap(F0, CaCertPems),
146+
[{cacerts, CaCertsDerEncoded} | TlsOpts1];
147+
_ ->
148+
TlsOpts1
149+
end,
150+
TlsOpts3 = case maps:get(<<"verify">>, SslOptionsMap, undefined) of
151+
undefined ->
152+
TlsOpts2;
153+
Verify ->
154+
VerifyStr = unicode:characters_to_list(Verify),
155+
[{verify, list_to_existing_atom(VerifyStr)} | TlsOpts2]
156+
end,
157+
TlsOpts4 = case maps:get(<<"server_name_indication">>, SslOptionsMap, disable) of
158+
disable ->
159+
TlsOpts3;
160+
SniValue ->
161+
SniStr = unicode:characters_to_list(SniValue),
162+
[{server_name_indication, SniStr} | TlsOpts3]
163+
end,
164+
TlsOpts5 = case maps:get(<<"depth">>, SslOptionsMap, undefined) of
165+
undefined ->
166+
TlsOpts4;
167+
DepthValue ->
168+
Depth = rabbit_data_coercion:to_integer(DepthValue),
169+
[{depth, Depth} | TlsOpts4]
170+
end,
171+
TlsOpts6 = case maps:get(<<"versions">>, SslOptionsMap, undefined) of
172+
undefined ->
173+
TlsOpts5;
174+
VersionStrs when is_list(VersionStrs) ->
175+
F1 = fun (VStr) ->
176+
try
177+
{true, list_to_existing_atom(VStr)}
178+
catch error:badarg ->
179+
throw({bad_request, "invalid TLS version passed to "
180+
"/ldap/validate/simple-bind ssl_options.versions"})
181+
end
182+
end,
183+
Versions = lists:filtermap(F1, VersionStrs),
184+
[{versions, Versions} | TlsOpts5]
185+
end,
186+
TlsOpts7 = case maps:get(<<"ssl_hostname_verification">>, SslOptionsMap, undefined) of
187+
undefined ->
188+
TlsOpts6;
189+
"wildcard" ->
190+
[{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]} | TlsOpts6];
191+
_ ->
192+
throw({bad_request, "invalid value passed to "
193+
"/ldap/validate/simple-bind ssl_options.ssl_hostname_verification"})
194+
end,
195+
{ok, TlsOpts7};
196+
tls_options(_BodyMap) ->
197+
{ok, []}.

0 commit comments

Comments
 (0)