From 3921469678cc69f0e3bc5a24b3cf4d5614338b08 Mon Sep 17 00:00:00 2001 From: maciek <42122011+xkcm@users.noreply.github.com> Date: Tue, 5 Jul 2022 15:01:02 +0200 Subject: [PATCH 1/6] KP-825 Add text parser (#16) --- parsers.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/parsers.js b/parsers.js index 712da2c..bd997d4 100644 --- a/parsers.js +++ b/parsers.js @@ -12,9 +12,10 @@ function resolveParser(type) { return boolean; case "vault": case "options": - case "text": case "string": return string; + case "text": + return text; case "autocomplete": return autocomplete; case "array": @@ -68,6 +69,12 @@ function string(value) { throw new Error(`Value ${value} is not a valid string`); } +function text(value) { + if (_.isNil(value)) { return ""; } + if (_.isString(value)) { return value; } + throw new Error(`Value ${value} is not a valid text`); +} + function autocomplete(value) { if (_.isNil(value)) { return ""; } if (_.isString(value)) { return value; } @@ -94,4 +101,5 @@ module.exports = { number, object, array, + text, }; From a982e0f0eb8efe7b689e18e27d2560ea43ed1b34 Mon Sep 17 00:00:00 2001 From: maciek <42122011+xkcm@users.noreply.github.com> Date: Mon, 11 Jul 2022 11:56:15 +0200 Subject: [PATCH 2/6] KP-829 [Plugin Library] Parsing and validation doesn't check Account params (#17) --- config-loader.js | 28 ++++++ helpers.js | 38 ++++---- helpers.test.js | 163 +++++++++++++++++++++++++++++++++++ mocks/account-config.json | 98 +++++++++++++++++++++ mocks/no-account-config.json | 74 ++++++++++++++++ 5 files changed, 386 insertions(+), 15 deletions(-) create mode 100644 config-loader.js create mode 100644 mocks/account-config.json create mode 100644 mocks/no-account-config.json diff --git a/config-loader.js b/config-loader.js new file mode 100644 index 0000000..b5fa3a3 --- /dev/null +++ b/config-loader.js @@ -0,0 +1,28 @@ +const path = require("path"); + +function loadMethodFromConfiguration(methodName) { + const config = loadConfiguration(); + return config.methods.find((m) => m.name === methodName); +} + +function loadAccountFromConfiguration() { + const config = loadConfiguration(); + return config.auth; +} + +function loadConfiguration() { + try { + const pluginModulePath = process.argv[2] || "../../../app.js"; + const configPath = path.resolve(path.dirname(pluginModulePath), "config.json"); + // eslint-disable-next-line global-require, import/no-unresolved, import/no-dynamic-require + return require(configPath); + } catch (exception) { + console.error(exception); + throw new Error("Could not retrieve the plugin configuration"); + } +} + +module.exports = { + loadMethodFromConfiguration, + loadAccountFromConfiguration, +}; diff --git a/helpers.js b/helpers.js index 349b04f..719ba45 100644 --- a/helpers.js +++ b/helpers.js @@ -5,6 +5,10 @@ const exec = util.promisify(require("child_process").exec); const parsers = require("./parsers"); const validators = require("./validators"); +const { + loadMethodFromConfiguration, + loadAccountFromConfiguration, +} = require("./config-loader"); const CREATE_TEMPORARY_FILE_LINUX_COMMAND = "mktemp --tmpdir kaholo_plugin_library.XXX"; const DEFAULT_PATH_ARGUMENT_REGEX = /(?<=\s|^|\w+=)((?:fileb?:\/\/)?(?:\.\/|\/)(?:[A-Za-z0-9-_]+\/?)*|"(?:fileb?:\/\/)?(?:\.\/|\/)(?:[^"][A-Za-z0-9-_ ]+\/?)*"|'(?:fileb?:\/\/)?(?:\.\/|\/)(?:[^'][A-Za-z0-9-_ ]+\/?)*'|(?:fileb?:\/\/)(?:[A-Za-z0-9-_]+\/?)*|"(?:fileb?:\/\/)(?:[^"][A-Za-z0-9-_ ]+\/?)*"|'(?:fileb?:\/\/)(?:[^'][A-Za-z0-9-_ ]+\/?)*')(?=\s|$)/g; @@ -13,6 +17,7 @@ const FILE_PREFIX_REGEX = /^fileb?:\/\//; function readActionArguments(action, settings) { const method = loadMethodFromConfiguration(action.method.name); + const account = loadAccountFromConfiguration(); const paramValues = removeUndefinedAndEmpty(action.params); const settingsValues = removeUndefinedAndEmpty(settings); @@ -36,6 +41,24 @@ function readActionArguments(action, settings) { } }); + if (account) { + account.params.forEach((paramDefinition) => { + paramValues[paramDefinition.name] = parseMethodParameter( + paramDefinition, + paramValues[paramDefinition.name], + settingsValues[paramDefinition.name], + ); + + const { validationType } = paramDefinition; + if (validationType) { + validateParamValue( + paramValues[paramDefinition.name], + validationType, + ); + } + }); + } + return removeUndefinedAndEmpty(paramValues); } @@ -141,21 +164,6 @@ function validateParamValue( return validate(parameterValue); } -function loadMethodFromConfiguration(methodName) { - const config = loadConfiguration(); - return config.methods.find((m) => m.name === methodName); -} - -function loadConfiguration() { - try { - // eslint-disable-next-line global-require, import/no-unresolved - return require("../../../config.json"); - } catch (exception) { - console.error(exception); - throw new Error("Could not retrieve the plugin configuration"); - } -} - function generateRandomTemporaryPath() { return `/tmp/kaholo_tmp_path_${generateRandomString()}`; } diff --git a/helpers.test.js b/helpers.test.js index 2358a20..7e2520e 100644 --- a/helpers.test.js +++ b/helpers.test.js @@ -3,6 +3,12 @@ const exec = util.promisify(require("child_process").exec); const { open } = require("fs/promises"); const helpers = require("./helpers"); +const { + loadAccountFromConfiguration, + loadMethodFromConfiguration, +} = require("./config-loader"); + +jest.mock("./config-loader"); describe("temporaryFileSentinel", () => { it("Should create and pass the file properly to the function", async () => { @@ -205,3 +211,160 @@ describe("Unique random functions", () => { }); }); }); + +describe("readActionArguments", () => { + const { readActionArguments } = helpers; + + describe("testing config with account", () => { + beforeAll(() => { + // eslint-disable-next-line global-require + const accountConfig = require("./mocks/account-config.json"); + loadMethodFromConfiguration.mockImplementation((methodName) => ( + accountConfig.methods.find((m) => m.name === methodName) + )); + loadAccountFromConfiguration.mockImplementation(() => accountConfig.auth); + }); + + it("should parse parameters and accounts accordingly to the config", () => { + const account = { + email: "test@example.com", + password: "test123", + namespaceConfig: JSON.stringify({ name: "test-namespace" }), + objects: "object-1\nobject-2\nobject-3", + }; + const action = { + method: { name: "testMethodOne" }, + params: { + ...account, + testParameterOne: "test", + testParameterTwo: "1", + testParameterThree: { id: "autocomplete-item-1", value: "Autocomplete 1" }, + }, + }; + const settings = {}; + + const readArguments = readActionArguments(action, settings); + + expect(readArguments.testParameterOne).toStrictEqual("test"); + expect(readArguments.testParameterTwo).toEqual(1); + expect(readArguments.testParameterThree).toStrictEqual("autocomplete-item-1"); + expect(readArguments.email).toStrictEqual("test@example.com"); + expect(readArguments.password).toStrictEqual("test123"); + expect(readArguments.namespaceConfig).toStrictEqual({ name: "test-namespace" }); + expect(readArguments.objects).toStrictEqual(["object-1", "object-2", "object-3"]); + }); + + it("should parse parameters accordingly to the config and validate them", () => { + const EXAMPLE_SSH_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nasdasdasdasasd\n-----END OPENSSH PRIVATE KEY-----\n"; + const account = { + email: "test@example.com", + password: "test123", + }; + const action = { + method: { name: "testMethodTwo" }, + params: { + ...account, + testParameterFour: "test", + testParameterFive: EXAMPLE_SSH_KEY, + }, + }; + const settings = {}; + + const readArguments = readActionArguments(action, settings); + + expect(readArguments.testParameterFive).toStrictEqual(EXAMPLE_SSH_KEY); + }); + + it("should throw if no suitable parser is found", () => { + const EXAMPLE_INVALID_SSH_KEY = "INVALID SSH"; + const account = { + email: "test@example.com", + password: "test123", + }; + const action = { + method: { name: "testMethodTwo" }, + params: { + ...account, + testParameterFour: "test", + testParameterFive: EXAMPLE_INVALID_SSH_KEY, + }, + }; + const settings = {}; + + expect(() => { + readActionArguments(action, settings); + }).toThrowError("Missing line feed character at the end of the file."); + }); + }); + + describe("testing config with no account", () => { + beforeAll(() => { + loadMethodFromConfiguration.mockImplementation((methodName) => ( + // eslint-disable-next-line global-require + require("./mocks/no-account-config.json").methods.find((m) => m.name === methodName) + )); + loadAccountFromConfiguration.mockImplementation(() => null); + }); + + it("should fail if method definition is missing", () => { + const action = { + method: { name: "nonExistentMethod" }, + }; + const settings = {}; + + expect(() => { + readActionArguments(action, settings); + }).toThrowError("Could not find a method \"nonExistentMethod\" in config.json"); + }); + + it("should parse parameters accordingly to the config", () => { + const action = { + method: { name: "testMethodOne" }, + params: { + testParameterOne: "test", + testParameterTwo: "1", + testParameterThree: { id: "autocomplete-item-1", value: "Autocomplete 1" }, + }, + }; + const settings = {}; + + const readArguments = readActionArguments(action, settings); + + expect(readArguments.testParameterOne).toStrictEqual("test"); + expect(readArguments.testParameterTwo).toEqual(1); + expect(readArguments.testParameterThree).toStrictEqual("autocomplete-item-1"); + }); + + it("should use settings if parameters are missing", () => { + const action = { + method: { name: "testMethodTwo" }, + params: { + testParameterFour: null, + }, + }; + const settings = { + testParameterFour: "settings-test", + }; + + const readArguments = readActionArguments(action, settings); + + expect(readArguments.testParameterFour).toStrictEqual("settings-test"); + }); + + it("should use defaultValue if parameters and settings are missing", () => { + const action = { + method: { name: "testMethodThree" }, + params: { + testParameterFive: null, + }, + }; + const settings = { + testParameterFive: null, + }; + + const readArguments = readActionArguments(action, settings); + + expect(readArguments.testParameterFive).toStrictEqual("line-1\nline-2"); + }); + }); +}); diff --git a/mocks/account-config.json b/mocks/account-config.json new file mode 100644 index 0000000..df1f95a --- /dev/null +++ b/mocks/account-config.json @@ -0,0 +1,98 @@ +{ + "name": "test-plugin", + "viewName": "Test Plugin", + "type": "executer", + "main": "app.js", + "execProgram": "node", + "description": "Test Plugin for testing things", + "version": "0.1.0", + "imgUrl": "logo.png", + "auth": { + "authId": "testPluginAccount", + "params": [ + { + "name": "email", + "viewName": "Email", + "type": "string", + "required": true + }, + { + "name": "password", + "viewName": "Password", + "type": "vault", + "required": true + }, + { + "name": "namespaceConfig", + "viewName": "Namespace Config", + "type": "text", + "parserType": "object" + }, + { + "name": "objects", + "viewName": "Objects", + "type": "text", + "parserType": "array" + } + ] + }, + "settings": [ + { + "name": "testParameterFour", + "viewName": "Test Parameter Four", + "type": "string" + } + ], + "methods": [ + { + "name": "testMethodOne", + "viewName": "Test Method One", + "params": [ + { + "name": "testParameterOne", + "viewName": "Test Parameter One", + "type": "string", + "required": true + }, + { + "name": "testParameterTwo", + "viewName": "Test Parameter Two", + "type": "options", + "options": [ + { + "id": "1", + "name": "Test 1" + } + ], + "default": "1", + "parserType": "int" + }, + { + "name": "testParameterThree", + "viewName": "Test Parameter Three", + "type": "autocomplete", + "autocompleteType": "function", + "functionName": "testAutocomplete" + } + ] + }, + { + "name": "testMethodTwo", + "viewName": "Test Method Two", + "params": [ + { + "name": "testParameterFour", + "viewName": "Test Parameter Four", + "type": "string", + "required": true + }, + { + "name": "testParameterFive", + "viewName": "Test Parameter Five", + "type": "text", + "validationType": "ssh" + } + ] + } + ] +} \ No newline at end of file diff --git a/mocks/no-account-config.json b/mocks/no-account-config.json new file mode 100644 index 0000000..e4660d8 --- /dev/null +++ b/mocks/no-account-config.json @@ -0,0 +1,74 @@ +{ + "name": "test-plugin", + "viewName": "Test Plugin", + "type": "executer", + "main": "app.js", + "execProgram": "node", + "description": "Test Plugin for testing things", + "version": "0.1.0", + "imgUrl": "logo.png", + "settings": [ + { + "name": "testParameterFour", + "viewName": "Test Parameter Four", + "type": "string" + } + ], + "methods": [ + { + "name": "testMethodOne", + "viewName": "Test Method One", + "params": [ + { + "name": "testParameterOne", + "viewName": "Test Parameter One", + "type": "string", + "required": true + }, + { + "name": "testParameterTwo", + "viewName": "Test Parameter Two", + "type": "options", + "options": [ + { + "id": "1", + "name": "Test 1" + } + ], + "default": "1", + "parserType": "int" + }, + { + "name": "testParameterThree", + "viewName": "Test Parameter Three", + "type": "autocomplete", + "autocompleteType": "function", + "functionName": "testAutocomplete" + } + ] + }, + { + "name": "testMethodTwo", + "viewName": "Test Method Two", + "params": [ + { + "name": "testParameterFour", + "viewName": "Test Parameter Four", + "type": "string" + } + ] + }, + { + "name": "testMethodThree", + "viewName": "Test Method Three", + "params": [ + { + "name": "testParameterFive", + "viewName": "Test Parameter Five", + "type": "text", + "default": "line-1\nline-2" + } + ] + } + ] +} \ No newline at end of file From 4fb49be6e377c528c7913fbca42b84cea721c4e5 Mon Sep 17 00:00:00 2001 From: maciek <42122011+xkcm@users.noreply.github.com> Date: Fri, 15 Jul 2022 10:40:19 +0200 Subject: [PATCH 3/6] Add json parser (#18) --- parsers.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/parsers.js b/parsers.js index bd997d4..2c0527b 100644 --- a/parsers.js +++ b/parsers.js @@ -22,11 +22,31 @@ function resolveParser(type) { return array; case "sshKey": return string; + case "keyValuePairs": + return keyValuePairs; default: throw new Error(`Can't resolve parser of type "${type}"`); } } +function keyValuePairs(value) { + if (!_.isString(value) && !_.isPlainObject(value)) { + throw new Error(`Couldn't parse provided value as JSON: ${value}`); + } + + if (_.isPlainObject(value)) { + return value; + } + + const entries = value + .split("\n") + .map((line) => { + const [key, ...rest] = line.split("="); + return [key, rest.join("=")]; + }); + return Object.fromEntries(entries); +} + function object(value) { if (_.isObject(value)) { return value; } if (_.isString(value)) { @@ -102,4 +122,5 @@ module.exports = { object, array, text, + keyValuePairs, }; From faf351a19fd9190c28db37af7e3fd5e23dbaaa5a Mon Sep 17 00:00:00 2001 From: xkcm Date: Fri, 15 Jul 2022 10:43:47 +0200 Subject: [PATCH 4/6] Release v2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0faca35..3c4de5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaholo/plugin-library", - "version": "2.0.0", + "version": "2.1.0", "description": "Kaholo library for plugins", "main": "kaholo-plugin-library.js", "scripts": { From 9c72d023f64c72e9b44b4c5b7e1e7846795c5e6f Mon Sep 17 00:00:00 2001 From: graddaniel <48184737+graddaniel@users.noreply.github.com> Date: Wed, 10 Aug 2022 11:22:28 +0200 Subject: [PATCH 5/6] Nil param values are now allowed, but ignored. (#22) --- autocomplete.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/autocomplete.js b/autocomplete.js index bb7de5a..dd6a9b0 100644 --- a/autocomplete.js +++ b/autocomplete.js @@ -11,15 +11,17 @@ function mapAutocompleteFuncParamsToObject(params) { return params.reduce((acc, { value, name, type, valueType, }) => { - if (_.isNil(value)) { - throw new Error("Failed to map one of autocomplete parameters to object - `value` field is required"); - } if (_.isNil(name)) { throw new Error("Failed to map one of autocomplete parameters to object - `name` field is required"); } if (_.isNil(type || valueType)) { throw new Error("Failed to map one of autocomplete parameters to object - either `type` or `valueType` field is required"); } + + if (_.isNil(value)) { + return acc; + } + return { ...acc, [name]: parsers.resolveParser(type || valueType)(value), From c13668f804d0796ba740b09fd4d445c8f87c7125 Mon Sep 17 00:00:00 2001 From: Daniel Grad Date: Wed, 10 Aug 2022 11:32:46 +0200 Subject: [PATCH 6/6] Release v2.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c4de5b..951b779 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaholo/plugin-library", - "version": "2.1.0", + "version": "2.1.1", "description": "Kaholo library for plugins", "main": "kaholo-plugin-library.js", "scripts": {