Optionally return authz refusal reason to client

## What?

If the new config setting `authorization_failure_disclosure`
is set to `true`, (`false` by default), then some RabbitMQ authorization
backends (internal, HTTP, OAuth 2.0) will return the reason why access
was denied to the client. (In future, additional backends can be modified
to return a reason.)

 ## Why?

This helps debugging and troubleshooting directly in the client.
Some clients might not have access to the RabbitMQ logs, for other
clients 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 clients 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.
This commit is contained in:
David Ansari 2025-10-06 16:19:40 +02:00
parent 94a6c3f723
commit 7a00683770
14 changed files with 254 additions and 169 deletions

View File

@ -115,6 +115,7 @@ define PROJECT_ENV
{tracking_execution_timeout, 15000},
{stream_messages_soft_limit, 256},
{track_auth_attempt_source, false},
{authorization_failure_disclosure, false},
{credentials_obfuscation_fallback_secret, <<"nocookie">>},
{dead_letter_worker_consumer_prefetch, 32},
{dead_letter_worker_publisher_confirm_timeout, 180000},

View File

@ -361,6 +361,15 @@ check_access(Fun, Module, ErrStr, ErrArgs, ErrName) ->
ok;
false ->
rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs);
{false, Reason} ->
case application:get_env(rabbit, authorization_failure_disclosure) of
{ok, true} ->
FullErrStr = ErrStr ++ " by backend ~ts: ~ts",
FullErrArgs = ErrArgs ++ [Module, Reason],
rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs);
_ ->
rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs)
end;
{error, E} ->
FullErrStr = ErrStr ++ ", backend ~ts returned an error: ~tp",
FullErrArgs = ErrArgs ++ [Module, E],

View File

@ -143,7 +143,8 @@ check_resource_access(#auth_user{username = Username},
_AuthContext) ->
case rabbit_db_user:get_user_permissions(Username, VHostPath) of
undefined ->
false;
{false, rabbit_misc:format("user '~ts' has no permissions for vhost '~ts'",
[Username, VHostPath])};
#user_permission{permission = P} ->
PermRegexp = case element(permission_index(Permission), P) of
%% <<"^$">> breaks Emacs' erlang mode
@ -151,8 +152,12 @@ check_resource_access(#auth_user{username = Username},
RE -> RE
end,
case re:run(Name, PermRegexp, [{capture, none}]) of
match -> true;
nomatch -> false
match ->
true;
nomatch ->
{false, rabbit_misc:format(
"'~ts' does not match the permission regex '~ts'",
[Name, PermRegexp])}
end
end.
@ -170,12 +175,17 @@ check_topic_access(#auth_user{username = Username},
RE -> RE
end,
PermRegexpExpanded = expand_topic_permission(
PermRegexp,
maps:get(variable_map, Context, undefined)
),
case re:run(maps:get(routing_key, Context), PermRegexpExpanded, [{capture, none}]) of
match -> true;
nomatch -> false
PermRegexp,
maps:get(variable_map, Context, undefined)
),
Topic = maps:get(routing_key, Context),
case re:run(Topic, PermRegexpExpanded, [{capture, none}]) of
match ->
true;
nomatch ->
{false, rabbit_misc:format(
"topic '~ts' does not match the regex '~ts'",
[Topic, PermRegexpExpanded])}
end
end.

View File

@ -32,25 +32,27 @@
%% Possible responses:
%% true
%% false
%% {false, Reason}
%% {error, Error}
%% Something went wrong. Log and die.
-callback check_vhost_access(AuthUser :: rabbit_types:auth_user(),
VHost :: rabbit_types:vhost(),
AuthzData :: rabbit_types:authz_data()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.
%% Given #auth_user, resource and permission, can a user access a resource?
%%
%% Possible responses:
%% true
%% false
%% {false, Reason}
%% {error, Error}
%% Something went wrong. Log and die.
-callback check_resource_access(rabbit_types:auth_user(),
rabbit_types:r(atom()),
rabbit_types:permission_atom(),
rabbit_types:authz_context()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.
%% Given #auth_user, topic as resource, permission, and context, can a user access the topic?
%%
@ -63,7 +65,7 @@
rabbit_types:r(atom()),
rabbit_types:permission_atom(),
rabbit_types:topic_access_context()) ->
boolean() | {'error', any()}.
boolean() | {false, Reason :: string()} | {'error', any()}.
%% Updates backend state that has expired.
%%

View File

@ -36,8 +36,10 @@ init_per_suite(Config) ->
Config1 = rabbit_ct_helpers:set_config(
Config,
[{rmq_nodename_suffix, ?MODULE}]),
Config2 = rabbit_ct_helpers:merge_app_env(
Config1, {rabbit, [{authorization_failure_disclosure, true}]}),
rabbit_ct_helpers:run_setup_steps(
Config1,
Config2,
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()).
@ -109,10 +111,12 @@ amqp_x_cc_annotation(Config) ->
condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
description = {utf8, Description1}}}}} ->
?assertEqual(
<<"write access to topic 'x.1' in exchange 'amq.topic' in vhost '/' refused for user 'guest'">>,
<<"write access to topic 'x.1' in exchange 'amq.topic' in vhost '/' "
"refused for user 'guest' by backend rabbit_auth_backend_internal: "
"topic 'x.1' does not match the regex '^a'">>,
Description1)
after 30_000 -> amqp_utils:flush(missing_ended),
ct:fail({missing_event, ?LINE})
ct:fail({missing_event, ?LINE})
end,
{ok, Session3} = amqp10_client:begin_session_sync(Connection),
@ -132,10 +136,12 @@ amqp_x_cc_annotation(Config) ->
condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
description = {utf8, Description2}}}}} ->
?assertEqual(
<<"write access to topic 'x.2' in exchange 'amq.topic' in vhost '/' refused for user 'guest'">>,
<<"write access to topic 'x.2' in exchange 'amq.topic' in vhost '/' "
"refused for user 'guest' by backend rabbit_auth_backend_internal: "
"topic 'x.2' does not match the regex '^a'">>,
Description2)
after 30_000 -> amqp_utils:flush(missing_ended),
ct:fail({missing_event, ?LINE})
ct:fail({missing_event, ?LINE})
end,
{ok, #{message_count := 0}} = rabbitmq_amqp_client:delete_queue(LinkPair, QName1),
@ -190,8 +196,9 @@ amqpl_headers(Header, Config) ->
props = #'P_basic'{headers = [{Header, array, [{longstr, <<"a.2">>}]}]}}),
ok = assert_channel_down(
Ch1,
<<"ACCESS_REFUSED - write access to topic 'x.1' in exchange "
"'amq.topic' in vhost '/' refused for user 'guest'">>),
<<"ACCESS_REFUSED - write access to topic 'x.1' in exchange 'amq.topic' "
"in vhost '/' refused for user 'guest' by backend rabbit_auth_backend_internal: "
"topic 'x.1' does not match the regex '^a'">>),
Ch2 = rabbit_ct_client_helpers:open_channel(Config),
monitor(process, Ch2),
@ -203,8 +210,9 @@ amqpl_headers(Header, Config) ->
props = #'P_basic'{headers = [{Header, array, [{longstr, <<"x.2">>}]}]}}),
ok = assert_channel_down(
Ch2,
<<"ACCESS_REFUSED - write access to topic 'x.2' in exchange "
"'amq.topic' in vhost '/' refused for user 'guest'">>),
<<"ACCESS_REFUSED - write access to topic 'x.2' in exchange 'amq.topic' "
"in vhost '/' refused for user 'guest' by backend rabbit_auth_backend_internal: "
"topic 'x.2' does not match the regex '^a'">>),
Ch3 = rabbit_ct_client_helpers:open_channel(Config),
?assertEqual(#'queue.delete_ok'{message_count = 1},
@ -337,12 +345,14 @@ topic_permission_checks1(_Config) ->
Context
) || Perm <- Permissions],
%% user has access to exchange, routing key does not match
[false = rabbit_auth_backend_internal:check_topic_access(
User,
Topic,
Perm,
#{routing_key => <<"x.y.z">>}
) || Perm <- Permissions],
[?assertEqual(
{false, "topic 'x.y.z' does not match the regex '^a'"},
rabbit_auth_backend_internal:check_topic_access(
User,
Topic,
Perm,
#{routing_key => <<"x.y.z">>}
)) || Perm <- Permissions],
%% user has access to exchange but not on this vhost
%% let pass when there's no match
[true = rabbit_auth_backend_internal:check_topic_access(
@ -379,17 +389,20 @@ topic_permission_checks1(_Config) ->
}
) || Perm <- Permissions],
%% routing key KO
[false = rabbit_auth_backend_internal:check_topic_access(
User,
Topic#resource{virtual_host = <<"other-vhost">>},
Perm,
#{routing_key => <<"services.default.accounts.dummy.notifications">>,
variable_map => #{
<<"username">> => <<"guest">>,
<<"vhost">> => <<"other-vhost">>
}
}
) || Perm <- Permissions],
[?assertEqual(
{false, "topic 'services.default.accounts.dummy.notifications' does not "
"match the regex 'services.other-vhost.accounts.guest.notifications'"},
rabbit_auth_backend_internal:check_topic_access(
User,
Topic#resource{virtual_host = <<"other-vhost">>},
Perm,
#{routing_key => <<"services.default.accounts.dummy.notifications">>,
variable_map => #{
<<"username">> => <<"guest">>,
<<"vhost">> => <<"other-vhost">>
}
}
)) || Perm <- Permissions],
ok.
@ -407,11 +420,10 @@ clear_topic_permissions(Config) ->
Config, 0, rabbit_auth_backend_internal, clear_topic_permissions,
[<<"guest">>, <<"/">>, <<"acting-user">>]).
assert_channel_down(Ch, Reason) ->
assert_channel_down(Ch, ExpectedReason) ->
receive {'DOWN', _MonitorRef, process, Ch,
{shutdown,
{server_initiated_close, 403, Reason}}} ->
ok
{shutdown, {server_initiated_close, 403, ActualReason}}} ->
?assertEqual(ExpectedReason, ActualReason)
after 30_000 ->
ct:fail({did_not_receive, Reason})
ct:fail({missing_down, ExpectedReason})
end.

View File

@ -36,30 +36,21 @@ user_login_authorization(Username, AuthProps) ->
end).
check_vhost_access(#auth_user{} = AuthUser, VHostPath, AuthzData) ->
with_cache(authz, {check_vhost_access, [AuthUser, VHostPath, AuthzData]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_vhost_access, [AuthUser, VHostPath, AuthzData]},
fun convert_backend_result/1).
check_resource_access(#auth_user{} = AuthUser,
#resource{} = Resource, Permission, AuthzContext) ->
with_cache(authz, {check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_resource_access, [AuthUser, Resource, Permission, AuthzContext]},
fun convert_backend_result/1).
check_topic_access(#auth_user{} = AuthUser,
#resource{} = Resource, Permission, Context) ->
with_cache(authz, {check_topic_access, [AuthUser, Resource, Permission, Context]},
fun(true) -> success;
(false) -> refusal;
({error, _} = Err) -> Err;
(_) -> unknown
end).
with_cache(authz,
{check_topic_access, [AuthUser, Resource, Permission, Context]},
fun convert_backend_result/1).
expiry_timestamp(_) -> never.
@ -67,6 +58,12 @@ expiry_timestamp(_) -> never.
%% Implementation
%%
convert_backend_result(true) -> success;
convert_backend_result(false) -> refusal;
convert_backend_result({false, _}) -> refusal;
convert_backend_result({error, _} = Err) -> Err;
convert_backend_result(_) -> unknown.
clear_cache_cluster_wide() ->
Nodes = rabbit_nodes:list_running(),
?LOG_WARNING("Clearing auth_backend_cache in all nodes : ~p", [Nodes]),

View File

@ -18,7 +18,7 @@
-callback clear() -> ok.
expiration(TTL) ->
erlang:system_time(milli_seconds) + TTL.
erlang:system_time(millisecond) + TTL.
expired(Exp) ->
erlang:system_time(milli_seconds) > Exp.
erlang:system_time(millisecond) > Exp.

View File

@ -6,6 +6,7 @@
%%
-module(rabbit_auth_backend_cache_SUITE).
-include_lib("eunit/include/eunit.hrl").
-include_lib("rabbit_common/include/rabbit.hrl").
-compile(export_all).
@ -89,14 +90,24 @@ access_response(Config) ->
true = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, AvailableResource, configure, #{}]),
true = rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, AvailableResource, configure, #{}]),
false = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, RestrictedResource, configure, #{}]),
false = rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, RestrictedResource, configure, #{}]),
ExpectedResourceAccess = {false, "user 'guest' has no permissions for vhost 'restricted'"},
?assertEqual(
ExpectedResourceAccess,
rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, RestrictedResource, configure, #{}])),
?assertEqual(
ExpectedResourceAccess,
rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, RestrictedResource, configure, #{}])),
true = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, AuthorisedTopicContext]),
true = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, AuthorisedTopicContext]),
false = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
false = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]).
ExpectedTopicAccess = {false, "topic 'b.b' does not match the regex '^a'"},
?assertEqual(
ExpectedTopicAccess,
rpc(Config, rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext])),
?assertEqual(
ExpectedTopicAccess,
rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext])).
cache_expiration(Config) ->
AvailableVhost = <<"/">>,
@ -124,7 +135,7 @@ cache_expiration(Config) ->
false = rpc(Config,rabbit_auth_backend_internal, check_vhost_access, [Auth, AvailableVhost, undefined]),
true = rpc(Config,rabbit_auth_backend_cache, check_vhost_access, [Auth, AvailableVhost, undefined]),
false = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, AvailableResource, configure, #{}]),
{false, _} = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, AvailableResource, configure, #{}]),
true = rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, AvailableResource, configure, #{}]),
{ok, TTL} = rpc(Config, application, get_env, [rabbitmq_auth_backend_cache, cache_ttl]),
@ -135,8 +146,8 @@ cache_expiration(Config) ->
false = rpc(Config,rabbit_auth_backend_internal, check_vhost_access, [Auth, AvailableVhost, undefined]),
false = rpc(Config,rabbit_auth_backend_cache, check_vhost_access, [Auth, AvailableVhost, undefined]),
false = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, AvailableResource, configure, #{}]),
false = rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, AvailableResource, configure, #{}]).
{false, _} = rpc(Config,rabbit_auth_backend_internal, check_resource_access, [Auth, AvailableResource, configure, #{}]),
{false, _} = rpc(Config,rabbit_auth_backend_cache, check_resource_access, [Auth, AvailableResource, configure, #{}]).
cache_expiration_topic(Config) ->
AvailableVhost = <<"/">>,
@ -153,14 +164,14 @@ cache_expiration_topic(Config) ->
<<"guest">>, <<"/">>, <<"amq.topic">>, <<"^a">>, <<"^b">>, <<"acting-user">>
]),
false = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
{false, _} = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
true = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
{ok, TTL} = rpc(Config, application, get_env, [rabbitmq_auth_backend_cache, cache_ttl]),
timer:sleep(TTL),
false = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
false = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]).
{false, _} = rpc(Config,rabbit_auth_backend_internal, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]),
{false, _} = rpc(Config,rabbit_auth_backend_cache, check_topic_access, [Auth, TopicResource, write, RestrictedTopicContext]).
rpc(Config, M, F, A) ->
rabbit_ct_broker_helpers:rpc(Config, 0, M, F, A).

View File

@ -120,34 +120,34 @@ check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost,
do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData) ->
OptionsParameters = context_as_parameters(AuthzData),
bool_req(vhost_path, [{username, Username},
{vhost, VHost},
{ip, Ip},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(vhost_path, [{username, Username},
{vhost, VHost},
{ip, Ip},
{tags, join_tags(Tags)}] ++ OptionsParameters).
check_resource_access(#auth_user{username = Username, tags = Tags},
#resource{virtual_host = VHost, kind = Type, name = Name},
Permission,
AuthzContext) ->
OptionsParameters = context_as_parameters(AuthzContext),
bool_req(resource_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(resource_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
check_topic_access(#auth_user{username = Username, tags = Tags},
#resource{virtual_host = VHost, kind = topic = Type, name = Name},
Permission,
Context) ->
OptionsParameters = context_as_parameters(Context),
bool_req(topic_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
req(topic_path, [{username, Username},
{vhost, VHost},
{resource, Type},
{name, Name},
{permission, Permission},
{tags, join_tags(Tags)}] ++ OptionsParameters).
expiry_timestamp(_) -> never.
@ -163,7 +163,7 @@ context_as_parameters(Options) when is_map(Options) ->
context_as_parameters(_) ->
[].
bool_req(PathName, Props) ->
req(PathName, Props) ->
Path = p(PathName),
Query = q(Props),
case http_req(Path, Query) of
@ -172,7 +172,7 @@ bool_req(PathName, Props) ->
"deny " ++ Reason ->
?LOG_INFO("HTTP authorisation denied for path ~ts with query ~ts: ~ts",
[Path, Query, Reason]),
false;
{false, Reason};
Body ->
case string:lowercase(Body) of
"deny" ->

View File

@ -4,7 +4,7 @@
%%
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
-module(auth_SUITE).
-module(auth_unit_SUITE).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").

View File

@ -24,37 +24,56 @@
-spec vhost_access(binary(), [binary()]) -> boolean().
vhost_access(VHost, Scopes) ->
PermissionScopes = get_scope_permissions(Scopes),
lists:any(
fun({VHostPattern, _, _, _}) ->
wildcard:match(VHost, VHostPattern)
end,
PermissionScopes).
case lists:any(
fun({VHostPattern, _, _, _}) ->
wildcard:match(VHost, VHostPattern)
end,
PermissionScopes) of
true ->
true;
false ->
{false, rabbit_misc:format("no scope in ~tp matches vhost '~ts'",
[Scopes, VHost])}
end.
-spec resource_access(rabbit_types:r(atom()), permission(), [binary()]) -> boolean().
resource_access(#resource{virtual_host = VHost, name = Name},
resource_access(#resource{virtual_host = VHost, name = Name} = Resource,
Permission, Scopes) ->
lists:any(
fun({VHostPattern, NamePattern, _, ScopeGrantedPermission}) ->
wildcard:match(VHost, VHostPattern) andalso
wildcard:match(Name, NamePattern) andalso
Permission =:= ScopeGrantedPermission
end,
get_scope_permissions(Scopes)).
case lists:any(
fun({VHostPattern, NamePattern, _, ScopeGrantedPermission}) ->
wildcard:match(VHost, VHostPattern) andalso
wildcard:match(Name, NamePattern) andalso
Permission =:= ScopeGrantedPermission
end,
get_scope_permissions(Scopes)) of
true ->
true;
false ->
{false, rabbit_misc:format("no scope in ~tp has '~s' permission for ~ts",
[Scopes, Permission, rabbit_misc:rs(Resource)])}
end.
-spec topic_access(rabbit_types:r(atom()), permission(), map(), [binary()]) -> boolean().
topic_access(#resource{virtual_host = VHost, name = ExchangeName},
topic_access(#resource{virtual_host = VHost, name = ExchangeName} = Resource,
Permission,
#{routing_key := RoutingKey},
Scopes) ->
lists:any(
fun({VHostPattern, ExchangeNamePattern, RoutingKeyPattern, ScopeGrantedPermission}) ->
is_binary(RoutingKeyPattern) andalso
wildcard:match(VHost, VHostPattern) andalso
wildcard:match(ExchangeName, ExchangeNamePattern) andalso
wildcard:match(RoutingKey, RoutingKeyPattern) andalso
Permission =:= ScopeGrantedPermission
end,
get_scope_permissions(Scopes)).
case lists:any(
fun({VHostPattern, ExchangeNamePattern, RoutingKeyPattern, ScopeGrantedPermission}) ->
is_binary(RoutingKeyPattern) andalso
wildcard:match(VHost, VHostPattern) andalso
wildcard:match(ExchangeName, ExchangeNamePattern) andalso
wildcard:match(RoutingKey, RoutingKeyPattern) andalso
Permission =:= ScopeGrantedPermission
end,
get_scope_permissions(Scopes)) of
true ->
true;
false ->
{false, rabbit_misc:format(
"no scope in ~tp has '~s' permission for exchange ~ts and topic '~ts'",
[Scopes, Permission, rabbit_misc:rs(Resource), RoutingKey])}
end.
%% Internal -------------------------------------------------------------------

View File

@ -378,14 +378,15 @@ configure_refused(Vhost, Resource, Scope) ->
resource_perm(Vhost, Resource, Scope, configure, false).
resource_perm(Vhost, Resource, Scopes, Permission, Result) when is_list(Scopes) ->
[ ?assertEqual(Result, rabbit_oauth2_scope:resource_access(
#resource{virtual_host = Vhost,
kind = Kind,
name = Resource},
Permission,
Scopes)) || Kind <- [queue, exchange] ];
resource_perm(Vhost, Name, Scopes, Permission, Expected) when is_list(Scopes) ->
[begin
Resource = #resource{virtual_host = Vhost,
kind = Kind,
name = Name},
Actual = rabbit_oauth2_scope:resource_access(Resource, Permission, Scopes),
assert(Expected, Actual)
end
|| Kind <- [queue, exchange]];
resource_perm(Vhost, Resource, Scope, Permission, Result) ->
resource_perm(Vhost, Resource, [Scope], Permission, Result).
@ -401,14 +402,19 @@ topic_read_refused(Vhost, Resource, RoutingKey, Scopes) when is_list(Scopes) ->
topic_read_refused(Vhost, Resource, RoutingKey, Scope) ->
topic_perm(Vhost, Resource, RoutingKey, Scope, read, false).
topic_perm(Vhost, Resource, RoutingKey, Scopes, Permission, Result) when is_list(Scopes) ->
?assertEqual(Result, rabbit_oauth2_scope:topic_access(
#resource{virtual_host = Vhost,
kind = topic,
name = Resource},
Permission,
#{routing_key => RoutingKey},
Scopes));
topic_perm(Vhost, Name, RoutingKey, Scopes, Permission, Expected) when is_list(Scopes) ->
Resource = #resource{virtual_host = Vhost,
kind = topic,
name = Name},
Actual = rabbit_oauth2_scope:topic_access(Resource,
Permission,
#{routing_key => RoutingKey},
Scopes),
assert(Expected, Actual);
topic_perm(Vhost, Resource, RoutingKey, Scope, Permission, Result) ->
topic_perm(Vhost, Resource, RoutingKey, [Scope], Permission, Result).
assert(true, Actual) ->
?assert(Actual);
assert(false, Actual) ->
?assertMatch({false, _Reason}, Actual).

View File

@ -1131,16 +1131,21 @@ test_restricted_vhost_access_with_a_valid_token(_) ->
Jwk = ?UTIL_MOD:fixture_jwk(),
Token = ?UTIL_MOD:sign_token_hs(?UTIL_MOD:token_with_sub(
?UTIL_MOD:fixture_token(), Username), Jwk),
?UTIL_MOD:fixture_token(), Username), Jwk),
UaaEnv = [{signing_keys, #{<<"token-key">> => {map, Jwk}}}],
set_env(key_config, UaaEnv),
%% this user can authenticate successfully and access certain vhosts
{ok, #auth_user{username = Username, tags = []} = User} =
user_login_authentication(Username, [{password, Token}]),
user_login_authentication(Username, [{password, Token}]),
%% access to a different vhost
?assertEqual(false, check_vhost_access(User, <<"different vhost">>, none)).
?assertEqual({false,
"no scope in [<<\"configure:vhost/foo\">>,<<\"read:vhost/bar\">>,\n"
" <<\"read:vhost/bar/%23%2Ffoo\">>,<<\"read:vhost/foo\">>,\n"
" <<\"write:vhost/foo\">>]"
" matches vhost 'different vhost'"},
check_vhost_access(User, <<"different vhost">>, none)).
test_insufficient_permissions_in_a_valid_token(_) ->
VHost = <<"vhost">>,
@ -1158,9 +1163,19 @@ test_insufficient_permissions_in_a_valid_token(_) ->
%% access to these resources is not granted
assert_resource_access_denied(User, VHost, <<"foo1">>, configure),
assert_resource_access_denied(User, VHost, <<"bar">>, write),
assert_topic_access_refused(User, VHost, <<"bar">>, read,
#{routing_key => <<"foo/#">>}).
ExpectedReason0 = "no scope in [<<\"configure:vhost/foo\">>,<<\"read:vhost/bar\">>,\n"
" <<\"read:vhost/bar/%23%2Ffoo\">>,<<\"read:vhost/foo\">>,\n"
" <<\"write:vhost/foo\">>]"
" has 'write' permission for queue 'bar' in vhost 'vhost'",
assert_resource_access_response({false, ExpectedReason0}, User, VHost, <<"bar">>, write),
ExpectedReason1 = "no scope in [<<\"configure:vhost/foo\">>,<<\"read:vhost/bar\">>,\n"
" <<\"read:vhost/bar/%23%2Ffoo\">>,<<\"read:vhost/foo\">>,\n"
" <<\"write:vhost/foo\">>]"
" has 'read' permission for exchange 'bar' in vhost 'vhost' and topic 'foo/#'",
assert_topic_access_response({false, ExpectedReason1}, User, VHost, <<"bar">>, read,
#{routing_key => <<"foo/#">>}).
test_invalid_signature(_) ->
Username = <<"username">>,
@ -1470,6 +1485,7 @@ test_extract_scope_from_path_expression(_) ->
set_env(Par, Var) ->
application:set_env(rabbitmq_auth_backend_oauth2, Par, Var).
unset_env(Par) ->
application:unset_env(rabbitmq_auth_backend_oauth2, Par).
@ -1479,9 +1495,8 @@ assert_vhost_access_granted(AuthUser, VHost) ->
assert_vhost_access_denied(AuthUser, VHost) ->
assert_vhost_access_response(false, AuthUser, VHost).
assert_vhost_access_response(ExpectedResult, AuthUser, VHost) ->
?assertEqual(ExpectedResult,
check_vhost_access(AuthUser, VHost, none)).
assert_vhost_access_response(Expected, AuthUser, VHost) ->
assert(Expected, check_vhost_access(AuthUser, VHost, none)).
assert_resource_access_granted(AuthUser, VHost, ResourceName, PermissionKind) ->
assert_resource_access_response(true, AuthUser, VHost, ResourceName,
@ -1496,13 +1511,10 @@ assert_resource_access_errors(ExpectedError, AuthUser, VHost, ResourceName,
assert_resource_access_response({error, ExpectedError}, AuthUser, VHost,
ResourceName, PermissionKind).
assert_resource_access_response(ExpectedResult, AuthUser, VHost, ResourceName,
PermissionKind) ->
?assertEqual(ExpectedResult,
rabbit_auth_backend_oauth2:check_resource_access(
AuthUser,
rabbit_misc:r(VHost, queue, ResourceName),
PermissionKind, #{})).
assert_resource_access_response(ExpectedResult, AuthUser, VHost,
ResourceName, PermissionKind) ->
assert_resource_access_response(ExpectedResult, AuthUser, VHost, queue,
ResourceName, PermissionKind).
assert_resource_access_granted(AuthUser, VHost, ResourceKind, ResourceName,
PermissionKind) ->
@ -1519,13 +1531,13 @@ assert_resource_access_errors(ExpectedError, AuthUser, VHost, ResourceKind,
assert_resource_access_response({error, ExpectedError}, AuthUser, VHost,
ResourceKind, ResourceName, PermissionKind).
assert_resource_access_response(ExpectedResult, AuthUser, VHost, ResourceKind,
ResourceName, PermissionKind) ->
?assertEqual(ExpectedResult,
rabbit_auth_backend_oauth2:check_resource_access(
AuthUser,
rabbit_misc:r(VHost, ResourceKind, ResourceName),
PermissionKind, #{})).
assert_resource_access_response(Expected, AuthUser, VHost, ResourceKind,
ResourceName, PermissionKind) ->
Actual = rabbit_auth_backend_oauth2:check_resource_access(
AuthUser,
rabbit_misc:r(VHost, ResourceKind, ResourceName),
PermissionKind, #{}),
assert(Expected, Actual).
assert_topic_access_granted(AuthUser, VHost, ResourceName, PermissionKind,
AuthContext) ->
@ -1537,13 +1549,18 @@ assert_topic_access_refused(AuthUser, VHost, ResourceName, PermissionKind,
assert_topic_access_response(false, AuthUser, VHost, ResourceName,
PermissionKind, AuthContext).
assert_topic_access_response(ExpectedResult, AuthUser, VHost, ResourceName,
PermissionKind, AuthContext) ->
?assertEqual(ExpectedResult,
rabbit_auth_backend_oauth2:check_topic_access(
AuthUser,
#resource{virtual_host = VHost,
kind = topic,
name = ResourceName},
PermissionKind,
AuthContext)).
assert_topic_access_response(Expected, AuthUser, VHost, ResourceName,
PermissionKind, AuthContext) ->
Actual = rabbit_auth_backend_oauth2:check_topic_access(
AuthUser,
#resource{virtual_host = VHost,
kind = topic,
name = ResourceName},
PermissionKind,
AuthContext),
assert(Expected, Actual).
assert(false, Actual) ->
?assertMatch({false, _Reason}, Actual);
assert(Expected, Actual) ->
?assertEqual(Expected, Actual).

View File

@ -1609,8 +1609,9 @@ binding_action_with_checks(QName, TopicFilter, BindingArgs, Action,
fun rabbit_binding:Action/2, AuthState)
else
{error, Reason} = Err ->
?LOG_ERROR("Failed to ~s binding between ~s and ~s for topic filter ~s: ~p",
[Action, rabbit_misc:rs(ExchangeName), rabbit_misc:rs(QName), TopicFilter, Reason]),
?LOG_ERROR(
"Failed to ~s binding between ~ts and ~ts for topic filter ~ts: ~tp",
[Action, rabbit_misc:rs(ExchangeName), rabbit_misc:rs(QName), TopicFilter, Reason]),
Err
end.
@ -2292,7 +2293,7 @@ check_resource_access(User, Resource, Perm, Context) ->
catch
exit:#amqp_error{name = access_refused,
explanation = Msg} ->
?LOG_ERROR("MQTT resource access refused: ~s", [Msg]),
?LOG_ERROR("MQTT resource access refused: ~ts", [Msg]),
{error, access_refused}
end
end.
@ -2326,7 +2327,7 @@ check_topic_access(
catch
exit:#amqp_error{name = access_refused,
explanation = Msg} ->
?LOG_ERROR("MQTT topic access refused: ~s", [Msg]),
?LOG_ERROR("MQTT topic access refused: ~ts", [Msg]),
{error, access_refused}
end
end.