From 52b7fa474d353821d000e516aa2ef1fac8797254 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 9 Feb 2021 11:16:03 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20dark=20mode=20?= =?UTF-8?q?toggle=20to=20desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/desktop/app/routes/application.js | 26 ++++++++++++ ui/desktop/app/templates/application.hbs | 23 +++++++++++ .../tests/acceptance/authentication-test.js | 41 +++++++++++++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 009d73daf..45c73b24a 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -98,4 +98,30 @@ export default class ApplicationRoute extends Route.extend( } return true; } + + /** + * Applies the specified color theme to the root ember element. + * @param {string} theme - "light", "dark", or nullish (system default) + */ + @action + toggleTheme(theme) { + const rootElementSelector = getOwner(this).rootElement; + const rootEl = getOwner(this) + .lookup('service:-document') + .querySelector(rootElementSelector); + this.session.set('data.theme', theme); + switch (theme) { + case 'light': + rootEl.classList.add('rose-theme-light'); + rootEl.classList.remove('rose-theme-dark'); + break; + case 'dark': + rootEl.classList.add('rose-theme-dark'); + rootEl.classList.remove('rose-theme-light'); + break; + default: + rootEl.classList.remove('rose-theme-dark'); + rootEl.classList.remove('rose-theme-light'); + } + } } diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index 9c504be7b..7101dd522 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -22,6 +22,29 @@ > {{t 'actions.deauthenticate'}} + + + + + + + + + + + + + + + + + + + {{/if}} diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index 703ca0ce9..d0f0e3fa6 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -3,19 +3,20 @@ import { visit, currentURL, //fillIn, - //click, + click, find, //findAll, - //getRootElement + getRootElement, //setupOnerror, } from '@ember/test-helpers'; +import { run, later } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; //import { Response } from 'miragejs'; //import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { currentSession, - //authenticateSession, + authenticateSession, invalidateSession, } from 'ember-simple-auth/test-support'; @@ -159,4 +160,38 @@ module('Acceptance | authentication', function (hooks) { assert.equal(currentURL(), urls.authenticate.global); assert.ok(find('.rose-message')); }); + + test('color theme is applied from session data', async function (assert) { + assert.expect(12); + authenticateSession({ + scope: { + id: instances.scopes.global.id, + type: instances.scopes.global.type + }, + }); + later(async () => { + run.cancelTimers(); + // system default + assert.notOk(currentSession().get('data.theme')); + assert.notOk(getRootElement().classList.contains('rose-theme-light')); + assert.notOk(getRootElement().classList.contains('rose-theme-dark')); + // toggle light mode + await click('[name="theme"][value="light"]'); + assert.equal(currentSession().get('data.theme'), 'light'); + assert.ok(getRootElement().classList.contains('rose-theme-light')); + assert.notOk(getRootElement().classList.contains('rose-theme-dark')); + // toggle dark mode + await click('[name="theme"][value="dark"]'); + assert.equal(currentSession().get('data.theme'), 'dark'); + assert.notOk(getRootElement().classList.contains('rose-theme-light')); + assert.ok(getRootElement().classList.contains('rose-theme-dark')); + // toggle system default + await click('[name="theme"][value=""]'); + assert.notOk(currentSession().get('data.theme')); + assert.notOk(getRootElement().classList.contains('rose-theme-light')); + assert.notOk(getRootElement().classList.contains('rose-theme-dark')); + }, 750); + + await visit(urls.scopes.org); + }); }); From e5d991a17bad34281837781e9ccc9b3552f62e88 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 9 Feb 2021 18:52:25 -0500 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=F0=9F=A4=96=20sanitize=20and=20va?= =?UTF-8?q?lidate=20input=20to=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routes/scopes/scope/projects/targets.js | 16 +++-- .../scope/projects/targets/target/hosts.js | 63 +------------------ .../scopes/scope/projects/targets/index.hbs | 2 +- ui/desktop/electron-app/package.json | 3 +- ui/desktop/electron-app/src/boundary-cli.js | 28 +++++++-- ui/desktop/electron-app/src/handlers.js | 4 +- ui/desktop/electron-app/yarn.lock | 5 ++ 7 files changed, 48 insertions(+), 73 deletions(-) diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 22e8c1d16..f02dd4f0f 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -101,21 +101,25 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { /** * Establish a session to current target. * @param {TargetModel} model + * @param {HostModel} host */ @action - async connect(model) { + async connect(model, host) { // TODO: Connect: Refactor into an addon try { // Check for CLI const cliExists = await this.ipc.invoke('cliExists'); if (!cliExists) throw new Error('Cannot find Boundary CLI.'); + const options = { + target_id: model.id, + token: this.session.data.authenticated.token + }; + + if (host) options.host_id = host.id; + // Create target session - const connectionDetails = await this.ipc.invoke('connect', { - target_id: model.target.id, - token: this.session.data.authenticated.token, - addr: this.origin.rendererOrigin - }); + const connectionDetails = await this.ipc.invoke('connect', options); // Associate the connection details with the session const { session_id, address, port } = connectionDetails; diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js index 90365f8cd..0362726f5 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js @@ -1,7 +1,5 @@ import Route from '@ember/routing/route'; import { all, hash } from 'rsvp'; -import { later } from '@ember/runloop'; -import { action } from '@ember/object'; import { inject as service } from '@ember/service'; export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route { @@ -23,7 +21,7 @@ export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route { const { target } = this.modelFor('scopes.scope.projects.targets.target'); // Load all host-sets const hostSets = await all( - target.host_sets.map(({ host_set_id }) => + target.host_sets.map(({ host_set_id }) => this.store.findRecord('host-set', host_set_id) ) ); @@ -32,7 +30,7 @@ export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route { const hostIds = hostSets.map(({host_ids}) => host_ids.content.map((hostId) => hostId.value - ) + ) ) // Load unique hosts @@ -41,65 +39,10 @@ export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route { return hash({ target, hosts: all( - [...uniqueHostIds].map((hostId) => + [...uniqueHostIds].map((hostId) => this.store.findRecord('host', hostId) ) ) }) } - - /** - * Establish a session to host within target. - * @param {TargetModel} target - * @param {HostModel} host - */ - @action - async connect(target, host) { - // TODO: Connect: Refactor into an addon - try { - // Check for CLI - const cliExists = await this.ipc.invoke('cliExists'); - if (!cliExists) throw new Error('Cannot find Boundary CLI.'); - - // Create target session - const connectionDetails = await this.ipc.invoke('connect', { - host_id: host.id, - target_id: target.id, - token: this.session.data.authenticated.token, - addr: this.origin.rendererOrigin - }); - - // Associate the connection details with the session - const { session_id, address, port } = connectionDetails; - await this.store.findRecord('session', session_id); - - // Update the session record with proxy information from the CLI - // This is read-only information that shouldn't dirty the session, - // so we use store.push here. In the future, it may make sense to push - // this off to the API so that we don't have to manually persist the - // proxy details. - later(() => { - this.store.push({ - data: { - id: session_id, - type: 'session', - attributes: { - proxy_address: address, - proxy_port: port - } - } - }); - }, 150); - - // Show the user a modal with basic connection info. - // We don't await because this modal is purely informational. - this.confirm.confirm(connectionDetails, { isConnectSuccess: true }); - - } catch(e) { - this.confirm.confirm(e.message, { isConnectError: true }) - // Retry - .then(() => this.connect(target, host)) - .catch(() => null /* no op */); - } - } } diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index 2651665a7..b816ced05 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -66,7 +66,7 @@ - + {{t "resources.session.actions.connect"}} diff --git a/ui/desktop/electron-app/package.json b/ui/desktop/electron-app/package.json index c0e3a57e4..1b57071e1 100644 --- a/ui/desktop/electron-app/package.json +++ b/ui/desktop/electron-app/package.json @@ -25,7 +25,8 @@ "electron-devtools-installer": "^3.1.1", "electron-is-dev": "^1.2.0", "electron-squirrel-startup": "^1.0.0", - "lookpath": "^1.1.0" + "lookpath": "^1.1.0", + "shell-quote": "^1.7.2" }, "devDependencies": { "@electron-forge/cli": "6.0.0-beta.53", diff --git a/ui/desktop/electron-app/src/boundary-cli.js b/ui/desktop/electron-app/src/boundary-cli.js index ff82009af..241f288fc 100644 --- a/ui/desktop/electron-app/src/boundary-cli.js +++ b/ui/desktop/electron-app/src/boundary-cli.js @@ -1,23 +1,43 @@ const path = require('path'); const { spawnAsyncJSONPromise, spawnSync } = require('./spawn-promise'); +const origin = require('./origin.js'); const cliPath = async () => path.resolve(__dirname, '..', 'cli', 'boundary'); +/** + * Super paranoid shell quote/escape and validation. Input must be base62. + * @param {string} str + */ +const escapeAndValidateBase62 = (str) => { + const candidate = str.toString(); + if (candidate.match(/^[A-Za-z0-9_]*$/)) return candidate; + throw new Error(` + Could not invoke command: + input contained unsafe characters. + `); +}; + module.exports = { // Check boundary cli existence exists: () => Boolean(cliPath()), // Initiate connection and return output - connect: (target_id, token, addr, host_id) => { + connect: (target_id, token, host_id) => { + const addr = origin.origin; + const sanitized = { + target_id: escapeAndValidateBase62(target_id), + token: escapeAndValidateBase62(token) + }; + if (host_id) sanitized.host_id = escapeAndValidateBase62(host_id); const command = [ 'connect', - `-target-id=${target_id}`, - `-token=${token}`, + `-target-id=${sanitized.target_id}`, + `-token=${sanitized.token}`, `-addr=${addr}`, '-format=json', '--output-json-errors' ]; - if(host_id) command.push(`-host-id=${host_id}`) + if (host_id) command.push(`-host-id=${sanitized.host_id}`) return spawnAsyncJSONPromise(command); }, // Returns JSON-formatted version information from the CLI diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js index 85ecdb65f..f4b9a62b4 100644 --- a/ui/desktop/electron-app/src/handlers.js +++ b/ui/desktop/electron-app/src/handlers.js @@ -25,4 +25,6 @@ handle('cliExists', () => boundaryCli.exists()); /** * Establishes a boundary session and returns session details. */ -handle('connect', ({ target_id, token, addr, host_id }) => boundaryCli.connect(target_id, token, addr, host_id)); +handle('connect', ({ target_id, token, host_id }) => + boundaryCli.connect(target_id, token, host_id) +); diff --git a/ui/desktop/electron-app/yarn.lock b/ui/desktop/electron-app/yarn.lock index 9a631aa58..a0631b91d 100644 --- a/ui/desktop/electron-app/yarn.lock +++ b/ui/desktop/electron-app/yarn.lock @@ -3108,6 +3108,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-quote@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" From 1fd146939f5b9a37b021093c6de25cce8a68ae51 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Tue, 9 Feb 2021 23:01:06 -0500 Subject: [PATCH 3/3] fix: Connecting from a target view raises toString error --- .../templates/scopes/scope/projects/targets/target/-actions.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs index b82595c94..e59af9f97 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs @@ -1,3 +1,3 @@ - + {{t "resources.session.actions.connect"}}