Skip to content

Commit c296733

Browse files
MB-61292: Fix for PUT /../secrets/<ID>
Fix permissions check: should check not only usages that are being set but also usages that are being replaced. Without this check, for example, bucket admin can overwrite a secret created by full admin that was supposed to be used for things like config encryption. Also this change fixes a race scenario when two parallel changes can hypothetically overwrite some settings of the secret being modified: 1. PUT takes current secret properties and prepares new properties based on that value; 2. Another process modifies some secret properties (auto-rotation); 3. PUT finishes and sets the properties prepared at step #1 4. Change made by step #2 is lost This obvious race was considered imposible in the very first implementation, but then after several changes it became possible:( Change-Id: I3c508e9eb8d8b367bc63bb8aaadfc050c4204160 Reviewed-on: https://review.couchbase.org/c/ns_server/+/216863 Tested-by: Timofey Barmin <[email protected]> Well-Formed: Build Bot <[email protected]> Reviewed-by: Navdeep S Boparai <[email protected]>
1 parent 96db79e commit c296733

File tree

2 files changed

+66
-47
lines changed

2 files changed

+66
-47
lines changed

apps/ns_server/src/cb_cluster_secrets.erl

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
-export([start_link_node_monitor/0,
4444
start_link_master_monitor/0,
4545
add_new_secret/1,
46-
replace_secret/2,
46+
replace_secret/3,
4747
delete_secret/1,
4848
get_all/0,
4949
get_secret/1,
@@ -67,7 +67,7 @@
6767

6868
%% Can be called by other nodes:
6969
-export([add_new_secret_internal/1,
70-
replace_secret_internal/2,
70+
replace_secret_internal/3,
7171
rotate_internal/1,
7272
sync_with_node_monitor/0,
7373
delete_secret_internal/1,
@@ -218,44 +218,46 @@ add_new_secret_internal(Props) ->
218218
{error, _} = Error -> Error
219219
end.
220220

221-
-spec replace_secret(secret_props(), map()) ->
222-
{ok, secret_props()} |
223-
{error, not_found | bad_encrypt_id() |
224-
bad_usage_change()}.
225-
replace_secret(OldProps, NewProps) ->
226-
execute_on_master({?MODULE, replace_secret_internal, [OldProps, NewProps]}).
227-
228-
-spec replace_secret_internal(secret_props(), map()) ->
229-
{ok, secret_props()} |
230-
{error, not_found | bad_encrypt_id() |
231-
bad_usage_change()}.
232-
replace_secret_internal(OldProps, NewProps) ->
221+
-spec replace_secret(secret_id(), map(), fun((secret_props()) -> boolean())) ->
222+
{ok, secret_props()} |
223+
{error, not_found | bad_encrypt_id() | bad_usage_change() |
224+
forbidden}.
225+
replace_secret(Id, NewProps, IsSecretWritableFun) ->
226+
execute_on_master({?MODULE, replace_secret_internal,
227+
[Id, NewProps, IsSecretWritableFun]}).
228+
229+
-spec replace_secret_internal(secret_id(), map(),
230+
fun((secret_props()) -> boolean())) ->
231+
{ok, secret_props()} |
232+
{error, not_found | bad_encrypt_id() | bad_usage_change() |
233+
forbidden}.
234+
replace_secret_internal(Id, NewProps, IsSecretWritableFun) ->
233235
%% Make sure we have most recent information about which secrets are in use
234236
%% This is needed for verification of 'usage' modification
235237
maybe_reset_deks_counters(),
236-
Props = copy_static_props(OldProps, NewProps),
237238
Res =
238239
chronicle_compat:txn(
239240
fun (Txn) ->
240-
Snapshot = fetch_snapshot_in_txn(Txn),
241-
CurList = get_all(Snapshot),
242-
case replace_secret_in_list(Props, CurList) of
243-
false -> %% already removed, we should not create new
244-
{abort, {error, not_found}};
245-
NewList ->
246-
case validate_secret_in_txn(Props, OldProps, Snapshot) of
247-
ok ->
248-
{commit,
249-
[{set, ?CHRONICLE_SECRETS_KEY, NewList}]};
250-
{error, _} = Error ->
251-
{abort, Error}
252-
end
241+
maybe
242+
Snapshot = fetch_snapshot_in_txn(Txn),
243+
{ok, OldProps} ?= get_secret(Id, Snapshot),
244+
true ?= IsSecretWritableFun(OldProps),
245+
Props = copy_static_props(OldProps, NewProps),
246+
CurList = get_all(Snapshot),
247+
NewList = replace_secret_in_list(Props, CurList),
248+
ok ?= validate_secret_in_txn(Props, OldProps, Snapshot),
249+
{commit, [{set, ?CHRONICLE_SECRETS_KEY, NewList}], Props}
250+
else
251+
false ->
252+
{abort, {error, forbidden}};
253+
{error, _} = Err ->
254+
{abort, Err}
253255
end
254256
end),
255257
case Res of
256-
{ok, _} ->
258+
{ok, _, ResProps} ->
257259
sync_with_all_node_monitors(),
258-
{ok, Props};
260+
{ok, ResProps};
259261
{error, _} = Error -> Error
260262
end.
261263

apps/ns_server/src/menelaus_web_secrets.erl

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ handle_post_secret(Req) ->
5353
fun (RawProps) ->
5454
maybe
5555
ToAdd = import_secret(RawProps),
56-
[_] ?= write_filter_secrets_by_permission([ToAdd], Req),
56+
true ?= is_writable(ToAdd, Req),
5757
{ok, Res} ?= cb_cluster_secrets:add_new_secret(ToAdd),
5858
Formatted = export_secret(Res),
5959
menelaus_util:reply_json(Req, {Formatted})
6060
else
61-
[] ->
61+
false ->
6262
menelaus_util:web_exception(403, "Forbidden");
6363
{error, {encrypt_id, not_found}} ->
6464
menelaus_util:reply_global_error(
@@ -73,19 +73,27 @@ handle_post_secret(Req) ->
7373
end, Req, json, secret_validators(#{})).
7474

7575
handle_put_secret(IdStr, Req) ->
76-
case cb_cluster_secrets:get_secret(parse_id(IdStr)) of
76+
Id = parse_id(IdStr),
77+
case cb_cluster_secrets:get_secret(Id) of
7778
{ok, CurProps} ->
7879
validator:handle(
7980
fun (RawProps) ->
8081
maybe
8182
Props = import_secret(RawProps),
82-
[_] ?= write_filter_secrets_by_permission([Props], Req),
83-
{ok, Res} ?= cb_cluster_secrets:replace_secret(CurProps,
84-
Props),
83+
%% Note: All "usages" should be writable by current user.
84+
%% This includes "new usages" (usages that are being set)
85+
%% and "old usages" (usages that are being replaced)
86+
%% Checking "new usages" here:
87+
true ?= is_writable(Props, Req),
88+
%% replace_secret will check "old usages" inside txn
89+
{ok, Res} ?= cb_cluster_secrets:replace_secret(
90+
Id, Props, is_writable(_, Req)),
8591
Formatted = export_secret(Res),
8692
menelaus_util:reply_json(Req, {Formatted})
8793
else
88-
[] ->
94+
false ->
95+
menelaus_util:web_exception(403, "Forbidden");
96+
{error, forbidden} ->
8997
menelaus_util:web_exception(403, "Forbidden");
9098
{error, {usage, in_use}} ->
9199
menelaus_util:reply_global_error(
@@ -109,6 +117,9 @@ handle_put_secret(IdStr, Req) ->
109117
menelaus_util:reply_not_found(Req)
110118
end.
111119

120+
%% Note: CurProps can only be used for static fields validation here.
121+
%% Any field that can be modified and needs to use CurProps should be
122+
%% checked in transaction in cb_cluster_secret:replace_secret_internal.
112123
secret_validators(CurProps) ->
113124
[validator:string(name, _),
114125
validator:required(name, _),
@@ -272,6 +283,9 @@ validate_key_usage(Name, State) ->
272283
end
273284
end, false, State).
274285

286+
%% Note: CurSecretProps can only be used for static fields validation here.
287+
%% Any field that can be modified and needs to use CurProps should be
288+
%% checked in transaction in cb_cluster_secret:replace_secret_internal.
275289
validate_secrets_data(Name, CurSecretProps, State) ->
276290
Type = validator:get_value(type, State),
277291
CurType = maps:get(type, CurSecretProps, Type),
@@ -295,6 +309,9 @@ validate_secrets_data(Name, CurSecretProps, State) ->
295309
enforce_static_field_validator(type, CurType, State)
296310
end.
297311

312+
%% Note: CurSecretProps can only be used for static fields validation here.
313+
%% Any field that can be modified and needs to use CurProps should be
314+
%% checked in transaction in cb_cluster_secret:replace_secret_internal.
298315
generated_key_validators(CurSecretProps) ->
299316
[validator:boolean(autoRotation, _),
300317
validator:default(autoRotation, true, _),
@@ -339,6 +356,9 @@ validate_encrypt_secret_id(Name, CurSecretProps, State) ->
339356
ok
340357
end, Name, encryptBy, State).
341358

359+
%% Note: CurSecretProps can only be used for static fields validation here.
360+
%% Any field that can be modified and needs to use CurProps should be
361+
%% checked in transaction in cb_cluster_secret:replace_secret_internal.
342362
awskms_key_validators(CurSecretProps) ->
343363
[validator:string(keyARN, _),
344364
validator:required(keyARN, _),
@@ -423,16 +443,13 @@ is_usage_allowed(config_encryption, read, Req) ->
423443
menelaus_auth:has_permission({[admin, security], read}, Req).
424444

425445
read_filter_secrets_by_permission(Secrets, Req) ->
426-
lists:filter(
427-
fun (#{usage := List}) when List /= [] ->
428-
lists:any(is_usage_allowed(_, read, Req), List)
429-
end, Secrets).
430-
431-
write_filter_secrets_by_permission(Secrets, Req) ->
432-
lists:filter(
433-
fun (#{usage := List}) when List /= [] ->
434-
lists:all(is_usage_allowed(_, write, Req), List)
435-
end, Secrets).
446+
lists:filter(is_readable(_, Req), Secrets).
447+
448+
is_readable(#{usage := Usages}, Req) when Usages /= [] ->
449+
lists:any(is_usage_allowed(_, read, Req), Usages).
450+
451+
is_writable(#{usage := Usages}, Req) when Usages /= [] ->
452+
lists:all(is_usage_allowed(_, write, Req), Usages).
436453

437454
parse_id(Str) when is_list(Str) ->
438455
try list_to_integer(Str) of

0 commit comments

Comments
 (0)