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:
Arnaud Cogoluègnes 2024-01-09 11:18:36 +01:00
parent 5abeb753f3
commit 33c64d06ea
No known key found for this signature in database
GPG Key ID: D5C8C4DFAD43AFA8
13 changed files with 182 additions and 8 deletions

View File

@ -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",

9
deps/rabbit/app.bzl vendored
View File

@ -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,

View File

@ -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).

View File

@ -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};

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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]).

View File

@ -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
%%

View File

@ -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) ->

View File

@ -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) ->

View File

@ -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) ->

View File

@ -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,