diff --git a/docs/NooBaaNonContainerized/NooBaaCLI.md b/docs/NooBaaNonContainerized/NooBaaCLI.md index 442df60168..d0f708910d 100644 --- a/docs/NooBaaNonContainerized/NooBaaCLI.md +++ b/docs/NooBaaNonContainerized/NooBaaCLI.md @@ -25,8 +25,14 @@ 1. [Upgrade Start](#upgrade-start) 2. [Upgrade Status](#upgrade-status) 3. [Upgrade History](#upgrade-history) -10. [Global Options](#global-options) -11. [Examples](#examples) +10. [Connections](#connection) + 1. [Add Connection](#add-connection) + 2. [Update Connection](#update-connection) + 3. [Connection Status](#connection-status) + 4. [List Connections][#list-connections] + 5. [Delete Connection](#delete-connection) +11. [Global Options](#global-options) +12. [Examples](#examples) 1. [Bucket Commands Examples](#bucket-commands-examples) 2. [Account Commands Examples](#account-commands-examples) 3. [White List Server IP Command Example](#white-list-server-ip-command-example) @@ -525,6 +531,109 @@ The available history information is an array of upgrade information - upgrade s noobaa-cli upgrade history ``` +## Managing Connections + +A connection file holds information needed to send out a notification to an external server. +The connection files is specified in each notification configuration of the bucket. + +- **[Add Connection](#add-connection)**: Create new connections with customizable options. +- **[Update Connection](#update-connection)**: Modify the settings and configurations of existing connections. +- **[Connection Status](#connection-status)**: Retrieve the current status and detailed information about a specific connection. +- **[List Connections](#list-connections)**: Display a list of all existing connections. +- **[Delete Connection](#delete-connection)**: Remove unwanted or obsolete connections from the system. + +### Add Connection + +The `connection add` command is used to create a new connection with customizable options. + +#### Usage +```sh +noobaa-cli connection add --from_file +``` +#### Flags - + +- `name` (Required) + - Type: String + - Description: A name to identify the connection. + +- `notification_protocol` (Required) + - Type: String + - Enum: http | https | kafka + - Description - Target external server's protocol. + +- `agent_request_object` + - Type: Object + - Description: An object given as options to node http(s) agent. + +- `request_options_object` + - Type: Object + - Description: An object given as options to node http(s) request. If "auth" field is specified, it's value is encrypted. + +- `from_file` + - Type: String + - Description: Path to a JSON file which includes connection properties. When using `from_file` flag the connection details must only appear inside the options JSON file. See example below. + +### Update Connection + +The `connection update` command is used to update an existing bucket with customizable options. + +#### Usage +```sh +noobaa-cli connection update --name --key [--value] [--remove_key] +``` +#### Flags - +- `name` (Required) + - Type: String + - Description: Specifies the name of the updated connection. + +- `key` (Required) + - Type: String + - Description: Specifies the key to be updated. + +- `value` + - Type: String + - Description: Specifies the new value of the specified key. + +- `remove_key` + - Type: Boolean + - Description: Specifies that the specified key should be removed. + +### Connection Status + +The `connection status` command is used to print the status of the connection. + +#### Usage +```sh +noobaa-cli connection status --name +``` +#### Flags - +- `name` (Required) + - Type: String + - Description: Specifies the name of the connection. + +### List Connections + +The `connection list` command is used to display a list of all existing connections. + + +#### Usage +```sh +noobaa-cli connection list +``` + +### Delete Connection + +The `connection delete` command is used to delete an existing connection. + +#### Usage +```sh +noobaa-cli connection delete --name +``` +#### Flags - +- `name` (Required) + - Type: String + - Description: Specifies the name of the connection to be deleted. + ## Global Options Global options used by the CLI to define the config directory settings. @@ -658,7 +767,21 @@ sudo noobaa-cli bucket delete --name bucket1 2>/dev/null ``` ----- +### Connection Commands Examples + +#### Create Connection in CLI + +```sh +sudo noobaa-cli connection add --name conn1 --notification_protocol http --request_options_object '{"auth": "user:passw"}' +``` +#### Update Connection Field + +```sh +sudo noobaa-cli connection update --name conn1 --key request_options_object --value '{"auth":"user2:pw2"}' +``` + +----- #### `--from-file` flag usage example Using `from_file` flag: @@ -695,6 +818,21 @@ sudo noobaa-cli account add --from_file sudo noobaa-cli bucket add --from_file ``` +##### 2. Create JSON file for connection: + +```json +{ + "name": "http_conn", + "notification_protocol": "http", + "agent_request_object": {"host": "localhost", "port": 9999, "timeout": 100}, + "request_options_object": {"auth": "user:passw", "path": "/query"} +} +``` + +```bash +sudo noobaa-cli connection add --from_file +``` + ------ ### White List Server IP command example diff --git a/src/cmd/manage_nsfs.js b/src/cmd/manage_nsfs.js index be4cb9905a..2ec811c5b1 100644 --- a/src/cmd/manage_nsfs.js +++ b/src/cmd/manage_nsfs.js @@ -74,6 +74,8 @@ async function main(argv = minimist(process.argv.slice(2))) { await noobaa_cli_upgrade.manage_upgrade_operations(action, user_input, config_fs); } else if (type === TYPES.NOTIFICATION) { await notification_management(); + } else if (type === TYPES.CONNECTION) { + await connection_management(action, user_input); } else { throw_cli_error(ManageCLIError.InvalidType); } @@ -642,6 +644,19 @@ async function list_config_files(type, wide, show_secrets, filters = {}) { return config_files_list; } +/** + * list_connections + * @returns An array with names of all connection files. + */ +async function list_connections() { + let conns = await config_fs.list_connections(); + // it inserts undefined for the entry '.noobaa-config-nsfs' and we wish to remove it + // in case the entry was deleted during the list it also inserts undefined + conns = conns.filter(item => item); + + return conns; +} + /** * get_access_keys will return the access_keys and new_access_key according to the user input * and action @@ -725,12 +740,51 @@ async function logging_management() { } async function notification_management() { - new notifications_util.Notificator({ + await new notifications_util.Notificator({ fs_context: config_fs.fs_context, connect_files_dir: config_fs.connections_dir_path, nc_config_fs: config_fs, }).process_notification_files(); } +async function connection_management(action, user_input) { + manage_nsfs_validations.validate_connection_args(user_input, action); + + let response = {}; + let data; + + switch (action) { + case ACTIONS.ADD: + data = await notifications_util.add_connect_file(user_input, config_fs); + response = { code: ManageCLIResponse.ConnectionCreated, detail: data }; + break; + case ACTIONS.DELETE: + await config_fs.delete_connection_config_file(user_input.name); + response = { code: ManageCLIResponse.ConnectionDeleted }; + break; + case ACTIONS.UPDATE: + await notifications_util.update_connect_file(user_input.name, user_input.key, + user_input.value, user_input.remove_key, config_fs); + response = { code: ManageCLIResponse.ConnectionUpdated }; + break; + case ACTIONS.STATUS: + data = await new notifications_util.Notificator({ + fs_context: config_fs.fs_context, + connect_files_dir: config_fs.connections_dir_path, + nc_config_fs: config_fs, + }).parse_connect_file(user_input.name, user_input.decrypt); + response = { code: ManageCLIResponse.ConnectionStatus, detail: data }; + break; + case ACTIONS.LIST: + data = await list_connections(); + response = { code: ManageCLIResponse.ConnectionList, detail: data }; + break; + default: + throw_cli_error(ManageCLIError.InvalidAction); + } + + write_stdout_response(response.code, response.detail, response.event_arg); +} + exports.main = main; if (require.main === module) main(); diff --git a/src/manage_nsfs/manage_nsfs_cli_errors.js b/src/manage_nsfs/manage_nsfs_cli_errors.js index 92f849c1e7..86784384cd 100644 --- a/src/manage_nsfs/manage_nsfs_cli_errors.js +++ b/src/manage_nsfs/manage_nsfs_cli_errors.js @@ -94,7 +94,7 @@ ManageCLIError.InvalidArgumentType = Object.freeze({ ManageCLIError.InvalidType = Object.freeze({ code: 'InvalidType', - message: 'Invalid type, available types are account, bucket, logging, whitelist, upgrade or notification', + message: 'Invalid type, available types are account, bucket, logging, whitelist, upgrade, notification or connection.', http_code: 400, }); @@ -490,6 +490,16 @@ ManageCLIError.ConfigDirUpdateBlocked = Object.freeze({ http_code: 500, }); +/////////////////////////////// +// CONNECTION ERRORS // +/////////////////////////////// + +ManageCLIError.MissingCliParam = Object.freeze({ + code: 'MissingCliParam', + message: 'Required cli parameter is missing.', + http_code: 400, +}); + /////////////////////////////// // ERRORS MAPPING // /////////////////////////////// diff --git a/src/manage_nsfs/manage_nsfs_cli_responses.js b/src/manage_nsfs/manage_nsfs_cli_responses.js index 94be1f1ab3..942dbefc55 100644 --- a/src/manage_nsfs/manage_nsfs_cli_responses.js +++ b/src/manage_nsfs/manage_nsfs_cli_responses.js @@ -144,6 +144,34 @@ ManageCLIResponse.UpgradeHistory = Object.freeze({ status: {} }); +/////////////////////////////// +// CONNECTION RESPONSES // +/////////////////////////////// + +ManageCLIResponse.ConnectionCreated = Object.freeze({ + code: 'ConnectionCreated', + status: {} +}); + +ManageCLIResponse.ConnectionDeleted = Object.freeze({ + code: 'ConnectionDeleted', +}); + +ManageCLIResponse.ConnectionUpdated = Object.freeze({ + code: 'ConnectionUpdated', + status: {} +}); + +ManageCLIResponse.ConnectionStatus = Object.freeze({ + code: 'ConnectionStatus', + status: {} +}); + +ManageCLIResponse.ConnectionList = Object.freeze({ + code: 'ConnectionList', + list: {} +}); + /////////////////////////////// // RESPONSES-EVENT MAPPING // /////////////////////////////// diff --git a/src/manage_nsfs/manage_nsfs_constants.js b/src/manage_nsfs/manage_nsfs_constants.js index ae3b374bf0..b69d967617 100644 --- a/src/manage_nsfs/manage_nsfs_constants.js +++ b/src/manage_nsfs/manage_nsfs_constants.js @@ -9,7 +9,8 @@ const TYPES = Object.freeze({ LOGGING: 'logging', DIAGNOSE: 'diagnose', UPGRADE: 'upgrade', - NOTIFICATION: 'notification' + NOTIFICATION: 'notification', + CONNECTION: 'connection' }); const ACTIONS = Object.freeze({ @@ -84,6 +85,16 @@ const VALID_OPTIONS_UPGRADE = { 'history': new Set([...CLI_MUTUAL_OPTIONS]) }; +const VALID_OPTIONS_NOTIFICATION = {}; + +const VALID_OPTIONS_CONNECTION = { + 'add': new Set(['name', 'notification_protocol', 'agent_request_object', 'request_options_object', FROM_FILE, ...CLI_MUTUAL_OPTIONS]), + 'update': new Set(['name', 'key', 'value', 'remove_key', ...CLI_MUTUAL_OPTIONS]), + 'delete': new Set(['name', ...CLI_MUTUAL_OPTIONS]), + 'list': new Set(CLI_MUTUAL_OPTIONS), + 'status': new Set(['name', 'decrypt', ...CLI_MUTUAL_OPTIONS]), +}; + const VALID_OPTIONS_WHITELIST = new Set(['ips', ...CLI_MUTUAL_OPTIONS]); @@ -97,7 +108,9 @@ const VALID_OPTIONS = { from_file_options: VALID_OPTIONS_FROM_FILE, anonymous_account_options: VALID_OPTIONS_ANONYMOUS_ACCOUNT, diagnose_options: VALID_OPTIONS_DIAGNOSE, - upgrade_options: VALID_OPTIONS_UPGRADE + upgrade_options: VALID_OPTIONS_UPGRADE, + notification_options: VALID_OPTIONS_NOTIFICATION, + connection_options: VALID_OPTIONS_CONNECTION, }; const OPTION_TYPE = { @@ -137,8 +150,14 @@ const OPTION_TYPE = { expected_hosts: 'string', custom_upgrade_scripts_dir: 'string', skip_verification: 'boolean', - //notifications - notifications: 'object' + //connection + notification_protocol: 'string', + agent_request_object: 'string', + request_options_object: 'string', + decrypt: 'boolean', + key: 'string', + value: 'string', + remove_key: 'boolean', }; const BOOLEAN_STRING_VALUES = ['true', 'false']; diff --git a/src/manage_nsfs/manage_nsfs_help_utils.js b/src/manage_nsfs/manage_nsfs_help_utils.js index edaae03068..56846f6c38 100644 --- a/src/manage_nsfs/manage_nsfs_help_utils.js +++ b/src/manage_nsfs/manage_nsfs_help_utils.js @@ -68,6 +68,25 @@ List of actions supported: `; +const CONNECTION_ACTIONS = ` +Help: + + Use this CLI to execute all the connection related actions. + +Usage: + + connection [flags] + +List of actions supported: + + add + update + list + status + delete + +`; + const WHITELIST_FLAGS = ` Help: @@ -458,6 +477,86 @@ Usage: `; +const CONNECTION_FLAGS_ADD = ` +Help: + + Use this CLI to add a connection. + +Usage: + + connection add [flags] + +Flags: + + --name Set the name for the connection + --agent_request_object Value of agent request objects, used for http(s) connection, as defined by nodejs http(s) agent options + --request_options_object Value of http(s) request option, as defined by nodejs http(s) request option. "auth" field would be encrypted. + --notification_protocol One of http, https, kafka. + --from_file (optional) Use details from the JSON file, there is no need to mention all the properties individually in the CLI + +`; + +const CONNECTION_FLAGS_UPDATE = ` +Help: + + Use this CLI to update a connection. + +Usage: + + connection update [flags] + +Flags: + + --name The name of the connection to update. + --key Name of field to update + --value Value of the field to update + --remove_key Removes a key from the connection. +`; + +const CONNECTION_FLAGS_DELETE = ` +Help: + + Use this CLI to delete a connection. + +Usage: + + connection delete [flags] + +Flags: + + --name The name of the connection to delete. + +`; + +const CONNECTION_FLAGS_STATUS = ` +Help: + + Use this CLI to get connection status. + +Usage: + + connection status [flags] + +Flags: + + --name The name of the connection. + --decrypt Wether to decrypt the auth field of the request. + +`; + +const CONNECTION_FLAGS_LIST = ` +Help: + + Use this CLI to list connections. + +Usage: + + connection list [flags] + +Flags: + +`; + /** * print_usage would print the help according to the arguments that were passed * @param {string} type @@ -486,6 +585,9 @@ function print_usage(type, action) { case TYPES.UPGRADE: print_help_upgrade(action); break; + case TYPES.CONNECTION: + print_help_connection(action); + break; default: process.stdout.write(HELP + '\n'); process.stdout.write(USAGE.trimStart() + '\n'); @@ -605,6 +707,33 @@ function print_help_upgrade(action) { } } +/** + * print_help_connection would print the help options for connection + * @param {string} action + */ +function print_help_connection(action) { + switch (action) { + case ACTIONS.ADD: + process.stdout.write(CONNECTION_FLAGS_ADD.trimStart() + CLI_MUTUAL_FLAGS); + break; + case ACTIONS.UPDATE: + process.stdout.write(CONNECTION_FLAGS_UPDATE.trimStart() + CLI_MUTUAL_FLAGS); + break; + case ACTIONS.DELETE: + process.stdout.write(CONNECTION_FLAGS_DELETE.trimStart() + CLI_MUTUAL_FLAGS); + break; + case ACTIONS.STATUS: + process.stdout.write(CONNECTION_FLAGS_STATUS.trimStart() + CLI_MUTUAL_FLAGS); + break; + case ACTIONS.LIST: + process.stdout.write(CONNECTION_FLAGS_LIST.trimStart() + CLI_MUTUAL_FLAGS); + break; + default: + process.stdout.write(CONNECTION_ACTIONS.trimStart()); + } + process.exit(0); +} + // EXPORTS exports.print_usage = print_usage; diff --git a/src/manage_nsfs/manage_nsfs_validations.js b/src/manage_nsfs/manage_nsfs_validations.js index 2ae0d76a36..df73b60036 100644 --- a/src/manage_nsfs/manage_nsfs_validations.js +++ b/src/manage_nsfs/manage_nsfs_validations.js @@ -46,7 +46,7 @@ async function validate_input_types(type, action, argv) { // currently we use from_file only in add action const path_to_json_options = argv.from_file ? String(argv.from_file) : ''; - if ((type === TYPES.ACCOUNT || type === TYPES.BUCKET) && action === ACTIONS.ADD && path_to_json_options) { + if ((type === TYPES.ACCOUNT || type === TYPES.BUCKET || type === TYPES.CONNECTION) && action === ACTIONS.ADD && path_to_json_options) { const input_options_with_data_from_file = await get_options_from_file(path_to_json_options); const input_options_from_file = Object.keys(input_options_with_data_from_file); if (input_options_from_file.includes(FROM_FILE)) { @@ -112,7 +112,7 @@ function validate_identifier(type, action, input_options, is_options_from_file) */ function validate_no_extra_options(type, action, input_options, is_options_from_file) { let valid_options; // for performance, we use Set as data structure - const from_file_condition = (type === TYPES.ACCOUNT || type === TYPES.BUCKET) && + const from_file_condition = (type === TYPES.ACCOUNT || type === TYPES.BUCKET || type === TYPES.CONNECTION) && action === ACTIONS.ADD && input_options.includes(FROM_FILE); if (from_file_condition) { valid_options = VALID_OPTIONS.from_file_options; @@ -130,6 +130,10 @@ function validate_no_extra_options(type, action, input_options, is_options_from_ valid_options = VALID_OPTIONS.diagnose_options[action]; } else if (type === TYPES.UPGRADE) { valid_options = VALID_OPTIONS.upgrade_options[action]; + } else if (type === TYPES.NOTIFICATION) { + valid_options = VALID_OPTIONS.notification_options[action]; + } else if (type === TYPES.CONNECTION) { + valid_options = VALID_OPTIONS.connection_options[action]; } else { valid_options = VALID_OPTIONS.whitelist_options; } @@ -173,8 +177,11 @@ function validate_options_type_by_value(input_options_with_data) { if (BOOLEAN_STRING_OPTIONS.has(option) && validate_boolean_string_value(value)) { continue; } - // special case for bucket_policy and notifications(from_file) - if ((option === 'bucket_policy' || option === 'notifications') && type_of_value === 'object') { + // special case for bucket_policy, notifications and connections(from_file) + if ((option === 'bucket_policy' || + option === 'notifications' || + option === 'agent_request_object' || + option === 'request_options_object') && type_of_value === 'object') { continue; } //special case for supplemental groups @@ -636,6 +643,42 @@ function validate_whitelist_ips(ips_to_validate) { } } +/////////////////////////////////// +//// CONNECTION VALIDATIONS //// +/////////////////////////////////// + +/** + * Checks that combination of cli parameters is valid for the given action. + * @param {Object} user_input + * @param {string} action + */ +function validate_connection_args(user_input, action) { + + //name is mandatory for all except LIST + if (action !== ACTIONS.LIST && !user_input.name) { + throw_cli_error(ManageCLIError.MissingCliParam, "CLI parameter 'name' is mandatory."); + } + + //action specific mandatory options + switch (action) { + case ACTIONS.ADD: + if (!user_input.notification_protocol) { + throw_cli_error(ManageCLIError.MissingCliParam, "CLI parameter 'notification_protocol' is missing"); + } + break; + case ACTIONS.UPDATE: + if (!user_input.key) { + throw_cli_error(ManageCLIError.MissingCliParam, "CLI parameter 'key' is mandatory."); + } + if (!user_input.value && !user_input.remove_key) { + throw_cli_error(ManageCLIError.MissingCliParam, "Either 'value' or 'remove_key' is required."); + } + break; + default: + } +} + + // EXPORTS exports.validate_input_types = validate_input_types; exports.validate_bucket_args = validate_bucket_args; @@ -645,3 +688,4 @@ exports.validate_root_accounts_manager_update = validate_root_accounts_manager_u exports.validate_whitelist_arg = validate_whitelist_arg; exports.validate_whitelist_ips = validate_whitelist_ips; exports.validate_flags_combination = validate_flags_combination; +exports.validate_connection_args = validate_connection_args; diff --git a/src/sdk/config_fs.js b/src/sdk/config_fs.js index 1ed9c71659..01c1db3410 100644 --- a/src/sdk/config_fs.js +++ b/src/sdk/config_fs.js @@ -885,6 +885,29 @@ class ConfigFS { } } + ///////////////////////////////////////// + ///// CONNECTION CONFIG DIR FUNCS ////// + ///////////////////////////////////////// + + /** + * Returns the path to a connection file + * @param {string} connection_name name of the desired connection + * @returns {string} connection file path + */ + get_connection_path_by_name(connection_name) { + return path.join(this.connections_dir_path, this.json(connection_name)); + } + + /** + * Return content of connection file + * @param {string} connection_name + * @returns {Promise} connetion file content + */ + async get_connection_by_name(connection_name) { + const filepath = path.join(this.connections_dir_path, this.json(connection_name)); + return await this.get_config_data(filepath); + } + ////////////////////////////////////// ////// ACCESS KEYS INDEXES ////// ////////////////////////////////////// @@ -1328,6 +1351,49 @@ class ConfigFS { throw err; } } + + /** + * create_connection_config_file creates a new connection file in the config dir + * @param {Object} name filename for the new file + * @param {Object} connection_data content of new file + * @returns {Promise} + */ + async create_connection_config_file(name, connection_data) { + await this._throw_if_config_dir_locked(); + const filepath = this.get_connection_path_by_name(name); + await native_fs_utils.create_config_file(this.fs_context, this.connections_dir_path, filepath, JSON.stringify(connection_data)); + } + + /** + * delete_connection_config_file deletes a connection file + * @param {string} name connection file to delete + */ + async delete_connection_config_file(name) { + await this._throw_if_config_dir_locked(); + const filepath = this.get_connection_path_by_name(name); + await native_fs_utils.delete_config_file(this.fs_context, this.connections_dir_path, filepath); + } + + /** + * update_connection_file updates content of connection file + * @param {string} name connetion file to update + * @param {Object} data new content for connection file + */ + async update_connection_file(name, data) { + await this._throw_if_config_dir_locked(); + const filepath = this.get_connection_path_by_name(name); + await native_fs_utils.update_config_file(this.fs_context, this.config_root, filepath, JSON.stringify(data)); + } + + /** + * list_connections returns the array of connections that exists under the config dir + * @returns {Promise} + */ + async list_connections() { + const connections_entries = await nb_native().fs.readdir(this.fs_context, this.connections_dir_path); + const connection_names = this._get_config_entries_names(connections_entries, JSON_SUFFIX); + return connection_names; + } } // EXPORTS diff --git a/src/test/unit_tests/jest_tests/test_nc_nsfs_connection_cli.test.js b/src/test/unit_tests/jest_tests/test_nc_nsfs_connection_cli.test.js new file mode 100644 index 0000000000..7e4207be99 --- /dev/null +++ b/src/test/unit_tests/jest_tests/test_nc_nsfs_connection_cli.test.js @@ -0,0 +1,161 @@ +/* Copyright (C) 2025 NooBaa */ +/* eslint-disable max-lines-per-function */ +/* eslint max-lines: ['error', 4000] */ +'use strict'; + +// disabling init_rand_seed as it takes longer than the actual test execution +process.env.DISABLE_INIT_RANDOM_SEED = "true"; + +const fs = require('fs'); +const path = require('path'); +const os_util = require('../../../util/os_utils'); +const fs_utils = require('../../../util/fs_utils'); +const { ConfigFS } = require('../../../sdk/config_fs'); +const { TMP_PATH, set_nc_config_dir_in_config } = require('../../system_tests/test_utils'); +const { TYPES, ACTIONS } = require('../../../manage_nsfs/manage_nsfs_constants'); + +const tmp_fs_path = path.join(TMP_PATH, 'test_nc_nsfs_account_cli'); +const timeout = 5000; + +// eslint-disable-next-line max-lines-per-function +describe('manage nsfs cli connection flow', () => { + describe('cli create connection', () => { + const config_root = path.join(tmp_fs_path, 'config_root_manage_nsfs'); + const config_fs = new ConfigFS(config_root); + const root_path = path.join(tmp_fs_path, 'root_path_manage_nsfs/'); + const options_file = path.join(TMP_PATH, "conn1.json"); + const defaults = { + name: 'conn1', + agent_request_object: { + host: "localhost", + "port": 9999, + "timeout": 100 + }, + request_options_object: {auth: "user:passw"}, + notification_protocol: "http", + + }; + beforeEach(async () => { + await fs_utils.create_fresh_path(root_path); + set_nc_config_dir_in_config(config_root); + + fs.writeFileSync(options_file, JSON.stringify(defaults)); + const action = ACTIONS.ADD; + const conn_options = { config_root, from_file: options_file }; + await exec_manage_cli(TYPES.CONNECTION, action, conn_options); + }); + + afterEach(async () => { + await fs_utils.folder_delete(`${config_root}`); + await fs_utils.folder_delete(`${root_path}`); + }); + + it('cli create connection from file', async () => { + const connection = await config_fs.get_connection_by_name(defaults.name); + assert_connection(connection, defaults, true); + }, timeout); + + it('cli create connection from cli', async () => { + const conn_options = {...defaults, config_root}; + conn_options.name = "fromcli"; + await exec_manage_cli(TYPES.CONNECTION, ACTIONS.ADD, conn_options); + const connection = await config_fs.get_connection_by_name(conn_options.name); + assert_connection(connection, conn_options, true); + }, timeout); + + it('cli delete connection ', async () => { + await exec_manage_cli(TYPES.CONNECTION, ACTIONS.DELETE, {config_root, name: defaults.name}); + expect(fs.readdirSync(config_fs.connections_dir_path).filter(file => file.endsWith(".json")).length).toEqual(0); + }, timeout); + + //update notification_protocol field in the connection file. + it('cli update connection ', async () => { + await exec_manage_cli(TYPES.CONNECTION, ACTIONS.UPDATE, { + config_root, + name: defaults.name, + key: "notification_protocol", + value: "https" + }); + const updated = await config_fs.get_connection_by_name(defaults.name); + const updated_content = {...defaults}; + updated_content.notification_protocol = "https"; + assert_connection(updated, updated_content, true); + }, timeout); + + it('cli list connection ', async () => { + const res = JSON.parse(await exec_manage_cli(TYPES.CONNECTION, ACTIONS.LIST, {config_root})); + expect(res.response.reply[0]).toEqual(defaults.name); + }, timeout); + + it('cli status connection ', async () => { + const res = JSON.parse(await exec_manage_cli(TYPES.CONNECTION, ACTIONS.STATUS, { + config_root, + name: defaults.name, + decrypt: true + })); + assert_connection(res.response.reply, defaults, false); + }, timeout); + + }); +}); + + +/** + * assert_connection will verify the fields of the accounts + * @param {object} connection actual + * @param {object} connection_options expected + * @param {boolean} is_encrypted whether connection's auth field is encrypted + */ +function assert_connection(connection, connection_options, is_encrypted) { + expect(connection.name).toEqual(connection_options.name); + expect(connection.notification_protocol).toEqual(connection_options.notification_protocol); + expect(connection.agent_request_object).toStrictEqual(connection_options.agent_request_object); + if (is_encrypted) { + expect(connection.request_options_object).not.toStrictEqual(connection_options.request_options_object); + } else { + expect(connection.request_options_object).toStrictEqual(connection_options.request_options_object); + } +} + +/** + * exec_manage_cli will get the flags for the cli and runs the cli with it's flags + * @param {string} type + * @param {string} action + * @param {object} options + */ +async function exec_manage_cli(type, action, options) { + const command = create_command(type, action, options); + let res; + try { + res = await os_util.exec(command, { return_stdout: true }); + } catch (e) { + res = e; + } + return res; +} + +/** + * create_command would create the string needed to run the CLI command + * @param {string} type + * @param {string} action + * @param {object} options + */ +function create_command(type, action, options) { + let account_flags = ``; + for (const key in options) { + if (Object.hasOwn(options, key)) { + if (typeof options[key] === 'boolean') { + account_flags += `--${key} `; + } else if (typeof options[key] === 'object') { + const val = JSON.stringify(options[key]); + account_flags += `--${key} '${val}' `; + } else { + account_flags += `--${key} ${options[key]} `; + } + } + } + account_flags = account_flags.trim(); + + const command = `node src/cmd/manage_nsfs ${type} ${action} ${account_flags}`; + return command; +} diff --git a/src/util/notifications_util.js b/src/util/notifications_util.js index 1204720876..5a3799bee1 100644 --- a/src/util/notifications_util.js +++ b/src/util/notifications_util.js @@ -13,6 +13,7 @@ const path = require('path'); const { get_process_fs_context } = require('./native_fs_utils'); const nb_native = require('../util/nb_native'); const http_utils = require('../util/http_utils'); +const nc_mkm = require('../manage_nsfs/nc_master_key_manager').get_instance(); const OP_TO_EVENT = Object.freeze({ put_object: { name: 'ObjectCreated' }, @@ -120,7 +121,7 @@ class Notificator { dbg.log2("notifying with notification =", notif); let connect = this.notif_to_connect.get(notif.meta.name); if (!connect) { - connect = await this.parse_connect_file(notif.meta.connect); + connect = await this.parse_connect_file(notif.meta.connect, true); this.notif_to_connect.set(notif.meta.name, connect); } let connection = this.connect_str_to_connection.get(notif.meta.name); @@ -156,15 +157,22 @@ class Notificator { return true; } - async parse_connect_file(connect_filename) { - const filepath = path.join(this.connect_files_dir, connect_filename); + async parse_connect_file(connect_filename, decrypt = false) { let connect; if (this.nc_config_fs) { - connect = await this.nc_config_fs.get_config_data(filepath); + connect = await this.nc_config_fs.get_connection_by_name(connect_filename); } else { + const filepath = path.join(this.connect_files_dir, connect_filename); const connect_str = fs.readFileSync(filepath, 'utf-8'); connect = JSON.parse(connect_str); } + + //if connect file is encrypted (and decryption is requested), + //decrypt the auth field + if (connect.master_key_id && connect.request_options_object.auth && decrypt) { + connect.request_options_object.auth = await nc_mkm.decrypt( + connect.request_options_object.auth, connect.master_key_id); + } load_files(connect); return connect; } @@ -311,7 +319,7 @@ async function test_notifications(bucket, connect_files_dir) { } const notificator = new Notificator({connect_files_dir}); for (const notif of bucket.notifications) { - const connect = await notificator.parse_connect_file(notif.connect); + const connect = await notificator.parse_connect_file(notif.connect, true); dbg.log1("testing notif", notif); try { const connection = get_connection(connect); @@ -492,10 +500,68 @@ function get_notification_logger(locking, namespace, poll_interval) { }); } +/** + * add_connect_file Creates a new connection file from the given content. + * If content has an auth field in it's request_options_object, it is encrypted. + * @param {Object} content connection file content + * @param {Object} nc_config_fs NC config fs object + * @returns A possible encrypted target connection file + */ +async function add_connect_file(content, nc_config_fs) { + for (const key in content) { + if (key.endsWith("_object") && typeof content[key] === 'string') { + content[key] = JSON.parse(content[key]); + } + } + await encrypt_connect_file(content); + await nc_config_fs.create_connection_config_file(content.name, content); + return content; +} + +/** + * update_connect_file Updates given key in the connection file. + * If value is specified, this value is assigned to the key. + * If remove_key is specified, key is removed. + * @param {string} name connection file name + * @param {string} key key name to be updated + * @param {string} value new value for the key + * @param {boolean} remove_key should key be removed + * @param {Object} nc_config_fs NC config fs + */ +async function update_connect_file(name, key, value, remove_key, nc_config_fs) { + const data = await nc_config_fs.get_connection_by_name(name); + if (remove_key) { + delete data[key]; + } else { + //if update initiated in cli, object fields need to be parsed + if (key.endsWith('_object') && typeof value === 'string') { + value = JSON.parse(value); + } + data[key] = value; + } + await encrypt_connect_file(data); + await nc_config_fs.update_connection_file(name, data); +} + +/** + * encrypt_connect_file Encrypted request_options_object.auth field, if present. + * Sets the 'encrypt' field to true. + * @param {Object} data connection's file content + */ +async function encrypt_connect_file(data) { + if (data.request_options_object && data.request_options_object.auth && !data.master_key_id) { + await nc_mkm.init(); + data.request_options_object.auth = nc_mkm.encryptSync(data.request_options_object.auth); + data.master_key_id = nc_mkm.active_master_key.id; + } +} + exports.Notificator = Notificator; exports.test_notifications = test_notifications; exports.compose_notification_req = compose_notification_req; exports.compose_notification_lifecycle = compose_notification_lifecycle; exports.check_notif_relevant = check_notif_relevant; exports.get_notification_logger = get_notification_logger; +exports.add_connect_file = add_connect_file; +exports.update_connect_file = update_connect_file; exports.OP_TO_EVENT = OP_TO_EVENT;