Skip to content

Commit ce1f682

Browse files
Khepri: distinguish missing keys from errors in one place
1 parent b4201b9 commit ce1f682

3 files changed

Lines changed: 115 additions & 5 deletions

File tree

deps/rabbit/src/rabbit_auth_backend_internal.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ check_topic_access(#auth_user{username = Username},
163163
case rabbit_db_user:get_topic_permissions(Username, VHostPath, Name) of
164164
undefined ->
165165
true;
166+
{error, _} = Err ->
167+
Err;
166168
#topic_permission{permission = P} ->
167169
PermRegexp = case element(permission_index(Permission), P) of
168170
%% <<"^$">> breaks Emacs' erlang mode

deps/rabbit/src/rabbit_db_user.erl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,14 @@ clear_all_permissions_for_vhost_in_khepri_tx(VHostName) ->
404404
Username :: rabbit_types:username(),
405405
VHostName :: vhost:name(),
406406
ExchangeName :: binary(),
407-
Ret :: TopicPermission | undefined,
407+
Ret :: TopicPermission | undefined | {error, term()},
408408
TopicPermission :: #topic_permission{}.
409409
%% @doc Returns the topic permissions for the given user and exchange in the
410410
%% given virtual host.
411411
%%
412-
%% @returns the topic permissions record if any, or `undefined'.
412+
%% @returns the topic permissions record if any, `undefined' if no topic
413+
%% permissions are set, or `{error, Reason}' if the metadata store query
414+
%% fails.
413415
%%
414416
%% @private
415417

@@ -419,8 +421,12 @@ get_topic_permissions(Username, VHostName, ExchangeName)
419421
is_binary(ExchangeName) ->
420422
Path = khepri_topic_permission_path(Username, VHostName, ExchangeName),
421423
case rabbit_khepri:get(Path) of
422-
{ok, TopicPermission} -> TopicPermission;
423-
_ -> undefined
424+
{ok, TopicPermission} ->
425+
TopicPermission;
426+
{error, {khepri, node_not_found, _}} ->
427+
undefined;
428+
{error, _} = Error ->
429+
Error
424430
end.
425431

426432
%% -------------------------------------------------------------------

deps/rabbit/test/topic_permission_SUITE.erl

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
-module(topic_permission_SUITE).
99

10+
-include_lib("proper/include/proper.hrl").
1011
-include_lib("eunit/include/eunit.hrl").
1112
-include_lib("amqp10_common/include/amqp10_framing.hrl").
1213
-include_lib("amqp_client/include/amqp_client.hrl").
@@ -26,7 +27,9 @@ groups() ->
2627
amqpl_cc_headers,
2728
amqpl_bcc_headers,
2829
topic_permission_database_access,
29-
topic_permission_checks
30+
topic_permission_checks,
31+
topic_permission_khepri_error_fails_closed,
32+
topic_permission_khepri_error_fails_closed_prop
3033
]}
3134
].
3235

@@ -393,6 +396,105 @@ topic_permission_checks1(_Config) ->
393396

394397
ok.
395398

399+
%% Topic permission checks must fail closed on metadata store errors.
400+
topic_permission_khepri_error_fails_closed(Config) ->
401+
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
402+
?MODULE, topic_permission_khepri_error_fails_closed1, [Config]).
403+
404+
topic_permission_khepri_error_fails_closed1(_Config) ->
405+
clear_tables(),
406+
rabbit_vhost:add(<<"/">>, <<"acting-user">>),
407+
rabbit_auth_backend_internal:add_user(<<"guest">>, <<"guest">>, <<"acting-user">>),
408+
ok = rabbit_auth_backend_internal:set_topic_permissions(
409+
<<"guest">>, <<"/">>, <<"amq.topic">>, "^a", "^a", <<"acting-user">>),
410+
411+
User = #auth_user{username = <<"guest">>},
412+
Topic = #resource{name = <<"amq.topic">>, virtual_host = <<"/">>,
413+
kind = topic},
414+
Context = #{routing_key => <<"secret.key">>},
415+
416+
%% Baseline: routing key does not match "^a", so access is denied
417+
false = rabbit_auth_backend_internal:check_topic_access(
418+
User, Topic, write, Context),
419+
420+
%% Mock rabbit_khepri:get to simulate a Khepri timeout
421+
ok = meck:new(rabbit_khepri, [passthrough]),
422+
meck:expect(rabbit_khepri, get,
423+
fun(_Path) -> {error, timeout} end),
424+
try
425+
%% Must return {error, timeout}, not 'true'
426+
{error, timeout} = rabbit_auth_backend_internal:check_topic_access(
427+
User, Topic, write, Context),
428+
%% Also test with noproc
429+
meck:expect(rabbit_khepri, get,
430+
fun(_Path) -> {error, noproc} end),
431+
{error, noproc} = rabbit_auth_backend_internal:check_topic_access(
432+
User, Topic, write, Context)
433+
after
434+
meck:unload(rabbit_khepri)
435+
end,
436+
437+
%% After unmocking, normal behavior resumes
438+
false = rabbit_auth_backend_internal:check_topic_access(
439+
User, Topic, write, Context),
440+
true = rabbit_auth_backend_internal:check_topic_access(
441+
User, Topic, write, #{routing_key => <<"a.b.c">>}),
442+
ok.
443+
444+
%% Property: for any error reason, check_topic_access must propagate it.
445+
topic_permission_khepri_error_fails_closed_prop(Config) ->
446+
ok = rabbit_ct_broker_helpers:rpc(Config, 0,
447+
?MODULE, topic_permission_khepri_error_fails_closed_prop1, [Config]).
448+
449+
topic_permission_khepri_error_fails_closed_prop1(_Config) ->
450+
clear_tables(),
451+
rabbit_vhost:add(<<"/">>, <<"acting-user">>),
452+
rabbit_auth_backend_internal:add_user(<<"guest">>, <<"guest">>, <<"acting-user">>),
453+
ok = rabbit_auth_backend_internal:set_topic_permissions(
454+
<<"guest">>, <<"/">>, <<"amq.topic">>, "^a", "^a", <<"acting-user">>),
455+
ok = meck:new(rabbit_khepri, [passthrough]),
456+
try
457+
Property = fun() -> prop_khepri_error_fails_closed() end,
458+
rabbit_ct_proper_helpers:run_proper(Property, [], 100)
459+
after
460+
meck:unload(rabbit_khepri)
461+
end,
462+
ok.
463+
464+
prop_khepri_error_fails_closed() ->
465+
?FORALL(
466+
{ErrorReason, RoutingKey, Permission},
467+
{khepri_error_reason(), topic_routing_key(), oneof([read, write])},
468+
begin
469+
meck:expect(rabbit_khepri, get,
470+
fun(_Path) -> {error, ErrorReason} end),
471+
User = #auth_user{username = <<"guest">>},
472+
Topic = #resource{name = <<"amq.topic">>, virtual_host = <<"/">>,
473+
kind = topic},
474+
Context = #{routing_key => RoutingKey},
475+
Result = rabbit_auth_backend_internal:check_topic_access(
476+
User, Topic, Permission, Context),
477+
?WHENFAIL(
478+
ct:pal("FAIL: error=~p, routing_key=~p, perm=~p, result=~p",
479+
[ErrorReason, RoutingKey, Permission, Result]),
480+
Result =:= {error, ErrorReason})
481+
end).
482+
483+
khepri_error_reason() ->
484+
oneof([timeout, noproc,
485+
{timeout, {gen_server, call, [fake_pid]}},
486+
{nodedown, 'node@host'},
487+
{no_more_servers_to_try, []},
488+
{badmatch, undefined}]).
489+
490+
topic_routing_key() ->
491+
oneof([<<"a.b.c">>, <<"secret.key">>, <<"public.1">>,
492+
<<"x.y.z">>, <<"">>,
493+
?LET(Parts,
494+
non_empty(list(oneof([<<"a">>, <<"b">>, <<"secret">>,
495+
<<"public">>, <<"x">>]))),
496+
iolist_to_binary(lists:join(<<".">>, Parts)))]).
497+
396498
clear_tables() ->
397499
ok = rabbit_db_vhost:clear(),
398500
ok = rabbit_db_user:clear().

0 commit comments

Comments
 (0)