From 3588a15a616a0e82bd883c0fd393f40395faa6a4 Mon Sep 17 00:00:00 2001 From: Utkarsh Srivastava Date: Wed, 10 Apr 2024 18:57:47 +0530 Subject: [PATCH 1/2] move away from running expiry on interval - use fixed hh:mm Signed-off-by: Utkarsh Srivastava address PR comments Signed-off-by: Utkarsh Srivastava set millis to 0 Signed-off-by: Utkarsh Srivastava (cherry picked from commit 90a2fa6f9b6385a8d82e970a40c425fdaa7f5730) --- config.js | 16 ++++- src/manage_nsfs/manage_nsfs_glacier.js | 86 +++++++++++++++++++++-- src/sdk/nsfs_glacier_backend/tapecloud.js | 2 +- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/config.js b/config.js index 62e9c56c60..3e7c2a2078 100644 --- a/config.js +++ b/config.js @@ -751,9 +751,19 @@ config.NSFS_GLACIER_MIGRATE_INTERVAL = 15 * 60 * 1000; // of `manage_nsfs glacier restore` config.NSFS_GLACIER_RESTORE_INTERVAL = 15 * 60 * 1000; -// NSFS_GLACIER_EXPIRY_INTERVAL indicates the interval between runs -// of `manage_nsfs glacier expiry` -config.NSFS_GLACIER_EXPIRY_INTERVAL = 12 * 60 * 60 * 1000; +// NSFS_GLACIER_EXPIRY_RUN_TIME must be of the format hh:mm which specifies +// when NooBaa should allow running glacier expiry process +// NOTE: This will also be in the same timezone as specified in +// NSFS_GLACIER_EXPIRY_TZ +config.NSFS_GLACIER_EXPIRY_RUN_TIME = '03:00'; + +// NSFS_GLACIER_EXPIRY_RUN_TIME_TOLERANCE_MINS configures the delay +// tolerance in minutes. +// +// eg. If the expiry run time is set to 03:00 and the tolerance is +// set to be 2 mins then the expiry can trigger till 03:02 (unless +// already triggered between 03:00 - 03:02 +config.NSFS_GLACIER_EXPIRY_RUN_DELAY_LIMIT_MINS = 2 * 60; /** @type {'UTC' | 'LOCAL'} */ config.NSFS_GLACIER_EXPIRY_TZ = 'LOCAL'; diff --git a/src/manage_nsfs/manage_nsfs_glacier.js b/src/manage_nsfs/manage_nsfs_glacier.js index 9352259d15..eef249532c 100644 --- a/src/manage_nsfs/manage_nsfs_glacier.js +++ b/src/manage_nsfs/manage_nsfs_glacier.js @@ -69,12 +69,65 @@ async function process_expiry() { const fs_context = native_fs_utils.get_process_fs_context(); await lock_and_run(fs_context, SCAN_LOCK, async () => { - if (!(await time_exceeded(fs_context, config.NSFS_GLACIER_EXPIRY_INTERVAL, GlacierBackend.EXPIRY_TIMESTAMP_FILE))) return; + const backend = getGlacierBackend(); + if ( + await backend.low_free_space() || + await is_desired_time( + fs_context, + new Date(), + config.NSFS_GLACIER_EXPIRY_RUN_TIME, + config.NSFS_GLACIER_EXPIRY_RUN_DELAY_LIMIT_MINS, + GlacierBackend.EXPIRY_TIMESTAMP_FILE, + ) + ) { + await backend.expiry(fs_context); + await record_current_time(fs_context, GlacierBackend.EXPIRY_TIMESTAMP_FILE); + } + }); +} +/** + * is_desired_time returns true if the given time matches with + * the desired time or if + * @param {nb.NativeFSContext} fs_context + * @param {Date} current + * @param {string} desire time in format 'hh:mm' + * @param {number} delay_limit_mins + * @param {string} timestamp_file + * @returns {Promise} + */ +async function is_desired_time(fs_context, current, desire, delay_limit_mins, timestamp_file) { + const [desired_hour, desired_min] = desire.split(':').map(Number); + if ( + isNaN(desired_hour) || + isNaN(desired_min) || + (desired_hour < 0 || desired_hour >= 24) || + (desired_min < 0 || desired_min >= 60) + ) { + throw new Error('invalid desired_time - must be hh:mm'); + } - await getGlacierBackend().expiry(fs_context); - await record_current_time(fs_context, GlacierBackend.EXPIRY_TIMESTAMP_FILE); - }); + const min_time = get_tz_date(desired_hour, desired_min, 0, config.NSFS_GLACIER_EXPIRY_TZ); + const max_time = get_tz_date(desired_hour, desired_min + delay_limit_mins, 0, config.NSFS_GLACIER_EXPIRY_TZ); + + if (current >= min_time && current <= max_time) { + try { + const { data } = await nb_native().fs.readFile(fs_context, path.join(config.NSFS_GLACIER_LOGS_DIR, timestamp_file)); + const lastrun = new Date(data.toString()); + + // Last run should NOT be in this window + if (lastrun >= min_time && lastrun <= max_time) return false; + } catch (error) { + if (error.code === 'ENOENT') return true; + console.error('failed to read last run timestamp:', error, 'timestamp_file:', timestamp_file); + + throw error; + } + + return true; + } + + return false; } /** @@ -134,6 +187,31 @@ async function run_glacier_operation(fs_context, log_namespace, cb) { } } +/** + * @param {number} hours + * @param {number} mins + * @param {number} secs + * @param {'UTC' | 'LOCAL'} tz + * @returns {Date} + */ +function get_tz_date(hours, mins, secs, tz) { + const date = new Date(); + + if (tz === 'UTC') { + date.setUTCHours(hours); + date.setUTCMinutes(hours); + date.setUTCSeconds(secs); + date.setUTCMilliseconds(0); + } else { + date.setHours(hours); + date.setMinutes(mins); + date.setSeconds(secs); + date.setMilliseconds(0); + } + + return date; +} + /** * lock_and_run acquires a flock and calls the given callback after * acquiring the lock diff --git a/src/sdk/nsfs_glacier_backend/tapecloud.js b/src/sdk/nsfs_glacier_backend/tapecloud.js index 8ff3553fe7..e9d2956437 100644 --- a/src/sdk/nsfs_glacier_backend/tapecloud.js +++ b/src/sdk/nsfs_glacier_backend/tapecloud.js @@ -278,7 +278,7 @@ class TapeCloudGlacierBackend extends GlacierBackend { async low_free_space() { const result = await exec(get_bin_path(LOW_FREE_SPACE_SCRIPT), { return_stdout: true }); - return result.toLowerCase() === 'true'; + return result.toLowerCase().trim() === 'true'; } // ============= PRIVATE FUNCTIONS ============= From e7931fee500534511b45b146c651fd037fb161c6 Mon Sep 17 00:00:00 2001 From: Romy <35330373+romayalon@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:10:10 +0300 Subject: [PATCH 2/2] nsfs nc secret keys encryption Signed-off-by: Romy <35330373+romayalon@users.noreply.github.com> (cherry picked from commit 1ed53101c5f1b7811c502d32d3d422d2aecbf04b) --- config.js | 9 + ...NonContainerizedDeveloperCustomizations.md | 68 ++++ src/cmd/manage_nsfs.js | 40 ++- src/manage_nsfs/nc_master_key_manager.js | 307 ++++++++++++++++++ src/sdk/bucketspace_fs.js | 3 + .../schemas/nsfs_account_schema.js | 8 +- .../schemas/nsfs_config_schema.js | 16 + .../test_ceph_nsfs_s3_config_setup.js | 4 +- .../jest_tests/test_nc_master_keys.test.js | 136 ++++++++ .../test_nc_nsfs_account_cli.test.js | 13 +- ..._nc_nsfs_account_schema_validation.test.js | 38 ++- src/test/unit_tests/nc_coretest.js | 6 +- src/test/unit_tests/test_bucketspace_fs.js | 4 +- src/test/unit_tests/test_nc_nsfs_cli.js | 22 +- src/util/os_utils.js | 5 + 15 files changed, 640 insertions(+), 39 deletions(-) create mode 100644 src/manage_nsfs/nc_master_key_manager.js create mode 100644 src/test/unit_tests/jest_tests/test_nc_master_keys.test.js diff --git a/config.js b/config.js index 3e7c2a2078..169e73f2ed 100644 --- a/config.js +++ b/config.js @@ -822,6 +822,15 @@ config.NSFS_WHITELIST = []; config.NSFS_HEALTH_ENDPOINT_RETRY_COUNT = 3; config.NSFS_HEALTH_ENDPOINT_RETRY_DELAY = 10; + +/** @type {'file' | 'executable'} */ +config.NC_MASTER_KEYS_STORE_TYPE = 'file'; +// unless override in config.json, the default will be the config_dir/master_keys.json +config.NC_MASTER_KEYS_FILE_LOCATION = ''; +config.NC_MASTER_KEYS_GET_EXECUTABLE = ''; +config.NC_MASTER_KEYS_PUT_EXECUTABLE = ''; +config.NC_MASTER_KEYS_MANAGER_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes + //Quota config.QUOTA_LOW_THRESHOLD = 80; config.QUOTA_MAX_OBJECTS = Number.MAX_SAFE_INTEGER; diff --git a/docs/dev_guide/NonContainerizedDeveloperCustomizations.md b/docs/dev_guide/NonContainerizedDeveloperCustomizations.md index d6fe621021..10dc2b2d17 100644 --- a/docs/dev_guide/NonContainerizedDeveloperCustomizations.md +++ b/docs/dev_guide/NonContainerizedDeveloperCustomizations.md @@ -337,6 +337,74 @@ Example: 3. systemctl restart noobaa_nsfs ``` +## 19. Set Master Keys Store type- +**Description -** This flag will set the type of the master keys store used by NooBaa. + +**Configuration Key -** NC_MASTER_KEYS_STORE_TYPE + +**Type -** string + +**Default -** 'file' +**Steps -** +``` +1. Open /path/to/config_dir/config.json file. +2. Set the config key - +Example: +"NC_MASTER_KEYS_STORE_TYPE": 'executable' +3. systemctl restart noobaa_nsfs +``` + +## 20. Set Master Keys File Location - +**Description -** This flag will set the location of the master keys file used by NooBaa. + +**Configuration Key -** NC_MASTER_KEYS_FILE_LOCATION + +**Type -** string + +**Default -** '/etc/noobaa.conf.d/master_keys.json' +**Steps -** +``` +1. Open /path/to/config_dir/config.json file. +2. Set the config key - +Example: +"NC_MASTER_KEYS_FILE_LOCATION": '/private/tmp/master_keys.json' +3. systemctl restart noobaa_nsfs +``` + +## 21. Set Master Keys GET executable script - +**Description -** This flag will set the location of the executable script for reading the master keys file used by NooBa. + +**Configuration Key -** NC_MASTER_KEYS_GET_EXECUTABLE + +**Type -** string + +**Default -** undefined +**Steps -** +``` +1. Open /path/to/config_dir/config.json file. +2. Set the config key - +Example: +"NC_MASTER_KEYS_GET_EXECUTABLE": '/private/tmp/get_master_keys.sh' +3. systemctl restart noobaa_nsfs +``` + +## 22. Set Master Keys PUT executable script - +**Description -** This flag will set the location of the executable script for updating the master keys file used by NooBa. + +**Configuration Key -** NC_MASTER_KEYS_PUT_EXECUTABLE + +**Type -** string + +**Default -** undefined +**Steps -** +``` +1. Open /path/to/config_dir/config.json file. +2. Set the config key - +Example: +"NC_MASTER_KEYS_PUT_EXECUTABLE": '/private/tmp/put_master_keys.sh' +3. systemctl restart noobaa_nsfs +``` + ## Config.json example ``` > cat /path/to/config_dir/config.json diff --git a/src/cmd/manage_nsfs.js b/src/cmd/manage_nsfs.js index b388cd8daf..7209a62a0e 100644 --- a/src/cmd/manage_nsfs.js +++ b/src/cmd/manage_nsfs.js @@ -25,6 +25,7 @@ const { print_usage } = require('../manage_nsfs/manage_nsfs_help_utils'); const { TYPES, ACTIONS, VALID_OPTIONS, OPTION_TYPE, FROM_FILE, BOOLEAN_STRING_VALUES, LIST_ACCOUNT_FILTERS, LIST_BUCKET_FILTERS, GLACIER_ACTIONS } = require('../manage_nsfs/manage_nsfs_constants'); const NoobaaEvent = require('../manage_nsfs/manage_nsfs_events_utils').NoobaaEvent; +const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance(); function throw_cli_error(error_code, detail, event_arg) { const error_event = NSFS_CLI_ERROR_EVENT_MAP[error_code.code]; @@ -200,7 +201,7 @@ async function add_bucket(data) { const fs_context = native_fs_utils.get_process_fs_context(config_root_backend); const bucket_conf_path = get_config_file_path(buckets_dir_path, data.name); const exists = await native_fs_utils.is_path_exists(fs_context, bucket_conf_path); - if (exists) throw_cli_error(ManageCLIError.BucketAlreadyExists, data.name, {bucket: data.name}); + if (exists) throw_cli_error(ManageCLIError.BucketAlreadyExists, data.name, { bucket: data.name }); data._id = mongo_utils.mongoObjectId(); data.owner_account = account_id; const data_json = JSON.stringify(data); @@ -209,7 +210,7 @@ async function add_bucket(data) { // for validating against the schema we need an object, hence we parse it back to object nsfs_schema_utils.validate_bucket_schema(JSON.parse(data_json)); await native_fs_utils.create_config_file(fs_context, buckets_dir_path, bucket_conf_path, data_json); - write_stdout_response(ManageCLIResponse.BucketCreated, data_json, {bucket: data.name}); + write_stdout_response(ManageCLIResponse.BucketCreated, data_json, { bucket: data.name }); } /** verify_bucket_owner will check if the bucket_owner has an account @@ -419,6 +420,7 @@ async function fetch_existing_account_data(target) { get_config_file_path(accounts_dir_path, target.name) : get_symlink_config_file_path(access_keys_dir_path, target.access_keys[0].access_key); source = await get_config_data(account_path, true); + source.access_keys = await nc_mkm.decrypt_access_keys(source); } catch (err) { dbg.log1('NSFS Manage command: Could not find account', target, err); if (err.code === 'ENOENT') { @@ -452,17 +454,20 @@ async function add_account(data) { throw_cli_error(err_code, event_arg, {account: event_arg}); } data._id = mongo_utils.mongoObjectId(); - data = JSON.stringify(data); + const encrypted_account = await nc_mkm.encrypt_access_keys(data); + data.master_key_id = encrypted_account.master_key_id; + const encrypted_data = JSON.stringify(encrypted_account); // We take an object that was stringify // (it unwraps ths sensitive strings, creation_date to string and removes undefined parameters) // for validating against the schema we need an object, hence we parse it back to object - nsfs_schema_utils.validate_account_schema(JSON.parse(data)); - await native_fs_utils.create_config_file(fs_context, accounts_dir_path, account_config_path, data); + nsfs_schema_utils.validate_account_schema(JSON.parse(encrypted_data)); + await native_fs_utils.create_config_file(fs_context, accounts_dir_path, account_config_path, encrypted_data); await native_fs_utils._create_path(access_keys_dir_path, fs_context, config.BASE_MODE_CONFIG_DIR); await nb_native().fs.symlink(fs_context, account_config_relative_path, account_config_access_key_path); - write_stdout_response(ManageCLIResponse.AccountCreated, data, {account: event_arg}); + write_stdout_response(ManageCLIResponse.AccountCreated, data, { account: event_arg }); } + async function update_account(data) { await validate_account_args(data, ACTIONS.UPDATE); @@ -475,12 +480,14 @@ async function update_account(data) { if (!update_name && !update_access_key) { const account_config_path = get_config_file_path(accounts_dir_path, data.name); - data = JSON.stringify(data); + const encrypted_account = await nc_mkm.encrypt_access_keys(data); + data.master_key_id = encrypted_account.master_key_id; + const encrypted_data = JSON.stringify(encrypted_account); // We take an object that was stringify // (it unwraps ths sensitive strings, creation_date to string and removes undefined parameters) // for validating against the schema we need an object, hence we parse it back to object - nsfs_schema_utils.validate_account_schema(JSON.parse(data)); - await native_fs_utils.update_config_file(fs_context, accounts_dir_path, account_config_path, data); + nsfs_schema_utils.validate_account_schema(JSON.parse(encrypted_data)); + await native_fs_utils.update_config_file(fs_context, accounts_dir_path, account_config_path, encrypted_data); write_stdout_response(ManageCLIResponse.AccountUpdated, data); return; } @@ -499,16 +506,21 @@ async function update_account(data) { const err_code = name_exists ? ManageCLIError.AccountNameAlreadyExists : ManageCLIError.AccountAccessKeyAlreadyExists; throw_cli_error(err_code); } - data = JSON.stringify(_.omit(data, ['new_name', 'new_access_key'])); + data = _.omit(data, ['new_name', 'new_access_key']); + const encrypted_account = await nc_mkm.encrypt_access_keys(data); + data.master_key_id = encrypted_account.master_key_id; + const encrypted_data = JSON.stringify(encrypted_account); + data = JSON.stringify(data); + // We take an object that was stringify // (it unwraps ths sensitive strings, creation_date to string and removes undefined parameters) // for validating against the schema we need an object, hence we parse it back to object - nsfs_schema_utils.validate_account_schema(JSON.parse(data)); + nsfs_schema_utils.validate_account_schema(JSON.parse(encrypted_data)); if (update_name) { - await native_fs_utils.create_config_file(fs_context, accounts_dir_path, new_account_config_path, data); + await native_fs_utils.create_config_file(fs_context, accounts_dir_path, new_account_config_path, encrypted_data); await native_fs_utils.delete_config_file(fs_context, accounts_dir_path, cur_account_config_path); } else if (update_access_key) { - await native_fs_utils.update_config_file(fs_context, accounts_dir_path, cur_account_config_path, data); + await native_fs_utils.update_config_file(fs_context, accounts_dir_path, cur_account_config_path, encrypted_data); } // TODO: safe_unlink can be better but the current impl causing ELOOP - Too many levels of symbolic links // need to find a better way for atomic unlinking of symbolic links @@ -560,6 +572,7 @@ async function get_account_status(data, show_secrets) { get_symlink_config_file_path(access_keys_dir_path, data.access_keys[0].access_key) : get_config_file_path(accounts_dir_path, data.name); const config_data = await get_config_data(account_path, show_secrets); + if (config_data.access_keys) config_data.access_keys = await nc_mkm.decrypt_access_keys(config_data); write_stdout_response(ManageCLIResponse.AccountStatus, config_data); } catch (err) { if (_.isUndefined(data.name)) { @@ -656,6 +669,7 @@ async function list_config_files(type, config_path, wide, show_secrets, filters) if (wide || should_filter) { const full_path = path.join(config_path, entry.name); const data = await get_config_data(full_path, show_secrets || should_filter); + if (data.access_keys) data.access_keys = await nc_mkm.decrypt_access_keys(data); if (should_filter && !filter_list_item(type, data, filters)) return undefined; // remove secrets on !show_secrets && should filter return wide ? _.omit(data, show_secrets ? [] : ['access_keys']) : { name: entry.name.slice(0, entry.name.indexOf('.json')) }; diff --git a/src/manage_nsfs/nc_master_key_manager.js b/src/manage_nsfs/nc_master_key_manager.js new file mode 100644 index 0000000000..4f80ba12b8 --- /dev/null +++ b/src/manage_nsfs/nc_master_key_manager.js @@ -0,0 +1,307 @@ +/* Copyright (C) 2023 NooBaa */ +'use strict'; + +const _ = require('lodash'); +const util = require('util'); +const path = require('path'); +const crypto = require('crypto'); +const P = require('../util/promise'); +const config = require('../../config'); +const os_util = require('../util/os_utils'); +const RpcError = require('../rpc/rpc_error'); +const nb_native = require('../util/nb_native'); +const db_client = require('../util/db_client').instance(); +const dbg = require('../util/debug_module')(__filename); +const native_fs_utils = require('../util/native_fs_utils'); + +const TYPE_FILE = 'file'; +const TYPE_EXEC = 'executable'; +const ACTIVE_MASTER_KEY = 'active_master_key'; +const exec_key_suffix = 'master_keys'; + +class NCMasterKeysManager { + constructor() { + this.active_master_key = undefined; + this.master_keys_by_id = {}; + this.last_init_time = 0; + } + + static get_instance() { + NCMasterKeysManager._instance = NCMasterKeysManager._instance || new NCMasterKeysManager(); + return NCMasterKeysManager._instance; + } + + /** + * init inits master_key_manager by the store type + */ + async init() { + const store_type = config.NC_MASTER_KEYS_STORE_TYPE; + if (store_type === TYPE_FILE) { + return this._init_from_file(); + } else if (store_type === TYPE_EXEC) { + return this._init_from_exec(); + } + throw new Error(`Invalid Master keys store type - ${store_type} - ${TYPE_EXEC}`); + } + + /** + * _set_keys sets the master_key_manager peoperties by master_keys object + * @param {Object} master_keys + * @returns {Promise} + */ + _set_keys(master_keys) { + if (!master_keys.active_master_key) throw new RpcError('INVALID_MASTER_KEYS_FILE', 'Invalid master_keys.json file'); + for (const [key_name, master_key] of Object.entries(master_keys)) { + try { + if (key_name === ACTIVE_MASTER_KEY) { + this.active_master_key = get_buffered_master_key(master_key); + } + this.master_keys_by_id[master_key.id] = get_buffered_master_key(master_key); + } catch (err) { + dbg.error('couldn\'t load master_keys.json file', err); + throw new RpcError('INVALID_MASTER_KEYS_FILE', 'Invalid master_keys.json file'); + } + } + dbg.log1(`_set_keys: master_key_manager updated successfully! active master key is: ${util.inspect(this.active_master_key)}`); + return this.active_master_key; + } + + /** + * _create_master_key generates a new master key + */ + async _create_master_key() { + const master_key = { + id: db_client.new_object_id(), + cipher_key: crypto.randomBytes(32), + cipher_iv: crypto.randomBytes(16), + encryption_type: 'aes-256-gcm' + }; + const store_type = config.NC_MASTER_KEYS_STORE_TYPE; + const stringed_master_key = get_stringed_master_key(master_key); + if (store_type === TYPE_FILE) { + // write master key to file + return this._create_master_keys_file(stringed_master_key); + } else if (store_type === TYPE_EXEC) { + return this._create_master_keys_exec(stringed_master_key); + } + throw new Error(`Invalid Master keys store type - ${store_type}`); + } + + //////////////// + // FILE API // + //////////////// + + /** + * _init_from_file reads master keys json file and loads its data + */ + async _init_from_file() { + const master_keys_path = get_master_keys_file_path(); + const fs_context = native_fs_utils.get_process_fs_context(); + for (let retries = 0; retries < 3;) { + try { + const stat = await nb_native().fs.stat(fs_context, master_keys_path); + if (stat.ctime.getTime() === this.last_init_time) return; + const master_keys = await native_fs_utils.read_file(fs_context, master_keys_path); + + this._set_keys(master_keys); + this.last_init_time = stat.ctime.getTime(); + return; + } catch (err) { + if (err.code === 'ENOENT') { + dbg.warn('init_from_file: couldn\'t find master keys file', master_keys_path); + await this._create_master_key(); + } else if (err.rpc_code === 'INVALID_MASTER_KEYS_FILE') { + dbg.error('init_from_file: master keys file is invalid', master_keys_path); + throw err; + } else { + dbg.error('init_from_file: couldn\'t load master keys file', master_keys_path); + retries += 1; + await P.delay(1000); + } + } + } + throw new Error('init_from_file exhausted'); + } + + /** + * _create_master_keys_file updates master_keys.json file + * @param {object} new_master_key + */ + async _create_master_keys_file(new_master_key) { + const master_keys_path = get_master_keys_file_path(); + const parent_dir = path.dirname(master_keys_path); + const fs_context = native_fs_utils.get_process_fs_context(); + try { + const data = JSON.stringify({ active_master_key: new_master_key }); + await native_fs_utils.create_config_file(fs_context, parent_dir, master_keys_path, data); + } catch (err) { + dbg.warn('create_master_keys_file got err', err); + if (err.code === 'EEXIST') return; + throw err; + } + } + + ////////////////////// + // EXECUTABLE API // + ////////////////////// + + /** + * _create_master_keys_exec executes get master keys executable and retuns its data + * @returns {Promise} + */ + async _create_master_keys_exec(master_key) { + const master_keys_json = JSON.stringify({ active_master_key: master_key }); + await os_util.spawn(config.NC_MASTER_KEYS_PUT_EXECUTABLE, [exec_key_suffix], { input: master_keys_json, stdio: [] }); + } + + /** + * _init_from_exec init master keys from executable + */ + async _init_from_exec() { + const command = `${config.NC_MASTER_KEYS_GET_EXECUTABLE} ${exec_key_suffix}`; + for (let retries = 0; retries < 3;) { + try { + if (this.last_init_time && + (new Date()).getTime() - this.last_init_time > config.NC_MASTER_KEYS_MANAGER_REFRESH_THRESHOLD) return; + const master_keys = await os_util.exec(command, { return_stdout: true }); + const parsed_master_keys = JSON.parse(master_keys); + this._set_keys(parsed_master_keys); + this.last_init_time = (new Date()).getTime(); + return; + } catch (err) { + if (err.stderr && err.stderr.trim() === 'NO_SUCH_KEY') { + dbg.warn('init_from_exec: couldn\'t find master keys', err, err.code); + await this._create_master_key(); + } else { + dbg.error('init_from_exec: couldn\'t load master keys', err, err.code); + retries += 1; + await P.delay(1000); + } + } + } + throw new Error('init_from_exec exhausted'); + } + + /** + * encrypt refreshes the cache encrypts a secret_key using the cached active master key + * @param {string} secret_key + * @param {nb.ID} master_key_id + * @returns {Promise} + */ + async encrypt(secret_key, master_key_id) { + await this.init(); + return this.encryptSync(secret_key, master_key_id); + } + + /** + * encryptSync encrypts a secret_key using the cached active master key + * @param {string} secret_key + * @param {nb.ID} master_key_id + * @returns {string} + */ + encryptSync(secret_key, master_key_id = this.active_master_key?.id) { + const { cipher_key, cipher_iv, encryption_type } = this.master_keys_by_id[master_key_id]; + const cipher = crypto.createCipheriv(encryption_type, cipher_key, cipher_iv); + const updated_value = cipher.update(Buffer.from(secret_key)); + const enccypted_value = updated_value.toString('base64'); + return enccypted_value; + } + + /** + * decrypt refreshes the cache and decrypts a secret_key using the cached master key + * @param {string} secret_key + * @param {nb.ID} master_key_id + * @returns {Promise} + */ + async decrypt(secret_key, master_key_id) { + await this.init(); + return this.decryptSync(secret_key, master_key_id); + } + + /** + * decrypt decrypts a secret_key using the cached master key + * @param {string} secret_key + * @param {nb.ID} master_key_id + * @returns {string} + */ + decryptSync(secret_key, master_key_id) { + const { cipher_key, cipher_iv, encryption_type } = this.master_keys_by_id[master_key_id]; + const decipher = crypto.createDecipheriv(encryption_type, cipher_key, cipher_iv); + const decrypted_secret_key = decipher.update(Buffer.from(secret_key, 'base64')).toString(); + return decrypted_secret_key; + } + + /** + * encrypt_access_keys encrypts the secret key of an account and returns the encypted account data + * @param {Object} account + * @returns {Promise} + */ + async encrypt_access_keys(account) { + await this.init(); + const master_key_id = this.active_master_key.id; + const encrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({ + access_key: access_keys.access_key, + encrypted_secret_key: await this.encrypt(access_keys.secret_key, master_key_id) + }))); + return { ...account, access_keys: encrypted_access_keys, master_key_id }; + } + + /** + * decrypt_access_keys decrypts the secret key of an account and returns the decrypted access keys + * @param {Object} account + * @returns {Promise} + */ + async decrypt_access_keys(account) { + const decrypted_access_keys = await P.all(_.map(account.access_keys, async access_keys => ({ + access_key: access_keys.access_key, + secret_key: await this.decrypt(access_keys.encrypted_secret_key, account.master_key_id) + }))); + return decrypted_access_keys; + } +} + +/** + * get_buffered_master_key converts cipher_key and cipher_iv base64 strings to buffers + * @param {object} master_key + */ +function get_buffered_master_key(master_key) { + try { + const buffered_master_key = { + ...master_key, + cipher_key: Buffer.from(master_key.cipher_key, 'base64'), + cipher_iv: Buffer.from(master_key.cipher_iv, 'base64'), + }; + return buffered_master_key; + } catch (err) { + throw new RpcError('INVALID_MASTER_KEYS_FILE', 'Invalid master_keys.json file'); + } +} + +/** + * get_stringed_master_key converts cipher_key and cipher_iv buffers of strings in base64 + * @param {object} master_key + */ +function get_stringed_master_key(master_key) { + const stringed_master_key = { + ...master_key, + cipher_key: master_key.cipher_key.toString('base64'), + cipher_iv: master_key.cipher_iv.toString('base64'), + }; + return stringed_master_key; +} + +/** + * get_master_keys_file_path returns the master_keys.json file + * @returns {string} + */ +function get_master_keys_file_path() { + const default_master_keys_path = path.join(config.NSFS_NC_CONF_DIR, 'master_keys.json'); + const master_keys_path = config.NC_MASTER_KEYS_FILE_LOCATION || default_master_keys_path; + return master_keys_path; +} + +NCMasterKeysManager._instance = undefined; + +// EXPORTS +exports.NCMasterKeysManager = NCMasterKeysManager; +exports.get_instance = NCMasterKeysManager.get_instance; diff --git a/src/sdk/bucketspace_fs.js b/src/sdk/bucketspace_fs.js index ad8e876830..f0edd4f31d 100644 --- a/src/sdk/bucketspace_fs.js +++ b/src/sdk/bucketspace_fs.js @@ -14,6 +14,8 @@ const _ = require('lodash'); const util = require('util'); const bucket_policy_utils = require('../endpoint/s3/s3_bucket_policy_utils'); const nsfs_schema_utils = require('../manage_nsfs/nsfs_schema_utils'); +const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance(); + const mongo_utils = require('../util/mongo_utils'); const { CONFIG_SUBDIRS } = require('../manage_nsfs/manage_nsfs_constants'); @@ -101,6 +103,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS { nsfs_schema_utils.validate_account_schema(account); account.name = new SensitiveString(account.name); account.email = new SensitiveString(account.email); + account.access_keys = await nc_mkm.decrypt_access_keys(account); for (const k of account.access_keys) { k.access_key = new SensitiveString(k.access_key); k.secret_key = new SensitiveString(k.secret_key); diff --git a/src/server/system_services/schemas/nsfs_account_schema.js b/src/server/system_services/schemas/nsfs_account_schema.js index d936186644..f15af935b9 100644 --- a/src/server/system_services/schemas/nsfs_account_schema.js +++ b/src/server/system_services/schemas/nsfs_account_schema.js @@ -12,6 +12,7 @@ module.exports = { 'nsfs_account_config', 'creation_date', 'allow_bucket_creation', + 'master_key_id' ], properties: { _id: { @@ -26,6 +27,9 @@ module.exports = { creation_date: { type: 'string', }, + master_key_id: { + objectid: true + }, allow_bucket_creation: { type: 'boolean', }, @@ -33,12 +37,12 @@ module.exports = { type: 'array', items: { type: 'object', - required: ['access_key', 'secret_key'], + required: ['access_key', 'encrypted_secret_key'], properties: { access_key: { type: 'string', }, - secret_key: { + encrypted_secret_key: { type: 'string', }, } diff --git a/src/server/system_services/schemas/nsfs_config_schema.js b/src/server/system_services/schemas/nsfs_config_schema.js index e51bcbe0de..aec9fe459c 100644 --- a/src/server/system_services/schemas/nsfs_config_schema.js +++ b/src/server/system_services/schemas/nsfs_config_schema.js @@ -110,6 +110,22 @@ const nsfs_node_config_schema = { type: 'boolean', default: false, description: 'This flag will enable the random seeding for the application' + }, + NC_MASTER_KEYS_STORE_TYPE: { + enum: ['file', 'executable'], + description: 'This flag will set the master keys store type' + }, + NC_MASTER_KEYS_FILE_LOCATION: { + type: 'string', + description: 'This flag will set the master keys file location' + }, + NC_MASTER_KEYS_GET_EXECUTABLE: { + type: 'string', + description: 'This flag will set the location of the executable script for reading the master keys file used by NooBa.' + }, + NC_MASTER_KEYS_PUT_EXECUTABLE: { + type: 'string', + description: 'This flag will set the location of the executable script for updating the master keys file used by NooBa.' } } }; diff --git a/src/test/system_tests/ceph_s3_tests/test_ceph_nsfs_s3_config_setup.js b/src/test/system_tests/ceph_s3_tests/test_ceph_nsfs_s3_config_setup.js index bf3475a9b7..51a5408c32 100644 --- a/src/test/system_tests/ceph_s3_tests/test_ceph_nsfs_s3_config_setup.js +++ b/src/test/system_tests/ceph_s3_tests/test_ceph_nsfs_s3_config_setup.js @@ -13,6 +13,7 @@ dbg.set_process_name('test_ceph_s3'); const os_utils = require('../../../util/os_utils'); const { CEPH_TEST, account_path, account_tenant_path } = require('./test_ceph_s3_constants.js'); +const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager').get_instance(); async function main() { try { @@ -71,7 +72,8 @@ async function get_access_keys(path) { const account_data = await fs.promises.readFile(path, 'utf8'); const data_json = JSON.parse(account_data); const access_key = data_json.access_keys[0].access_key; - const secret_key = data_json.access_keys[0].secret_key; + const encrypted_secret_key = data_json.access_keys[0].encrypted_secret_key; + const secret_key = await nc_mkm.decrypt(encrypted_secret_key, data_json.master_key_id); return {access_key, secret_key}; } diff --git a/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js b/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js new file mode 100644 index 0000000000..aa3f8b0924 --- /dev/null +++ b/src/test/unit_tests/jest_tests/test_nc_master_keys.test.js @@ -0,0 +1,136 @@ +/* Copyright (C) 2024 NooBaa */ +/* eslint-disable no-undef */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const config = require('../../../../config'); +const fs_utils = require('../../../util/fs_utils'); +const nb_native = require('../../../util/nb_native'); +const cloud_utils = require('../../../util/cloud_utils'); +const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager'); +const { get_process_fs_context } = require('../../../util/native_fs_utils'); + +const DEFAULT_FS_CONFIG = get_process_fs_context(); +const MASTER_KEYS_JSON_PATH = path.join(config.NSFS_NC_DEFAULT_CONF_DIR, 'master_keys.json'); + +describe('NC master key manager tests', () => { + + beforeAll(async () => { + await fs_utils.create_fresh_path(config.NSFS_NC_DEFAULT_CONF_DIR); + }); + + afterAll(async () => { + await fs.promises.rm(MASTER_KEYS_JSON_PATH); + await fs.promises.rm(config.NSFS_NC_DEFAULT_CONF_DIR, { recursive: true, force: true }); + }); + + let initial_master_key; + let initial_master_keys_by_id; + const nc_mkm_instance = nc_mkm.get_instance(); + + describe('NC master key manager tests - happy path', () => { + + it('init nc_mkm first time', async () => { + await nc_mkm_instance.init(); + const master_keys = await read_master_keys_json(); + const active_master_key = nc_mkm_instance.active_master_key; + validate_mkm_instance(nc_mkm_instance); + compare_master_keys_json_and_active_root_key(active_master_key, master_keys); + initial_master_key = active_master_key; + initial_master_keys_by_id = nc_mkm_instance.master_keys_by_id; + }); + + it('init nc_mkm second time - master_keys.json exists', async () => { + await nc_mkm_instance.init(); + const master_keys = await read_master_keys_json(); + const active_master_key = nc_mkm_instance.active_master_key; + + validate_mkm_instance(nc_mkm_instance); + compare_master_keys_json_and_active_root_key(active_master_key, master_keys); + + // check active_master_key was not changed + expect(JSON.stringify(initial_master_key)).toEqual(JSON.stringify(active_master_key)); + expect(JSON.stringify(initial_master_keys_by_id)).toEqual(JSON.stringify(nc_mkm_instance.master_keys_by_id)); + }); + + it('encrypt & decrypt value using nc_mkm', async () => { + const { secret_key } = cloud_utils.generate_access_keys(); + const unwrapped_secret_key = secret_key.unwrap(); + const encrypted_secret_key = await nc_mkm_instance.encrypt(unwrapped_secret_key); + const decrypted_secret_key = await nc_mkm_instance.decrypt(encrypted_secret_key, nc_mkm_instance.active_master_key.id); + expect(unwrapped_secret_key).toEqual(decrypted_secret_key); + }); + }); + + it('should fail - init nc_mkm - invalid master_keys.json - missing active_master_key', async () => { + await fs.promises.rm(MASTER_KEYS_JSON_PATH); + await fs.promises.writeFile(MASTER_KEYS_JSON_PATH, JSON.stringify({ 'non_active': 1 })); + const new_nc_mkm_instance = nc_mkm.get_instance(); + try { + await new_nc_mkm_instance.init(); + fail('should have failed on invalid master_keys.json file'); + } catch (err) { + expect(err.rpc_code).toEqual('INVALID_MASTER_KEYS_FILE'); + expect(err.message).toEqual('Invalid master_keys.json file'); + } + }); + + it('should fail - init nc_mkm - invalid master_keys.json - invalid active_master_key value', async () => { + await fs.promises.rm(MASTER_KEYS_JSON_PATH); + await fs.promises.writeFile(MASTER_KEYS_JSON_PATH, JSON.stringify({ 'active_master_key': 1 })); + const new_nc_mkm_instance = nc_mkm.get_instance(); + try { + await new_nc_mkm_instance.init(); + fail('should have failed on invalid master_keys.json file'); + } catch (err) { + expect(err.rpc_code).toEqual('INVALID_MASTER_KEYS_FILE'); + expect(err.message).toEqual('Invalid master_keys.json file'); + } + }); +}); + +/** + * validate_mkm_instance checks that - + * 1. is_initialized = true + * 2. active_master_key is populated + * 3. master_keys_by_id.active_master_key is populated with active_master_key + * @param {object} nc_mkm_instance + */ +function validate_mkm_instance(nc_mkm_instance) { + const active_master_key = nc_mkm_instance.active_master_key; + const master_keys_cache = nc_mkm_instance.master_keys_by_id; + const active_master_key_by_cache = master_keys_cache[active_master_key.id]; + + expect(nc_mkm_instance.active_master_key).toBeDefined(); + expect(nc_mkm_instance.active_master_key.id).toBeDefined(); + expect(nc_mkm_instance.active_master_key.cipher_key).toBeDefined(); + expect(nc_mkm_instance.active_master_key.cipher_iv).toBeDefined(); + expect(Object.entries(master_keys_cache).length === 1).toBeTruthy(); + expect(nc_mkm_instance.active_master_key).toEqual(active_master_key_by_cache); +} + +/** + * compare_master_keys_json_and_active_root_key compares the master_keys.json content with + * the active_master_key hanged on the nc_mkm instance + * @param {object} active_master_key + * @param {object} master_keys + */ +function compare_master_keys_json_and_active_root_key(active_master_key, master_keys) { + expect(master_keys.active_master_key.id.toString()).toEqual(active_master_key.id.toString()); + const buffered_cipher_iv = Buffer.from(master_keys.active_master_key.cipher_iv, 'base64'); + const buffered_cipher_key = Buffer.from(master_keys.active_master_key.cipher_key, 'base64'); + expect(buffered_cipher_key).toEqual(active_master_key.cipher_key); + expect(buffered_cipher_iv).toEqual(active_master_key.cipher_iv); +} + +/** + * read_master_keys_json reads master_keys.json data and parse it + * @returns {Promise} + */ +async function read_master_keys_json() { + const { data } = await nb_native().fs.readFile(DEFAULT_FS_CONFIG, MASTER_KEYS_JSON_PATH); + const master_keys = JSON.parse(data.toString()); + return master_keys; +} diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js index 738024f034..5cd6840820 100644 --- a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_cli.test.js @@ -21,8 +21,9 @@ const ManageCLIResponse = require('../../../manage_nsfs/manage_nsfs_cli_response const tmp_fs_path = path.join(TMP_PATH, 'test_nc_nsfs_account_cli'); const DEFAULT_FS_CONFIG = get_process_fs_context(); - +const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager').get_instance(); const timeout = 50000; +const config = require('../../../../config'); // eslint-disable-next-line max-lines-per-function describe('manage nsfs cli account flow', () => { @@ -39,7 +40,6 @@ describe('manage nsfs cli account flow', () => { access_key: 'GIGiFAnjaaE7OKD5N7hA', secret_key: 'U2AYaMpU3zRDcRFWmvzgQr9MoHIAsD+3oEXAMPLE', }; - beforeEach(async () => { await P.all(_.map([CONFIG_SUBDIRS.ACCOUNTS, CONFIG_SUBDIRS.ACCESS_KEYS], async dir => fs_utils.create_fresh_path(`${config_root}/${dir}`))); @@ -1208,6 +1208,8 @@ describe('cli account flow distinguished_name - permissions', function() { await delete_fs_user_by_platform(distinguished_name); } await fs_utils.folder_delete(config_root); + await fs_utils.folder_delete(config_root); + await fs_utils.folder_delete(config.NSFS_NC_DEFAULT_CONF_DIR); }, timeout); it('cli account create - should fail - user does not exist', async function() { @@ -1318,8 +1320,11 @@ describe('cli account flow distinguished_name - permissions', function() { async function read_config_file(config_root, schema_dir, config_file_name, is_symlink) { const config_path = path.join(config_root, schema_dir, config_file_name + (is_symlink ? '.symlink' : '.json')); const { data } = await nb_native().fs.readFile(DEFAULT_FS_CONFIG, config_path); - const config = JSON.parse(data.toString()); - return config; + const config_data = JSON.parse(data.toString()); + const encrypted_secret_key = config_data.access_keys[0].encrypted_secret_key; + config_data.access_keys[0].secret_key = await nc_mkm.decrypt(encrypted_secret_key, config_data.master_key_id); + delete config_data.access_keys[0].encrypted_secret_key; + return config_data; } /** diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js index 317ce4f5c7..06941fb201 100644 --- a/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_account_schema_validation.test.js @@ -75,7 +75,7 @@ describe('schema validation NC NSFS account', () => { account_data.new_access_key = [ // this is not part of the schema { access_key: access_key1, - secret_key: secret_key2 + encrypted_secret_key: secret_key2 }, ]; const reason = 'Test should have failed because of adding additional property ' + @@ -166,7 +166,7 @@ describe('schema validation NC NSFS account', () => { it('account without access_keys details (access_key and secret_key)', () => { const account_data = get_account_data(); delete account_data.access_keys[0].access_key; - delete account_data.access_keys[0].secret_key; + delete account_data.access_keys[0].encrypted_secret_key; const reason = 'Test should have failed because of missing required property ' + 'access_key'; const message = "must have required property 'access_key'"; @@ -193,19 +193,19 @@ describe('schema validation NC NSFS account', () => { it('account without secret_key', () => { const account_data = get_account_data(); - delete account_data.access_keys[0].secret_key; + delete account_data.access_keys[0].encrypted_secret_key; const reason = 'Test should have failed because of missing required property ' + - 'secret_key'; - const message = "must have required property 'secret_key'"; + 'encrypted_secret_key'; + const message = "must have required property 'encrypted_secret_key'"; assert_validation(account_data, reason, message); }); it('account with undefined secret_key', () => { const account_data = get_account_data(); - account_data.access_keys[0].secret_key = undefined; + account_data.access_keys[0].encrypted_secret_key = undefined; const reason = 'Test should have failed because of missing required property ' + - 'secret_key'; - const message = "must have required property 'secret_key'"; + 'encrypted_secret_key'; + const message = "must have required property 'encrypted_secret_key'"; assert_validation(account_data, reason, message); }); @@ -290,6 +290,24 @@ describe('schema validation NC NSFS account', () => { const message = "must have required property '_id'"; assert_validation(account_data, reason, message); }); + + it('account without master_key_id', () => { + const account_data = get_account_data(); + account_data.master_key_id = undefined; + const reason = 'Test should have failed because of missing required property ' + + 'master_key_id'; + const message = "must have required property 'master_key_id'"; + assert_validation(account_data, reason, message); + }); + + it('account with undefined master_key_id', () => { + const account_data = get_account_data(); + account_data.master_key_id = undefined; + const reason = 'Test should have failed because of missing required property ' + + 'master_key_id'; + const message = "must have required property 'master_key_id'"; + assert_validation(account_data, reason, message); + }); }); describe('account with wrong types', () => { @@ -337,6 +355,7 @@ describe('schema validation NC NSFS account', () => { function get_account_data() { const account_name = 'account1'; const id = '65a62e22ceae5e5f1a758aa9'; + const master_key_id = '65a62e22ceae5e5f1a758123'; const account_email = account_name; // temp, keep the email internally const access_key = 'GIGiFAnjaaE7OKD5N7hA'; const secret_key = 'U2AYaMpU3zRDcRFWmvzgQr9MoHIAsD+3oEXAMPLE'; @@ -350,9 +369,10 @@ function get_account_data() { _id: id, name: account_name, email: account_email, + master_key_id: master_key_id, access_keys: [{ access_key: access_key, - secret_key: secret_key + encrypted_secret_key: secret_key }, ], nsfs_account_config: { ...nsfs_account_config_uid_gid diff --git a/src/test/unit_tests/nc_coretest.js b/src/test/unit_tests/nc_coretest.js index 7ca2c5b5ec..ea9488edd8 100644 --- a/src/test/unit_tests/nc_coretest.js +++ b/src/test/unit_tests/nc_coretest.js @@ -13,6 +13,8 @@ const { TYPES, ACTIONS } = require('../../manage_nsfs/manage_nsfs_constants'); // keep me first - this is setting envs that should be set before other modules are required. const NC_CORETEST = 'nc_coretest'; +const config_dir_name = 'nc_coretest_config_root_path'; +const master_key_location = `${TMP_PATH}/${config_dir_name}/master_keys.json`; process.env.DEBUG_MODE = 'true'; process.env.ACCOUNTS_CACHE_EXPIRY = '1'; process.env.NC_CORETEST = 'true'; @@ -23,7 +25,7 @@ require('../../util/fips'); const config = require('../../../config.js'); config.test_mode = true; - +config.NC_MASTER_KEYS_FILE_LOCATION = master_key_location; const new_umask = process.env.NOOBAA_ENDPOINT_UMASK || 0o000; const old_umask = process.umask(new_umask); console.log('test_nsfs_access: replacing old umask: ', old_umask.toString(8), 'with new umask: ', new_umask.toString(8)); @@ -47,7 +49,7 @@ const http_address = `http://localhost:${http_port}`; const https_address = `https://localhost:${https_port}`; const FIRST_BUCKET = 'first.bucket'; -const NC_CORETEST_CONFIG_DIR_PATH = p.join(TMP_PATH, 'nc_coretest_config_root_path/'); +const NC_CORETEST_CONFIG_DIR_PATH = p.join(TMP_PATH, config_dir_name); const NC_CORETEST_REDIRECT_FILE_PATH = p.join(config.NSFS_NC_DEFAULT_CONF_DIR, '/config_dir_redirect'); const NC_CORETEST_STORAGE_PATH = p.join(TMP_PATH, '/nc_coretest_storage_root_path/'); const FIRST_BUCKET_PATH = p.join(NC_CORETEST_STORAGE_PATH, FIRST_BUCKET, '/'); diff --git a/src/test/unit_tests/test_bucketspace_fs.js b/src/test/unit_tests/test_bucketspace_fs.js index 1a4ba40a45..2e64f293f0 100644 --- a/src/test/unit_tests/test_bucketspace_fs.js +++ b/src/test/unit_tests/test_bucketspace_fs.js @@ -17,6 +17,7 @@ const NamespaceFS = require('../../sdk/namespace_fs'); const BucketSpaceFS = require('../../sdk/bucketspace_fs'); const { TMP_PATH } = require('../system_tests/test_utils'); const { CONFIG_SUBDIRS } = require('../../manage_nsfs/manage_nsfs_constants'); +const nc_mkm = require('../../manage_nsfs/nc_master_key_manager').get_instance(); const test_bucket = 'bucket1'; @@ -181,7 +182,8 @@ mocha.describe('bucketspace_fs', function() { await fs_utils.create_fresh_path(`${config_root}/${dir}`)) ); await fs_utils.create_fresh_path(new_buckets_path); - for (const account of [account_user1, account_user2, account_user3]) { + for (let account of [account_user1, account_user2, account_user3]) { + account = await nc_mkm.encrypt_access_keys(account); const account_path = get_config_file_path(CONFIG_SUBDIRS.ACCOUNTS, account.name); const account_access_path = get_access_key_symlink_path(CONFIG_SUBDIRS.ACCESS_KEYS, account.access_keys[0].access_key); await fs.promises.writeFile(account_path, JSON.stringify(account)); diff --git a/src/test/unit_tests/test_nc_nsfs_cli.js b/src/test/unit_tests/test_nc_nsfs_cli.js index 6c6579f32b..956add0212 100644 --- a/src/test/unit_tests/test_nc_nsfs_cli.js +++ b/src/test/unit_tests/test_nc_nsfs_cli.js @@ -16,6 +16,8 @@ const { ManageCLIResponse } = require('../../manage_nsfs/manage_nsfs_cli_respons const { exec_manage_cli, generate_s3_policy, create_fs_user_by_platform, delete_fs_user_by_platform, set_path_permissions_and_owner, TMP_PATH } = require('../system_tests/test_utils'); const { TYPES, ACTIONS, CONFIG_SUBDIRS } = require('../../manage_nsfs/manage_nsfs_constants'); +const nc_mkm = require('../../manage_nsfs/nc_master_key_manager').get_instance(); +const config = require('../../../config'); const tmp_fs_path = path.join(TMP_PATH, 'test_bucketspace_fs'); const DEFAULT_FS_CONFIG = get_process_fs_context(); @@ -31,8 +33,9 @@ mocha.describe('manage_nsfs cli', function() { await fs_utils.create_fresh_path(root_path); }); mocha.after(async () => { - fs_utils.folder_delete(`${config_root}`); - fs_utils.folder_delete(`${root_path}`); + await fs_utils.folder_delete(`${config_root}`); + await fs_utils.folder_delete(`${root_path}`); + await fs_utils.file_delete(path.join(config.NSFS_NC_DEFAULT_CONF_DIR, 'master_keys.json')); }); mocha.describe('cli bucket flow ', async function() { @@ -59,8 +62,8 @@ mocha.describe('manage_nsfs cli', function() { await fs_utils.create_fresh_path(root_path); }); mocha.after(async () => { - fs_utils.folder_delete(`${config_root}`); - fs_utils.folder_delete(`${root_path}`); + await fs_utils.folder_delete(config_root); + await fs_utils.folder_delete(root_path); }); mocha.it('cli bucket create without existing account - should fail', async function() { @@ -923,7 +926,7 @@ mocha.describe('manage_nsfs cli', function() { await write_config_file(config_root, '', 'config', config_options); }); mocha.after(async () => { - fs_utils.file_delete(path.join(config_root, 'config.json')); + await fs_utils.file_delete(path.join(config_root, 'config.json')); }); mocha.it('cli add whitelist ips first time (IPV4 format)', async function() { @@ -1015,8 +1018,13 @@ mocha.describe('manage_nsfs cli', function() { async function read_config_file(config_root, schema_dir, config_file_name, is_symlink) { const config_path = path.join(config_root, schema_dir, config_file_name + (is_symlink ? '.symlink' : '.json')); const { data } = await nb_native().fs.readFile(DEFAULT_FS_CONFIG, config_path); - const config = JSON.parse(data.toString()); - return config; + const config_data = JSON.parse(data.toString()); + if (config_data.access_keys) { + const encrypted_secret_key = config_data.access_keys[0].encrypted_secret_key; + config_data.access_keys[0].secret_key = await nc_mkm.decrypt(encrypted_secret_key, config_data.master_key_id); + delete config_data.access_keys[0].encrypted_secret_key; + } + return config_data; } async function write_config_file(config_root, schema_dir, config_file_name, data, is_symlink) { diff --git a/src/util/os_utils.js b/src/util/os_utils.js index 86617186f0..4e83450766 100644 --- a/src/util/os_utils.js +++ b/src/util/os_utils.js @@ -52,6 +52,7 @@ const warn_once = _.once((...args) => dbg.warn(...args)); * ignore_rc?: boolean, * return_stdout?: boolean, * trim_stdout?: boolean, + * env? : Object * }} [options] */ async function exec(command, options) { @@ -59,11 +60,14 @@ async function exec(command, options) { const ignore_rc = (options && options.ignore_rc) || false; const return_stdout = (options && options.return_stdout) || false; const trim_stdout = (options && options.trim_stdout) || false; + const env = (options && options.env) || undefined; + try { dbg.log2('promise exec', command, ignore_rc); const res = await exec_async(command, { maxBuffer: 5000 * 1024, //5MB, should be enough timeout: timeout_ms, + env }); if (return_stdout) { return trim_stdout ? res.stdout.trim() : res.stdout; @@ -124,6 +128,7 @@ function spawn(command, args, options, ignore_rc, unref, timeout_ms) { dbg.log0('spawn:', command, args.join(' '), options, ignore_rc); options.stdio = options.stdio || 'inherit'; const proc = child_process.spawn(command, args, options); + if (options.input) proc.stdin.end(options.input); proc.on('exit', code => { if (code === 0 || ignore_rc) { resolve();