Skip to content

Commit ea0b9cd

Browse files
authored
Merge pull request #8781 from shirady/nc-list-accounts-missing-master-key
NC | CLI | List Accounts When Decrypt Access Keys Fails
2 parents a383420 + 6f63da5 commit ea0b9cd

File tree

4 files changed

+152
-21
lines changed

4 files changed

+152
-21
lines changed

src/cmd/manage_nsfs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,10 @@ async function list_account_config_files(wide, show_secrets, filters = {}) {
597597
const options = {
598598
show_secrets: show_secrets || should_filter,
599599
decrypt_secret_key: show_secrets,
600+
// in case we have an error on secret_key decryption
601+
// we will neither return the secret_key nor the encrypted_secret_key
602+
// and add the property of decryption_err with the error we had
603+
return_on_decryption_error: true,
600604
silent_if_missing: true
601605
};
602606

src/sdk/config_fs.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,24 +231,35 @@ class ConfigFS {
231231
* and decrypts the account's secret_key if decrypt_secret_key is true
232232
* if silent_if_missing is true -
233233
* if the config file was deleted (encounter ENOENT error) - continue (returns undefined)
234+
* if decrypt_secret_key is true and the decryption failed with rpc error of INVALID_MASTER_KEY
235+
* and return_on_decryption_error is true -
236+
* add a property decryption_err with the error we've got
237+
* if return_on_decryption_error is false - throw the error (as it was before)
234238
* @param {string} config_file_path
235-
* @param {{show_secrets?: boolean, decrypt_secret_key?: boolean, silent_if_missing?: boolean}} [options]
239+
* @param {{show_secrets?: boolean, decrypt_secret_key?: boolean, silent_if_missing?: boolean,
240+
* return_on_decryption_error?: boolean}} [options]
236241
* @returns {Promise<Object>}
237242
*/
238243
async get_identity_config_data(config_file_path, options = {}) {
239244
const { show_secrets = false, decrypt_secret_key = false, silent_if_missing = false } = options;
245+
let config_data;
240246
try {
241247
const data = await this.get_config_data(config_file_path, options);
242248
if (!data && silent_if_missing) return;
243-
const config_data = _.omit(data, show_secrets ? [] : ['access_keys']);
249+
config_data = _.omit(data, show_secrets ? [] : ['access_keys']);
244250
if (decrypt_secret_key) config_data.access_keys = await nc_mkm.decrypt_access_keys(config_data);
245251
return config_data;
246252
} catch (err) {
247253
dbg.warn('get_identity_config_data: with config_file_path', config_file_path, 'got an error', err);
254+
if (err.rpc_code === 'INVALID_MASTER_KEY' && options.return_on_decryption_error) {
255+
config_data.decryption_err = err.message;
256+
return this.remove_encrypted_secret_key(config_data);
257+
}
248258
if (err.code === 'ENOENT' && silent_if_missing) return;
249259
throw err;
250260
}
251261
}
262+
252263
/**
253264
* get_config_data reads a config file and returns its content
254265
* @param {string} config_file_path
@@ -820,7 +831,6 @@ class ConfigFS {
820831
await native_fs_utils.folder_delete(account_dir_path, this.fs_context, undefined, true);
821832
}
822833

823-
824834
/**
825835
* _prepare_for_account_schema processes account data before writing it to the config dir and does the following -
826836
* 1. encrypts its access keys
@@ -839,6 +849,19 @@ class ConfigFS {
839849
return { parsed_account_data, string_account_data };
840850
}
841851

852+
/**
853+
* remove_encrypted_secret_key will remove the encrypted_secret_key property from an identity
854+
* @param {object} config_data
855+
* @returns {object}
856+
*/
857+
remove_encrypted_secret_key(config_data) {
858+
const size = config_data.access_keys.length;
859+
for (let index = 0; index < size; index++) {
860+
config_data.access_keys[index] = _.omit(config_data.access_keys[index], ['encrypted_secret_key']);
861+
}
862+
return config_data;
863+
}
864+
842865
/////////////////////////////////////
843866
////// ACCOUNT NAME INDEX //////
844867
/////////////////////////////////////

src/test/unit_tests/jest_tests/test_config_fs.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* Copyright (C) 2024 NooBaa */
22
'use strict';
33

4+
const _ = require('lodash');
45
const os = require('os');
56
const path = require('path');
67
const config = require('../../../../config');
@@ -71,3 +72,45 @@ describe('compare_host_and_config_dir_version', () => {
7172
`mentioned in system.json=${system_config_dir_version}, any updates to the config directory are blocked until the source code upgrade`);
7273
});
7374
});
75+
76+
describe('remove_encrypted_secret_key', () => {
77+
const account_data = {
78+
_id: '6784bccb9c05f2fb04c38dd5',
79+
name: 'account-1',
80+
email: 'account-1',
81+
creation_date: '2025-01-13T07:12:11.145Z',
82+
access_keys: [ {access_key: 'GIGiFBmjaaE7OKD5N7hA', encrypted_secret_key: 'jrE1UT9AKtqn2g57GlAAjNttqeKBtEyy4uIl4rjfqHSJ22gvt9dflw==EXAMPLE'} ],
83+
nsfs_account_config: {uid: 1001, gid: 1001, new_buckets_path: '/User/buckets'},
84+
allow_bucket_creation: true,
85+
master_key_id: '6767ff7b12869117a8221e62EXAMPLE',
86+
decryption_err: 'master key id is missing in master_keys_by_id',
87+
};
88+
89+
it('remove_encrypted_secret_key on account with 1 pair of access keys', () => {
90+
const account_data_without_encrypted_secret_key = config_fs.remove_encrypted_secret_key(account_data);
91+
expect(account_data_without_encrypted_secret_key.length).toEqual(account_data.length);
92+
expect(Array.isArray(account_data_without_encrypted_secret_key.access_keys)).toBe(true);
93+
expect(account_data_without_encrypted_secret_key.access_keys[0].encrypted_secret_key).toBeUndefined();
94+
});
95+
96+
it('remove_encrypted_secret_key on account with 0 pairs of access keys', () => {
97+
const account_data_no_access_keys = _.cloneDeep(account_data);
98+
account_data_no_access_keys.access_keys = [];
99+
const account_data_without_encrypted_secret_key = config_fs.remove_encrypted_secret_key(account_data_no_access_keys);
100+
expect(account_data_without_encrypted_secret_key.length).toEqual(account_data.length);
101+
expect(Array.isArray(account_data_without_encrypted_secret_key.access_keys)).toBe(true);
102+
expect(account_data_without_encrypted_secret_key.access_keys.length).toBe(0);
103+
});
104+
105+
it('remove_encrypted_secret_key on account with 2 pairs of access keys', () => {
106+
const account_data_with_2_pair_access_keys = _.cloneDeep(account_data);
107+
account_data_with_2_pair_access_keys.access_keys[1] = {
108+
access_key: 'GIGiFBmjaaE7OKD5N8kB',
109+
encrypted_secret_key: 'jrE1UT9AKtqn2g57GlAAjNttqeKBtEyy4uIl4rjfqHSJ22gvt9ddeu==EXAMPLE'
110+
};
111+
const account_data_without_encrypted_secret_key = config_fs.remove_encrypted_secret_key(account_data_with_2_pair_access_keys);
112+
expect(account_data_without_encrypted_secret_key.length).toEqual(account_data.length);
113+
expect(Array.isArray(account_data_without_encrypted_secret_key.access_keys)).toBe(true);
114+
expect(account_data_without_encrypted_secret_key.access_keys.length).toBe(2);
115+
});
116+
});

src/test/unit_tests/jest_tests/test_nc_account_invalid_mkm_integration.test.js

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,30 @@ const { exec_manage_cli, set_path_permissions_and_owner, TMP_PATH, set_nc_config
1111
const { TYPES, ACTIONS } = require('../../../manage_nsfs/manage_nsfs_constants');
1212
const ManageCLIError = require('../../../manage_nsfs/manage_nsfs_cli_errors').ManageCLIError;
1313
const ManageCLIResponse = require('../../../manage_nsfs/manage_nsfs_cli_responses').ManageCLIResponse;
14+
const { get_process_fs_context } = require('../../../util/native_fs_utils');
15+
const nb_native = require('../../../util/nb_native');
1416

1517
const tmp_fs_path = path.join(TMP_PATH, 'test_nc_invalid_mkm_integration');
1618
const config_root = path.join(tmp_fs_path, 'config_root_account_mkm_integration');
1719
const root_path = path.join(tmp_fs_path, 'root_path_account_mkm_integration/');
18-
const defaults = {
19-
_id: 'account1',
20+
const defaults_account1 = {
2021
type: TYPES.ACCOUNT,
2122
name: 'account1',
22-
new_buckets_path: `${root_path}new_buckets_path_mkm_integration/`,
23-
uid: 999,
24-
gid: 999,
23+
new_buckets_path: `${root_path}new_buckets_path_mkm_integration_account1/`,
24+
uid: 1001,
25+
gid: 1001,
2526
access_key: 'GIGiFAnjaaE7OKD5N7hA',
2627
secret_key: 'U2AYaMpU3zRDcRFWmvzgQr9MoHIAsD+3oEXAMPLE',
2728
};
29+
const defaults_account2 = {
30+
type: TYPES.ACCOUNT,
31+
name: 'account2',
32+
new_buckets_path: `${root_path}new_buckets_path_mkm_integration_account2/`,
33+
uid: 1002,
34+
gid: 1002,
35+
access_key: 'HIHiFAnjaaE7OKD5N7hA',
36+
secret_key: 'U3BYaMpU3zRDcRFWmvzgQr9MoHIAsD+3oEXAMPLE',
37+
};
2838

2939
describe('manage nsfs cli account flow + fauly master key flow', () => {
3040
describe('cli account ops - master key is missing', () => {
@@ -49,13 +59,13 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
4959
});
5060

5161
it('cli account list', async () => {
52-
const { name } = defaults;
62+
const { name } = defaults_account1;
5363
const list_res = await list_account_flow();
5464
expect(list_res.response.reply[0].name).toBe(name);
5565
});
5666

5767
it('cli account status', async () => {
58-
const { name, uid, gid, new_buckets_path } = defaults;
68+
const { name, uid, gid, new_buckets_path } = defaults_account1;
5969
const status_res = await status_account();
6070
expect(status_res.response.reply.name).toBe(name);
6171
expect(status_res.response.reply.email).toBe(name);
@@ -99,7 +109,7 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
99109

100110
it('should fail | cli create account', async () => {
101111
try {
102-
await create_account({ ...defaults, name: 'account_corrupted_mk' });
112+
await create_account({ ...defaults_account1, name: 'account_corrupted_mk' });
103113
fail('should have failed with InvalidMasterKey');
104114
} catch (err) {
105115
expect(JSON.parse(err.stdout).error.code).toBe(ManageCLIError.InvalidMasterKey.code);
@@ -116,13 +126,13 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
116126
});
117127

118128
it('cli account list', async () => {
119-
const { name } = defaults;
129+
const { name } = defaults_account1;
120130
const list_res = await list_account_flow();
121131
expect(list_res.response.reply[0].name).toBe(name);
122132
});
123133

124134
it('cli account status', async () => {
125-
const { name, uid, gid, new_buckets_path } = defaults;
135+
const { name, uid, gid, new_buckets_path } = defaults_account1;
126136
const status_res = await status_account();
127137
expect(status_res.response.reply.name).toBe(name);
128138
expect(status_res.response.reply.email).toBe(name);
@@ -165,7 +175,7 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
165175

166176
it('should fail | cli create account', async () => {
167177
try {
168-
await create_account({ ...defaults, name: 'account_corrupted_mk' });
178+
await create_account({ ...defaults_account1, name: 'account_corrupted_mk' });
169179
fail('should have failed with InvalidMasterKey');
170180
} catch (err) {
171181
expect(JSON.parse(err.stdout).error.code).toBe(ManageCLIError.InvalidMasterKey.code);
@@ -182,13 +192,13 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
182192
});
183193

184194
it('cli account list', async () => {
185-
const { name } = defaults;
195+
const { name } = defaults_account1;
186196
const list_res = await list_account_flow();
187197
expect(list_res.response.reply[0].name).toBe(name);
188198
});
189199

190200
it('cli account status', async () => {
191-
const { name, uid, gid, new_buckets_path } = defaults;
201+
const { name, uid, gid, new_buckets_path } = defaults_account1;
192202
const status_res = await status_account();
193203
expect(status_res.response.reply.name).toBe(name);
194204
expect(status_res.response.reply.email).toBe(name);
@@ -211,6 +221,34 @@ describe('manage nsfs cli account flow + fauly master key flow', () => {
211221
expect(delete_res.response.code).toBe(ManageCLIResponse.AccountDeleted.code);
212222
});
213223
});
224+
225+
describe('cli with renamed master key file (invalid)', () => {
226+
const type = TYPES.ACCOUNT;
227+
228+
beforeEach(async () => {
229+
await setup_nc_system_and_first_account();
230+
await setup_account(defaults_account2);
231+
await master_key_file_rename(true);
232+
});
233+
234+
afterEach(async () => {
235+
await master_key_file_rename(false);
236+
await fs_utils.folder_delete(`${config_root}`);
237+
await fs_utils.folder_delete(`${root_path}`);
238+
});
239+
240+
it('cli list with wide and show_secrets flags (will show encrypted_secret_key and warning property)', async () => {
241+
const action = ACTIONS.LIST;
242+
const account_options = { config_root, wide: true, show_secrets: true };
243+
const res = await exec_manage_cli(type, action, account_options);
244+
const account_array_res = JSON.parse(res).response.reply;
245+
for (const account_res of account_array_res) {
246+
expect(account_res.access_keys[0].encrypted_secret_key).toBeUndefined();
247+
expect(account_res.access_keys[0].secret_key).toBeUndefined();
248+
expect(account_res.decryption_err).toBeDefined();
249+
}
250+
});
251+
});
214252
});
215253

216254
async function create_account(account_options) {
@@ -224,7 +262,7 @@ async function create_account(account_options) {
224262

225263
async function update_account() {
226264
const action = ACTIONS.UPDATE;
227-
const { type, name, new_buckets_path } = defaults;
265+
const { type, name, new_buckets_path } = defaults_account1;
228266
const new_uid = '1111';
229267
const account_options = { config_root, name, new_buckets_path, uid: new_uid };
230268
const res = await exec_manage_cli(type, action, account_options);
@@ -234,7 +272,7 @@ async function update_account() {
234272

235273
async function status_account(show_secrets) {
236274
const action = ACTIONS.STATUS;
237-
const { type, name } = defaults;
275+
const { type, name } = defaults_account1;
238276
const account_options = { config_root, name, show_secrets };
239277
const res = await exec_manage_cli(type, action, account_options);
240278
const parsed_res = JSON.parse(res);
@@ -243,7 +281,7 @@ async function status_account(show_secrets) {
243281

244282
async function list_account_flow() {
245283
const action = ACTIONS.LIST;
246-
const { type } = defaults;
284+
const { type } = defaults_account1;
247285
const account_options = { config_root };
248286
const res = await exec_manage_cli(type, action, account_options);
249287
const parsed_res = JSON.parse(res);
@@ -252,7 +290,7 @@ async function list_account_flow() {
252290

253291
async function delete_account_flow() {
254292
const action = ACTIONS.DELETE;
255-
const { type, name } = defaults;
293+
const { type, name } = defaults_account1;
256294
const account_options = { config_root, name };
257295
const res = await exec_manage_cli(type, action, account_options);
258296
const parsed_res = JSON.parse(res);
@@ -269,11 +307,34 @@ function fail(reason) {
269307
async function setup_nc_system_and_first_account() {
270308
await fs_utils.create_fresh_path(root_path);
271309
set_nc_config_dir_in_config(config_root);
310+
await setup_account(defaults_account1);
311+
}
312+
313+
async function setup_account(account_defaults) {
272314
const action = ACTIONS.ADD;
273-
const { type, name, new_buckets_path, uid, gid } = defaults;
315+
const { type, name, new_buckets_path, uid, gid } = account_defaults;
274316
const account_options = { config_root, name, new_buckets_path, uid, gid };
275317
await fs_utils.create_fresh_path(new_buckets_path);
276318
await fs_utils.file_must_exist(new_buckets_path);
277319
await set_path_permissions_and_owner(new_buckets_path, account_options, 0o700);
278320
await exec_manage_cli(type, action, account_options);
279321
}
322+
323+
324+
/**
325+
* master_key_file_rename will rename the master_keys.json file
326+
* to mock a situation where master_key_id points to a missing master key
327+
* use the to_rename_temp false to rename it back (after the test)
328+
* @param {boolean} to_rename_temp
329+
*/
330+
async function master_key_file_rename(to_rename_temp) {
331+
const default_fs_config = get_process_fs_context();
332+
const source_path = path.join(config_root, 'master_keys.json');
333+
const dest_path = path.join(config_root, 'temp_master_keys.json');
334+
// eliminate the master key file by renaming it
335+
if (to_rename_temp) {
336+
await nb_native().fs.rename(default_fs_config, source_path, dest_path);
337+
} else {
338+
await nb_native().fs.rename(default_fs_config, dest_path, source_path);
339+
}
340+
}

0 commit comments

Comments
 (0)