Skip to content

Commit c98c401

Browse files
committed
and add the actual file...
1 parent 2a2af36 commit c98c401

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
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+
8+
-module(rabbit_web_dispatch_access_control).
9+
10+
-include("rabbitmq_web_dispatch_records.hrl").
11+
-include_lib("amqp_client/include/amqp_client.hrl").
12+
13+
-export([is_authorized/3, is_authorized/7, is_authorized_admin/3,
14+
is_authorized_admin/5, vhost/1, vhost_from_headers/1]).
15+
-export([is_authorized_vhost/3, is_authorized_user/4,
16+
is_authorized_user/5, is_authorized_user/6,
17+
is_authorized_monitor/3, is_authorized_policies/3,
18+
is_authorized_vhost_visible/3,
19+
is_authorized_vhost_visible_for_monitoring/3,
20+
is_authorized_global_parameters/3]).
21+
22+
-export([list_visible_vhosts/1, list_visible_vhosts_names/1, list_login_vhosts/2]).
23+
-export([id/2]).
24+
-export([not_authorised/3, halt_response/5]).
25+
26+
-export([is_admin/1, is_policymaker/1, is_monitor/1, is_mgmt_user/1]).
27+
28+
-import(rabbit_misc, [pget/2]).
29+
30+
is_authorized(ReqData, Context, AuthConfig) ->
31+
is_authorized(ReqData, Context, '', fun(_) -> true end, AuthConfig).
32+
33+
is_authorized_admin(ReqData, Context, AuthConfig) ->
34+
is_authorized(ReqData, Context,
35+
<<"Not administrator user">>,
36+
fun(#user{tags = Tags}) -> is_admin(Tags) end, AuthConfig).
37+
38+
is_authorized_admin(ReqData, Context, Username, Password, AuthConfig) ->
39+
case is_basic_auth_disabled(AuthConfig) of
40+
true ->
41+
Msg = "HTTP access denied: basic auth disabled",
42+
rabbit_log:warning(Msg),
43+
not_authorised(Msg, ReqData, Context);
44+
false ->
45+
is_authorized(ReqData, Context, Username, Password,
46+
<<"Not administrator user">>,
47+
fun(#user{tags = Tags}) -> is_admin(Tags) end, AuthConfig)
48+
end.
49+
50+
is_authorized_monitor(ReqData, Context, AuthConfig) ->
51+
is_authorized(ReqData, Context,
52+
<<"Not monitor user">>,
53+
fun(#user{tags = Tags}) -> is_monitor(Tags) end,
54+
AuthConfig).
55+
56+
is_authorized_vhost(ReqData, Context, AuthConfig) ->
57+
is_authorized(ReqData, Context,
58+
<<"User not authorised to access virtual host">>,
59+
fun(#user{tags = Tags} = User) ->
60+
is_admin(Tags) orelse user_matches_vhost(ReqData, User)
61+
end,
62+
AuthConfig).
63+
64+
is_authorized_vhost_visible(ReqData, Context, AuthConfig) ->
65+
is_authorized(ReqData, Context,
66+
<<"User not authorised to access virtual host">>,
67+
fun(#user{tags = Tags} = User) ->
68+
is_admin(Tags) orelse user_matches_vhost_visible(ReqData, User)
69+
end,
70+
AuthConfig).
71+
72+
is_authorized_vhost_visible_for_monitoring(ReqData, Context, AuthConfig) ->
73+
is_authorized(ReqData, Context,
74+
<<"User not authorised to access virtual host">>,
75+
fun(#user{tags = Tags} = User) ->
76+
is_admin(Tags)
77+
orelse is_monitor(Tags)
78+
orelse user_matches_vhost_visible(ReqData, User)
79+
end,
80+
AuthConfig).
81+
82+
is_authorized(ReqData, Context, ErrorMsg, Fun, AuthConfig) ->
83+
case cowboy_req:method(ReqData) of
84+
<<"OPTIONS">> -> {true, ReqData, Context};
85+
_ -> is_authorized1(ReqData, Context, ErrorMsg, Fun, AuthConfig)
86+
end.
87+
88+
is_authorized1(ReqData, Context, ErrorMsg, Fun, AuthConfig) ->
89+
case cowboy_req:parse_header(<<"authorization">>, ReqData) of
90+
{basic, Username, Password} ->
91+
case is_basic_auth_disabled(AuthConfig) of
92+
true ->
93+
Msg = "HTTP access denied: basic auth disabled",
94+
rabbit_log:warning(Msg),
95+
not_authorised(Msg, ReqData, Context);
96+
false ->
97+
is_authorized(ReqData, Context,
98+
Username, Password,
99+
ErrorMsg, Fun, AuthConfig)
100+
end;
101+
{bearer, Token} ->
102+
% Username is only used in case is_authorized is not able to parse the token
103+
% and extact the username from it
104+
Username = AuthConfig#auth_settings.oauth_client_id,
105+
is_authorized(ReqData, Context, Username, Token, ErrorMsg, Fun, AuthConfig);
106+
_ ->
107+
case is_basic_auth_disabled(AuthConfig) of
108+
true ->
109+
Msg = "HTTP access denied: basic auth disabled",
110+
rabbit_log:warning(Msg),
111+
not_authorised(Msg, ReqData, Context);
112+
false ->
113+
{{false, AuthConfig#auth_settings.auth_realm}, ReqData, Context}
114+
end
115+
end.
116+
117+
is_authorized_user(ReqData, Context, Username, Password, AuthConfig) ->
118+
Msg = <<"User not authorized">>,
119+
Fun = fun(_) -> true end,
120+
is_authorized(ReqData, Context, Username, Password, Msg, Fun, AuthConfig).
121+
122+
is_authorized_user(ReqData, Context, Username, Password, ReplyWhenFailed, AuthConfig) ->
123+
Msg = <<"User not authorized">>,
124+
Fun = fun(_) -> true end,
125+
is_authorized(ReqData, Context, Username, Password, Msg, Fun, AuthConfig, ReplyWhenFailed).
126+
127+
is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun, AuthConfig) ->
128+
is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun, AuthConfig, true).
129+
130+
is_authorized(ReqData, Context, Username, Password, ErrorMsg, Fun, AuthConfig, ReplyWhenFailed) ->
131+
ErrFun = fun (ResolvedUserName, Msg) ->
132+
rabbit_log:warning("HTTP access denied: user '~ts' - ~ts",
133+
[ResolvedUserName, Msg]),
134+
case ReplyWhenFailed of
135+
true -> not_authorised(Msg, ReqData, Context);
136+
false -> {false, ReqData, "Not_Authorized"}
137+
end
138+
end,
139+
AuthProps = [{password, Password}] ++ case vhost(ReqData) of
140+
VHost when is_binary(VHost) -> [{vhost, VHost}];
141+
_ -> []
142+
end,
143+
{IP, _} = cowboy_req:peer(ReqData),
144+
case rabbit_access_control:check_user_login(Username, AuthProps) of
145+
{ok, User = #user{username = ResolvedUsername, tags = Tags}} ->
146+
case rabbit_access_control:check_user_loopback(ResolvedUsername, IP) of
147+
ok ->
148+
case is_mgmt_user(Tags) of
149+
true ->
150+
case Fun(User) of
151+
true ->
152+
rabbit_core_metrics:auth_attempt_succeeded(IP, ResolvedUsername, http),
153+
{true, ReqData,
154+
Context#context{user = User,
155+
password = Password}};
156+
false ->
157+
rabbit_core_metrics:auth_attempt_failed(IP, ResolvedUsername, http),
158+
ErrFun(ResolvedUsername, ErrorMsg)
159+
end;
160+
false ->
161+
rabbit_core_metrics:auth_attempt_failed(IP, ResolvedUsername, http),
162+
ErrFun(ResolvedUsername, <<"Not management user">>)
163+
end;
164+
not_allowed ->
165+
rabbit_core_metrics:auth_attempt_failed(IP, ResolvedUsername, http),
166+
ErrFun(ResolvedUsername, <<"User can only log in via localhost">>)
167+
end;
168+
{refused, _Username, Msg, Args} ->
169+
rabbit_core_metrics:auth_attempt_failed(IP, Username, http),
170+
rabbit_log:warning("HTTP access denied: ~ts",
171+
[rabbit_misc:format(Msg, Args)]),
172+
case ReplyWhenFailed of
173+
true -> not_authenticated(<<"Not_Authorized">>, ReqData, Context, AuthConfig);
174+
false -> {false, ReqData, "Not_Authorized"}
175+
end
176+
end.
177+
178+
179+
%% Used for connections / channels. A normal user can only see / delete
180+
%% their own stuff. Monitors can see other users' and delete their
181+
%% own. Admins can do it all.
182+
is_authorized_user(ReqData, Context, Item, AuthConfig) ->
183+
is_authorized(ReqData, Context,
184+
<<"User not authorised to access object">>,
185+
fun(#user{username = Username, tags = Tags}) ->
186+
case cowboy_req:method(ReqData) of
187+
<<"DELETE">> -> is_admin(Tags);
188+
_ -> is_monitor(Tags)
189+
end orelse Username == pget(user, Item)
190+
end,
191+
AuthConfig).
192+
193+
%% For policies / parameters. Like is_authorized_vhost but you have to
194+
%% be a policymaker.
195+
is_authorized_policies(ReqData, Context, AuthConfig) ->
196+
is_authorized(ReqData, Context,
197+
<<"User not authorised to access object">>,
198+
fun(User = #user{tags = Tags}) ->
199+
is_admin(Tags) orelse
200+
(is_policymaker(Tags) andalso
201+
user_matches_vhost(ReqData, User))
202+
end,
203+
AuthConfig).
204+
205+
%% For global parameters. Must be policymaker.
206+
is_authorized_global_parameters(ReqData, Context, AuthConfig) ->
207+
is_authorized(ReqData, Context,
208+
<<"User not authorised to access object">>,
209+
fun(#user{tags = Tags}) ->
210+
is_policymaker(Tags)
211+
end,
212+
AuthConfig).
213+
214+
vhost_from_headers(ReqData) ->
215+
case cowboy_req:header(<<"x-vhost">>, ReqData) of
216+
undefined -> none;
217+
%% blank x-vhost means "All hosts" is selected in the UI
218+
<<>> -> none;
219+
VHost -> VHost
220+
end.
221+
222+
vhost(ReqData) ->
223+
Value = case id(vhost, ReqData) of
224+
none -> vhost_from_headers(ReqData);
225+
VHost -> VHost
226+
end,
227+
case Value of
228+
none -> none;
229+
Name ->
230+
case rabbit_vhost:exists(Name) of
231+
true -> Name;
232+
false -> not_found
233+
end
234+
end.
235+
236+
is_admin(T) -> intersects(T, [administrator]).
237+
is_policymaker(T) -> intersects(T, [administrator, policymaker]).
238+
is_monitor(T) -> intersects(T, [administrator, monitoring]).
239+
is_mgmt_user(T) -> intersects(T, [administrator, monitoring, policymaker,
240+
management]).
241+
242+
intersects(A, B) -> lists:any(fun(I) -> lists:member(I, B) end, A).
243+
244+
user_matches_vhost(ReqData, User) ->
245+
case vhost(ReqData) of
246+
not_found -> true;
247+
none -> true;
248+
V ->
249+
AuthzData = get_authz_data(ReqData),
250+
lists:member(V, list_login_vhosts_names(User, AuthzData))
251+
end.
252+
253+
user_matches_vhost_visible(ReqData, User) ->
254+
case vhost(ReqData) of
255+
not_found -> true;
256+
none -> true;
257+
V ->
258+
AuthzData = get_authz_data(ReqData),
259+
lists:member(V, list_visible_vhosts_names(User, AuthzData))
260+
end.
261+
262+
get_authz_data(ReqData) ->
263+
{PeerAddress, _PeerPort} = cowboy_req:peer(ReqData),
264+
{ip, PeerAddress}.
265+
266+
267+
not_authorised(Reason, ReqData, Context) ->
268+
%% TODO: consider changing to 403 in 4.0
269+
halt_response(401, not_authorised, Reason, ReqData, Context).
270+
271+
halt_response(Code, Type, Reason, ReqData, Context) ->
272+
ReasonFormatted = format_reason(Reason),
273+
Json = #{<<"error">> => Type,
274+
<<"reason">> => ReasonFormatted},
275+
ReqData1 = cowboy_req:reply(Code,
276+
#{<<"content-type">> => <<"application/json">>},
277+
rabbit_json:encode(Json), ReqData),
278+
{stop, ReqData1, Context}.
279+
280+
not_authenticated(Reason, ReqData, Context,
281+
#auth_settings{auth_realm = AuthRealm} = AuthConfig) ->
282+
case is_oauth2_enabled(AuthConfig) of
283+
false ->
284+
ReqData1 = cowboy_req:set_resp_header(<<"www-authenticate">>, AuthRealm, ReqData),
285+
halt_response(401, not_authorized, Reason, ReqData1, Context);
286+
true ->
287+
halt_response(401, not_authorized, Reason, ReqData, Context)
288+
end.
289+
290+
format_reason(Tuple) when is_tuple(Tuple) ->
291+
tuple(Tuple);
292+
format_reason(Binary) when is_binary(Binary) ->
293+
Binary;
294+
format_reason(Other) ->
295+
case is_string(Other) of
296+
true -> print("~ts", [Other]);
297+
false -> print("~tp", [Other])
298+
end.
299+
300+
print(Fmt, Val) when is_list(Val) ->
301+
list_to_binary(lists:flatten(io_lib:format(Fmt, Val))).
302+
303+
is_string(List) when is_list(List) ->
304+
lists:all(
305+
fun(El) -> is_integer(El) andalso El > 0 andalso El < 16#10ffff end,
306+
List);
307+
is_string(_) -> false.
308+
309+
tuple(unknown) -> unknown;
310+
tuple(Tuple) when is_tuple(Tuple) -> [tuple(E) || E <- tuple_to_list(Tuple)];
311+
tuple(Term) -> Term.
312+
313+
id(Key, ReqData) when Key =:= exchange;
314+
Key =:= source;
315+
Key =:= destination ->
316+
case id0(Key, ReqData) of
317+
<<"amq.default">> -> <<"">>;
318+
Name -> Name
319+
end;
320+
id(Key, ReqData) ->
321+
id0(Key, ReqData).
322+
323+
id0(Key, ReqData) ->
324+
case cowboy_req:binding(Key, ReqData) of
325+
undefined -> none;
326+
Id -> Id
327+
end.
328+
329+
330+
list_visible_vhosts_names(User) ->
331+
list_visible_vhosts(User, undefined).
332+
333+
list_visible_vhosts_names(User, AuthzData) ->
334+
list_visible_vhosts(User, AuthzData).
335+
336+
list_visible_vhosts(User) ->
337+
list_visible_vhosts(User, undefined).
338+
339+
list_visible_vhosts(User = #user{tags = Tags}, AuthzData) ->
340+
case is_monitor(Tags) of
341+
true -> rabbit_vhost:list_names();
342+
false -> list_login_vhosts_names(User, AuthzData)
343+
end.
344+
345+
list_login_vhosts_names(User, AuthzData) ->
346+
[V || V <- rabbit_vhost:list_names(),
347+
case catch rabbit_access_control:check_vhost_access(User, V, AuthzData, #{}) of
348+
ok -> true;
349+
NotOK ->
350+
log_access_control_result(NotOK),
351+
false
352+
end].
353+
354+
list_login_vhosts(User, AuthzData) ->
355+
[V || V <- rabbit_vhost:all(),
356+
case catch rabbit_access_control:check_vhost_access(User, vhost:get_name(V), AuthzData, #{}) of
357+
ok -> true;
358+
NotOK ->
359+
log_access_control_result(NotOK),
360+
false
361+
end].
362+
363+
% rabbitmq/rabbitmq-auth-backend-http#100
364+
log_access_control_result(NotOK) ->
365+
rabbit_log:debug("rabbit_access_control:check_vhost_access result: ~tp", [NotOK]).
366+
367+
is_basic_auth_disabled(#auth_settings{basic_auth_enabled = Enabled}) ->
368+
not Enabled.
369+
370+
is_oauth2_enabled(#auth_settings{oauth2_enabled = Enabled}) ->
371+
Enabled.

0 commit comments

Comments
 (0)