diff --git a/deps/rabbit/BUILD.bazel b/deps/rabbit/BUILD.bazel index 4a18caaeda..c42ec15d67 100644 --- a/deps/rabbit/BUILD.bazel +++ b/deps/rabbit/BUILD.bazel @@ -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", diff --git a/deps/rabbit/app.bzl b/deps/rabbit/app.bzl index b8c2e83ccc..7ecf3c9af5 100644 --- a/deps/rabbit/app.bzl +++ b/deps/rabbit/app.bzl @@ -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, diff --git a/deps/rabbit/src/rabbit_access_control.erl b/deps/rabbit/src/rabbit_access_control.erl index 06003a85cd..2800676ef6 100644 --- a/deps/rabbit/src/rabbit_access_control.erl +++ b/deps/rabbit/src/rabbit_access_control.erl @@ -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). diff --git a/deps/rabbit/src/rabbit_auth_backend_internal.erl b/deps/rabbit/src/rabbit_auth_backend_internal.erl index 91a81cc97e..30816225ba 100644 --- a/deps/rabbit/src/rabbit_auth_backend_internal.erl +++ b/deps/rabbit/src/rabbit_auth_backend_internal.erl @@ -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}; diff --git a/deps/rabbit/test/rabbit_access_control_SUITE.erl b/deps/rabbit/test/rabbit_access_control_SUITE.erl new file mode 100644 index 0000000000..a2e4660ffa --- /dev/null +++ b/deps/rabbit/test/rabbit_access_control_SUITE.erl @@ -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. diff --git a/deps/rabbit/test/rabbit_auth_backend_context_propagation_mock.erl b/deps/rabbit/test/rabbit_auth_backend_context_propagation_mock.erl index 8e842b613a..7a5334f029 100644 --- a/deps/rabbit/test/rabbit_auth_backend_context_propagation_mock.erl +++ b/deps/rabbit/test/rabbit_auth_backend_context_propagation_mock.erl @@ -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). diff --git a/deps/rabbit_common/src/rabbit_auth_backend_dummy.erl b/deps/rabbit_common/src/rabbit_auth_backend_dummy.erl index 53459b6440..4937d1097e 100644 --- a/deps/rabbit_common/src/rabbit_auth_backend_dummy.erl +++ b/deps/rabbit_common/src/rabbit_auth_backend_dummy.erl @@ -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. diff --git a/deps/rabbit_common/src/rabbit_authz_backend.erl b/deps/rabbit_common/src/rabbit_authz_backend.erl index c5aba4f4a7..6d7c1bd1b6 100644 --- a/deps/rabbit_common/src/rabbit_authz_backend.erl +++ b/deps/rabbit_common/src/rabbit_authz_backend.erl @@ -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]). diff --git a/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl b/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl index baf47a50c7..05c88729e0 100644 --- a/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl +++ b/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_backend_cache.erl @@ -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 %% diff --git a/deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl b/deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl index a137eea1d5..406f302e0d 100644 --- a/deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl +++ b/deps/rabbitmq_auth_backend_http/src/rabbit_auth_backend_http.erl @@ -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) -> diff --git a/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap.erl b/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap.erl index 5f93dad3bb..39ff51b8ab 100644 --- a/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap.erl +++ b/deps/rabbitmq_auth_backend_ldap/src/rabbit_auth_backend_ldap.erl @@ -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) -> diff --git a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl index 8b4b7165da..9a08ca528f 100644 --- a/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl +++ b/deps/rabbitmq_auth_backend_oauth2/src/rabbit_auth_backend_oauth2.erl @@ -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) -> diff --git a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl index 0899a0b5b8..31de34a9be 100644 --- a/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl +++ b/deps/rabbitmq_auth_backend_oauth2/test/unit_SUITE.erl @@ -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,