diff --git a/deps/rabbit/src/rabbit_access_control.erl b/deps/rabbit/src/rabbit_access_control.erl index b0b2ecf0f899..b1d6761d5b3a 100644 --- a/deps/rabbit/src/rabbit_access_control.erl +++ b/deps/rabbit/src/rabbit_access_control.erl @@ -361,6 +361,10 @@ check_access(Fun, Module, ErrStr, ErrArgs, ErrName) -> ok; false -> rabbit_misc:protocol_error(ErrName, ErrStr, ErrArgs); + {false, Reason} -> + FullErrStr = ErrStr ++ " by backend ~ts: ~ts", + FullErrArgs = ErrArgs ++ [Module, Reason], + rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs); {error, E} -> FullErrStr = ErrStr ++ ", backend ~ts returned an error: ~tp", FullErrArgs = ErrArgs ++ [Module, E], diff --git a/deps/rabbit/src/rabbit_authz_backend.erl b/deps/rabbit/src/rabbit_authz_backend.erl index fdf9c6c561b0..9c25abd44e78 100644 --- a/deps/rabbit/src/rabbit_authz_backend.erl +++ b/deps/rabbit/src/rabbit_authz_backend.erl @@ -32,38 +32,41 @@ %% Possible responses: %% true %% false +%% {false, Reason} %% {error, Error} %% Something went wrong. Log and die. -callback check_vhost_access(AuthUser :: rabbit_types:auth_user(), VHost :: rabbit_types:vhost(), AuthzData :: rabbit_types:authz_data()) -> - boolean() | {'error', any()}. + boolean() | {false, Reason :: string()} | {'error', any()}. %% Given #auth_user, resource and permission, can a user access a resource? %% %% Possible responses: %% true %% false +%% {false, Reason} %% {error, Error} %% Something went wrong. Log and die. -callback check_resource_access(rabbit_types:auth_user(), rabbit_types:r(atom()), rabbit_types:permission_atom(), rabbit_types:authz_context()) -> - boolean() | {'error', any()}. + boolean() | {false, Reason :: string()} | {'error', any()}. %% Given #auth_user, topic as resource, permission, and context, can a user access the topic? %% %% Possible responses: %% true %% false +%% {false, Reason} %% {error, Error} %% Something went wrong. Log and die. -callback check_topic_access(rabbit_types:auth_user(), rabbit_types:r(atom()), rabbit_types:permission_atom(), rabbit_types:topic_access_context()) -> - boolean() | {'error', any()}. + boolean() | {false, Reason :: string()} | {'error', any()}. %% Updates backend state that has expired. %% 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 a952ccb0470f..748cc37d199b 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 @@ -36,30 +36,21 @@ user_login_authorization(Username, AuthProps) -> end). check_vhost_access(#auth_user{} = AuthUser, VHostPath, AuthzData) -> - with_cache(authz, {check_vhost_access, [AuthUser, VHostPath, AuthzData]}, - fun(true) -> success; - (false) -> refusal; - ({error, _} = Err) -> Err; - (_) -> unknown - end). + with_cache(authz, + {check_vhost_access, [AuthUser, VHostPath, AuthzData]}, + fun convert_backend_result/1). check_resource_access(#auth_user{} = AuthUser, #resource{} = Resource, Permission, AuthzContext) -> - with_cache(authz, {check_resource_access, [AuthUser, Resource, Permission, AuthzContext]}, - fun(true) -> success; - (false) -> refusal; - ({error, _} = Err) -> Err; - (_) -> unknown - end). + with_cache(authz, + {check_resource_access, [AuthUser, Resource, Permission, AuthzContext]}, + fun convert_backend_result/1). check_topic_access(#auth_user{} = AuthUser, #resource{} = Resource, Permission, Context) -> - with_cache(authz, {check_topic_access, [AuthUser, Resource, Permission, Context]}, - fun(true) -> success; - (false) -> refusal; - ({error, _} = Err) -> Err; - (_) -> unknown - end). + with_cache(authz, + {check_topic_access, [AuthUser, Resource, Permission, Context]}, + fun convert_backend_result/1). expiry_timestamp(_) -> never. @@ -67,6 +58,12 @@ expiry_timestamp(_) -> never. %% Implementation %% +convert_backend_result(true) -> success; +convert_backend_result(false) -> refusal; +convert_backend_result({false, _}) -> refusal; +convert_backend_result({error, _} = Err) -> Err; +convert_backend_result(_) -> unknown. + clear_cache_cluster_wide() -> Nodes = rabbit_nodes:list_running(), ?LOG_WARNING("Clearing auth_backend_cache in all nodes : ~p", [Nodes]), diff --git a/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl b/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl index a8171133e9fb..3de66c4e36f5 100644 --- a/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl +++ b/deps/rabbitmq_auth_backend_cache/src/rabbit_auth_cache.erl @@ -18,7 +18,7 @@ -callback clear() -> ok. expiration(TTL) -> - erlang:system_time(milli_seconds) + TTL. + erlang:system_time(millisecond) + TTL. expired(Exp) -> - erlang:system_time(milli_seconds) > Exp. + erlang:system_time(millisecond) > Exp. diff --git a/deps/rabbitmq_auth_backend_http/Makefile b/deps/rabbitmq_auth_backend_http/Makefile index 67709e9afb1d..ac2906e389f0 100644 --- a/deps/rabbitmq_auth_backend_http/Makefile +++ b/deps/rabbitmq_auth_backend_http/Makefile @@ -10,7 +10,8 @@ define PROJECT_ENV {user_path, "http://localhost:8000/auth/user"}, {vhost_path, "http://localhost:8000/auth/vhost"}, {resource_path, "http://localhost:8000/auth/resource"}, - {topic_path, "http://localhost:8000/auth/topic"} + {topic_path, "http://localhost:8000/auth/topic"}, + {authorization_failure_disclosure, false} ] endef @@ -20,7 +21,7 @@ endef LOCAL_DEPS = ssl inets crypto public_key DEPS = rabbit_common rabbit amqp_client -TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy +TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers cowboy rabbitmq_amqp_client DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk diff --git a/deps/rabbitmq_auth_backend_http/README.md b/deps/rabbitmq_auth_backend_http/README.md index fefb2889d862..1c998db508fc 100644 --- a/deps/rabbitmq_auth_backend_http/README.md +++ b/deps/rabbitmq_auth_backend_http/README.md @@ -53,6 +53,7 @@ auth_http.user_path = http://some-server/auth/user auth_http.vhost_path = http://some-server/auth/vhost auth_http.resource_path = http://some-server/auth/resource auth_http.topic_path = http://some-server/auth/topic +auth_http.authorization_failure_disclosure = false ``` In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanced-config-file): @@ -65,7 +66,8 @@ In the [`advanced.config` format](https://www.rabbitmq.com/configure.html#advanc {user_path, "http(s)://some-server/auth/user"}, {vhost_path, "http(s)://some-server/auth/vhost"}, {resource_path, "http(s)://some-server/auth/resource"}, - {topic_path, "http(s)://some-server/auth/topic"}]} + {topic_path, "http(s)://some-server/auth/topic"}, + {authorization_failure_disclosure, false}]} ]. ``` @@ -123,11 +125,13 @@ Your web server should always return HTTP 200 OK, with a body containing: * `deny`: deny access to the user / vhost / resource +* `deny `: deny access to the user / vhost / resource. RabbitMQ will log the `` at INFO level. + If `auth_http.authorization_failure_disclosure` is set to `true` (the default is `false` for security reasons) + RabbitMQ will additionally forward the `` to AMQP clients. * `allow`: allow access to the user / vhost / resource * `allow [list of tags]` (for `user_path` only): allow access, and mark the user as an having the tags listed ## Using TLS/HTTPS - If your Web server uses HTTPS and certificate verification, you need to configure the plugin to use a CA and client certificate/key pair using the `rabbitmq_auth_backend_http.ssl_options` config variable: diff --git a/deps/rabbitmq_auth_backend_http/priv/schema/rabbitmq_auth_backend_http.schema b/deps/rabbitmq_auth_backend_http/priv/schema/rabbitmq_auth_backend_http.schema index fccf97383ce8..426575d18921 100644 --- a/deps/rabbitmq_auth_backend_http/priv/schema/rabbitmq_auth_backend_http.schema +++ b/deps/rabbitmq_auth_backend_http/priv/schema/rabbitmq_auth_backend_http.schema @@ -23,6 +23,9 @@ {mapping, "auth_http.connection_timeout", "rabbitmq_auth_backend_http.connection_timeout", [{datatype, integer}]}. +{mapping, "auth_http.authorization_failure_disclosure", "rabbitmq_auth_backend_http.authorization_failure_disclosure", [ + {datatype, {enum, [true, false]}}]}. + %% TLS options {mapping, "auth_http.ssl_options", "rabbitmq_auth_backend_http.ssl_options", [ 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 4b5d0c9ad648..9cb956ff0065 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 @@ -25,6 +25,8 @@ -define(SUCCESSFUL_RESPONSE_CODES, [200, 201]). +-define(APP, rabbitmq_auth_backend_http). + %%-------------------------------------------------------------------- description() -> @@ -34,16 +36,29 @@ description() -> %%-------------------------------------------------------------------- user_login_authentication(Username, AuthProps) -> - case http_req(p(user_path), q([{username, Username}] ++ extract_other_credentials(AuthProps))) of - {error, _} = E -> E; - "deny" -> {refused, "Denied by the backing HTTP service", []}; - "allow" ++ Rest -> Tags = [rabbit_data_coercion:to_atom(T) || - T <- string:tokens(Rest, " ")], - - {ok, #auth_user{username = Username, - tags = Tags, - impl = fun() -> proplists:delete(username, AuthProps) end}}; - Other -> {error, {bad_response, Other}} + Path = p(user_path), + Query = q([{username, Username}] ++ extract_other_credentials(AuthProps)), + case http_req(Path, Query) of + {error, _} = Err -> + Err; + "deny " ++ Reason -> + ?LOG_INFO("HTTP authentication denied for user '~ts': ~ts", + [Username, Reason]), + {refused, "Denied by the backing HTTP service", []}; + Body -> + case string:lowercase(Body) of + "deny" -> + {refused, "Denied by the backing HTTP service", []}; + "allow" ++ Rest -> + Tags = [rabbit_data_coercion:to_atom(T) + || T <- string:tokens(Rest, " ")], + {ok, #auth_user{ + username = Username, + tags = Tags, + impl = fun() -> proplists:delete(username, AuthProps) end}}; + Other -> + {error, {bad_response, Other}} + end end. %% When a protocol plugin uses an internal AMQP 0-9-1 client to interact with RabbitMQ core, @@ -109,34 +124,34 @@ check_vhost_access(#auth_user{username = Username, tags = Tags}, VHost, do_check_vhost_access(Username, Tags, VHost, Ip, AuthzData) -> OptionsParameters = context_as_parameters(AuthzData), - bool_req(vhost_path, [{username, Username}, - {vhost, VHost}, - {ip, Ip}, - {tags, join_tags(Tags)}] ++ OptionsParameters). + req(vhost_path, [{username, Username}, + {vhost, VHost}, + {ip, Ip}, + {tags, join_tags(Tags)}] ++ OptionsParameters). check_resource_access(#auth_user{username = Username, tags = Tags}, #resource{virtual_host = VHost, kind = Type, name = Name}, Permission, AuthzContext) -> OptionsParameters = context_as_parameters(AuthzContext), - bool_req(resource_path, [{username, Username}, - {vhost, VHost}, - {resource, Type}, - {name, Name}, - {permission, Permission}, - {tags, join_tags(Tags)}] ++ OptionsParameters). + req(resource_path, [{username, Username}, + {vhost, VHost}, + {resource, Type}, + {name, Name}, + {permission, Permission}, + {tags, join_tags(Tags)}] ++ OptionsParameters). check_topic_access(#auth_user{username = Username, tags = Tags}, #resource{virtual_host = VHost, kind = topic = Type, name = Name}, Permission, Context) -> OptionsParameters = context_as_parameters(Context), - bool_req(topic_path, [{username, Username}, - {vhost, VHost}, - {resource, Type}, - {name, Name}, - {permission, Permission}, - {tags, join_tags(Tags)}] ++ OptionsParameters). + req(topic_path, [{username, Username}, + {vhost, VHost}, + {resource, Type}, + {name, Name}, + {permission, Permission}, + {tags, join_tags(Tags)}] ++ OptionsParameters). expiry_timestamp(_) -> never. @@ -152,14 +167,32 @@ context_as_parameters(Options) when is_map(Options) -> context_as_parameters(_) -> []. -bool_req(PathName, Props) -> - case http_req(p(PathName), q(Props)) of - "deny" -> false; - "allow" -> true; - E -> E +req(PathName, Props) -> + Path = p(PathName), + Query = q(Props), + case http_req(Path, Query) of + {error, _} = Err -> + Err; + "deny " ++ Reason -> + ?LOG_INFO("HTTP authorization denied for path ~ts with query '~ts': ~ts", + [Path, Query, Reason]), + case application:get_env(?APP, authorization_failure_disclosure) of + {ok, true} -> + {false, Reason}; + _ -> + false + end; + Body -> + case string:lowercase(Body) of + "deny" -> + false; + "allow" -> + true + end end. -http_req(Path, Query) -> http_req(Path, Query, ?RETRY_ON_KEEPALIVE_CLOSED). +http_req(Path, Query) -> + http_req(Path, Query, ?RETRY_ON_KEEPALIVE_CLOSED). http_req(Path, Query, Retry) -> case do_http_req(Path, Query) of @@ -177,7 +210,7 @@ do_http_req(Path0, Query) -> {host, Host} = lists:keyfind(host, 1, URI), {port, Port} = lists:keyfind(port, 1, URI), HostHdr = rabbit_misc:format("~ts:~b", [Host, Port]), - {ok, Method} = application:get_env(rabbitmq_auth_backend_http, http_method), + {ok, Method} = application:get_env(?APP, http_method), Request = case rabbit_data_coercion:to_atom(Method) of get -> Path = Path0 ++ "?" ++ Query, @@ -188,34 +221,37 @@ do_http_req(Path0, Query) -> {Path0, [{"Host", HostHdr}], "application/x-www-form-urlencoded", Query} end, RequestTimeout = - case application:get_env(rabbitmq_auth_backend_http, request_timeout) of + case application:get_env(?APP, request_timeout) of {ok, Val1} -> Val1; _ -> infinity end, ConnectionTimeout = - case application:get_env(rabbitmq_auth_backend_http, connection_timeout) of + case application:get_env(?APP, connection_timeout) of {ok, Val2} -> Val2; _ -> RequestTimeout end, ?LOG_DEBUG("auth_backend_http: request timeout: ~tp, connection timeout: ~tp", [RequestTimeout, ConnectionTimeout]), HttpOpts = [{timeout, RequestTimeout}, {connect_timeout, ConnectionTimeout}] ++ ssl_options(), - case httpc:request(Method, Request, HttpOpts, []) of - {ok, {{_HTTP, Code, _}, _Headers, Body}} -> - ?LOG_DEBUG("auth_backend_http: response code is ~tp, body: ~tp", [Code, Body]), + case httpc:request(Method, Request, HttpOpts, [{body_format, binary}]) of + {ok, {{_HTTP, Code, _}, _Headers, BodyBin}} -> + Body = unicode:characters_to_list(BodyBin), + ?LOG_DEBUG("auth_backend_http: response code is ~tp, body: '~ts'", [Code, Body]), case lists:member(Code, ?SUCCESSFUL_RESPONSE_CODES) of - true -> parse_resp(Body); - false -> {error, {Code, Body}} + true -> + string:strip(Body); + false -> + {error, {Code, Body}} end; {error, _} = E -> E end. ssl_options() -> - case application:get_env(rabbitmq_auth_backend_http, ssl_options) of + case application:get_env(?APP, ssl_options) of {ok, Opts0} when is_list(Opts0) -> Opts1 = [{ssl, rabbit_ssl_options:fix_client(Opts0)}], - case application:get_env(rabbitmq_auth_backend_http, ssl_hostname_verification) of + case application:get_env(?APP, ssl_hostname_verification) of {ok, wildcard} -> ?LOG_DEBUG("Enabling wildcard-aware hostname verification for HTTP client connections"), %% Needed for HTTPS connections that connect to servers that use wildcard certificates. @@ -228,7 +264,7 @@ ssl_options() -> end. p(PathName) -> - {ok, Path} = application:get_env(rabbitmq_auth_backend_http, PathName), + {ok, Path} = application:get_env(?APP, PathName), Path. q(Args) -> @@ -240,8 +276,6 @@ escape(K, Map) when is_map(Map) -> escape(K, V) -> rabbit_data_coercion:to_list(K) ++ "=" ++ rabbit_http_util:quote_plus(V). -parse_resp(Resp) -> string:to_lower(string:strip(Resp)). - join_tags([]) -> ""; join_tags(Tags) -> Strings = [rabbit_data_coercion:to_list(T) || T <- Tags], diff --git a/deps/rabbitmq_auth_backend_http/test/auth_SUITE.erl b/deps/rabbitmq_auth_backend_http/test/auth_SUITE.erl index e7bddd59f04a..fcd005ae60de 100644 --- a/deps/rabbitmq_auth_backend_http/test/auth_SUITE.erl +++ b/deps/rabbitmq_auth_backend_http/test/auth_SUITE.erl @@ -2,195 +2,105 @@ %% 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) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% -module(auth_SUITE). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). --include_lib("rabbit_common/include/rabbit.hrl"). - --compile(export_all). - --define(AUTH_PORT, 8000). --define(USER_PATH, "/auth/user"). --define(ALLOWED_USER, #{username => <<"Ala1">>, - password => <<"Kocur">>, - expected_credentials => [username, password], - tags => [policymaker, monitoring]}). --define(ALLOWED_USER_2, #{username => <<"Ala3">>, - expected_credentials => [username], - tags => [policymaker, monitoring]}). --define(ALLOWED_USER_WITH_EXTRA_CREDENTIALS, #{username => <<"Ala2">>, - password => <<"Kocur">>, - client_id => <<"some_id">>, - expected_credentials => [username, password, client_id], - tags => [policymaker, monitoring]}). --define(DENIED_USER, #{username => <<"Alice">>, - password => <<"Cat">> - }). +-include_lib("amqp10_common/include/amqp10_framing.hrl"). + +-compile([nowarn_export_all, + export_all]). all() -> [ - {group, over_https}, - {group, over_http} + {group, cluster_size_1} ]. groups() -> [ - {over_http, [], shared()}, - {over_https, [], shared()} - ]. - -shared() -> - [ - grants_access_to_user, - denies_access_to_user, - grants_access_to_user_passing_additional_required_authprops, - grants_access_to_user_skipping_internal_authprops, - grants_access_to_user_with_credentials_in_rabbit_auth_backend_http, - grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache, - grants_access_to_ssl_user_without_a_password - ]. + {cluster_size_1, [shuffle], + [ + authorization_failure_disclosure + ] + }]. init_per_suite(Config) -> - rabbit_ct_helpers:run_setup_steps(Config) ++ - [{allowed_user, ?ALLOWED_USER}, - {allowed_user_2, ?ALLOWED_USER_2}, - {allowed_user_with_extra_credentials, ?ALLOWED_USER_WITH_EXTRA_CREDENTIALS}, - {denied_user, ?DENIED_USER}]. - -init_per_group(over_http, Config) -> - configure_http_auth_backend("http", Config), - {User1, Tuple1} = extractUserTuple(?ALLOWED_USER), - {User2, Tuple2} = extractUserTuple(?ALLOWED_USER_WITH_EXTRA_CREDENTIALS), - start_http_auth_server(?AUTH_PORT, ?USER_PATH, #{User1 => Tuple1, User2 => Tuple2}), - Config; - -init_per_group(over_https, Config) -> - configure_http_auth_backend("https", Config), - {User1, Tuple1} = extractUserTuple(?ALLOWED_USER), - {User2, Tuple2} = extractUserTuple(?ALLOWED_USER_2), - {User3, Tuple3} = extractUserTuple(?ALLOWED_USER_WITH_EXTRA_CREDENTIALS), - CertsDir = ?config(rmq_certsdir, Config), - start_https_auth_server(?AUTH_PORT, CertsDir, ?USER_PATH, #{ - User1 => Tuple1, - User3 => Tuple3, - User2 => Tuple2}), - Config ++ [{group, over_https}]. - -extractUserTuple(User) -> - #{username := Username, tags := Tags, expected_credentials := ExpectedCredentials} = User, - Password = case maps:get(password, User, undefined) of - undefined -> none; - P -> P - end, - {Username, {Password, Tags, ExpectedCredentials}}. + {ok, _} = application:ensure_all_started(rabbitmq_amqp_client), + start_http_auth_server(), + rabbit_ct_helpers:log_environment(), + Config. end_per_suite(Config) -> + ok = stop_http_auth_server(), Config. -end_per_group(over_http, Config) -> - undo_configure_http_auth_backend("http", Config), - stop_http_auth_server(); -end_per_group(over_https, Config) -> - undo_configure_http_auth_backend("https", Config), - stop_http_auth_server(). - -grants_access_to_user(Config) -> - #{username := U, password := P, tags := T} = ?config(allowed_user, Config), - AuthProps = [{password, P}], - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), - ?assertMatch({U, T, AuthProps}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). - -grants_access_to_ssl_user_without_a_password(Config) -> - case ?config(group, Config) of - over_https -> - #{username := U, tags := T} = ?config(allowed_user_2, Config), - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, []), - ?assertMatch({U, T, []}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}); - _ ->{skip, "Requires https"} - end. - -denies_access_to_user(Config) -> - #{username := U, password := P} = ?config(denied_user, Config), - ?assertMatch({refused, "Denied by the backing HTTP service", []}, - rabbit_auth_backend_http:user_login_authentication(U, [{password, P}])). - -grants_access_to_user_passing_additional_required_authprops(Config) -> - #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), - AuthProps = [{password, P}, {client_id, ClientId}], - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), - ?assertMatch({U, T, AuthProps}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). - -grants_access_to_user_skipping_internal_authprops(Config) -> - #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), - AuthProps = [{password, P}, {client_id, ClientId}, {rabbit_any_internal_property, <<"some value">>}], - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), - ?assertMatch({U, T, AuthProps}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). - -grants_access_to_user_with_credentials_in_rabbit_auth_backend_http(Config) -> - #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), - AuthProps = [{rabbit_auth_backend_http, fun() -> [{password, P}, {client_id, ClientId}] end}], - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), - ?assertMatch({U, T, AuthProps}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). - -grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache(Config) -> - #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), - AuthProps = [{rabbit_auth_backend_cache, fun() -> [{password, P}, {client_id, ClientId}] end}], - {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), - ?assertMatch({U, T, AuthProps}, - {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). - -%%% HELPERS - -configure_http_auth_backend(Scheme, Config) -> - [application:set_env(rabbitmq_auth_backend_http, K, V) || {K, V} <- generate_backend_config(Scheme, Config)]. -undo_configure_http_auth_backend(Scheme, Config) -> - [application:unset_env(rabbitmq_auth_backend_http, K) || {K, _V} <- generate_backend_config(Scheme, Config)]. - -start_http_auth_server(Port, Path, Users) -> - {ok, _} = application:ensure_all_started(inets), - {ok, _} = application:ensure_all_started(cowboy), - Dispatch = cowboy_router:compile([{'_', [{Path, auth_http_mock, Users}]}]), - {ok, _} = cowboy:start_clear( - mock_http_auth_listener, [{port, Port}], #{env => #{dispatch => Dispatch}}). - -start_https_auth_server(Port, CertsDir, Path, Users) -> +init_per_group(_Group, Config) -> + Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"), + Config1 = rabbit_ct_helpers:set_config( + Config, [{rmq_nodename_suffix, Suffix}]), + Config2 = rabbit_ct_helpers:merge_app_env( + Config1, + [ + {rabbit, [{auth_backends, [rabbit_auth_backend_http]}]}, + {rabbitmq_auth_backend_http, [{authorization_failure_disclosure, true}]} + ]), + rabbit_ct_helpers:run_setup_steps( + Config2, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()). + +end_per_group(_Group, Config) -> + rabbit_ct_helpers:run_teardown_steps( + Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +start_http_auth_server() -> {ok, _} = application:ensure_all_started(inets), - {ok, _} = application:ensure_all_started(ssl), {ok, _} = application:ensure_all_started(cowboy), - Dispatch = cowboy_router:compile([{'_', [{Path, auth_http_mock, Users}]}]), - {ok, _} = cowboy:start_tls(mock_http_auth_listener, - [{port, Port}, - {certfile, filename:join([CertsDir, "server", "cert.pem"])}, - {keyfile, filename:join([CertsDir, "server", "key.pem"])}], - #{env => #{dispatch => Dispatch}}). + Dispatch = cowboy_router:compile([{'_', [{'_', auth_http_server, #{}}]}]), + {ok, _} = cowboy:start_clear(auth_http_listener, + [{port, 8000}], + #{env => #{dispatch => Dispatch}}). stop_http_auth_server() -> - cowboy:stop_listener(mock_http_auth_listener). - -generate_backend_config(Scheme, Config) -> - Config0 = [{http_method, get}, - {user_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ ?USER_PATH}, - {vhost_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/vhost"}, - {resource_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/resource"}, - {topic_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/topic"}], - Config1 = case Scheme of - "https" -> - CertsDir = ?config(rmq_certsdir, Config), - [{ssl_options, [ - {cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])}, - {certfile, filename:join([CertsDir, "server", "cert.pem"])}, - {keyfile, filename:join([CertsDir, "server", "key.pem"])}, - {verify, verify_peer}, - {fail_if_no_peer_cert, false}] - }]; - "http" -> [] - end, - Config0 ++ Config1. + cowboy:stop_listener(auth_http_listener). + +authorization_failure_disclosure(Config) -> + OpnConf = amqp_connection_config(Config), + {ok, Connection} = amqp10_client:open_connection(OpnConf), + {ok, Session} = amqp10_client:begin_session_sync(Connection), + {ok, LinkPair} = rabbitmq_amqp_client:attach_management_link_pair_sync(Session, <<"pair">>), + + QName = <<"my-queue">>, + {ok, _} = rabbitmq_amqp_client:declare_queue(LinkPair, QName, #{}), + {ok, _} = rabbitmq_amqp_client:delete_queue(LinkPair, QName), + + XName = <<"my-exchange">>, + {error, {session_ended, Error}} = rabbitmq_amqp_client:declare_exchange(LinkPair, XName, #{}), + %% We expect to receive the full denial reason as sent by the HTTP auth server. + ExpectedReason = <<"configure access to exchange 'my-exchange' in vhost '/' refused for user " + "'guest' by backend rabbit_auth_backend_http: Creating or deleting " + "exchanges is forbidden for all client apps ❌"/utf8>>, + ?assertEqual(#'v1_0.error'{condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS, + description = {utf8, ExpectedReason}}, + Error), + + ok = amqp10_client:close_connection(Connection). + +amqp_connection_config(Config) -> + Host = proplists:get_value(rmq_hostname, Config), + Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), + #{address => Host, + port => Port, + container_id => <<"my container">>, + sasl => {plain, <<"guest">>, <<"guest">>}}. diff --git a/deps/rabbitmq_auth_backend_http/test/auth_http_mock.erl b/deps/rabbitmq_auth_backend_http/test/auth_http_mock.erl index 5a5e724e9117..5d5fb76e025e 100644 --- a/deps/rabbitmq_auth_backend_http/test/auth_http_mock.erl +++ b/deps/rabbitmq_auth_backend_http/test/auth_http_mock.erl @@ -28,5 +28,5 @@ authenticate(QsVals, Users) -> {_OtherPassword, _, _} -> <<"deny">>; undefined -> - <<"deny">> + <<"deny unknown_user">> end. diff --git a/deps/rabbitmq_auth_backend_http/test/auth_http_server.erl b/deps/rabbitmq_auth_backend_http/test/auth_http_server.erl new file mode 100644 index 000000000000..c8e6d9876bab --- /dev/null +++ b/deps/rabbitmq_auth_backend_http/test/auth_http_server.erl @@ -0,0 +1,34 @@ +%% 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) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +%% + +-module(auth_http_server). + +-export([init/2]). + +init(Req0, #{} = State) -> + Path = cowboy_req:path(Req0), + Query = cowboy_req:parse_qs(Req0), + ct:pal("~s received request on path ~s with query ~tp", + [?MODULE, Path, Query]), + RespBody = handle(Path, Query), + Req = cowboy_req:reply(200, + #{<<"content-type">> => <<"text/plain; charset=utf-8">>}, + RespBody, + Req0), + {ok, Req, State}. + +handle(<<"/auth/user">>, _Query) -> + <<"allow">>; +handle(<<"/auth/vhost">>, _Query) -> + <<"allow">>; +handle(<<"/auth/resource">>, Query) -> + case proplists:get_value(<<"resource">>, Query) of + <<"queue">> -> + <<"allow">>; + <<"exchange">> -> + <<"deny Creating or deleting exchanges is forbidden for all client apps ❌"/utf8>> + end. diff --git a/deps/rabbitmq_auth_backend_http/test/auth_unit_SUITE.erl b/deps/rabbitmq_auth_backend_http/test/auth_unit_SUITE.erl new file mode 100644 index 000000000000..82050b3aae22 --- /dev/null +++ b/deps/rabbitmq_auth_backend_http/test/auth_unit_SUITE.erl @@ -0,0 +1,196 @@ +%% 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) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +-module(auth_unit_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("rabbit_common/include/rabbit.hrl"). + +-compile(export_all). + +-define(AUTH_PORT, 8001). +-define(USER_PATH, "/auth/user"). +-define(ALLOWED_USER, #{username => <<"Ala1">>, + password => <<"Kocur">>, + expected_credentials => [username, password], + tags => [policymaker, monitoring]}). +-define(ALLOWED_USER_2, #{username => <<"Ala3">>, + expected_credentials => [username], + tags => [policymaker, monitoring]}). +-define(ALLOWED_USER_WITH_EXTRA_CREDENTIALS, #{username => <<"Ala2">>, + password => <<"Kocur">>, + client_id => <<"some_id">>, + expected_credentials => [username, password, client_id], + tags => [policymaker, monitoring]}). +-define(DENIED_USER, #{username => <<"Alice">>, + password => <<"Cat">> + }). + +all() -> + [ + {group, over_https}, + {group, over_http} + ]. + +groups() -> + [ + {over_http, [], shared()}, + {over_https, [], shared()} + ]. + +shared() -> + [ + grants_access_to_user, + denies_access_to_user, + grants_access_to_user_passing_additional_required_authprops, + grants_access_to_user_skipping_internal_authprops, + grants_access_to_user_with_credentials_in_rabbit_auth_backend_http, + grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache, + grants_access_to_ssl_user_without_a_password + ]. + +init_per_suite(Config) -> + rabbit_ct_helpers:run_setup_steps(Config) ++ + [{allowed_user, ?ALLOWED_USER}, + {allowed_user_2, ?ALLOWED_USER_2}, + {allowed_user_with_extra_credentials, ?ALLOWED_USER_WITH_EXTRA_CREDENTIALS}, + {denied_user, ?DENIED_USER}]. + +init_per_group(over_http, Config) -> + configure_http_auth_backend("http", Config), + {User1, Tuple1} = extractUserTuple(?ALLOWED_USER), + {User2, Tuple2} = extractUserTuple(?ALLOWED_USER_WITH_EXTRA_CREDENTIALS), + start_http_auth_server(?AUTH_PORT, ?USER_PATH, #{User1 => Tuple1, User2 => Tuple2}), + Config; + +init_per_group(over_https, Config) -> + configure_http_auth_backend("https", Config), + {User1, Tuple1} = extractUserTuple(?ALLOWED_USER), + {User2, Tuple2} = extractUserTuple(?ALLOWED_USER_2), + {User3, Tuple3} = extractUserTuple(?ALLOWED_USER_WITH_EXTRA_CREDENTIALS), + CertsDir = ?config(rmq_certsdir, Config), + start_https_auth_server(?AUTH_PORT, CertsDir, ?USER_PATH, #{ + User1 => Tuple1, + User3 => Tuple3, + User2 => Tuple2}), + Config ++ [{group, over_https}]. + +extractUserTuple(User) -> + #{username := Username, tags := Tags, expected_credentials := ExpectedCredentials} = User, + Password = case maps:get(password, User, undefined) of + undefined -> none; + P -> P + end, + {Username, {Password, Tags, ExpectedCredentials}}. + +end_per_suite(Config) -> + Config. + +end_per_group(over_http, Config) -> + undo_configure_http_auth_backend("http", Config), + stop_http_auth_server(); +end_per_group(over_https, Config) -> + undo_configure_http_auth_backend("https", Config), + stop_http_auth_server(). + +grants_access_to_user(Config) -> + #{username := U, password := P, tags := T} = ?config(allowed_user, Config), + AuthProps = [{password, P}], + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), + ?assertMatch({U, T, AuthProps}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). + +grants_access_to_ssl_user_without_a_password(Config) -> + case ?config(group, Config) of + over_https -> + #{username := U, tags := T} = ?config(allowed_user_2, Config), + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, []), + ?assertMatch({U, T, []}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}); + _ ->{skip, "Requires https"} + end. + +denies_access_to_user(Config) -> + #{username := U, password := P} = ?config(denied_user, Config), + ?assertMatch({refused, "Denied by the backing HTTP service", []}, + rabbit_auth_backend_http:user_login_authentication(U, [{password, P}])). + +grants_access_to_user_passing_additional_required_authprops(Config) -> + #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), + AuthProps = [{password, P}, {client_id, ClientId}], + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), + ?assertMatch({U, T, AuthProps}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). + +grants_access_to_user_skipping_internal_authprops(Config) -> + #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), + AuthProps = [{password, P}, {client_id, ClientId}, {rabbit_any_internal_property, <<"some value">>}], + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), + ?assertMatch({U, T, AuthProps}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). + +grants_access_to_user_with_credentials_in_rabbit_auth_backend_http(Config) -> + #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), + AuthProps = [{rabbit_auth_backend_http, fun() -> [{password, P}, {client_id, ClientId}] end}], + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), + ?assertMatch({U, T, AuthProps}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). + +grants_access_to_user_with_credentials_in_rabbit_auth_backend_cache(Config) -> + #{username := U, password := P, tags := T, client_id := ClientId} = ?config(allowed_user_with_extra_credentials, Config), + AuthProps = [{rabbit_auth_backend_cache, fun() -> [{password, P}, {client_id, ClientId}] end}], + {ok, User} = rabbit_auth_backend_http:user_login_authentication(U, AuthProps), + ?assertMatch({U, T, AuthProps}, + {User#auth_user.username, User#auth_user.tags, (User#auth_user.impl)()}). + +%%% HELPERS + +configure_http_auth_backend(Scheme, Config) -> + [application:set_env(rabbitmq_auth_backend_http, K, V) || {K, V} <- generate_backend_config(Scheme, Config)]. +undo_configure_http_auth_backend(Scheme, Config) -> + [application:unset_env(rabbitmq_auth_backend_http, K) || {K, _V} <- generate_backend_config(Scheme, Config)]. + +start_http_auth_server(Port, Path, Users) -> + {ok, _} = application:ensure_all_started(inets), + {ok, _} = application:ensure_all_started(cowboy), + Dispatch = cowboy_router:compile([{'_', [{Path, auth_http_mock, Users}]}]), + {ok, _} = cowboy:start_clear( + mock_http_auth_listener, [{port, Port}], #{env => #{dispatch => Dispatch}}). + +start_https_auth_server(Port, CertsDir, Path, Users) -> + {ok, _} = application:ensure_all_started(inets), + {ok, _} = application:ensure_all_started(ssl), + {ok, _} = application:ensure_all_started(cowboy), + Dispatch = cowboy_router:compile([{'_', [{Path, auth_http_mock, Users}]}]), + {ok, _} = cowboy:start_tls(mock_http_auth_listener, + [{port, Port}, + {certfile, filename:join([CertsDir, "server", "cert.pem"])}, + {keyfile, filename:join([CertsDir, "server", "key.pem"])}], + #{env => #{dispatch => Dispatch}}). + +stop_http_auth_server() -> + cowboy:stop_listener(mock_http_auth_listener). + +generate_backend_config(Scheme, Config) -> + Config0 = [{http_method, get}, + {user_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ ?USER_PATH}, + {vhost_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/vhost"}, + {resource_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/resource"}, + {topic_path, Scheme ++ "://localhost:" ++ integer_to_list(?AUTH_PORT) ++ "/auth/topic"}], + Config1 = case Scheme of + "https" -> + CertsDir = ?config(rmq_certsdir, Config), + [{ssl_options, [ + {cacertfile, filename:join([CertsDir, "testca", "cacert.pem"])}, + {certfile, filename:join([CertsDir, "server", "cert.pem"])}, + {keyfile, filename:join([CertsDir, "server", "key.pem"])}, + {verify, verify_peer}, + {fail_if_no_peer_cert, false}] + }]; + "http" -> [] + end, + Config0 ++ Config1. diff --git a/deps/rabbitmq_auth_backend_http/test/config_schema_SUITE_data/rabbitmq_auth_backend_http.snippets b/deps/rabbitmq_auth_backend_http/test/config_schema_SUITE_data/rabbitmq_auth_backend_http.snippets index 7d94d78bbc16..7d630e6dfca4 100644 --- a/deps/rabbitmq_auth_backend_http/test/config_schema_SUITE_data/rabbitmq_auth_backend_http.snippets +++ b/deps/rabbitmq_auth_backend_http/test/config_schema_SUITE_data/rabbitmq_auth_backend_http.snippets @@ -3,13 +3,15 @@ auth_http.http_method = post auth_http.user_path = http://some-server/auth/user auth_http.vhost_path = http://some-server/auth/vhost - auth_http.resource_path = http://some-server/auth/resource", + auth_http.resource_path = http://some-server/auth/resource + auth_http.authorization_failure_disclosure = true", [{rabbit,[{auth_backends,[rabbit_auth_backend_http]}]}, {rabbitmq_auth_backend_http, [{http_method, post}, {user_path,"http://some-server/auth/user"}, {vhost_path,"http://some-server/auth/vhost"}, - {resource_path,"http://some-server/auth/resource"}]}], + {resource_path,"http://some-server/auth/resource"}, + {authorization_failure_disclosure, true}]}], [rabbitmq_auth_backend_http]}, {default_http_method, "auth_backends.1 = http diff --git a/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl b/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl index 48ab2a86977d..e20c0b3267c3 100644 --- a/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl +++ b/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl @@ -1609,8 +1609,9 @@ binding_action_with_checks(QName, TopicFilter, BindingArgs, Action, fun rabbit_binding:Action/2, AuthState) else {error, Reason} = Err -> - ?LOG_ERROR("Failed to ~s binding between ~s and ~s for topic filter ~s: ~p", - [Action, rabbit_misc:rs(ExchangeName), rabbit_misc:rs(QName), TopicFilter, Reason]), + ?LOG_ERROR( + "Failed to ~s binding between ~ts and ~ts for topic filter ~ts: ~tp", + [Action, rabbit_misc:rs(ExchangeName), rabbit_misc:rs(QName), TopicFilter, Reason]), Err end. @@ -2292,7 +2293,7 @@ check_resource_access(User, Resource, Perm, Context) -> catch exit:#amqp_error{name = access_refused, explanation = Msg} -> - ?LOG_ERROR("MQTT resource access refused: ~s", [Msg]), + ?LOG_ERROR("MQTT resource access refused: ~ts", [Msg]), {error, access_refused} end end. @@ -2326,7 +2327,7 @@ check_topic_access( catch exit:#amqp_error{name = access_refused, explanation = Msg} -> - ?LOG_ERROR("MQTT topic access refused: ~s", [Msg]), + ?LOG_ERROR("MQTT topic access refused: ~ts", [Msg]), {error, access_refused} end end.