Skip to content

Commit 9c075ad

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 auth backend supports 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, 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.
1 parent abd8568 commit 9c075ad

File tree

13 files changed

+397
-232
lines changed

13 files changed

+397
-232
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,41 @@
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
%%
5759
%% Possible responses:
5860
%% true
5961
%% false
62+
%% {false, Reason}
6063
%% {error, Error}
6164
%% Something went wrong. Log and die.
6265
-callback check_topic_access(rabbit_types:auth_user(),
6366
rabbit_types:r(atom()),
6467
rabbit_types:permission_atom(),
6568
rabbit_types:topic_access_context()) ->
66-
boolean() | {'error', any()}.
69+
boolean() | {false, Reason :: string()} | {'error', any()}.
6770

6871
%% Updates backend state that has expired.
6972
%%

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/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ auth_http.user_path = http://some-server/auth/user
5353
auth_http.vhost_path = http://some-server/auth/vhost
5454
auth_http.resource_path = http://some-server/auth/resource
5555
auth_http.topic_path = http://some-server/auth/topic
56+
auth_http.authorization_failure_disclosure = false
5657
```
5758

5859
In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanced-config-file):
@@ -65,7 +66,8 @@ In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanc
6566
{user_path, "http(s)://some-server/auth/user"},
6667
{vhost_path, "http(s)://some-server/auth/vhost"},
6768
{resource_path, "http(s)://some-server/auth/resource"},
68-
{topic_path, "http(s)://some-server/auth/topic"}]}
69+
{topic_path, "http(s)://some-server/auth/topic"},
70+
{authorization_failure_disclosure, false}]}
6971
].
7072
```
7173

@@ -124,6 +126,8 @@ containing:
124126

125127
* `deny`: deny access to the user / vhost / resource
126128
* `deny <Reason>`: deny access to the user / vhost / resource. RabbitMQ will log the `<Reason>` at INFO level.
129+
If `auth_http.authorization_failure_disclosure` is set to `true` (the default is `false` for security reasons)
130+
RabbitMQ will additionally forward the `<Reason>` to AMQP clients.
127131
* `allow`: allow access to the user / vhost / resource
128132
* `allow [list of tags]` (for `user_path` only): allow access, and mark the user as an having the tags listed
129133

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: 40 additions & 30 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() ->
@@ -37,7 +39,7 @@ user_login_authentication(Username, AuthProps) ->
3739
Path = p(user_path),
3840
Query = q([{username, Username}] ++ extract_other_credentials(AuthProps)),
3941
case http_req(Path, Query) of
40-
{error, _} = Err ->
42+
{error, _} = Err ->
4143
Err;
4244
"deny " ++ Reason ->
4345
?LOG_INFO("HTTP authentication denied for user '~ts': ~ts",
@@ -53,7 +55,9 @@ user_login_authentication(Username, AuthProps) ->
5355
{ok, #auth_user{
5456
username = Username,
5557
tags = Tags,
56-
impl = fun() -> proplists:delete(username, AuthProps) end}}
58+
impl = fun() -> proplists:delete(username, AuthProps) end}};
59+
Other ->
60+
{error, {bad_response, Other}}
5761
end
5862
end.
5963

@@ -120,34 +124,34 @@ check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost,
120124

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

128132
check_resource_access(#auth_user{username = Username, tags = Tags},
129133
#resource{virtual_host = VHost, kind = Type, name = Name},
130134
Permission,
131135
AuthzContext) ->
132136
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).
137+
req(resource_path, [{username, Username},
138+
{vhost, VHost},
139+
{resource, Type},
140+
{name, Name},
141+
{permission, Permission},
142+
{tags, join_tags(Tags)}] ++ OptionsParameters).
139143

140144
check_topic_access(#auth_user{username = Username, tags = Tags},
141145
#resource{virtual_host = VHost, kind = topic = Type, name = Name},
142146
Permission,
143147
Context) ->
144148
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).
149+
req(topic_path, [{username, Username},
150+
{vhost, VHost},
151+
{resource, Type},
152+
{name, Name},
153+
{permission, Permission},
154+
{tags, join_tags(Tags)}] ++ OptionsParameters).
151155

152156
expiry_timestamp(_) -> never.
153157

@@ -163,16 +167,21 @@ context_as_parameters(Options) when is_map(Options) ->
163167
context_as_parameters(_) ->
164168
[].
165169

166-
bool_req(PathName, Props) ->
170+
req(PathName, Props) ->
167171
Path = p(PathName),
168172
Query = q(Props),
169173
case http_req(Path, Query) of
170174
{error, _} = Err ->
171175
Err;
172176
"deny " ++ Reason ->
173-
?LOG_INFO("HTTP authorisation denied for path ~ts with query ~ts: ~ts",
177+
?LOG_INFO("HTTP authorization denied for path ~ts with query '~ts': ~ts",
174178
[Path, Query, Reason]),
175-
false;
179+
case application:get_env(?APP, authorization_failure_disclosure) of
180+
{ok, true} ->
181+
{false, Reason};
182+
_ ->
183+
false
184+
end;
176185
Body ->
177186
case string:lowercase(Body) of
178187
"deny" ->
@@ -201,7 +210,7 @@ do_http_req(Path0, Query) ->
201210
{host, Host} = lists:keyfind(host, 1, URI),
202211
{port, Port} = lists:keyfind(port, 1, URI),
203212
HostHdr = rabbit_misc:format("~ts:~b", [Host, Port]),
204-
{ok, Method} = application:get_env(rabbitmq_auth_backend_http, http_method),
213+
{ok, Method} = application:get_env(?APP, http_method),
205214
Request = case rabbit_data_coercion:to_atom(Method) of
206215
get ->
207216
Path = Path0 ++ "?" ++ Query,
@@ -212,21 +221,22 @@ do_http_req(Path0, Query) ->
212221
{Path0, [{"Host", HostHdr}], "application/x-www-form-urlencoded", Query}
213222
end,
214223
RequestTimeout =
215-
case application:get_env(rabbitmq_auth_backend_http, request_timeout) of
224+
case application:get_env(?APP, request_timeout) of
216225
{ok, Val1} -> Val1;
217226
_ -> infinity
218227
end,
219228
ConnectionTimeout =
220-
case application:get_env(rabbitmq_auth_backend_http, connection_timeout) of
229+
case application:get_env(?APP, connection_timeout) of
221230
{ok, Val2} -> Val2;
222231
_ -> RequestTimeout
223232
end,
224233
?LOG_DEBUG("auth_backend_http: request timeout: ~tp, connection timeout: ~tp", [RequestTimeout, ConnectionTimeout]),
225234
HttpOpts = [{timeout, RequestTimeout},
226235
{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]),
236+
case httpc:request(Method, Request, HttpOpts, [{body_format, binary}]) of
237+
{ok, {{_HTTP, Code, _}, _Headers, BodyBin}} ->
238+
Body = unicode:characters_to_list(BodyBin),
239+
?LOG_DEBUG("auth_backend_http: response code is ~tp, body: '~ts'", [Code, Body]),
230240
case lists:member(Code, ?SUCCESSFUL_RESPONSE_CODES) of
231241
true ->
232242
string:strip(Body);
@@ -238,10 +248,10 @@ do_http_req(Path0, Query) ->
238248
end.
239249

240250
ssl_options() ->
241-
case application:get_env(rabbitmq_auth_backend_http, ssl_options) of
251+
case application:get_env(?APP, ssl_options) of
242252
{ok, Opts0} when is_list(Opts0) ->
243253
Opts1 = [{ssl, rabbit_ssl_options:fix_client(Opts0)}],
244-
case application:get_env(rabbitmq_auth_backend_http, ssl_hostname_verification) of
254+
case application:get_env(?APP, ssl_hostname_verification) of
245255
{ok, wildcard} ->
246256
?LOG_DEBUG("Enabling wildcard-aware hostname verification for HTTP client connections"),
247257
%% Needed for HTTPS connections that connect to servers that use wildcard certificates.
@@ -254,7 +264,7 @@ ssl_options() ->
254264
end.
255265

256266
p(PathName) ->
257-
{ok, Path} = application:get_env(rabbitmq_auth_backend_http, PathName),
267+
{ok, Path} = application:get_env(?APP, PathName),
258268
Path.
259269

260270
q(Args) ->

0 commit comments

Comments
 (0)