diff --git a/deps/rabbit/priv/schema/rabbit.schema b/deps/rabbit/priv/schema/rabbit.schema index fec9737cff..533d61b51b 100644 --- a/deps/rabbit/priv/schema/rabbit.schema +++ b/deps/rabbit/priv/schema/rabbit.schema @@ -616,8 +616,6 @@ end}. {datatype, string} ]}. - - %% %% Default User / VHost %% ==================== @@ -681,64 +679,95 @@ fun(Conf) -> [list_to_binary(Configure), list_to_binary(Read), list_to_binary(Write)] end}. +%% +%% Extra Default Users +%% ==================== +%% + +{mapping, "default_users.$name.vhost_pattern", "rabbit.default_users", [ + {validators, ["valid_regex"]}, + {datatype, string} +]}. + +{mapping, "default_users.$name.password", "rabbit.default_users", [ + {datatype, string} +]}. + +{mapping, "default_users.$name.configure", "rabbit.default_users", [ + {validators, ["valid_regex"]}, + {datatype, string} +]}. + +{mapping, "default_users.$name.read", "rabbit.default_users", [ + {validators, ["valid_regex"]}, + {datatype, string} +]}. + +{mapping, "default_users.$name.write", "rabbit.default_users", [ + {validators, ["valid_regex"]}, + {datatype, string} +]}. + +{mapping, "default_users.$name.tags", "rabbit.default_users", [ + {datatype, {list, atom}} +]}. + +{mapping, "default_users.$name.password", "rabbit.default_users", [ + {datatype, string} +]}. + +{translation, "rabbit.default_users", fun(Conf) -> + case rabbit_cuttlefish:aggregate_props(Conf, ["default_users"]) of + [] -> cuttlefish:unset(); + Props -> Props + end +end}. + +%% +%% Default Policies +%% ==================== +%% + {mapping, "default_policies.operator.$id.vhost_pattern", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, ".*"}, {validators, ["valid_regex"]}, {datatype, string} ]}. {mapping, "default_policies.operator.$id.queue_pattern", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, ".*"}, {validators, ["valid_regex"]}, {datatype, string} ]}. {mapping, "default_policies.operator.$id.expires", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, "1s"}, {datatype, {duration, ms}} ]}. {mapping, "default_policies.operator.$id.message_ttl", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, "1s"}, {datatype, {duration, ms}} ]}. {mapping, "default_policies.operator.$id.max_length", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, 100}, {validators, ["non_zero_positive_integer"]}, {datatype, integer} ]}. {mapping, "default_policies.operator.$id.max_length_bytes", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, "1GB"}, {validators, ["non_zero_positive_integer"]}, {datatype, bytesize} ]}. {mapping, "default_policies.operator.$id.max_in_memory_bytes", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, "1GB"}, {validators, ["non_zero_positive_integer"]}, {datatype, bytesize} ]}. {mapping, "default_policies.operator.$id.max_in_memory_length", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, 1000}, {validators, ["non_zero_positive_integer"]}, {datatype, integer} ]}. {mapping, "default_policies.operator.$id.delivery_limit", "rabbit.default_policies.operator", [ - {include_default, 1}, - {commented, 1}, {validators, ["non_zero_positive_integer"]}, {datatype, integer} ]}. @@ -759,55 +788,37 @@ end}. {["default_policies","operator",ID|T],V}; (E) -> E end), - Props1 = lists:map( - fun({K, Ss}) -> - {K, - lists:map(fun({N, V}) -> - {binary:replace(N, <<"_">>, <<"-">>, [global]), V} - end, Ss)} - end, Props), - case Props1 of + case Props of [] -> cuttlefish:unset(); - _ -> Props1 - end, - Props1 + Props -> Props + end end}. +%% +%% Default VHost Limits +%% ==================== +%% + {mapping, "default_limits.vhosts.$id.pattern", "rabbit.default_limits.vhosts", [ - {include_default, 1}, - {commented, ".*"}, {validators, ["valid_regex"]}, {datatype, string} ]}. {mapping, "default_limits.vhosts.$id.max_connections", "rabbit.default_limits.vhosts", [ - {include_default, 1}, - {commented, 1000}, {validators, [ "non_zero_positive_integer"]}, {datatype, integer} ]}. {mapping, "default_limits.vhosts.$id.max_queues", "rabbit.default_limits.vhosts", [ - {include_default, 1}, - {commented, 100}, {validators, [ "non_zero_positive_integer"]}, {datatype, integer} ]}. {translation, "rabbit.default_limits.vhosts", fun(Conf) -> - Props = rabbit_cuttlefish:aggregate_props(Conf, ["default_limits", "vhosts"]), - Props1 = lists:map( - fun({K, Ss}) -> - {K, - lists:map(fun({N, V}) -> - {binary:replace(N, <<"_">>, <<"-">>, [global]), V} - end, Ss)} - end, Props), - case Props1 of + case rabbit_cuttlefish:aggregate_props(Conf, ["default_limits", "vhosts"]) of [] -> cuttlefish:unset(); - _ -> Props1 - end, - Props1 + Props -> Props + end end}. %% Tags for default user diff --git a/deps/rabbit/src/rabbit_vhost.erl b/deps/rabbit/src/rabbit_vhost.erl index 1da1524f68..ced5c79136 100644 --- a/deps/rabbit/src/rabbit_vhost.erl +++ b/deps/rabbit/src/rabbit_vhost.erl @@ -141,40 +141,6 @@ parse_tags(Val) when is_list(Val) -> [trim_tag(Tag) || Tag <- re:split(ValUnicode, ",", [unicode, {return, list}])] end. --spec default_limits(vhost:name()) -> proplists:proplist(). -default_limits(Name) -> - AllLimits = application:get_env(rabbit, default_limits, []), - VHostLimits = proplists:get_value(vhosts, AllLimits, []), - Match = lists:search(fun({_, Ss}) -> - RE = proplists:get_value(<<"pattern">>, Ss, ".*"), - re:run(Name, RE, [{capture, none}]) =:= match - end, VHostLimits), - case Match of - {value, {_, Ss}} -> - proplists:delete(<<"pattern">>, Ss); - _ -> - [] - end. - --spec default_operator_policies(vhost:name()) -> - {binary(), binary(), proplists:proplist()} | not_found. -default_operator_policies(Name) -> - AllPolicies = application:get_env(rabbit, default_policies, []), - OpPolicies = proplists:get_value(operator, AllPolicies, []), - Match = lists:search(fun({_, Ss}) -> - RE = proplists:get_value(<<"vhost-pattern">>, Ss, ".*"), - re:run(Name, RE, [{capture, none}]) =:= match - end, OpPolicies), - case Match of - {value, {PolicyName, Ss}} -> - QPattern = proplists:get_value(<<"queue-pattern">>, Ss, ".*"), - Ss1 = proplists:delete(<<"queue-pattern">>, Ss), - Ss2 = proplists:delete(<<"vhost-pattern">>, Ss1), - {PolicyName, list_to_binary(QPattern), Ss2}; - _ -> - not_found - end. - -spec add(vhost:name(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()). add(VHost, ActingUser) -> @@ -227,7 +193,7 @@ do_add(Name, Metadata, ActingUser) -> rabbit_log:info("Adding vhost '~ts' (description: '~ts', tags: ~tp)", [Name, Description, Tags]) end, - DefaultLimits = default_limits(Name), + DefaultLimits = rabbit_vhost_defaults:list_limits(Name), {NewOrNot, VHost} = rabbit_db_vhost:create_or_get(Name, DefaultLimits, Metadata), case NewOrNot of new -> @@ -235,23 +201,7 @@ do_add(Name, Metadata, ActingUser) -> existing -> ok end, - case DefaultLimits of - [] -> - ok; - _ -> - ok = rabbit_vhost_limit:set(Name, DefaultLimits, ActingUser), - rabbit_log:info("Applied default limits to vhost '~tp': ~tp", - [Name, DefaultLimits]) - end, - case default_operator_policies(Name) of - not_found -> - ok; - {PolicyName, QPattern, Definition} = Policy -> - ok = rabbit_policy:set_op(Name, PolicyName, QPattern, Definition, - undefined, undefined, ActingUser), - rabbit_log:info("Applied default operator policy to vhost '~tp': ~tp", - [Name, Policy]) - end, + rabbit_vhost_defaults:apply(Name, ActingUser), _ = [begin Resource = rabbit_misc:r(Name, exchange, ExchangeName), rabbit_log:debug("Will declare an exchange ~tp", [Resource]), diff --git a/deps/rabbit/src/rabbit_vhost_defaults.erl b/deps/rabbit/src/rabbit_vhost_defaults.erl new file mode 100644 index 0000000000..1506dd7bdd --- /dev/null +++ b/deps/rabbit/src/rabbit_vhost_defaults.erl @@ -0,0 +1,169 @@ +%% 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) 2023-2023 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_vhost_defaults). + +-export([apply/2]). +-export([list_limits/1, list_operator_policies/1, list_users/1]). + +-type definitions() :: [{binary(), term()}]. + +-record(policy, { + name :: binary(), + queue_pattern = <<".*">> :: binary(), + definition = [] :: definitions() +}). + +-type user() :: #{ + name := binary(), + configure := binary(), + read := binary(), + write := binary(), + password := binary(), + tags := [atom()], + _ => _ +}. + +%% Apply all matching defaults to a VHost. +-spec apply(vhost:name(), rabbit_types:username()) -> ok. +apply(VHost, ActingUser) -> + case list_limits(VHost) of + [] -> + ok; + L -> + ok = rabbit_vhost_limit:set(VHost, L, ActingUser), + rabbit_log:info("Applied default limits to vhost '~tp': ~tp", [VHost, L]) + end, + lists:foreach( + fun(P) -> + ok = rabbit_policy:set_op(VHost, P#policy.name, P#policy.queue_pattern, P#policy.definition, + undefined, undefined, ActingUser), + rabbit_log:info("Applied default operator policy to vhost '~tp': ~tp", [VHost, P]) + end, + list_operator_policies(VHost) + ), + lists:foreach( + fun(U) -> + ok = add_user(VHost, U, ActingUser), + rabbit_log:info("Added default user to vhost '~tp': ~tp", [VHost, maps:remove(password, U)]) + end, + list_users(VHost) + ), + ok. + +%% +%% Helpers +%% + +%% Limits that were configured with a matching vhost pattern. +-spec list_limits(vhost:name()) -> proplists:proplist(). +list_limits(VHost) -> + AllLimits = application:get_env(rabbit, default_limits, []), + VHostLimits = proplists:get_value(vhosts, AllLimits, []), + Match = lists:search( + fun({_, Ss}) -> + RE = proplists:get_value(<<"pattern">>, Ss, ".*"), + re:run(VHost, RE, [{capture, none}]) =:= match + end, + VHostLimits + ), + case Match of + {value, {_, Ss}} -> + Ss1 = proplists:delete(<<"pattern">>, Ss), + underscore_to_dash(Ss1); + _ -> + [] + end. + +%% Operator policies that were configured with a matching vhost pattern. +-spec list_operator_policies(vhost:name()) -> [#policy{}]. +list_operator_policies(VHost) -> + AllPolicies = application:get_env(rabbit, default_policies, []), + OpPolicies = proplists:get_value(operator, AllPolicies, []), + lists:filtermap( + fun({PolicyName, Ss}) -> + RE = proplists:get_value(<<"vhost_pattern">>, Ss, ".*"), + case re:run(VHost, RE, [{capture, none}]) of + match -> + QPattern = proplists:get_value(<<"queue_pattern">>, Ss, <<".*">>), + Ss1 = proplists:delete(<<"queue_pattern">>, Ss), + Ss2 = proplists:delete(<<"vhost_pattern">>, Ss1), + {true, #policy{ + name = PolicyName, + queue_pattern = QPattern, + definition = underscore_to_dash(Ss2) + }}; + _ -> + false + end + end, + OpPolicies + ). + +%% Users (permissions) that were configured with a matching vhost pattern. +-spec list_users(vhost:name()) -> [user()]. +list_users(VHost) -> + Users = application:get_env(rabbit, default_users, []), + lists:filtermap( + fun({Username, Ss}) -> + RE = proplists:get_value(<<"vhost_pattern">>, Ss, ".*"), + case re:run(VHost, RE, [{capture, none}]) of + match -> + C = rabbit_data_coercion:to_binary( + proplists:get_value(<<"configure">>, Ss, <<".*">>) + ), + R = rabbit_data_coercion:to_binary( + proplists:get_value(<<"read">>, Ss, <<".*">>) + ), + W = rabbit_data_coercion:to_binary( + proplists:get_value(<<"write">>, Ss, <<".*">>) + ), + U0 = #{ + name => Username, + tags => proplists:get_value(<<"tags">>, Ss, []), + configure => C, + read => R, + write => W + }, + %% rabbit_auth_backend_internal:put_user relies on maps:is_key, can't pass + %% undefined through. + U1 = case proplists:get_value(<<"password">>, Ss, undefined) of + undefined -> + U0; + V -> + U0#{password => rabbit_data_coercion:to_binary(V)} + end, + {true, U1}; + _ -> + false + end + end, + Users + ). + +%% +%% Private +%% + +%% Translate underscores to dashes in prop keys. +-spec underscore_to_dash(definitions()) -> definitions(). +underscore_to_dash(Props) -> + lists:map( + fun({N, V}) -> + {binary:replace(N, <<"_">>, <<"-">>, [global]), V} + end, + Props + ). + +%% Add user iff it doesn't exist & set permissions per vhost. +-spec add_user(rabbit_types:vhost(), user(), rabbit_types:username()) -> ok. +add_user(VHost, #{name := Name, configure := C, write := W, read := R} = User, ActingUser) -> + %% put_user has its own existence check, but it still updates password if the user exists. + %% We want only the newly created users to have password set from the config. + rabbit_auth_backend_internal:exists(Name) orelse + rabbit_auth_backend_internal:put_user(User, ActingUser), + rabbit_auth_backend_internal:set_permissions(Name, VHost, C, W, R, ActingUser). diff --git a/deps/rabbit/test/config_schema_SUITE_data/rabbit.snippets b/deps/rabbit/test/config_schema_SUITE_data/rabbit.snippets index 95ff9e84e6..a04eef4fa0 100644 --- a/deps/rabbit/test/config_schema_SUITE_data/rabbit.snippets +++ b/deps/rabbit/test/config_schema_SUITE_data/rabbit.snippets @@ -1,3 +1,6 @@ +% vim:ft=erlang: +% + [{internal_auth_backend, "auth_backends.1 = internal", [{rabbit,[{auth_backends,[rabbit_auth_backend_internal]}]}], @@ -118,6 +121,20 @@ ssl_options.fail_if_no_peer_cert = true", "disk_free_limit.absolute = 50000", [{rabbit, [{disk_free_limit, 50000}]}],[]}, + {default_users, + " + default_users.a.vhost_pattern = banana + default_users.a.tags = administrator,operator + default_users.a.password = SECRET + default_users.a.read = .* + ", + [{rabbit, [{default_users, [ + {<<"a">>, [{<<"vhost_pattern">>, "banana"}, + {<<"tags">>, [administrator, operator]}, + {<<"password">>, "SECRET"}, + {<<"read">>, ".*"}]}]}]}], + []}, + {default_policies_operator, " default_policies.operator.a.expires = 1h @@ -128,10 +145,10 @@ ssl_options.fail_if_no_peer_cert = true", ", [{rabbit, [{default_policies, [{operator, [ {<<"a">>, [{<<"expires">>, 3600000}, - {<<"ha-mode">>, "exactly"}, - {<<"ha-params">>, 2}, - {<<"queue-pattern">>, "apple"}, - {<<"vhost-pattern">>, "banana"}]}]}]}]}], + {<<"ha_mode">>, "exactly"}, + {<<"ha_params">>, 2}, + {<<"queue_pattern">>, "apple"}, + {<<"vhost_pattern">>, "banana"}]}]}]}]}], []}, {default_vhost_limits, @@ -141,7 +158,7 @@ ssl_options.fail_if_no_peer_cert = true", ", [{rabbit, [{default_limits, [{vhosts, [ {<<"a">>, [{<<"pattern">>, "banana"}, - {<<"max-queues">>, 10}]}]}]}]}], + {<<"max_queues">>, 10}]}]}]}]}], []}, {default_user_settings, diff --git a/deps/rabbit/test/vhost_SUITE.erl b/deps/rabbit/test/vhost_SUITE.erl index e49e827b8e..44362ce9fe 100644 --- a/deps/rabbit/test/vhost_SUITE.erl +++ b/deps/rabbit/test/vhost_SUITE.erl @@ -25,6 +25,7 @@ groups() -> ClusterSize1Tests = [ vhost_is_created_with_default_limits, vhost_is_created_with_operator_policies, + vhost_is_created_with_default_user, single_node_vhost_deletion_forces_connection_closure, vhost_failure_forces_connection_closure, vhost_creation_idempotency, @@ -375,6 +376,44 @@ vhost_is_created_with_operator_policies(Config) -> ?assertNotEqual(not_found, rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_policy, lookup_op, [VHost, PolicyName])). +vhost_is_created_with_default_user(Config) -> + VHost = <<"vhost1">>, + Username = <<"banana">>, + Perm = "apple", + Tags = [arbitrary], + Pwd = "SECRET", + Env = [{Username, [{<<"configure">>, Perm}, {<<"tags">>, [arbitrary]}, {<<"password">>, Pwd}]}], + WantUser = [{user, Username},{tags, Tags}], + WantPermissions = [[{vhost, VHost}, {configure, list_to_binary(Perm)}, {write, <<".*">>}, {read, <<".*">>}]], + ?assertEqual(ok, rabbit_ct_broker_helpers:rpc(Config, 0, + application, set_env, [rabbit, default_users, Env])), + ?assertEqual(false, rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, exists, [Username])), + ?assertEqual(ok, rabbit_ct_broker_helpers:add_vhost(Config, VHost)), + ct:pal("HAVE: ~p", [rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, list_user_permissions, [Username])]), + ct:pal("WANT: ~p", [WantPermissions]), + ?assertEqual(WantPermissions, rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, list_user_permissions, [Username])), + HaveUser = lists:search( + fun (U) -> + case proplists:get_value(user, U) of + Username -> true; + undefined -> false + end + end, + rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, list_users, []) + ), + ?assertEqual({value, WantUser}, HaveUser), + ?assertMatch({ok, _}, rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, user_login_authentication, [Username, [{password, list_to_binary(Pwd)}]])), + ?assertEqual(ok, rabbit_ct_broker_helpers:rpc(Config, 0, + application, unset_env, [rabbit, default_users])), + ?assertEqual(ok, rabbit_ct_broker_helpers:rpc(Config, 0, + rabbit_auth_backend_internal, delete_user, [Username, + <<"acting-user">>])). + parse_tags(Config) -> rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, parse_tags1, [Config]).