Skip to content

Commit bfa0d17

Browse files
Merge pull request #13015 from rabbitmq/mk-virtual-host-protection-from-accidental-deletion
4.1.0: Introduce a way to protect a virtual host from deletion
2 parents ac7dcc9 + faa4f5a commit bfa0d17

File tree

44 files changed

+960
-131
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+960
-131
lines changed

deps/rabbit/src/rabbit_db_vhost.erl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
count_all/0,
2424
list/0,
2525
update/2,
26+
enable_protection_from_deletion/1,
27+
disable_protection_from_deletion/1,
2628
with_fun_in_mnesia_tx/2,
2729
with_fun_in_khepri_tx/2,
2830
delete/1,
@@ -224,6 +226,36 @@ set_tags_in_khepri(VHostName, Tags) ->
224226
UpdateFun = fun(VHost) -> do_set_tags(VHost, Tags) end,
225227
update_in_khepri(VHostName, UpdateFun).
226228

229+
%% -------------------------------------------------------------------
230+
%% Deletion protection
231+
%% -------------------------------------------------------------------
232+
233+
-spec enable_protection_from_deletion(VHostName) -> Ret when
234+
VHostName :: vhost:name(),
235+
Ret :: {ok, VHost} |
236+
{error, {no_such_vhost, VHostName}} |
237+
rabbit_khepri:timeout_error(),
238+
VHost :: vhost:vhost().
239+
enable_protection_from_deletion(VHostName) ->
240+
MetadataPatch = #{
241+
protected_from_deletion => true
242+
},
243+
rabbit_log:info("Enabling deletion protection for virtual host '~ts'", [VHostName]),
244+
merge_metadata(VHostName, MetadataPatch).
245+
246+
-spec disable_protection_from_deletion(VHostName) -> Ret when
247+
VHostName :: vhost:name(),
248+
Ret :: {ok, VHost} |
249+
{error, {no_such_vhost, VHostName}} |
250+
rabbit_khepri:timeout_error(),
251+
VHost :: vhost:vhost().
252+
disable_protection_from_deletion(VHostName) ->
253+
MetadataPatch = #{
254+
protected_from_deletion => false
255+
},
256+
rabbit_log:info("Disabling deletion protection for virtual host '~ts'", [VHostName]),
257+
merge_metadata(VHostName, MetadataPatch).
258+
227259
%% -------------------------------------------------------------------
228260
%% exists().
229261
%% -------------------------------------------------------------------

deps/rabbit/src/rabbit_definitions.erl

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -786,8 +786,17 @@ add_vhost(VHost, ActingUser) ->
786786
case rabbit_vhost:put_vhost(Name, Description, Tags, DefaultQueueType, IsTracingEnabled, ActingUser) of
787787
ok ->
788788
ok;
789-
{error, _} = Err ->
790-
throw(Err)
789+
{error, _} = Err1 ->
790+
throw(Err1)
791+
end,
792+
793+
%% The newly created virtual host won't have all the metadata keys. Rather than
794+
%% changing the functions above, simply update the metadata as a separate step.
795+
case rabbit_vhost:update_metadata(Name, Metadata, ActingUser) of
796+
ok ->
797+
ok;
798+
{error, _} = Err2 ->
799+
throw(Err2)
791800
end.
792801

793802
add_permission(Permission, ActingUser) ->

deps/rabbit/src/rabbit_vhost.erl

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
-include("vhost.hrl").
1212

1313
-export([recover/0, recover/1, read_config/1]).
14-
-export([add/2, add/3, add/4, delete/2, exists/1, assert/1,
14+
-export([add/2, add/3, add/4, delete/2, delete_ignoring_protection/2, exists/1, assert/1,
1515
set_limits/2, vhost_cluster_state/1, is_running_on_all_nodes/1, await_running_on_all_nodes/2,
1616
list/0, count/0, list_names/0, all/0, all_tagged_with/1]).
1717
-export([parse_tags/1, update_tags/3]).
18-
-export([update_metadata/3]).
18+
-export([update_metadata/3, enable_protection_from_deletion/1, disable_protection_from_deletion/1]).
1919
-export([lookup/1, default_name/0]).
2020
-export([info/1, info/2, info_all/0, info_all/1, info_all/2, info_all/3]).
2121
-export([dir/1, msg_store_dir_path/1, msg_store_dir_wildcard/0, msg_store_dir_base/0, config_file_path/1, ensure_config_file/1]).
@@ -253,20 +253,37 @@ declare_default_exchanges(VHostName, ActingUser) ->
253253
end, DefaultExchanges).
254254

255255
-spec update_metadata(vhost:name(), vhost:metadata(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
256+
update_metadata(Name, undefined, _ActingUser) ->
257+
case rabbit_db_vhost:exists(Name) of
258+
true ->
259+
ok;
260+
false ->
261+
{error, {no_such_vhost, Name}}
262+
end;
263+
update_metadata(Name, Metadata0, _ActingUser) when is_map(Metadata0) andalso map_size(Metadata0) =:= 0 ->
264+
case rabbit_db_vhost:exists(Name) of
265+
true ->
266+
ok;
267+
false ->
268+
{error, {no_such_vhost, Name}}
269+
end;
256270
update_metadata(Name, Metadata0, ActingUser) ->
257-
Metadata = maps:with([description, tags, default_queue_type], Metadata0),
271+
KnownKeys = [description, tags, default_queue_type, protected_from_deletion],
272+
Metadata = maps:with(KnownKeys, Metadata0),
258273

259274
case rabbit_db_vhost:merge_metadata(Name, Metadata) of
260275
{ok, VHost} ->
261276
Description = vhost:get_description(VHost),
262277
Tags = vhost:get_tags(VHost),
263278
DefaultQueueType = vhost:get_default_queue_type(VHost),
279+
IsProtected = vhost:is_protected_from_deletion(VHost),
264280
rabbit_event:notify(
265281
vhost_updated,
266282
info(VHost) ++ [{user_who_performed_action, ActingUser},
267283
{description, Description},
268284
{tags, Tags},
269-
{default_queue_type, DefaultQueueType}]),
285+
{default_queue_type, DefaultQueueType},
286+
{deletion_protection, IsProtected}]),
270287
ok;
271288
{error, _} = Error ->
272289
Error
@@ -278,45 +295,61 @@ update(Name, Description, Tags, DefaultQueueType, ActingUser) ->
278295
update_metadata(Name, Metadata, ActingUser).
279296

280297
-spec delete(vhost:name(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
281-
delete(VHost, ActingUser) ->
298+
delete(Name, ActingUser) ->
299+
case rabbit_db_vhost:get(Name) of
300+
%% preserve the original behavior for backwards compatibility
301+
undefined -> delete_ignoring_protection(Name, ActingUser);
302+
VHost ->
303+
case vhost:is_protected_from_deletion(VHost) of
304+
true ->
305+
Msg = "Refusing to delete virtual host '~ts' because it is protected from deletion",
306+
rabbit_log:debug(Msg, [Name]),
307+
{error, protected_from_deletion};
308+
false ->
309+
delete_ignoring_protection(Name, ActingUser)
310+
end
311+
end.
312+
313+
-spec delete_ignoring_protection(vhost:name(), rabbit_types:username()) -> rabbit_types:ok_or_error(any()).
314+
delete_ignoring_protection(Name, ActingUser) ->
282315
%% FIXME: We are forced to delete the queues and exchanges outside
283316
%% the TX below. Queue deletion involves sending messages to the queue
284317
%% process, which in turn results in further database actions and
285318
%% eventually the termination of that process. Exchange deletion causes
286319
%% notifications which must be sent outside the TX
287-
rabbit_log:info("Deleting vhost '~ts'", [VHost]),
320+
rabbit_log:info("Deleting vhost '~ts'", [Name]),
288321
%% TODO: This code does a lot of "list resources, walk through the list to
289322
%% delete each resource". This feature should be provided by each called
290323
%% modules, like `rabbit_amqqueue:delete_all_for_vhost(VHost)'. These new
291324
%% calls would be responsible for the atomicity, not this code.
292325
%% Clear the permissions first to prohibit new incoming connections when deleting a vhost
293-
rabbit_log:info("Clearing permissions in vhost '~ts' because it's being deleted", [VHost]),
294-
ok = rabbit_auth_backend_internal:clear_all_permissions_for_vhost(VHost, ActingUser),
295-
rabbit_log:info("Deleting queues in vhost '~ts' because it's being deleted", [VHost]),
326+
rabbit_log:info("Clearing permissions in vhost '~ts' because it's being deleted", [Name]),
327+
ok = rabbit_auth_backend_internal:clear_all_permissions_for_vhost(Name, ActingUser),
328+
rabbit_log:info("Deleting queues in vhost '~ts' because it's being deleted", [Name]),
296329
QDelFun = fun (Q) -> rabbit_amqqueue:delete(Q, false, false, ActingUser) end,
297330
[begin
298-
Name = amqqueue:get_name(Q),
299-
assert_benign(rabbit_amqqueue:with(Name, QDelFun), ActingUser)
300-
end || Q <- rabbit_amqqueue:list(VHost)],
301-
rabbit_log:info("Deleting exchanges in vhost '~ts' because it's being deleted", [VHost]),
302-
ok = rabbit_exchange:delete_all(VHost, ActingUser),
303-
rabbit_log:info("Clearing policies and runtime parameters in vhost '~ts' because it's being deleted", [VHost]),
304-
_ = rabbit_runtime_parameters:clear_vhost(VHost, ActingUser),
305-
rabbit_log:debug("Removing vhost '~ts' from the metadata storage because it's being deleted", [VHost]),
306-
Ret = case rabbit_db_vhost:delete(VHost) of
331+
QName = amqqueue:get_name(Q),
332+
assert_benign(rabbit_amqqueue:with(QName, QDelFun), ActingUser)
333+
end || Q <- rabbit_amqqueue:list(Name)],
334+
rabbit_log:info("Deleting exchanges in vhost '~ts' because it's being deleted", [Name]),
335+
ok = rabbit_exchange:delete_all(Name, ActingUser),
336+
rabbit_log:info("Clearing policies and runtime parameters in vhost '~ts' because it's being deleted", [Name]),
337+
_ = rabbit_runtime_parameters:clear_vhost(Name, ActingUser),
338+
rabbit_log:debug("Removing vhost '~ts' from the metadata storage because it's being deleted", [Name]),
339+
Ret = case rabbit_db_vhost:delete(Name) of
307340
true ->
308341
ok = rabbit_event:notify(
309342
vhost_deleted,
310-
[{name, VHost},
343+
[{name, Name},
311344
{user_who_performed_action, ActingUser}]);
312345
false ->
313-
{error, {no_such_vhost, VHost}};
346+
{error, {no_such_vhost, Name}};
314347
{error, _} = Err ->
315348
Err
316349
end,
317350
%% After vhost was deleted from the database, we try to stop vhost
318351
%% supervisors on all the nodes.
319-
rabbit_vhost_sup_sup:delete_on_all_nodes(VHost),
352+
rabbit_vhost_sup_sup:delete_on_all_nodes(Name),
320353
Ret.
321354

322355
-spec put_vhost(vhost:name(),
@@ -530,6 +563,14 @@ lookup(VHostName) ->
530563
VHost -> VHost
531564
end.
532565

566+
-spec enable_protection_from_deletion(vhost:name()) -> vhost:vhost() | rabbit_types:ok_or_error(any()).
567+
enable_protection_from_deletion(VHostName) ->
568+
rabbit_db_vhost:enable_protection_from_deletion(VHostName).
569+
570+
-spec disable_protection_from_deletion(vhost:name()) -> vhost:vhost() | rabbit_types:ok_or_error(any()).
571+
disable_protection_from_deletion(VHostName) ->
572+
rabbit_db_vhost:disable_protection_from_deletion(VHostName).
573+
533574
-spec assert(vhost:name()) -> 'ok'.
534575
assert(VHostName) ->
535576
case exists(VHostName) of
@@ -624,6 +665,7 @@ i(cluster_state, VHost) -> vhost_cluster_state(vhost:get_name(VHost));
624665
i(description, VHost) -> vhost:get_description(VHost);
625666
i(tags, VHost) -> vhost:get_tags(VHost);
626667
i(default_queue_type, VHost) -> rabbit_queue_type:short_alias_of(default_queue_type(vhost:get_name(VHost)));
668+
i(protected_from_deletion, VHost) -> vhost:is_protected_from_deletion(VHost);
627669
i(metadata, VHost) ->
628670
DQT = rabbit_queue_type:short_alias_of(default_queue_type(vhost:get_name(VHost))),
629671
case vhost:get_metadata(VHost) of

deps/rabbit/src/vhost.erl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
set_limits/2,
3131
set_metadata/2,
3232
merge_metadata/2,
33+
34+
is_protected_from_deletion/1,
35+
enable_protection_from_deletion/1,
36+
disable_protection_from_deletion/1,
37+
3338
new_metadata/3,
3439
is_tagged_with/2,
3540

@@ -89,6 +94,8 @@
8994
vhost_pattern/0,
9095
vhost_v2_pattern/0]).
9196

97+
-define(DELETION_PROTECTION_KEY, protected_from_deletion).
98+
9299
-spec new(name(), limits()) -> vhost().
93100
new(Name, Limits) ->
94101
#vhost{virtual_host = Name, limits = Limits}.
@@ -123,6 +130,7 @@ info_keys() ->
123130
description,
124131
tags,
125132
default_queue_type,
133+
protected_from_deletion,
126134
metadata,
127135
tracing,
128136
cluster_state].
@@ -187,6 +195,23 @@ metadata_merger(default_queue_type, _, NewVHostDefaultQueueType) ->
187195
metadata_merger(_, _, NewMetadataValue) ->
188196
NewMetadataValue.
189197

198+
-spec is_protected_from_deletion(vhost()) -> boolean().
199+
is_protected_from_deletion(VHost) ->
200+
case get_metadata(VHost) of
201+
Map when map_size(Map) =:= 0 -> false;
202+
#{?DELETION_PROTECTION_KEY := true} -> true;
203+
#{?DELETION_PROTECTION_KEY := false} -> false;
204+
_ -> false
205+
end.
206+
207+
-spec enable_protection_from_deletion(vhost()) -> vhost().
208+
enable_protection_from_deletion(VHost) ->
209+
merge_metadata(VHost, #{?DELETION_PROTECTION_KEY => true}).
210+
211+
-spec disable_protection_from_deletion(vhost()) -> vhost().
212+
disable_protection_from_deletion(VHost) ->
213+
merge_metadata(VHost, #{?DELETION_PROTECTION_KEY => false}).
214+
190215
-spec new_metadata(binary(), [atom()], rabbit_queue_type:queue_type() | 'undefined') -> metadata().
191216
new_metadata(Description, Tags, undefined) ->
192217
#{description => Description,

deps/rabbit/test/definition_import_SUITE.erl

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ groups() ->
5353
import_case18,
5454
import_case19,
5555
import_case20,
56-
import_case21
56+
import_case21,
57+
import_case22
5758
]},
5859

5960
{boot_time_import_using_classic_source, [], [
@@ -273,7 +274,7 @@ import_case16(Config) ->
273274
Val when is_tuple(Val) ->
274275
?assertEqual(<<"A case16 description">>, vhost:get_description(VHostRec)),
275276
?assertEqual(<<"quorum">>, vhost:get_default_queue_type(VHostRec)),
276-
?assertEqual([multi_dc_replication,ab,cde], vhost:get_tags(VHostRec))
277+
?assertEqual([<<"multi_dc_replication">>,<<"ab">>,<<"cde">>], vhost:get_tags(VHostRec))
277278
end,
278279

279280
ok.
@@ -315,6 +316,25 @@ import_case20(Config) ->
315316

316317
import_case21(Config) -> import_invalid_file_case(Config, "failing_case21").
317318

319+
import_case22(Config) ->
320+
import_file_case(Config, "case22"),
321+
Name = <<"protected">>,
322+
VirtualHostIsImported =
323+
fun () ->
324+
case vhost_lookup(Config, Name) of
325+
{error, not_found} -> false;
326+
_ -> true
327+
end
328+
end,
329+
rabbit_ct_helpers:await_condition(VirtualHostIsImported, 20000),
330+
VHost = vhost_lookup(Config, Name),
331+
?assertEqual(true, vhost:is_protected_from_deletion(VHost)),
332+
333+
VHost2 = vhost_lookup(Config, <<"non-protected">>),
334+
?assertEqual(false, vhost:is_protected_from_deletion(VHost2)),
335+
336+
ok.
337+
318338
export_import_round_trip_case1(Config) ->
319339
case rabbit_ct_helpers:is_mixed_versions() of
320340
false ->
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"bindings": [],
3+
"exchanges": [],
4+
"global_parameters": [
5+
{
6+
"name": "cluster_name",
7+
"value": "rabbitmq@localhost"
8+
}
9+
],
10+
"parameters": [],
11+
"permissions": [
12+
{
13+
"configure": ".*",
14+
"read": ".*",
15+
"user": "guest",
16+
"vhost": "/",
17+
"write": ".*"
18+
}
19+
],
20+
"policies": [],
21+
"queues": [],
22+
"rabbit_version": "4.0.5",
23+
"rabbitmq_version": "4.0.5",
24+
"topic_permissions": [],
25+
"users": [
26+
{
27+
"hashing_algorithm": "rabbit_password_hashing_sha256",
28+
"limits": {"max-connections" : 2},
29+
"name": "limited_guest",
30+
"password_hash": "wS4AT3B4Z5RpWlFn1FA30osf2C75D7WA3gem591ACDZ6saO6",
31+
"tags": [
32+
"administrator"
33+
]
34+
}
35+
],
36+
"vhosts": [
37+
{
38+
"limits": [],
39+
"name": "non-protected",
40+
"description": "",
41+
"metadata": {
42+
"description": "",
43+
"tags": [],
44+
"default_queue_type": "classic"
45+
},
46+
"tags": [],
47+
"default_queue_type": "classic"
48+
},
49+
{
50+
"name": "protected",
51+
"description": "protected, DQT = quorum",
52+
"metadata": {
53+
"description": "DQT = quorum",
54+
"tags": [],
55+
"default_queue_type": "quorum",
56+
"protected_from_deletion": true
57+
},
58+
"tags": [],
59+
"default_queue_type": "quorum"
60+
},
61+
{
62+
"limits": [],
63+
"name": "tagged"
64+
}
65+
]
66+
}

0 commit comments

Comments
 (0)