Add expiry_timestamp/1 callback to authz backend behavior
Backends return 'never' or the timestamp of the expiry time of the credentials. Only the OAuth2 backend returns a timestamp, other RabbitMQ authz backends return 'never'. Client code uses rabbit_access_control, so it contains now a new expiry_timestamp/1 function that returns the earliest expiry time of the underlying backends. Fixes #10298
This commit is contained in:
parent
5abeb753f3
commit
33c64d06ea
|
@ -823,6 +823,16 @@ rabbitmq_suite(
|
|||
],
|
||||
)
|
||||
|
||||
rabbitmq_suite(
|
||||
name = "rabbit_access_control_SUITE",
|
||||
runtime_deps = [
|
||||
"@meck//:erlang_app",
|
||||
],
|
||||
deps = [
|
||||
"//deps/rabbit_common:erlang_app",
|
||||
],
|
||||
)
|
||||
|
||||
rabbitmq_integration_suite(
|
||||
name = "rabbit_stream_queue_SUITE",
|
||||
size = "large",
|
||||
|
|
|
@ -1441,6 +1441,15 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
|
|||
app_name = "rabbit",
|
||||
erlc_opts = "//:test_erlc_opts",
|
||||
)
|
||||
erlang_bytecode(
|
||||
name = "rabbit_access_control_SUITE_beam_files",
|
||||
testonly = True,
|
||||
srcs = ["test/rabbit_access_control_SUITE.erl"],
|
||||
outs = ["test/rabbit_access_control_SUITE.beam"],
|
||||
app_name = "rabbit",
|
||||
erlc_opts = "//:test_erlc_opts",
|
||||
deps = ["//deps/rabbit_common:erlang_app"],
|
||||
)
|
||||
erlang_bytecode(
|
||||
name = "rabbitmq_queues_cli_integration_SUITE_beam_files",
|
||||
testonly = True,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
-export([check_user_pass_login/2, check_user_login/2, check_user_loopback/2,
|
||||
check_vhost_access/4, check_resource_access/4, check_topic_access/4]).
|
||||
|
||||
-export([permission_cache_can_expire/1, update_state/2]).
|
||||
-export([permission_cache_can_expire/1, update_state/2, expiry_timestamp/1]).
|
||||
|
||||
%%----------------------------------------------------------------------------
|
||||
|
||||
|
@ -256,3 +256,17 @@ update_state(User = #user{authz_backends = Backends0}, NewState) ->
|
|||
%% otherwise returns false.
|
||||
permission_cache_can_expire(#user{authz_backends = Backends}) ->
|
||||
lists:any(fun ({Module, _State}) -> Module:state_can_expire() end, Backends).
|
||||
|
||||
-spec expiry_timestamp(User :: rabbit_types:user()) -> integer | never.
|
||||
expiry_timestamp(User = #user{authz_backends = Modules}) ->
|
||||
lists:foldl(fun({Module, Impl}, Ts0) ->
|
||||
case Module:expiry_timestamp(auth_user(User, Impl)) of
|
||||
Ts1 when is_integer(Ts0) andalso is_integer(Ts1)
|
||||
andalso Ts1 > Ts0 ->
|
||||
Ts0;
|
||||
Ts1 when is_integer(Ts1) ->
|
||||
Ts1;
|
||||
_ ->
|
||||
Ts0
|
||||
end
|
||||
end, never, Modules).
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
list_user_vhost_permissions/2,
|
||||
list_user_topic_permissions/1, list_vhost_topic_permissions/1, list_user_vhost_topic_permissions/2]).
|
||||
|
||||
-export([state_can_expire/0]).
|
||||
-export([state_can_expire/0, expiry_timestamp/1]).
|
||||
|
||||
-export([hashing_module_for_user/1, expand_topic_permission/2]).
|
||||
|
||||
|
@ -111,6 +111,8 @@ user_login_authentication(Username, AuthProps) ->
|
|||
|
||||
state_can_expire() -> false.
|
||||
|
||||
expiry_timestamp(_) -> never.
|
||||
|
||||
user_login_authorization(Username, _AuthProps) ->
|
||||
case user_login_authentication(Username, []) of
|
||||
{ok, #auth_user{impl = Impl, tags = Tags}} -> {ok, Impl, Tags};
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
%% This Source Code Form is subject to the terms of the Mozilla Public
|
||||
%% License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
%%
|
||||
%% Copyright (c) 2024 Broadcom. All Rights Reserved.
|
||||
%% The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
|
||||
%%
|
||||
|
||||
|
||||
-module(rabbit_access_control_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("rabbit_common/include/rabbit.hrl").
|
||||
|
||||
%%%===================================================================
|
||||
%%% Common Test callbacks
|
||||
%%%===================================================================
|
||||
|
||||
|
||||
all() ->
|
||||
[{group, tests}].
|
||||
|
||||
%% replicate eunit like test resolution
|
||||
all_tests() ->
|
||||
[F
|
||||
|| {F, _} <- ?MODULE:module_info(functions),
|
||||
re:run(atom_to_list(F), "_test$") /= nomatch].
|
||||
|
||||
groups() ->
|
||||
[{tests, [], all_tests()}].
|
||||
|
||||
init_per_suite(Config) ->
|
||||
Config.
|
||||
|
||||
end_per_suite(_Config) ->
|
||||
ok.
|
||||
|
||||
init_per_group(_Group, Config) ->
|
||||
Config.
|
||||
|
||||
end_per_group(_Group, _Config) ->
|
||||
ok.
|
||||
|
||||
init_per_testcase(_TestCase, Config) ->
|
||||
Config.
|
||||
|
||||
end_per_testcase(_TestCase, _Config) ->
|
||||
meck:unload(),
|
||||
ok.
|
||||
|
||||
expiry_timestamp_test(_) ->
|
||||
%% test rabbit_access_control:expiry_timestamp/1 returns the earliest expiry time
|
||||
Now = os:system_time(seconds),
|
||||
BeforeNow = Now - 60,
|
||||
%% returns now
|
||||
ok = meck:new(rabbit_expiry_backend, [non_strict]),
|
||||
meck:expect(rabbit_expiry_backend, expiry_timestamp, fun (_) -> Now end),
|
||||
%% return a bit before now (so the earliest expiry time)
|
||||
ok = meck:new(rabbit_earlier_expiry_backend, [non_strict]),
|
||||
meck:expect(rabbit_earlier_expiry_backend, expiry_timestamp, fun (_) -> BeforeNow end),
|
||||
%% return 'never' (no expiry)
|
||||
ok = meck:new(rabbit_no_expiry_backend, [non_strict]),
|
||||
meck:expect(rabbit_no_expiry_backend, expiry_timestamp, fun (_) -> never end),
|
||||
|
||||
%% never expires
|
||||
User1 = #user{authz_backends = [{rabbit_no_expiry_backend, unused}]},
|
||||
?assertEqual(never, rabbit_access_control:expiry_timestamp(User1)),
|
||||
|
||||
%% returns the result from the backend that expires
|
||||
User2 = #user{authz_backends = [{rabbit_expiry_backend, unused},
|
||||
{rabbit_no_expiry_backend, unused}]},
|
||||
?assertEqual(Now, rabbit_access_control:expiry_timestamp(User2)),
|
||||
|
||||
%% returns earliest expiry time
|
||||
User3 = #user{authz_backends = [{rabbit_expiry_backend, unused},
|
||||
{rabbit_earlier_expiry_backend, unused},
|
||||
{rabbit_no_expiry_backend, unused}]},
|
||||
?assertEqual(BeforeNow, rabbit_access_control:expiry_timestamp(User3)),
|
||||
|
||||
%% returns earliest expiry time
|
||||
User4 = #user{authz_backends = [{rabbit_earlier_expiry_backend, unused},
|
||||
{rabbit_expiry_backend, unused},
|
||||
{rabbit_no_expiry_backend, unused}]},
|
||||
?assertEqual(BeforeNow, rabbit_access_control:expiry_timestamp(User4)),
|
||||
|
||||
%% returns earliest expiry time
|
||||
User5 = #user{authz_backends = [{rabbit_no_expiry_backend, unused},
|
||||
{rabbit_earlier_expiry_backend, unused},
|
||||
{rabbit_expiry_backend, unused}]},
|
||||
?assertEqual(BeforeNow, rabbit_access_control:expiry_timestamp(User5)),
|
||||
|
||||
%% returns earliest expiry time
|
||||
User6 = #user{authz_backends = [{rabbit_no_expiry_backend, unused},
|
||||
{rabbit_expiry_backend, unused},
|
||||
{rabbit_earlier_expiry_backend, unused}]},
|
||||
?assertEqual(BeforeNow, rabbit_access_control:expiry_timestamp(User6)),
|
||||
|
||||
%% returns the result from the backend that expires
|
||||
User7 = #user{authz_backends = [{rabbit_no_expiry_backend, unused},
|
||||
{rabbit_expiry_backend, unused}]},
|
||||
?assertEqual(Now, rabbit_access_control:expiry_timestamp(User7)),
|
||||
ok.
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
||||
state_can_expire/0,
|
||||
state_can_expire/0, expiry_timestamp/1,
|
||||
get/1, init/0]).
|
||||
|
||||
init() ->
|
||||
|
@ -42,5 +42,8 @@ check_topic_access(#auth_user{}, #resource{}, _Permission, TopicContext) ->
|
|||
|
||||
state_can_expire() -> false.
|
||||
|
||||
expiry_timestamp(_) ->
|
||||
never.
|
||||
|
||||
get(K) ->
|
||||
ets:lookup(?MODULE, K).
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
-export([user/0]).
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4]).
|
||||
-export([state_can_expire/0]).
|
||||
-export([state_can_expire/0, expiry_timestamp/1]).
|
||||
|
||||
-spec user() -> rabbit_types:user().
|
||||
|
||||
|
@ -37,3 +37,4 @@ check_resource_access(#auth_user{}, #resource{}, _Permission, _Context) -> true.
|
|||
check_topic_access(#auth_user{}, #resource{}, _Permission, _Context) -> true.
|
||||
|
||||
state_can_expire() -> false.
|
||||
expiry_timestamp(_) -> never.
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
boolean() | {'error', any()}.
|
||||
|
||||
%% Returns true for backends that support state or credential expiration (e.g. use JWTs).
|
||||
%% @deprecated Please use {@link expiry_timestamp/1} instead.
|
||||
-callback state_can_expire() -> boolean().
|
||||
|
||||
%% Updates backend state that has expired.
|
||||
|
@ -85,4 +86,14 @@
|
|||
{'refused', string(), [any()]} |
|
||||
{'error', any()}.
|
||||
|
||||
%% Get expiry timestamp for the user.
|
||||
%%
|
||||
%% Possible responses:
|
||||
%% never
|
||||
%% The user token/credentials never expire.
|
||||
%% Timestamp
|
||||
%% The expiry time (POSIX) in seconds of the token/credentials.
|
||||
-callback expiry_timestamp(AuthUser :: rabbit_types:auth_user()) ->
|
||||
integer() | never.
|
||||
|
||||
-optional_callbacks([update_state/2]).
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
||||
state_can_expire/0]).
|
||||
state_can_expire/0, expiry_timestamp/1]).
|
||||
|
||||
%% API
|
||||
|
||||
|
@ -62,6 +62,8 @@ check_topic_access(#auth_user{} = AuthUser,
|
|||
|
||||
state_can_expire() -> false.
|
||||
|
||||
expiry_timestamp(_) -> never.
|
||||
|
||||
%%
|
||||
%% Implementation
|
||||
%%
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
-export([description/0, p/1, q/1, join_tags/1]).
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
||||
state_can_expire/0]).
|
||||
state_can_expire/0, expiry_timestamp/1]).
|
||||
|
||||
%% If keepalive connection is closed, retry N times before failing.
|
||||
-define(RETRY_ON_KEEPALIVE_CLOSED, 3).
|
||||
|
@ -131,6 +131,8 @@ check_topic_access(#auth_user{username = Username, tags = Tags},
|
|||
|
||||
state_can_expire() -> false.
|
||||
|
||||
expiry_timestamp(_) -> never.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
context_as_parameters(Options) when is_map(Options) ->
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
||||
state_can_expire/0, format_multi_attr/1, format_multi_attr/2]).
|
||||
state_can_expire/0, expiry_timestamp/1,
|
||||
format_multi_attr/1, format_multi_attr/2]).
|
||||
|
||||
-export([get_connections/0]).
|
||||
|
||||
|
@ -169,6 +170,8 @@ check_topic_access(User = #auth_user{username = Username,
|
|||
|
||||
state_can_expire() -> false.
|
||||
|
||||
expiry_timestamp(_) -> never.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
ensure_rabbit_authz_backend_result(true) ->
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
-export([description/0]).
|
||||
-export([user_login_authentication/2, user_login_authorization/2,
|
||||
check_vhost_access/3, check_resource_access/4,
|
||||
check_topic_access/4, check_token/1, state_can_expire/0, update_state/2]).
|
||||
check_topic_access/4, check_token/1, state_can_expire/0, update_state/2,
|
||||
expiry_timestamp/1]).
|
||||
|
||||
% for testing
|
||||
-export([post_process_payload/1, get_expanded_scopes/2]).
|
||||
|
@ -120,6 +121,14 @@ update_state(AuthUser, NewToken) ->
|
|||
impl = fun() -> DecodedToken end}}
|
||||
end.
|
||||
|
||||
expiry_timestamp(#auth_user{impl = DecodedTokenFun}) ->
|
||||
case DecodedTokenFun() of
|
||||
#{<<"exp">> := Exp} when is_integer(Exp) ->
|
||||
Exp;
|
||||
_ ->
|
||||
never
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
authenticate(_, AuthProps0) ->
|
||||
|
|
|
@ -1081,6 +1081,10 @@ test_token_expiration(_) ->
|
|||
|
||||
assert_resource_access_granted(User, VHost, <<"foo">>, configure),
|
||||
assert_resource_access_granted(User, VHost, <<"foo">>, write),
|
||||
Now = os:system_time(seconds),
|
||||
ExpiryTs = rabbit_auth_backend_oauth2:expiry_timestamp(User),
|
||||
?assert(ExpiryTs > (Now - 10)),
|
||||
?assert(ExpiryTs < (Now + 10)),
|
||||
|
||||
?UTIL_MOD:wait_for_token_to_expire(),
|
||||
#{<<"exp">> := Exp} = TokenData,
|
||||
|
|
Loading…
Reference in New Issue