Skip to content

Commit 16fcc15

Browse files
MB-61292: Add formatting for delete secret errors
Needed mostly for UI and future CLI Change-Id: Id8d4d66fa929b321ba6468d307567fe601970829 Reviewed-on: https://review.couchbase.org/c/ns_server/+/216782 Well-Formed: Build Bot <[email protected]> Reviewed-by: Navdeep S Boparai <[email protected]> Tested-by: Timofey Barmin <[email protected]>
1 parent 5ea7c36 commit 16fcc15

File tree

3 files changed

+107
-17
lines changed

3 files changed

+107
-17
lines changed

apps/ns_server/src/cb_cluster_secrets.erl

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@
129129
-type bad_encrypt_id() :: {encrypt_id, not_allowed | not_found}.
130130
-type bad_usage_change() :: {usage, in_use}.
131131

132-
-type secret_in_use() :: {used_by, [{bucket, string()} |
133-
{secret, secret_id()}]}.
132+
-type secret_in_use() :: {used_by, #{by_config := [cb_deks:dek_kind()],
133+
by_secret := [secret_id()],
134+
by_deks := [cb_deks:dek_kind()]}}.
134135

135136
-type deks_info() :: #{active_id := cb_deks:dek_id() | undefined,
136137
deks := [cb_deks:dek()],
@@ -1141,7 +1142,7 @@ can_delete_secret(#{id := Id}, Snapshot) ->
11411142
case call_dek_callback(encryption_method_callback, Kind,
11421143
[Snapshot]) of
11431144
{succ, {ok, {secret, Id}}} ->
1144-
{true, maps:get(name, cb_deks:dek_config(Kind))};
1145+
{true, Kind};
11451146
{succ, {ok, _}} ->
11461147
false;
11471148
{succ, {error, not_found}} ->
@@ -1153,12 +1154,19 @@ can_delete_secret(#{id := Id}, Snapshot) ->
11531154
%% Places where this secret is used to encrypt deks (such deks can exist
11541155
%% even if encryption is disabled for this entity)
11551156
Deks = get_dek_kinds_used_by_secret_id(Id, Snapshot),
1156-
AllEntities =
1157-
EncryptionConfigUsages ++ [{secret, SId} || SId <- Secrets] ++
1158-
[{deks, D} || D <- Deks],
1159-
case AllEntities of
1160-
[] -> ok;
1161-
_ -> {error, {used_by, AllEntities}}
1157+
SecretNames =
1158+
lists:map(fun (SId) ->
1159+
{ok, #{name := SName}} = get_secret(SId, Snapshot),
1160+
SName
1161+
end, Secrets),
1162+
1163+
case length(EncryptionConfigUsages) + length(SecretNames) + length(Deks) of
1164+
0 -> ok;
1165+
_ ->
1166+
M = #{by_config => EncryptionConfigUsages,
1167+
by_secrets => SecretNames,
1168+
by_deks => Deks},
1169+
{error, {used_by, M}}
11621170
end.
11631171

11641172
-spec get_secrets_used_by_secret_id(secret_id(), chronicle_snapshot()) ->

apps/ns_server/src/cb_deks.erl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,7 @@ dek_chronicle_keys_filter(Key) ->
316316
%% required_usage - the secret usage that secret must contain in order to be
317317
%% allowed to encrypt this kind of deks
318318
dek_config(chronicleDek) ->
319-
#{name => chronicle,
320-
encryption_method_callback => cb_crypto:get_encryption_method(
319+
#{encryption_method_callback => cb_crypto:get_encryption_method(
321320
config_encryption, _),
322321
set_active_key_callback => fun chronicle_local:set_active_dek/1,
323322
lifetime_callback => cb_crypto:get_dek_kind_lifetime(
@@ -329,8 +328,7 @@ dek_config(chronicleDek) ->
329328
chronicle_txn_keys => [?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY],
330329
required_usage => config_encryption};
331330
dek_config(configDek) ->
332-
#{name => configuration,
333-
encryption_method_callback => cb_crypto:get_encryption_method(
331+
#{encryption_method_callback => cb_crypto:get_encryption_method(
334332
config_encryption, _),
335333
set_active_key_callback => fun set_config_active_key/1,
336334
lifetime_callback => cb_crypto:get_dek_kind_lifetime(
@@ -342,8 +340,7 @@ dek_config(configDek) ->
342340
chronicle_txn_keys => [?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY],
343341
required_usage => config_encryption};
344342
dek_config({bucketDek, Bucket}) ->
345-
#{name => {bucket, Bucket},
346-
encryption_method_callback => ns_bucket:get_encryption(Bucket, _),
343+
#{encryption_method_callback => ns_bucket:get_encryption(Bucket, _),
347344
set_active_key_callback => ns_memcached:set_active_dek_for_bucket(Bucket,
348345
_),
349346
lifetime_callback => ns_bucket:get_dek_lifetime(Bucket, _),

apps/ns_server/src/menelaus_web_secrets.erl

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
-include("ns_common.hrl").
1616
-include_lib("ns_common/include/cut.hrl").
1717
-include("cb_cluster_secrets.hrl").
18+
-ifdef(TEST).
19+
-include_lib("eunit/include/eunit.hrl").
20+
-endif.
1821

1922
-export([handle_get_secrets/1,
2023
handle_get_secret/2,
@@ -125,9 +128,10 @@ handle_delete_secret(IdStr, Req) ->
125128
menelaus_util:reply(Req, 200);
126129
{error, not_found} ->
127130
menelaus_util:reply_not_found(Req);
128-
{error, Reason} ->
131+
{error, {used_by, UsedByList}} ->
132+
Formatted = format_secrets_used_by_list(UsedByList),
129133
menelaus_util:reply_global_error(
130-
Req, io_lib:format("Error: ~p", [Reason]))
134+
Req, io_lib:format("Can't be removed because ~s", [Formatted]))
131135
end.
132136

133137
handle_rotate(IdStr, Req) ->
@@ -402,3 +406,84 @@ parse_id(Str) when is_list(Str) ->
402406
_:_ ->
403407
menelaus_util:web_exception(404, menelaus_util:reply_text_404())
404408
end.
409+
410+
format_secrets_used_by_list(UsedByMap) ->
411+
UsedByCfg = maps:get(by_config, UsedByMap, []),
412+
Secrets = maps:get(by_secrets, UsedByMap, []),
413+
UsedByDeks = maps:get(by_deks, UsedByMap, []),
414+
415+
FormatUsages =
416+
fun (Usages) ->
417+
{Buckets, Other} = misc:partitionmap(
418+
fun ({bucket_encryption, B}) -> {left, B};
419+
(K) -> {right, K}
420+
end, Usages),
421+
FormattedUsages = lists:map(fun (config_encryption) ->
422+
"configuration"
423+
end, Other),
424+
Buckets2 = ["\"" ++ B ++ "\"" || B <- Buckets],
425+
BucketsStr =
426+
case length(Buckets) of
427+
0 -> [];
428+
1 -> ["bucket " ++ Buckets2];
429+
_ -> ["buckets " ++ lists:join(", ", Buckets2)]
430+
end,
431+
FormattedUsages ++ BucketsStr
432+
end,
433+
Kind2Usage = ?cut(maps:get(required_usage, cb_deks:dek_config(_))),
434+
UsagesUsedByCfg = lists:uniq(lists:map(Kind2Usage, UsedByCfg)),
435+
UsagesUsedByDeks = lists:uniq(lists:map(Kind2Usage, UsedByDeks)),
436+
SecretsStrs = ["secrets " ++ lists:join(", ", Secrets) || Secrets /= []],
437+
Strings1 = FormatUsages(UsagesUsedByCfg) ++ SecretsStrs,
438+
Strings2 = FormatUsages(UsagesUsedByDeks -- UsagesUsedByCfg),
439+
440+
case {Strings1, Strings2} of
441+
{_, []} ->
442+
"this secret is configured to encrypt " ++
443+
lists:join(", ", Strings1);
444+
{[], _} ->
445+
"this secret still encrypts some data in " ++
446+
lists:join(", ", Strings2);
447+
{_ , _} ->
448+
"this secret is configured to encrypt " ++
449+
lists:join(", ", Strings1) ++
450+
"; it also still encrypts some data in " ++
451+
lists:join(", ", Strings2)
452+
end.
453+
454+
-ifdef(TEST).
455+
456+
format_secrets_used_by_list_test() ->
457+
All = cb_deks:dek_kinds_list(#{bucket_names => {["b1", "b2"], 1}}),
458+
Secrets = ["s1", "s2"],
459+
F = ?cut(lists:flatten(format_secrets_used_by_list(_))),
460+
?assertEqual("this secret is configured to encrypt configuration, "
461+
"buckets \"b1\", \"b2\"",
462+
F(#{by_deks => All, by_config => All, by_secrets => []})),
463+
?assertEqual("this secret is configured to encrypt configuration, "
464+
"buckets \"b1\", \"b2\"",
465+
F(#{by_deks => [], by_config => All, by_secrets => []})),
466+
?assertEqual("this secret still encrypts some data in configuration, "
467+
"buckets \"b1\", \"b2\"",
468+
F(#{by_deks => All, by_config => [], by_secrets => []})),
469+
?assertEqual("this secret is configured to encrypt secrets s1, s2",
470+
F(#{by_deks => [], by_config => [], by_secrets => Secrets})),
471+
?assertEqual("this secret is configured to encrypt configuration, "
472+
"buckets \"b1\", \"b2\", secrets s1, s2",
473+
F(#{by_deks => [], by_config => All, by_secrets => Secrets})),
474+
?assertEqual("this secret is configured to encrypt configuration, "
475+
"buckets \"b1\", \"b2\", secrets s1, s2",
476+
F(#{by_deks => All, by_config => All, by_secrets => Secrets})),
477+
?assertEqual("this secret is configured to encrypt configuration; it also "
478+
"still encrypts some data in buckets \"b1\", \"b2\"",
479+
F(#{by_deks => All, by_config => [configDek],
480+
by_secrets => []})),
481+
?assertEqual("this secret is configured to encrypt configuration, "
482+
"bucket \"b2\", secrets s1, s2; "
483+
"it also still encrypts some data in bucket \"b1\"",
484+
F(#{by_deks => [configDek, {bucketDek, "b1"}],
485+
by_config => [{bucketDek, "b2"}, chronicleDek],
486+
by_secrets => Secrets})).
487+
488+
489+
-endif.

0 commit comments

Comments
 (0)