diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index b50c8de6a..2f1d05f63 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -47,6 +47,10 @@ const EnvironmentVariables = ({ environment, collection }) => { variable.enabled = e.target.checked; break; } + case 'secret': { + variable.secret = e.target.checked; + break; + } } reducerDispatch({ type: 'UPDATE_VAR', @@ -66,8 +70,10 @@ const EnvironmentVariables = ({ environment, collection }) => { + + @@ -76,6 +82,14 @@ const EnvironmentVariables = ({ environment, collection }) => { ? variables.map((variable, index) => { return ( + + ); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js index c72bf7b24..a5aa3e0c1 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js @@ -12,6 +12,7 @@ const reducer = (state, action) => { name: '', value: '', type: 'text', + secret: false, enabled: true }); draft.hasChanges = true; @@ -24,6 +25,7 @@ const reducer = (state, action) => { variable.name = action.variable.name; variable.value = action.variable.value; variable.enabled = action.variable.enabled; + variable.secret = action.variable.secret; draft.hasChanges = true; }); } diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index b3c7780ad..64fc0da91 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -54,11 +54,22 @@ const deleteUidsInEnvs = (envs) => { }); }; +const deleteSecretsInEnvs = (envs) => { + each(envs, (env) => { + each(env.variables, (variable) => { + if (variable.secret) { + variable.value = ''; + } + }); + }); +}; + const exportCollection = (collection) => { // delete uids delete collection.uid; deleteUidsInItems(collection.items); deleteUidsInEnvs(collection.environments); + deleteSecretsInEnvs(collection.environments); transformItem(collection.items); const fileName = `${collection.name}.json`; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index bb5314957..1334f3f5d 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -10,7 +10,8 @@ "clean": "rimraf dist", "dev": "electron .", "dist": "electron-builder --mac --config electron-builder-config.js", - "pack": "electron-builder --dir" + "pack": "electron-builder --dir", + "test": "jest" }, "dependencies": { "@usebruno/js": "0.4.0", @@ -32,6 +33,7 @@ "lodash": "^4.17.21", "mustache": "^4.2.0", "nanoid": "3.3.4", + "node-machine-id": "^1.1.12", "qs": "^6.11.0", "uuid": "^9.0.0", "vm2": "^3.9.13", diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 5e6b27206..3fd2b08a2 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -10,7 +10,11 @@ const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBru const { itemSchema } = require('@usebruno/schema'); const { uuid } = require('../utils/common'); const { getRequestUid } = require('../cache/requestUids'); +const { decryptString } = require('../utils/encryption'); const { setDotEnvVars } = require('../store/process-env'); +const EnvironmentSecretsStore = require('../store/env-secrets'); + +const environmentSecretsStore = new EnvironmentSecretsStore(); const isJsonEnvironmentConfig = (pathname, collectionPath) => { const dirname = path.dirname(pathname); @@ -56,7 +60,13 @@ const hydrateRequestWithUuid = (request, pathname) => { return request; }; -const addEnvironmentFile = async (win, pathname, collectionUid) => { +const envHasSecrets = (environment = {}) => { + const secrets = _.filter(environment.variables, (v) => v.secret); + + return secrets && secrets.length > 0; +}; + +const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => { try { const basename = path.basename(pathname); const file = { @@ -79,13 +89,25 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => { file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); + + // hydrate environment variables with secrets + if (envHasSecrets(file.data)) { + const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data); + _.each(envSecrets, (secret) => { + const variable = _.find(file.data.variables, (v) => v.name === secret.name); + if (variable) { + variable.value = decryptString(secret.value); + } + }); + } + win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file); } catch (err) { console.error(err); } }; -const changeEnvironmentFile = async (win, pathname, collectionUid) => { +const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => { try { const basename = path.basename(pathname); const file = { @@ -102,6 +124,17 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => { file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); + // hydrate environment variables with secrets + if (envHasSecrets(file.data)) { + const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data); + _.each(envSecrets, (secret) => { + const variable = _.find(file.data.variables, (v) => v.name === secret.name); + if (variable) { + variable.value = decryptString(secret.value); + } + }); + } + // we are reusing the addEnvironmentFile event itself // this is because the uid of the pathname remains the same // and the collection tree will be able to update the existing environment @@ -180,7 +213,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => { } if (isBruEnvironmentConfig(pathname, collectionPath)) { - return addEnvironmentFile(win, pathname, collectionUid); + return addEnvironmentFile(win, pathname, collectionUid, collectionPath); } // migrate old json files to bru @@ -268,7 +301,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { } if (isBruEnvironmentConfig(pathname, collectionPath)) { - return changeEnvironmentFile(win, pathname, collectionUid); + return changeEnvironmentFile(win, pathname, collectionUid, collectionPath); } if (hasBruExtension(pathname)) { diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 6b1d73cdf..2a0d21da5 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -15,7 +15,7 @@ const bruToEnvJson = (bru) => { return json; } catch (error) { - return Promise.reject(e); + return Promise.reject(error); } }; @@ -24,7 +24,7 @@ const envJsonToBru = (json) => { const bru = envJsonToBruV2(json); return bru; } catch (error) { - return Promise.reject(e); + return Promise.reject(error); } }; @@ -32,7 +32,7 @@ const envJsonToBru = (json) => { * The transformer function for converting a BRU file to JSON. * * We map the json response from the bru lang and transform it into the DSL - * format that the app users + * format that the app uses * * @param {string} bru The BRU file content. * @returns {object} The JSON representation of the BRU file. diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 42eef5a21..2a62dd969 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -18,7 +18,7 @@ setContentSecurityPolicy(` connect-src * 'unsafe-inline'; base-uri 'none'; form-action 'none'; - img-src 'self' data:image/svg+xml + img-src 'self' data:image/svg+xml; `); const menu = Menu.buildFromTemplate(menuTemplate); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index cf931a072..7431d4fed 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -18,6 +18,15 @@ const { openCollectionDialog, openCollection } = require('../app/collections'); const { generateUidBasedOnHash } = require('../utils/common'); const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids'); const { setPreferences } = require('../store/preferences'); +const EnvironmentSecretsStore = require('../store/env-secrets'); + +const environmentSecretsStore = new EnvironmentSecretsStore(); + +const envHasSecrets = (environment = {}) => { + const secrets = _.filter(environment.variables, (v) => v.secret); + + return secrets && secrets.length > 0; +}; const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { // browse directory @@ -153,6 +162,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`environment: ${envFilePath} does not exist`); } + if (envHasSecrets(environment)) { + environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); + } + const content = envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { @@ -175,6 +188,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } fs.renameSync(envFilePath, newEnvFilePath); + + environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName); } catch (error) { return Promise.reject(error); } @@ -190,6 +205,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } fs.unlinkSync(envFilePath); + + environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName); } catch (error) { return Promise.reject(error); } diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js new file mode 100644 index 000000000..b3d26c723 --- /dev/null +++ b/packages/bruno-electron/src/store/env-secrets.js @@ -0,0 +1,126 @@ +const _ = require('lodash'); +const Store = require('electron-store'); +const { encryptString } = require('../utils/encryption'); + +/** + * Sample secrets store file + * + * { + * "collections": [{ + * "path": "/Users/anoop/Code/acme-acpi-collection", + * "environments" : [{ + * "name": "Local", + * "secrets": [{ + * "name": "token", + * "value": "abracadabra" + * }] + * }] + * }] + * } + */ + +class EnvironmentSecretsStore { + constructor() { + this.store = new Store({ + name: 'secrets', + clearInvalidConfig: true + }); + } + + isValidValue(val) { + return val && typeof val === 'string' && val.length > 0; + } + + storeEnvSecrets(collectionPathname, environment) { + const envVars = []; + _.each(environment.variables, (v) => { + if (v.secret) { + envVars.push({ + name: v.name, + value: this.isValidValue(v.value) ? encryptString(v.value) : '' + }); + } + }); + + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + + // if collection doesn't exist, create it, add the environment and save + if (!collection) { + collections.push({ + path: collectionPathname, + environments: [ + { + name: environment.name, + secrets: envVars + } + ] + }); + + this.store.set('collections', collections); + return; + } + + // if collection exists, check if environment exists + // if environment doesn't exist, add the environment and save + collection.environments = collection.environments || []; + const env = _.find(collection.environments, (e) => e.name === environment.name); + if (!env) { + collection.environments.push({ + name: environment.name, + secrets: envVars + }); + + this.store.set('collections', collections); + return; + } + + // if environment exists, update the secrets and save + env.secrets = envVars; + this.store.set('collections', collections); + } + + getEnvSecrets(collectionPathname, environment) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return []; + } + + const env = _.find(collection.environments, (e) => e.name === environment.name); + if (!env) { + return []; + } + + return env.secrets || []; + } + + renameEnvironment(collectionPathname, oldName, newName) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return; + } + + const env = _.find(collection.environments, (e) => e.name === oldName); + if (!env) { + return; + } + + env.name = newName; + this.store.set('collections', collections); + } + + deleteEnvironment(collectionPathname, environmentName) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return; + } + + _.remove(collection.environments, (e) => e.name === environmentName); + this.store.set('collections', collections); + } +} + +module.exports = EnvironmentSecretsStore; diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js new file mode 100644 index 000000000..980311ff9 --- /dev/null +++ b/packages/bruno-electron/src/utils/encryption.js @@ -0,0 +1,100 @@ +const crypto = require('crypto'); +const { machineIdSync } = require('node-machine-id'); +const { safeStorage } = require('electron'); + +// Constants for algorithm identification +const ELECTRONSAFESTORAGE_ALGO = '00'; +const AES256_ALGO = '01'; + +// AES-256 encryption and decryption functions +function aes256Encrypt(data) { + const key = machineIdSync(); + const cipher = crypto.createCipher('aes-256-cbc', key); + let encrypted = cipher.update(data, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + return encrypted; +} + +function aes256Decrypt(data) { + const key = machineIdSync(); + const decipher = crypto.createDecipher('aes-256-cbc', key); + let decrypted = decipher.update(data, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} + +// electron safe storage encryption and decryption functions +function safeStorageEncrypt(str) { + let encryptedStringBuffer = safeStorage.encryptString(str); + + // Convert the encrypted buffer to a hexadecimal string + const encryptedString = encryptedStringBuffer.toString('hex'); + + return encryptedString; +} +function safeStorageDecrypt(str) { + // Convert the hexadecimal string to a buffer + const encryptedStringBuffer = Buffer.from(str, 'hex'); + + // Decrypt the buffer + const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer); + + // Convert the decrypted buffer to a string + const decryptedString = decryptedStringBuffer.toString(); + + return decryptedString; +} + +function encryptString(str) { + if (!str || typeof str !== 'string' || str.length === 0) { + throw new Error('Encrypt failed: invalid string'); + } + + let encryptedString = ''; + + if (safeStorage && safeStorage.isEncryptionAvailable()) { + encryptedString = safeStorageEncrypt(str); + return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`; + } + + // fallback to aes256 + encryptedString = aes256Encrypt(str); + + return `$${AES256_ALGO}:${encryptedString}`; +} + +function decryptString(str) { + if (!str) { + throw new Error('Decrypt failed: unrecognized string format'); + } + + // Find the index of the first colon + const colonIndex = str.indexOf(':'); + + if (colonIndex === -1) { + throw new Error('Decrypt failed: unrecognized string format'); + } + + // Extract algo and encryptedString based on the colon index + const algo = str.substring(1, colonIndex); + const encryptedString = str.substring(colonIndex + 1); + + if ([ELECTRONSAFESTORAGE_ALGO, AES256_ALGO].indexOf(algo) === -1) { + throw new Error('Decrypt failed: Invalid algo'); + } + + if (algo === ELECTRONSAFESTORAGE_ALGO) { + return safeStorageDecrypt(encryptedString); + } + + if (algo === AES256_ALGO) { + return aes256Decrypt(encryptedString); + } +} + +module.exports = { + encryptString, + decryptString +}; diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js new file mode 100644 index 000000000..b00772284 --- /dev/null +++ b/packages/bruno-electron/tests/utils/encryption.spec.js @@ -0,0 +1,31 @@ +const { encryptString, decryptString } = require('../../src/utils/encryption'); + +// We can only unit test aes 256 fallback as safeStorage is only available +// in the main process + +describe('Encryption and Decryption Tests', () => { + it('should encrypt and decrypt using AES-256', () => { + const plaintext = 'bruno is awesome'; + const encrypted = encryptString(plaintext); + const decrypted = decryptString(encrypted); + + expect(decrypted).toBe(plaintext); + }); + + it('encrypt should throw an error for invalid string', () => { + expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string'); + expect(() => encryptString('')).toThrow('Encrypt failed: invalid string'); + }); + + it('decrypt should throw an error for invalid string', () => { + expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format'); + expect(() => decryptString('')).toThrow('Decrypt failed: unrecognized string format'); + expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format'); + }); + + it('decrypt should throw an error for invalid algorithm', () => { + const invalidAlgo = '$99:abcdefg'; + + expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo'); + }); +}); diff --git a/packages/bruno-lang/v2/src/envToJson.js b/packages/bruno-lang/v2/src/envToJson.js index 63bd7f2bf..eef4de375 100644 --- a/packages/bruno-lang/v2/src/envToJson.js +++ b/packages/bruno-lang/v2/src/envToJson.js @@ -2,7 +2,7 @@ const ohm = require('ohm-js'); const _ = require('lodash'); const grammar = ohm.grammar(`Bru { - BruEnvFile = (vars)* + BruEnvFile = (vars | secretvars)* nl = "\\r"? "\\n" st = " " | "\\t" @@ -19,6 +19,13 @@ const grammar = ohm.grammar(`Bru { key = keychar* value = valuechar* + // Array Blocks + array = st* "[" stnl* valuelist stnl* "]" + valuelist = stnl* arrayvalue stnl* ("," stnl* arrayvalue)* + arrayvalue = arrayvaluechar* + arrayvaluechar = ~(nl | st | "[" | "]" | ",") any + + secretvars = "vars:secret" array vars = "vars" dictionary }`); @@ -44,6 +51,29 @@ const mapPairListToKeyValPairs = (pairList = []) => { }); }; +const mapArrayListToKeyValPairs = (arrayList = []) => { + arrayList = arrayList.filter((v) => v && v.length); + + if (!arrayList.length) { + return []; + } + + return _.map(arrayList, (value) => { + let name = value; + let enabled = true; + if (name && name.length && name.charAt(0) === '~') { + name = name.slice(1); + enabled = false; + } + + return { + name, + value: null, + enabled + }; + }); +}; + const concatArrays = (objValue, srcValue) => { if (_.isArray(objValue) && _.isArray(srcValue)) { return objValue.concat(srcValue); @@ -66,6 +96,15 @@ const sem = grammar.createSemantics().addAttribute('ast', { {} ); }, + array(_1, _2, _3, valuelist, _4, _5) { + return valuelist.ast; + }, + arrayvalue(chars) { + return chars.sourceString ? chars.sourceString.trim() : ''; + }, + valuelist(_1, value, _2, _3, _4, rest) { + return [value.ast, ...rest.ast]; + }, dictionary(_1, _2, pairlist, _3) { return pairlist.ast; }, @@ -97,6 +136,18 @@ const sem = grammar.createSemantics().addAttribute('ast', { }, vars(_1, dictionary) { const vars = mapPairListToKeyValPairs(dictionary.ast); + _.each(vars, (v) => { + v.secret = false; + }); + return { + variables: vars + }; + }, + secretvars: (_1, array) => { + const vars = mapArrayListToKeyValPairs(array.ast); + _.each(vars, (v) => { + v.secret = true; + }); return { variables: vars }; diff --git a/packages/bruno-lang/v2/src/jsonToEnv.js b/packages/bruno-lang/v2/src/jsonToEnv.js index 46106f336..42d0a4281 100644 --- a/packages/bruno-lang/v2/src/jsonToEnv.js +++ b/packages/bruno-lang/v2/src/jsonToEnv.js @@ -2,22 +2,42 @@ const _ = require('lodash'); const envToJson = (json) => { const variables = _.get(json, 'variables', []); - const vars = variables.map((variable) => { - const { name, value, enabled } = variable; - const prefix = enabled ? '' : '~'; - return ` ${prefix}${name}: ${value}`; - }); + const vars = variables + .filter((variable) => !variable.secret) + .map((variable) => { + const { name, value, enabled } = variable; + const prefix = enabled ? '' : '~'; + return ` ${prefix}${name}: ${value}`; + }); - if (!vars || !vars.length) { + const secretVars = variables + .filter((variable) => variable.secret) + .map((variable) => { + const { name, enabled } = variable; + const prefix = enabled ? '' : '~'; + return ` ${prefix}${name}`; + }); + + if (!variables || !variables.length) { return `vars { } `; } - const output = `vars { + let output = ''; + if (vars.length) { + output += `vars { ${vars.join('\n')} } `; + } + + if (secretVars.length) { + output += `vars:secret [ +${secretVars.join(',\n')} +] +`; + } return output; }; diff --git a/packages/bruno-lang/v2/tests/envToJson.spec.js b/packages/bruno-lang/v2/tests/envToJson.spec.js index a082e2cf4..fbb74f2b9 100644 --- a/packages/bruno-lang/v2/tests/envToJson.spec.js +++ b/packages/bruno-lang/v2/tests/envToJson.spec.js @@ -26,7 +26,8 @@ vars { { name: 'url', value: 'http://localhost:3000', - enabled: true + enabled: true, + secret: false } ] }; @@ -48,17 +49,20 @@ vars { { name: 'url', value: 'http://localhost:3000', - enabled: true + enabled: true, + secret: false }, { name: 'port', value: '3000', - enabled: true + enabled: true, + secret: false }, { name: 'token', value: 'secret', - enabled: false + enabled: false, + secret: false } ] }; @@ -82,12 +86,14 @@ vars { { name: 'url', value: 'http://localhost:3000', - enabled: true + enabled: true, + secret: false }, { name: 'port', value: '3000', - enabled: true + enabled: true, + secret: false } ] }; @@ -110,17 +116,197 @@ vars { { name: 'url', value: '', - enabled: true + enabled: true, + secret: false }, { name: 'phone', value: '', - enabled: true + enabled: true, + secret: false }, { name: 'api-key', value: '', - enabled: true + enabled: true, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse empty secret vars', () => { + const input = ` +vars { + url: http://localhost:3000 +} + +vars:secret [ + +] +`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true, + secret: false + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse secret vars', () => { + const input = ` +vars { + url: http://localhost:3000 +} + +vars:secret [ + token +] +`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true, + secret: false + }, + { + name: 'token', + value: null, + enabled: true, + secret: true + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse multiline secret vars', () => { + const input = ` +vars { + url: http://localhost:3000 +} + +vars:secret [ + access_token, + access_secret, + + ~access_password +] +`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true, + secret: false + }, + { + name: 'access_token', + value: null, + enabled: true, + secret: true + }, + { + name: 'access_secret', + value: null, + enabled: true, + secret: true + }, + { + name: 'access_password', + value: null, + enabled: false, + secret: true + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse inline secret vars', () => { + const input = ` +vars { + url: http://localhost:3000 +} + +vars:secret [access_key] +`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true, + secret: false + }, + { + name: 'access_key', + value: null, + enabled: true, + secret: true + } + ] + }; + + expect(output).toEqual(expected); + }); + + it('should parse inline multiple secret vars', () => { + const input = ` +vars { + url: http://localhost:3000 +} + +vars:secret [access_key,access_secret, access_password ] +`; + + const output = parser(input); + const expected = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true, + secret: false + }, + { + name: 'access_key', + value: null, + enabled: true, + secret: true + }, + { + name: 'access_secret', + value: null, + enabled: true, + secret: true + }, + { + name: 'access_password', + value: null, + enabled: true, + secret: true } ] }; diff --git a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js index 7aee11428..62b7aa269 100644 --- a/packages/bruno-lang/v2/tests/jsonToEnv.spec.js +++ b/packages/bruno-lang/v2/tests/jsonToEnv.spec.js @@ -57,4 +57,87 @@ describe('env parser', () => { const output = parser(input); expect(output).toEqual(expected); }); + + it('should parse secret vars', () => { + const input = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true + }, + { + name: 'token', + value: 'abracadabra', + enabled: true, + secret: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + url: http://localhost:3000 +} +vars:secret [ + token +] +`; + expect(output).toEqual(expected); + }); + + it('should parse multiple secret vars', () => { + const input = { + variables: [ + { + name: 'url', + value: 'http://localhost:3000', + enabled: true + }, + { + name: 'access_token', + value: 'abracadabra', + enabled: true, + secret: true + }, + { + name: 'access_secret', + value: 'abracadabra', + enabled: false, + secret: true + } + ] + }; + + const output = parser(input); + const expected = `vars { + url: http://localhost:3000 +} +vars:secret [ + access_token, + ~access_secret +] +`; + expect(output).toEqual(expected); + }); + + it('should parse even if the only secret vars are present', () => { + const input = { + variables: [ + { + name: 'token', + value: 'abracadabra', + enabled: true, + secret: true + } + ] + }; + + const output = parser(input); + const expected = `vars:secret [ + token +] +`; + expect(output).toEqual(expected); + }); }); diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index fb5195488..ba2256a53 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -6,7 +6,8 @@ const environmentVariablesSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), type: Yup.string().oneOf(['text']).required('type is required'), - enabled: Yup.boolean().defined() + enabled: Yup.boolean().defined(), + secret: Yup.boolean() }) .noUnknown(true) .strict();
Enabled Name ValueSecret
+ handleVarChange(e, variable, 'enabled')} + /> + { /> -
- handleVarChange(e, variable, 'enabled')} - /> - -
+ handleVarChange(e, variable, 'secret')} + /> +
+