Skip to content

Commit f9b9fa0

Browse files
committed
Optionally return authz refusal reason to client
## What? If the new config setting `authorization_failure_disclosure` for an authz backend is set to `true`, (`false` by default), RabbitMQ will return the reason why access was denied to the client. For now, only the HTTP and OAuth 2.0 auth backends support this new config setting. ## Why? This helps debugging and troubleshooting directly in the client. Some users might not have access to the RabbitMQ logs, for other users it's cumbersome to correlate authz denial in the client with logs on the broker. For example, in dev environments, it may be useful for users to learn why vhost/resource/topic access was denied for a given OAuth 2.0 token. Another example is that some customers would like to pass the reason why authorization was denied from their custom HTTP auth backend via RabbitMQ back to the client. ## How? Authz backends can now return `{false, Reason}` as an alternative to just `false` if access is denied. For security reasons, the additional denial reason by the authz backend will be returned to the client only if the operator opted in by setting `authorization_failure_disclosure` to `true`. Note that `authorization_failure_disclosure` applies only to already authenticated clients when they try to access resources (e.g. vhosts, exchanges, queues, topics). For security reasons, no detailed denial reason is returned to the client if **authentication** fails. Also note that `authorization_failure_disclosure` is set separately per auth backend instead of being set globally for all auth backends. This more fine granular configurability helps for use cases where the broker should reveal the authz denial reason for only a specific auth backend, e.g. only for HTTP backend, but not for OAuth 2.0 backend.
1 parent c312044 commit f9b9fa0

File tree

18 files changed

+538
-316
lines changed

18 files changed

+538
-316
lines changed

deps/rabbit/src/rabbit_access_control.erl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ check_access(Fun, Module, ErrStr, ErrArgs, ErrName) ->
361361
ok;
362362
false ->
363363
rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs);
364+
{false, Reason} ->
365+
FullErrStr = ErrStr ++ " by backend ~ts: ~ts",
366+
FullErrArgs = ErrArgs ++ [Module, Reason],
367+
rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs);
364368
{error, E} ->
365369
FullErrStr = ErrStr ++ ", backend ~ts returned an error: ~tp",
366370
FullErrArgs = ErrArgs ++ [Module, E],

deps/rabbit/src/rabbit_authz_backend.erl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,27 @@
3232
%% Possible responses:
3333
%% true
3434
%% false
35+
%% {false, Reason}
3536
%% {error, Error}
3637
%% Something went wrong. Log and die.
3738
-callback check_vhost_access(AuthUser :: rabbit_types:auth_user(),
3839
VHost :: rabbit_types:vhost(),
3940
AuthzData :: rabbit_types:authz_data()) ->
40-
boolean() | {'error', any()}.
41+
boolean() | {false, Reason :: string()} | {'error', any()}.
4142

4243
%% Given #auth_user, resource and permission, can a user access a resource?
4344
%%
4445
%% Possible responses:
4546
%% true
4647
%% false
48+
%% {false, Reason}
4749
%% {error, Error}
4850
%% Something went wrong. Log and die.
4951
-callback check_resource_access(rabbit_types:auth_user(),
5052
rabbit_types:r(atom()),
5153
rabbit_types:permission_atom(),
5254
rabbit_types:authz_context()) ->
53-
boolean() | {'error', any()}.
55+
boolean() | {false, Reason :: string()} | {'error', any()}.
5456

5557
%% Given #auth_user, topic as resource, permission, and context, can a user access the topic?
5658
%%
@@ -63,7 +65,7 @@
6365
rabbit_types:r(atom()),
6466
rabbit_types:permission_atom(),
6567
rabbit_types:topic_access_context()) ->
66-
boolean() | {'error', any()}.
68+
boolean() | {false, Reason :: string()} | {'error', any()}.
6769

6870
%% Updates backend state that has expired.
6971
%%

deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,34 @@ user_login_authorization(Username, AuthProps) ->
3636
end).
3737

3838
check_vhost_access(#auth_user{} = AuthUser, VHostPath, AuthzData) ->
39-
with_cache(authz, {check_vhost_access, [AuthUser, VHostPath, AuthzData]},
40-
fun(true) -> success;
41-
(false) -> refusal;
42-
({error, _} = Err) -> Err;
43-
(_) -> unknown
44-
end).
39+
with_cache(authz,
40+
{check_vhost_access, [AuthUser, VHostPath, AuthzData]},
41+
fun convert_backend_result/1).
4542

4643
check_resource_access(#auth_user{} = AuthUser,
4744
#resource{} = Resource, Permission, AuthzContext) ->
48-
with_cache(authz, {check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
49-
fun(true) -> success;
50-
(false) -> refusal;
51-
({error, _} = Err) -> Err;
52-
(_) -> unknown
53-
end).
45+
with_cache(authz,
46+
{check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
47+
fun convert_backend_result/1).
5448

5549
check_topic_access(#auth_user{} = AuthUser,
5650
#resource{} = Resource, Permission, Context) ->
57-
with_cache(authz, {check_topic_access, [AuthUser, Resource, Permission, Context]},
58-
fun(true) -> success;
59-
(false) -> refusal;
60-
({error, _} = Err) -> Err;
61-
(_) -> unknown
62-
end).
51+
with_cache(authz,
52+
{check_topic_access, [AuthUser, Resource, Permission, Context]},
53+
fun convert_backend_result/1).
6354

6455
expiry_timestamp(_) -> never.
6556

6657
%%
6758
%% Implementation
6859
%%
6960

61+
convert_backend_result(true) -> success;
62+
convert_backend_result(false) -> refusal;
63+
convert_backend_result({false, _}) -> refusal;
64+
convert_backend_result({error, _} = Err) -> Err;
65+
convert_backend_result(_) -> unknown.
66+
7067
clear_cache_cluster_wide() ->
7168
Nodes = rabbit_nodes:list_running(),
7269
?LOG_WARNING("Clearing auth_backend_cache in all nodes : ~p", [Nodes]),

deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
-callback clear() -> ok.
1919

2020
expiration(TTL) ->
21-
erlang:system_time(milli_seconds) + TTL.
21+
erlang:system_time(millisecond) + TTL.
2222

2323
expired(Exp) ->
24-
erlang:system_time(milli_seconds) > Exp.
24+
erlang:system_time(millisecond) > Exp.

deps/rabbitmq_auth_backend_http/Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ define PROJECT_ENV
1010
{user_path, "http://localhost:8000/auth/user"},
1111
{vhost_path, "http://localhost:8000/auth/vhost"},
1212
{resource_path, "http://localhost:8000/auth/resource"},
13-
{topic_path, "http://localhost:8000/auth/topic"}
13+
{topic_path, "http://localhost:8000/auth/topic"},
14+
{authorization_failure_disclosure, false}
1415
]
1516
endef
1617

@@ -20,7 +21,7 @@ endef
2021

2122
LOCAL_DEPS = ssl inets crypto public_key
2223
DEPS = rabbit_common rabbit amqp_client
23-
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy
24+
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy rabbitmq_amqp_client
2425

2526
DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk
2627
DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk

deps/rabbitmq_auth_backend_http/priv/schema/rabbitmq_auth_backend_http.schema

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
{mapping, "auth_http.connection_timeout", "rabbitmq_auth_backend_http.connection_timeout",
2424
[{datatype, integer}]}.
2525

26+
{mapping, "auth_http.authorization_failure_disclosure", "rabbitmq_auth_backend_http.authorization_failure_disclosure", [
27+
{datatype, {enum, [true, false]}}]}.
28+
2629
%% TLS options
2730

2831
{mapping, "auth_http.ssl_options", "rabbitmq_auth_backend_http.ssl_options", [

deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
-define(SUCCESSFUL_RESPONSE_CODES, [200, 201]).
2727

28+
-define(APP, rabbitmq_auth_backend_http).
29+
2830
%%--------------------------------------------------------------------
2931

3032
description() ->
@@ -120,34 +122,34 @@ check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost,
120122

121123
do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData) ->
122124
OptionsParameters = context_as_parameters(AuthzData),
123-
bool_req(vhost_path, [{username, Username},
124-
{vhost, VHost},
125-
{ip, Ip},
126-
{tags, join_tags(Tags)}] ++ OptionsParameters).
125+
req(vhost_path, [{username, Username},
126+
{vhost, VHost},
127+
{ip, Ip},
128+
{tags, join_tags(Tags)}] ++ OptionsParameters).
127129

128130
check_resource_access(#auth_user{username = Username, tags = Tags},
129131
#resource{virtual_host = VHost, kind = Type, name = Name},
130132
Permission,
131133
AuthzContext) ->
132134
OptionsParameters = context_as_parameters(AuthzContext),
133-
bool_req(resource_path, [{username, Username},
134-
{vhost, VHost},
135-
{resource, Type},
136-
{name, Name},
137-
{permission, Permission},
138-
{tags, join_tags(Tags)}] ++ OptionsParameters).
135+
req(resource_path, [{username, Username},
136+
{vhost, VHost},
137+
{resource, Type},
138+
{name, Name},
139+
{permission, Permission},
140+
{tags, join_tags(Tags)}] ++ OptionsParameters).
139141

140142
check_topic_access(#auth_user{username = Username, tags = Tags},
141143
#resource{virtual_host = VHost, kind = topic = Type, name = Name},
142144
Permission,
143145
Context) ->
144146
OptionsParameters = context_as_parameters(Context),
145-
bool_req(topic_path, [{username, Username},
146-
{vhost, VHost},
147-
{resource, Type},
148-
{name, Name},
149-
{permission, Permission},
150-
{tags, join_tags(Tags)}] ++ OptionsParameters).
147+
req(topic_path, [{username, Username},
148+
{vhost, VHost},
149+
{resource, Type},
150+
{name, Name},
151+
{permission, Permission},
152+
{tags, join_tags(Tags)}] ++ OptionsParameters).
151153

152154
expiry_timestamp(_) -> never.
153155

@@ -163,7 +165,7 @@ context_as_parameters(Options) when is_map(Options) ->
163165
context_as_parameters(_) ->
164166
[].
165167

166-
bool_req(PathName, Props) ->
168+
req(PathName, Props) ->
167169
Path = p(PathName),
168170
Query = q(Props),
169171
case http_req(Path, Query) of
@@ -172,7 +174,12 @@ bool_req(PathName, Props) ->
172174
"deny " ++ Reason ->
173175
?LOG_INFO("HTTP authorisation denied for path ~ts with query ~ts: ~ts",
174176
[Path, Query, Reason]),
175-
false;
177+
case application:get_env(?APP, authorization_failure_disclosure) of
178+
{ok, true} ->
179+
{false, Reason};
180+
_ ->
181+
false
182+
end;
176183
Body ->
177184
case string:lowercase(Body) of
178185
"deny" ->
@@ -201,7 +208,7 @@ do_http_req(Path0, Query) ->
201208
{host, Host} = lists:keyfind(host, 1, URI),
202209
{port, Port} = lists:keyfind(port, 1, URI),
203210
HostHdr = rabbit_misc:format("~ts:~b", [Host, Port]),
204-
{ok, Method} = application:get_env(rabbitmq_auth_backend_http, http_method),
211+
{ok, Method} = application:get_env(?APP, http_method),
205212
Request = case rabbit_data_coercion:to_atom(Method) of
206213
get ->
207214
Path = Path0 ++ "?" ++ Query,
@@ -212,21 +219,22 @@ do_http_req(Path0, Query) ->
212219
{Path0, [{"Host", HostHdr}], "application/x-www-form-urlencoded", Query}
213220
end,
214221
RequestTimeout =
215-
case application:get_env(rabbitmq_auth_backend_http, request_timeout) of
222+
case application:get_env(?APP, request_timeout) of
216223
{ok, Val1} -> Val1;
217224
_ -> infinity
218225
end,
219226
ConnectionTimeout =
220-
case application:get_env(rabbitmq_auth_backend_http, connection_timeout) of
227+
case application:get_env(?APP, connection_timeout) of
221228
{ok, Val2} -> Val2;
222229
_ -> RequestTimeout
223230
end,
224231
?LOG_DEBUG("auth_backend_http: request timeout: ~tp, connection timeout: ~tp", [RequestTimeout, ConnectionTimeout]),
225232
HttpOpts = [{timeout, RequestTimeout},
226233
{connect_timeout, ConnectionTimeout}] ++ ssl_options(),
227-
case httpc:request(Method, Request, HttpOpts, []) of
228-
{ok, {{_HTTP, Code, _}, _Headers, Body}} ->
229-
?LOG_DEBUG("auth_backend_http: response code is ~tp, body: ~tp", [Code, Body]),
234+
case httpc:request(Method, Request, HttpOpts, [{body_format, binary}]) of
235+
{ok, {{_HTTP, Code, _}, _Headers, BodyBin}} ->
236+
Body = unicode:characters_to_list(BodyBin),
237+
?LOG_DEBUG("auth_backend_http: response code is ~tp, body: ~ts", [Code, Body]),
230238
case lists:member(Code, ?SUCCESSFUL_RESPONSE_CODES) of
231239
true ->
232240
string:strip(Body);
@@ -238,10 +246,10 @@ do_http_req(Path0, Query) ->
238246
end.
239247

240248
ssl_options() ->
241-
case application:get_env(rabbitmq_auth_backend_http, ssl_options) of
249+
case application:get_env(?APP, ssl_options) of
242250
{ok, Opts0} when is_list(Opts0) ->
243251
Opts1 = [{ssl, rabbit_ssl_options:fix_client(Opts0)}],
244-
case application:get_env(rabbitmq_auth_backend_http, ssl_hostname_verification) of
252+
case application:get_env(?APP, ssl_hostname_verification) of
245253
{ok, wildcard} ->
246254
?LOG_DEBUG("Enabling wildcard-aware hostname verification for HTTP client connections"),
247255
%% Needed for HTTPS connections that connect to servers that use wildcard certificates.
@@ -254,7 +262,7 @@ ssl_options() ->
254262
end.
255263

256264
p(PathName) ->
257-
{ok, Path} = application:get_env(rabbitmq_auth_backend_http, PathName),
265+
{ok, Path} = application:get_env(?APP, PathName),
258266
Path.
259267

260268
q(Args) ->

0 commit comments

Comments
 (0)