Skip to content

Commit 6162328

Browse files
Merge pull request #838 from rabbitmq/auth-attempt-metrics
Query and rest auth attempt metrics
2 parents 6bd8fe0 + 5377d5e commit 6162328

File tree

5 files changed

+180
-6
lines changed

5 files changed

+180
-6
lines changed

deps/rabbitmq_management/priv/www/api/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,26 @@ <h2>Reference</h2>
10571057
Provides status for all federation links. Requires the <code>rabbitmq_federation_management</code> plugin to be enabled.
10581058
</td>
10591059
</tr>
1060+
<tr>
1061+
<td>X</td>
1062+
<td></td>
1063+
<td>X</td>
1064+
<td></td>
1065+
<td class="path">/api/auth/attempts/<i>node</i></td>
1066+
<td>
1067+
A list of authentication attempts.
1068+
</td>
1069+
</tr>
1070+
<tr>
1071+
<td>X</td>
1072+
<td></td>
1073+
<td>X</td>
1074+
<td></td>
1075+
<td class="path">/api/auth/attempts/<i>node</i>/source</td>
1076+
<td>
1077+
A list of authentication attempts by remote address and username.
1078+
</td>
1079+
</tr>
10601080
</table>
10611081

10621082

deps/rabbitmq_management/src/rabbit_mgmt_dispatcher.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,7 @@ dispatcher() ->
179179
{"/reset/:node", rabbit_mgmt_wm_reset, []},
180180
{"/rebalance/queues", rabbit_mgmt_wm_rebalance_queues, [{queues, all}]},
181181
{"/auth", rabbit_mgmt_wm_auth, []},
182+
{"/auth/attempts/:node", rabbit_mgmt_wm_auth_attempts, [all]},
183+
{"/auth/attempts/:node/source", rabbit_mgmt_wm_auth_attempts, [by_source]},
182184
{"/login", rabbit_mgmt_wm_login, []}
183185
].

deps/rabbitmq_management/src/rabbit_mgmt_util.erl

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,26 +248,36 @@ is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun) ->
248248
VHost when is_binary(VHost) -> [{vhost, VHost}];
249249
_ -> []
250250
end,
251+
{IP, _} = cowboy_req:peer(ReqData),
252+
RemoteAddress = list_to_binary(inet:ntoa(IP)),
251253
case rabbit_access_control:check_user_login(Username, AuthProps) of
252254
{ok, User = #user{tags = Tags}} ->
253-
{IP, _} = cowboy_req:peer(ReqData),
254255
case rabbit_access_control:check_user_loopback(Username, IP) of
255256
ok ->
256257
case is_mgmt_user(Tags) of
257258
true ->
258259
case Fun(User) of
259-
true -> {true, ReqData,
260-
Context#context{user = User,
261-
password = Password}};
262-
false -> ErrFun(ErrorMsg)
260+
true ->
261+
rabbit_core_metrics:auth_attempt_succeeded(RemoteAddress,
262+
Username, http),
263+
{true, ReqData,
264+
Context#context{user = User,
265+
password = Password}};
266+
false ->
267+
rabbit_core_metrics:auth_attempt_failed(RemoteAddress,
268+
Username, http),
269+
ErrFun(ErrorMsg)
263270
end;
264271
false ->
272+
rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http),
265273
ErrFun(<<"Not management user">>)
266274
end;
267275
not_allowed ->
276+
rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http),
268277
ErrFun(<<"User can only log in via localhost">>)
269278
end;
270279
{refused, _Username, Msg, Args} ->
280+
rabbit_core_metrics:auth_attempt_failed(RemoteAddress, Username, http),
271281
rabbit_log:warning("HTTP access denied: ~s",
272282
[rabbit_misc:format(Msg, Args)]),
273283
not_authorised(<<"Login failed">>, ReqData, Context)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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-2020 VMware, Inc. or its affiliates. All rights reserved.
6+
%%
7+
8+
-module(rabbit_mgmt_wm_auth_attempts).
9+
10+
-export([init/2, to_json/2, content_types_provided/2, allowed_methods/2, is_authorized/2,
11+
delete_resource/2, resource_exists/2]).
12+
-export([variances/2]).
13+
14+
-import(rabbit_misc, [pget/2]).
15+
16+
-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl").
17+
-include_lib("rabbit_common/include/rabbit.hrl").
18+
19+
%%--------------------------------------------------------------------
20+
init(Req, [Mode]) ->
21+
{cowboy_rest, rabbit_mgmt_headers:set_common_permission_headers(Req, ?MODULE),
22+
{Mode, #context{}}}.
23+
24+
variances(Req, Context) ->
25+
{[<<"accept-encoding">>, <<"origin">>], Req, Context}.
26+
27+
content_types_provided(ReqData, Context) ->
28+
{rabbit_mgmt_util:responder_map(to_json), ReqData, Context}.
29+
30+
allowed_methods(ReqData, Context) ->
31+
{[<<"HEAD">>, <<"GET">>, <<"DELETE">>, <<"OPTIONS">>], ReqData, Context}.
32+
33+
resource_exists(ReqData, Context) ->
34+
{node_exists(ReqData, get_node(ReqData)), ReqData, Context}.
35+
36+
to_json(ReqData, {Mode, Context}) ->
37+
rabbit_mgmt_util:reply(augment(Mode, ReqData), ReqData, Context).
38+
39+
is_authorized(ReqData, {Mode, Context}) ->
40+
{Res, Req2, Context2} = rabbit_mgmt_util:is_authorized_monitor(ReqData, Context),
41+
{Res, Req2, {Mode, Context2}}.
42+
43+
delete_resource(ReqData, Context) ->
44+
Node = get_node(ReqData),
45+
case node_exists(ReqData, Node) of
46+
false ->
47+
{false, ReqData, Context};
48+
true ->
49+
case rpc:call(Node, rabbit_core_metrics, reset_auth_attempt_metrics, [], infinity) of
50+
{badrpc, _} -> {false, ReqData, Context};
51+
ok -> {true, ReqData, Context}
52+
end
53+
end.
54+
%%--------------------------------------------------------------------
55+
get_node(ReqData) ->
56+
list_to_atom(binary_to_list(rabbit_mgmt_util:id(node, ReqData))).
57+
58+
node_exists(ReqData, Node) ->
59+
case [N || N <- rabbit_mgmt_wm_nodes:all_nodes(ReqData),
60+
proplists:get_value(name, N) == Node] of
61+
[] -> false;
62+
[_] -> true
63+
end.
64+
65+
augment(Mode, ReqData) ->
66+
Node = get_node(ReqData),
67+
case node_exists(ReqData, Node) of
68+
false ->
69+
not_found;
70+
true ->
71+
Fun = case Mode of
72+
all -> get_auth_attempts;
73+
by_source -> get_auth_attempts_by_source
74+
end,
75+
case rpc:call(Node, rabbit_core_metrics, Fun, [], infinity) of
76+
{badrpc, _} -> not_available;
77+
Result -> Result
78+
end
79+
end.

deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ all_tests() -> [
148148
oauth_test,
149149
disable_basic_auth_test,
150150
login_test,
151-
csp_headers_test
151+
csp_headers_test,
152+
auth_attempts_test
152153
].
153154

154155
%% -------------------------------------------------------------------
@@ -3386,6 +3387,62 @@ disable_basic_auth_test(Config) ->
33863387
%% Defaults to 'false' when config is invalid
33873388
http_get(Config, "/overview", ?OK).
33883389

3390+
auth_attempts_test(Config) ->
3391+
rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_core_metrics, reset_auth_attempt_metrics, []),
3392+
{Conn, _Ch} = open_connection_and_channel(Config),
3393+
close_connection(Conn),
3394+
[NodeData] = http_get(Config, "/nodes"),
3395+
Node = binary_to_atom(maps:get(name, NodeData), utf8),
3396+
Map = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node), ?OK),
3397+
Http = get_auth_attempts(<<"http">>, Map),
3398+
Amqp091 = get_auth_attempts(<<"amqp091">>, Map),
3399+
?assertEqual(false, maps:is_key(remote_address, Amqp091)),
3400+
?assertEqual(false, maps:is_key(username, Amqp091)),
3401+
?assertEqual(1, maps:get(auth_attempts, Amqp091)),
3402+
?assertEqual(1, maps:get(auth_attempts_succeeded, Amqp091)),
3403+
?assertEqual(0, maps:get(auth_attempts_failed, Amqp091)),
3404+
?assertEqual(false, maps:is_key(remote_address, Http)),
3405+
?assertEqual(false, maps:is_key(username, Http)),
3406+
?assertEqual(2, maps:get(auth_attempts, Http)),
3407+
?assertEqual(2, maps:get(auth_attempts_succeeded, Http)),
3408+
?assertEqual(0, maps:get(auth_attempts_failed, Http)),
3409+
3410+
rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env,
3411+
[rabbit, track_auth_attempt_source, true]),
3412+
{Conn2, _Ch2} = open_connection_and_channel(Config),
3413+
close_connection(Conn2),
3414+
Map2 = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node) ++ "/source", ?OK),
3415+
Map3 = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node), ?OK),
3416+
Http2 = get_auth_attempts(<<"http">>, Map2),
3417+
Http3 = get_auth_attempts(<<"http">>, Map3),
3418+
Amqp091_2 = get_auth_attempts(<<"amqp091">>, Map2),
3419+
Amqp091_3 = get_auth_attempts(<<"amqp091">>, Map3),
3420+
?assertEqual(<<"127.0.0.1">>, maps:get(remote_address, Http2)),
3421+
?assertEqual(<<"guest">>, maps:get(username, Http2)),
3422+
?assertEqual(1, maps:get(auth_attempts, Http2)),
3423+
?assertEqual(1, maps:get(auth_attempts_succeeded, Http2)),
3424+
?assertEqual(0, maps:get(auth_attempts_failed, Http2)),
3425+
3426+
?assertEqual(false, maps:is_key(remote_address, Http3)),
3427+
?assertEqual(false, maps:is_key(username, Http3)),
3428+
?assertEqual(4, maps:get(auth_attempts, Http3)),
3429+
?assertEqual(4, maps:get(auth_attempts_succeeded, Http3)),
3430+
?assertEqual(0, maps:get(auth_attempts_failed, Http3)),
3431+
3432+
?assertEqual(true, <<>> =/= maps:get(remote_address, Amqp091_2)),
3433+
?assertEqual(<<"guest">>, maps:get(username, Amqp091_2)),
3434+
?assertEqual(1, maps:get(auth_attempts, Amqp091_2)),
3435+
?assertEqual(1, maps:get(auth_attempts_succeeded, Amqp091_2)),
3436+
?assertEqual(0, maps:get(auth_attempts_failed, Amqp091_2)),
3437+
3438+
?assertEqual(false, maps:is_key(remote_address, Amqp091_3)),
3439+
?assertEqual(false, maps:is_key(username, Amqp091_3)),
3440+
?assertEqual(2, maps:get(auth_attempts, Amqp091_3)),
3441+
?assertEqual(2, maps:get(auth_attempts_succeeded, Amqp091_3)),
3442+
?assertEqual(0, maps:get(auth_attempts_failed, Amqp091_3)),
3443+
3444+
passed.
3445+
33893446
%% -------------------------------------------------------------------
33903447
%% Helpers.
33913448
%% -------------------------------------------------------------------
@@ -3477,3 +3534,9 @@ format_multipart_filedata(Boundary, Files) ->
34773534
EndingParts = [lists:concat(["--", Boundary, "--"]), ""],
34783535
Parts = lists:append([FileParts2, EndingParts]),
34793536
string:join(Parts, "\r\n").
3537+
3538+
get_auth_attempts(Protocol, Map) ->
3539+
[A] = lists:filter(fun(#{protocol := P}) ->
3540+
P == Protocol
3541+
end, Map),
3542+
A.

0 commit comments

Comments
 (0)