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(
|
rabbitmq_integration_suite(
|
||||||
name = "rabbit_stream_queue_SUITE",
|
name = "rabbit_stream_queue_SUITE",
|
||||||
size = "large",
|
size = "large",
|
||||||
|
|
|
@ -1441,6 +1441,15 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
|
||||||
app_name = "rabbit",
|
app_name = "rabbit",
|
||||||
erlc_opts = "//:test_erlc_opts",
|
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(
|
erlang_bytecode(
|
||||||
name = "rabbitmq_queues_cli_integration_SUITE_beam_files",
|
name = "rabbitmq_queues_cli_integration_SUITE_beam_files",
|
||||||
testonly = True,
|
testonly = True,
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
-export([check_user_pass_login/2, check_user_login/2, check_user_loopback/2,
|
-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]).
|
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.
|
%% otherwise returns false.
|
||||||
permission_cache_can_expire(#user{authz_backends = Backends}) ->
|
permission_cache_can_expire(#user{authz_backends = Backends}) ->
|
||||||
lists:any(fun ({Module, _State}) -> Module:state_can_expire() end, 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_vhost_permissions/2,
|
||||||
list_user_topic_permissions/1, list_vhost_topic_permissions/1, list_user_vhost_topic_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]).
|
-export([hashing_module_for_user/1, expand_topic_permission/2]).
|
||||||
|
|
||||||
|
@ -111,6 +111,8 @@ user_login_authentication(Username, AuthProps) ->
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
|
||||||
|
expiry_timestamp(_) -> never.
|
||||||
|
|
||||||
user_login_authorization(Username, _AuthProps) ->
|
user_login_authorization(Username, _AuthProps) ->
|
||||||
case user_login_authentication(Username, []) of
|
case user_login_authentication(Username, []) of
|
||||||
{ok, #auth_user{impl = Impl, tags = Tags}} -> {ok, Impl, Tags};
|
{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,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
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]).
|
get/1, init/0]).
|
||||||
|
|
||||||
init() ->
|
init() ->
|
||||||
|
@ -42,5 +42,8 @@ check_topic_access(#auth_user{}, #resource{}, _Permission, TopicContext) ->
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
|
||||||
|
expiry_timestamp(_) ->
|
||||||
|
never.
|
||||||
|
|
||||||
get(K) ->
|
get(K) ->
|
||||||
ets:lookup(?MODULE, K).
|
ets:lookup(?MODULE, K).
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
-export([user/0]).
|
-export([user/0]).
|
||||||
-export([user_login_authentication/2, user_login_authorization/2,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4]).
|
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().
|
-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.
|
check_topic_access(#auth_user{}, #resource{}, _Permission, _Context) -> true.
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
expiry_timestamp(_) -> never.
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
boolean() | {'error', any()}.
|
boolean() | {'error', any()}.
|
||||||
|
|
||||||
%% Returns true for backends that support state or credential expiration (e.g. use JWTs).
|
%% 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().
|
-callback state_can_expire() -> boolean().
|
||||||
|
|
||||||
%% Updates backend state that has expired.
|
%% Updates backend state that has expired.
|
||||||
|
@ -85,4 +86,14 @@
|
||||||
{'refused', string(), [any()]} |
|
{'refused', string(), [any()]} |
|
||||||
{'error', 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]).
|
-optional_callbacks([update_state/2]).
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
-export([user_login_authentication/2, user_login_authorization/2,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
||||||
state_can_expire/0]).
|
state_can_expire/0, expiry_timestamp/1]).
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
|
|
||||||
|
@ -62,6 +62,8 @@ check_topic_access(#auth_user{} = AuthUser,
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
|
||||||
|
expiry_timestamp(_) -> never.
|
||||||
|
|
||||||
%%
|
%%
|
||||||
%% Implementation
|
%% Implementation
|
||||||
%%
|
%%
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
-export([description/0, p/1, q/1, join_tags/1]).
|
-export([description/0, p/1, q/1, join_tags/1]).
|
||||||
-export([user_login_authentication/2, user_login_authorization/2,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
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.
|
%% If keepalive connection is closed, retry N times before failing.
|
||||||
-define(RETRY_ON_KEEPALIVE_CLOSED, 3).
|
-define(RETRY_ON_KEEPALIVE_CLOSED, 3).
|
||||||
|
@ -131,6 +131,8 @@ check_topic_access(#auth_user{username = Username, tags = Tags},
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
|
||||||
|
expiry_timestamp(_) -> never.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
context_as_parameters(Options) when is_map(Options) ->
|
context_as_parameters(Options) when is_map(Options) ->
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
|
|
||||||
-export([user_login_authentication/2, user_login_authorization/2,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4, check_topic_access/4,
|
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]).
|
-export([get_connections/0]).
|
||||||
|
|
||||||
|
@ -169,6 +170,8 @@ check_topic_access(User = #auth_user{username = Username,
|
||||||
|
|
||||||
state_can_expire() -> false.
|
state_can_expire() -> false.
|
||||||
|
|
||||||
|
expiry_timestamp(_) -> never.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
ensure_rabbit_authz_backend_result(true) ->
|
ensure_rabbit_authz_backend_result(true) ->
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
-export([description/0]).
|
-export([description/0]).
|
||||||
-export([user_login_authentication/2, user_login_authorization/2,
|
-export([user_login_authentication/2, user_login_authorization/2,
|
||||||
check_vhost_access/3, check_resource_access/4,
|
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
|
% for testing
|
||||||
-export([post_process_payload/1, get_expanded_scopes/2]).
|
-export([post_process_payload/1, get_expanded_scopes/2]).
|
||||||
|
@ -120,6 +121,14 @@ update_state(AuthUser, NewToken) ->
|
||||||
impl = fun() -> DecodedToken end}}
|
impl = fun() -> DecodedToken end}}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
expiry_timestamp(#auth_user{impl = DecodedTokenFun}) ->
|
||||||
|
case DecodedTokenFun() of
|
||||||
|
#{<<"exp">> := Exp} when is_integer(Exp) ->
|
||||||
|
Exp;
|
||||||
|
_ ->
|
||||||
|
never
|
||||||
|
end.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
authenticate(_, AuthProps0) ->
|
authenticate(_, AuthProps0) ->
|
||||||
|
|
|
@ -1081,6 +1081,10 @@ test_token_expiration(_) ->
|
||||||
|
|
||||||
assert_resource_access_granted(User, VHost, <<"foo">>, configure),
|
assert_resource_access_granted(User, VHost, <<"foo">>, configure),
|
||||||
assert_resource_access_granted(User, VHost, <<"foo">>, write),
|
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(),
|
?UTIL_MOD:wait_for_token_to_expire(),
|
||||||
#{<<"exp">> := Exp} = TokenData,
|
#{<<"exp">> := Exp} = TokenData,
|
||||||
|
|
Loading…
Reference in New Issue