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/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/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/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/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"}}
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"
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);
+ });
});