Skip to content

Commit 0b4eca9

Browse files
Merge pull request #16942 from rabbitmq/mk-item-v8qj
Federation: be more strict when validating URIs
2 parents b9a9047 + 32f3650 commit 0b4eca9

4 files changed

Lines changed: 208 additions & 11 deletions

File tree

deps/rabbitmq_federation_common/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ define PROJECT_APP_EXTRA_KEYS
1414
endef
1515

1616
DEPS = rabbit_common rabbit amqp_client
17-
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers
17+
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers proper
1818

1919
PLT_APPS += rabbitmq_cli
2020

deps/rabbitmq_federation_common/src/rabbit_federation_parameters.erl

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
-behaviour(rabbit_policy_validator).
1111

1212
-include("rabbit_federation.hrl").
13+
-include_lib("amqp_client/include/amqp_client.hrl").
1314

1415
-export([validate/5, notify/5, notify_clear/4]).
1516
-export([register/0, unregister/0, validate_policy/1, adjust/1]).
@@ -47,10 +48,10 @@ validate(_VHost, <<"federation-upstream-set">>, Name, Term0, _User) ->
4748
shared_validation()], Upstream)
4849
|| Upstream <- Term];
4950

50-
validate(_VHost, <<"federation-upstream">>, Name, Term0, _User) ->
51+
validate(_VHost, <<"federation-upstream">>, Name, Term0, User) ->
5152
Term = rabbit_data_coercion:to_proplist(Term0),
5253
rabbit_parameter_validation:proplist(
53-
Name, [{<<"uri">>, fun validate_uri/2, mandatory} |
54+
Name, [{<<"uri">>, validate_uri(User), mandatory} |
5455
shared_validation()], Term);
5556

5657
validate(_VHost, _Component, Name, _Term, _User) ->
@@ -95,25 +96,41 @@ shared_validation() ->
9596
{<<"channel-use-mode">>, rabbit_parameter_validation:enum(
9697
['multiple', 'single']), optional}].
9798

98-
validate_uri(Name, Term) when is_binary(Term) ->
99+
validate_uri(User) ->
100+
fun (Name, Term) -> validate_uri(Name, Term, User) end.
101+
102+
validate_uri(Name, Term, User) when is_binary(Term) ->
99103
case rabbit_parameter_validation:binary(Name, Term) of
100104
ok -> case amqp_uri:parse(binary_to_list(Term)) of
101-
{ok, _} -> ok;
105+
{ok, Params} -> validate_uri_vhost_access(Params, User);
102106
{error, E} -> {error, "\"~ts\" not a valid URI: ~tp", [Term, E]}
103107
end;
104108
E -> E
105109
end;
106-
validate_uri(Name, Term) ->
110+
validate_uri(Name, Term, User) ->
107111
case rabbit_parameter_validation:list(Name, Term) of
108112
ok -> case [V || U <- Term,
109-
V <- [validate_uri(Name, U)],
113+
V <- [validate_uri(Name, U, User)],
110114
element(1, V) =:= error] of
111115
[] -> ok;
112116
[E | _] -> E
113117
end;
114118
E -> E
115119
end.
116120

121+
%% A direct URI carries its own virtual host; check access to it here.
122+
validate_uri_vhost_access(#amqp_params_direct{}, none) ->
123+
ok;
124+
validate_uri_vhost_access(#amqp_params_direct{virtual_host = VHost}, User = #user{}) ->
125+
try rabbit_access_control:check_vhost_access(User, VHost, undefined, #{}) of
126+
ok -> ok
127+
catch
128+
_:_ -> {error, "user \"~ts\" may not connect to vhost \"~ts\"",
129+
[User#user.username, VHost]}
130+
end;
131+
validate_uri_vhost_access(_Params, _User) ->
132+
ok.
133+
117134
%%----------------------------------------------------------------------------
118135

119136
validate_policy([{<<"federation-upstream-set">>, Value}])

deps/rabbitmq_federation_common/test/unit_inbroker_SUITE.erl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,26 +154,26 @@ upstream_set_validation(_Config) ->
154154
<<"a-name">>,
155155
[[{<<"upstream">>, <<"devtest1">>}],
156156
[{<<"upstream">>, <<"devtest2">>}]],
157-
<<"acting-user">>),
157+
none),
158158
[[ok], [ok]]),
159159
?assertEqual(rabbit_federation_parameters:validate(<<"/">>, <<"federation-upstream-set">>,
160160
<<"a-name">>,
161161
[#{<<"upstream">> => <<"devtest3">>},
162162
#{<<"upstream">> => <<"devtest4">>}],
163-
<<"acting-user">>),
163+
none),
164164
[[ok], [ok]]),
165165
ok.
166166

167167
upstream_validation(_Config) ->
168168
?assertEqual(rabbit_federation_parameters:validate(<<"/">>, <<"federation-upstream">>,
169169
<<"a-name">>,
170170
[{<<"uri">>, <<"amqp://127.0.0.1/%2f">>}],
171-
<<"acting-user">>),
171+
none),
172172
[ok]),
173173
?assertEqual(rabbit_federation_parameters:validate(<<"/">>, <<"federation-upstream">>,
174174
<<"a-name">>,
175175
#{<<"uri">> => <<"amqp://127.0.0.1/%2f">>},
176-
<<"acting-user">>),
176+
none),
177177
[ok]),
178178
ok.
179179

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
%% This Source Code Form is subject to the terms of the Mozilla Public
2+
%% License, v. 2.0. If a copy of the MPL was not distributed with this
3+
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
%%
5+
%% Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
6+
%%
7+
8+
-module(upstream_validation_SUITE).
9+
10+
-include_lib("common_test/include/ct.hrl").
11+
-include_lib("proper/include/proper.hrl").
12+
-include_lib("eunit/include/eunit.hrl").
13+
14+
-compile(export_all).
15+
16+
-define(ITERATIONS, 100).
17+
18+
-define(USER, <<"upstream-validation-user">>).
19+
-define(PASSWORD, <<"upstream-validation-password">>).
20+
-define(ALLOWED_VHOST, <<"upstream-validation-allowed">>).
21+
-define(DENIED_VHOST, <<"upstream-validation-denied">>).
22+
-define(NONEXISTENT_VHOST, <<"upstream-validation-nonexistent">>).
23+
24+
all() ->
25+
[
26+
{group, tests}
27+
].
28+
29+
groups() ->
30+
[
31+
{tests, [], [
32+
direct_uri_allowed,
33+
direct_uri_denied,
34+
direct_uri_nonexistent_vhost,
35+
direct_uri_no_user,
36+
network_uri_allowed,
37+
uri_list_denied,
38+
uri_list_allowed,
39+
map_uri_denied,
40+
uri_list_mixed_network_and_denied_direct,
41+
prop_uri_list_validation
42+
]}
43+
].
44+
45+
%% -------------------------------------------------------------------
46+
%% Testsuite setup/teardown
47+
%% -------------------------------------------------------------------
48+
49+
init_per_suite(Config) ->
50+
rabbit_ct_helpers:log_environment(),
51+
Config1 = rabbit_ct_helpers:set_config(Config, [{rmq_nodename_suffix, ?MODULE}]),
52+
Config2 = rabbit_ct_helpers:run_setup_steps(Config1,
53+
rabbit_ct_broker_helpers:setup_steps() ++
54+
rabbit_ct_client_helpers:setup_steps()),
55+
ok = rabbit_ct_broker_helpers:add_vhost(Config2, ?ALLOWED_VHOST),
56+
ok = rabbit_ct_broker_helpers:add_vhost(Config2, ?DENIED_VHOST),
57+
ok = rabbit_ct_broker_helpers:add_user(Config2, ?USER, ?PASSWORD),
58+
ok = rabbit_ct_broker_helpers:set_full_permissions(Config2, ?USER, ?ALLOWED_VHOST),
59+
Config2.
60+
61+
end_per_suite(Config) ->
62+
rabbit_ct_broker_helpers:delete_user(Config, ?USER),
63+
rabbit_ct_broker_helpers:delete_vhost(Config, ?ALLOWED_VHOST),
64+
rabbit_ct_broker_helpers:delete_vhost(Config, ?DENIED_VHOST),
65+
rabbit_ct_helpers:run_teardown_steps(Config,
66+
rabbit_ct_client_helpers:teardown_steps() ++
67+
rabbit_ct_broker_helpers:teardown_steps()).
68+
69+
init_per_group(_, Config) ->
70+
Config.
71+
72+
end_per_group(_, Config) ->
73+
Config.
74+
75+
init_per_testcase(Testcase, Config) ->
76+
rabbit_ct_helpers:testcase_started(Config, Testcase).
77+
78+
end_per_testcase(Testcase, Config) ->
79+
rabbit_ct_helpers:testcase_finished(Config, Testcase).
80+
81+
%% -------------------------------------------------------------------
82+
%% Testcases
83+
%% -------------------------------------------------------------------
84+
85+
direct_uri_allowed(Config) ->
86+
?assertEqual([ok], validate_with_user(Config, direct_uri(?ALLOWED_VHOST))).
87+
88+
direct_uri_denied(Config) ->
89+
?assertMatch([{error, _, _}], validate_with_user(Config, direct_uri(?DENIED_VHOST))).
90+
91+
direct_uri_nonexistent_vhost(Config) ->
92+
?assertMatch([{error, _, _}], validate_with_user(Config, direct_uri(?NONEXISTENT_VHOST))).
93+
94+
direct_uri_no_user(Config) ->
95+
?assertEqual([ok], validate_with_none(Config, direct_uri(?DENIED_VHOST))).
96+
97+
%% A network URI authenticates over the wire on connect, so it is unchecked here.
98+
network_uri_allowed(Config) ->
99+
?assertEqual([ok], validate_with_user(Config, network_uri(?DENIED_VHOST))).
100+
101+
uri_list_denied(Config) ->
102+
Uris = [direct_uri(?ALLOWED_VHOST), direct_uri(?DENIED_VHOST)],
103+
?assertMatch([{error, _, _}], validate_with_user(Config, Uris)).
104+
105+
uri_list_allowed(Config) ->
106+
Uris = [direct_uri(?ALLOWED_VHOST), direct_uri(?ALLOWED_VHOST)],
107+
?assertEqual([ok], validate_with_user(Config, Uris)).
108+
109+
map_uri_denied(Config) ->
110+
Term = #{<<"uri">> => direct_uri(?DENIED_VHOST)},
111+
?assertMatch([{error, _, _}], validate_term_with_user(Config, Term)).
112+
113+
uri_list_mixed_network_and_denied_direct(Config) ->
114+
Uris = [network_uri(?DENIED_VHOST), direct_uri(?DENIED_VHOST)],
115+
?assertMatch([{error, _, _}], validate_with_user(Config, Uris)).
116+
117+
%% -------------------------------------------------------------------
118+
%% Property-based test
119+
%% -------------------------------------------------------------------
120+
121+
%% Runs on the broker node so each example is a local call, not an RPC.
122+
prop_uri_list_validation(Config) ->
123+
rabbit_ct_broker_helpers:rpc(Config, ?MODULE, run_prop_uri_list_validation, []).
124+
125+
run_prop_uri_list_validation() ->
126+
{ok, User} = rabbit_access_control:check_user_login(?USER, [{password, ?PASSWORD}]),
127+
rabbit_ct_proper_helpers:run_proper(
128+
fun() ->
129+
?FORALL(
130+
Choices, non_empty(list(oneof([allowed, denied]))),
131+
begin
132+
Uris = [direct_uri(vhost_for(Choice)) || Choice <- Choices],
133+
Result = validate(Uris, User),
134+
ExpectAllowed = lists:all(fun(C) -> C =:= allowed end, Choices),
135+
outcome(Result) =:= (case ExpectAllowed of
136+
true -> allowed;
137+
false -> denied
138+
end)
139+
end)
140+
end, [], ?ITERATIONS).
141+
142+
outcome([ok]) -> allowed;
143+
outcome([{error, _, _}]) -> denied.
144+
145+
%% -------------------------------------------------------------------
146+
%% Helpers
147+
%% -------------------------------------------------------------------
148+
149+
vhost_for(allowed) -> ?ALLOWED_VHOST;
150+
vhost_for(denied) -> ?DENIED_VHOST;
151+
vhost_for(nonexistent) -> ?NONEXISTENT_VHOST.
152+
153+
direct_uri(VHost) ->
154+
<<"amqp:///", VHost/binary>>.
155+
156+
network_uri(VHost) ->
157+
<<"amqp://localhost:5672/", VHost/binary>>.
158+
159+
validate_with_user(Config, Uris) ->
160+
validate_term_with_user(Config, [{<<"uri">>, Uris}]).
161+
162+
validate_term_with_user(Config, Term) ->
163+
rabbit_ct_broker_helpers:rpc(Config, ?MODULE, validate_term_with_user_1, [Term]).
164+
165+
validate_term_with_user_1(Term) ->
166+
{ok, User} = rabbit_access_control:check_user_login(?USER, [{password, ?PASSWORD}]),
167+
validate_term(Term, User).
168+
169+
validate_with_none(Config, Uris) ->
170+
rabbit_ct_broker_helpers:rpc(Config, ?MODULE, validate_with_none_1, [Uris]).
171+
172+
validate_with_none_1(Uris) ->
173+
validate(Uris, none).
174+
175+
validate(Uris, User) ->
176+
validate_term([{<<"uri">>, Uris}], User).
177+
178+
validate_term(Term, User) ->
179+
rabbit_federation_parameters:validate(
180+
<<"/">>, <<"federation-upstream">>, <<"a-name">>, Term, User).

0 commit comments

Comments
 (0)