diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index c1d3bd392..8289bf627 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -5,6 +5,14 @@ auth: no-scopes: No scopes were found. To authenticate, ask your administrator to create an scope and grant you access. choose-org: Choose a scope to authenticate. authenticating-with-org: "You are authenticating with {name}." +origin: + titles: + welcome: Welcome to Boundary + descriptions: + welcome: + get-started: To get started, you'll need to + step-1: Enter your controller URL + step-2: Use your credentials to authenticate in titles: home: Home iam: IAM diff --git a/ui/desktop/app/initializers/ipc.js b/ui/desktop/app/initializers/ipc.js index 1ac9a8287..74e9d8545 100644 --- a/ui/desktop/app/initializers/ipc.js +++ b/ui/desktop/app/initializers/ipc.js @@ -1,5 +1,4 @@ import Ember from 'ember'; -import { assert } from '@ember/debug'; import config from '../config/environment'; /** @@ -25,6 +24,8 @@ class MockIPC { this.origin = origin; return this.origin; } + + connect() {} } /** @@ -40,9 +41,10 @@ export function initialize() { window.addEventListener('message', async function (event) { if (event.origin !== window.location.origin) return; const { method, payload } = event?.data ?? {}; - assert(`IPC requires a method, you passed '${method}'.`, method); - const response = await mockIPC.invoke(method, payload); - event.ports[0].postMessage(response); + if (method) { + const response = await mockIPC.invoke(method, payload); + event.ports[0].postMessage(response); + } }); } } diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 0dc41c9bd..009d73daf 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -15,7 +15,7 @@ export default class ApplicationRoute extends Route.extend( // =services @service session; - @service ipc; + @service origin; // =attributes @@ -24,26 +24,13 @@ export default class ApplicationRoute extends Route.extend( */ routeIfUnauthenticated = 'index'; - /** - * @type {ApplicationAdapter} - */ - get adapter() { - return this.store.adapterFor('application'); - } - /** * Check that the origin specified in the renderer matches the origin * reported by the main process. If they differ, update the main process * origin so that the renderer's CSP can be rewritten to allow requests. */ - async beforeModel() { - const rendererOrigin = this.session.data.origin; - const mainOrigin = await this.ipc.invoke('getOrigin'); - const isMatch = rendererOrigin === mainOrigin; - this.adapter.host = rendererOrigin; - if (rendererOrigin && !isMatch) { - await this.ipc.invoke('setOrigin', rendererOrigin); - } + beforeModel() { + return this.origin.updateOrigin(); } /** diff --git a/ui/desktop/app/routes/origin.js b/ui/desktop/app/routes/origin.js index e46824ef8..a4cd6a621 100644 --- a/ui/desktop/app/routes/origin.js +++ b/ui/desktop/app/routes/origin.js @@ -5,8 +5,7 @@ import { action } from '@ember/object'; export default class OriginRoute extends Route { // =services - @service session; - @service ipc; + @service origin; @service intl; @service notify; @@ -22,23 +21,17 @@ export default class OriginRoute extends Route { // =methods /** - * Points the API to the specified origin and persists it to the session, - * as well as the main process. When the main process receives the origin, - * it is expected that the renderer will be restarted. + * Points the API to the specified origin. When the main process receives + * the origin, it is expected that the renderer will be restarted. * @param {string} origin */ @action async setOrigin(origin) { - this.adapter.host = origin; - // Check if we have a Boundary endpoint... try { - // If scopes load, this could be a Boundary API - await this.store.query('scope', {}); - this.session.set('data.origin', origin); - await this.ipc.invoke('setOrigin', origin); + await this.origin.setOrigin(origin); this.replaceWith('index'); } catch (e) { - // If scopes don't load, we assume this is not a Boundary API + // If scopes do not load, we assume this is not a Boundary API const errorMessage = this.intl.t( 'errors.origin-verification-failed.description' ); diff --git a/ui/desktop/app/routes/scopes/scope/authenticate.js b/ui/desktop/app/routes/scopes/scope/authenticate.js index 7e49a5b9b..d328b905b 100644 --- a/ui/desktop/app/routes/scopes/scope/authenticate.js +++ b/ui/desktop/app/routes/scopes/scope/authenticate.js @@ -6,6 +6,7 @@ export default class ScopesScopeAuthenticateRoute extends Route { // =services @service session; + @service origin; // =methods @@ -26,4 +27,8 @@ export default class ScopesScopeAuthenticateRoute extends Route { authMethods: this.store.query('auth-method', { scope_id }), }); } + + redirect() { + if (!this.origin.rendererOrigin) this.replaceWith('index'); + } } diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 393a7d11d..ff9190e77 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -1,11 +1,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { all } from 'rsvp'; +import { action } from '@ember/object'; export default class ScopesScopeProjectsTargetsRoute extends Route { // =services + @service ipc; @service session; + @service notify; // =methods @@ -35,4 +38,21 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { }; }); } + + // =actions + + /** + * Establish a session to current target. + */ + @action + async connect(model) { + try { + await this.ipc.invoke('connect', { + target_id: model.target.id, + auth_token: this.session.data.authenticated.token, + }); + } catch(e) { + this.notify.error(e.toString(), { closeAfter: null }); + } + } } diff --git a/ui/desktop/app/services/origin.js b/ui/desktop/app/services/origin.js new file mode 100644 index 000000000..5fe141900 --- /dev/null +++ b/ui/desktop/app/services/origin.js @@ -0,0 +1,76 @@ +import Service from '@ember/service'; +import { assert } from '@ember/debug'; +import { inject as service } from '@ember/service'; + +export default class OriginService extends Service { + + // =services + + @service ipc; + @service store; + @service session; + + // =attributes + + /** + * @type {ApplicationAdapter} + */ + get adapter() { + return this.store.adapterFor('application'); + } + + /** + * @type {?string} + */ + get rendererOrigin() { + return this.session.data.origin; + } + + /** + * @param {?string} origin + */ + set rendererOrigin(origin) { + this.session.set('data.origin', origin); + } + + /** + * @type {Promise{?string}} + */ + get mainOrigin() { + return this.ipc.invoke('getOrigin'); + } + + // =methods + + /** + * Sets the main origin equal to the current renderer origin (if any). + */ + async updateOrigin() { + const rendererOrigin = this.rendererOrigin; + if (rendererOrigin) await this.setOrigin(rendererOrigin); + } + + /** + * Validates that the specified origin is a Boundary API. If so, the app is + * updated to use the origin. If it is not a Boundary API, or an origin + * wasn't specified, throws an error. + * @param {string} origin - protocol://host:port + */ + async setOrigin(origin) { + const originalHost = this.adapter.host; + assert(`setOrigin expects a string, you passed ${origin}`, typeof origin === 'string'); + try { + this.adapter.host = origin; + await this.store.query('scope', {}); + this.rendererOrigin = origin; + if (origin !== await this.mainOrigin) { + await this.ipc.invoke('setOrigin', origin); + } + } catch (e) { + this.adapter.host = originalHost; + this.rendererOrigin = null; + throw e; + } + } + +} diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index c50a5abab..7826a721f 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -197,3 +197,9 @@ .nowrap { white-space: nowrap; } + +.centered { + display: inline-block; + margin: auto; + transform: translate(0%, -20%); +} diff --git a/ui/desktop/app/templates/origin.hbs b/ui/desktop/app/templates/origin.hbs index 5616eccb4..c8615aa41 100644 --- a/ui/desktop/app/templates/origin.hbs +++ b/ui/desktop/app/templates/origin.hbs @@ -1,19 +1,30 @@ - + +

{{t 'origin.titles.welcome'}}

+

{{t 'origin.descriptions.welcome.get-started'}}

+

+

    +
  1. {{t 'origin.descriptions.welcome.step-1'}}
  2. +
  3. {{t 'origin.descriptions.welcome.step-2'}}
  4. +
+

- + - + - + + +
+ \ No newline at end of file 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 d76bbbe13..7d2ea2383 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -36,7 +36,7 @@ - + {{t "resources.session.actions.connect"}} diff --git a/ui/desktop/config/environment.js b/ui/desktop/config/environment.js index 9a03eddb2..d549aba56 100644 --- a/ui/desktop/config/environment.js +++ b/ui/desktop/config/environment.js @@ -2,6 +2,7 @@ const APP_NAME = process.env.APP_NAME || 'Boundary'; const API_HOST = process.env.API_HOST || ''; +const ENABLE_MIRAGE = process.env.API_HOST ? false : true; module.exports = function (environment) { let ENV = { @@ -29,6 +30,7 @@ module.exports = function (environment) { isElectron: Boolean(process.env.EMBER_CLI_ELECTRON), 'ember-cli-mirage': { + enabled: ENABLE_MIRAGE, directory: '../../addons/api/mirage' }, diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js index 11c34a5b3..c9788898b 100644 --- a/ui/desktop/electron-app/src/handlers.js +++ b/ui/desktop/electron-app/src/handlers.js @@ -3,6 +3,8 @@ const isDev = require('electron-is-dev'); const log = (method, request) => { if (isDev) console.log(`[ipc-main](${method}): `, request); }; +const { lookpath } = require('lookpath'); +const { exec } = require('child_process'); let origin = 'serve://boundary'; @@ -28,3 +30,24 @@ ipcMain.handle('setOrigin', async (event, request) => { origin = request?.data; return origin; }); + +/** + * Establishes a boundary session and returns session details. + * TODO: Return session details. + */ +ipcMain.handle('connect', async (event, request) => { + log('connect payload', request); + + const cliAvailable = await lookpath('boundary'); + log('cli check', cliAvailable); + if(!cliAvailable) { throw new Error('CLI unavailable.'); } + + const connectCmd = `boundary connect -target-id=${request.target_id} -token=${request.auth_token}`; + log('connect command', connectCmd); + + exec(connectCmd, (output) => { + log('connect output', output); + }, (error) => { + log('connect error', error); + }); +}); diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 6069c5ed2..84f4c629b 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -33,11 +33,10 @@ "@ember/optional-features": "^1.3.0", "@glimmer/component": "^1.0.1", "@glimmer/tracking": "^1.0.0", - "core": "*", - "rose": "*", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "broccoli-asset-rev": "^3.0.0", + "core": "*", "doctoc": "^1.4.0", "ember-auto-import": "^1.6.0", "ember-cli": "~3.20.2", @@ -67,9 +66,11 @@ "eslint-plugin-node": "^11.1.0", "faker": "^5.1.0", "loader.js": "^4.7.0", + "lookpath": "^1.1.0", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", "qunit-dom": "^1.2.0", + "rose": "*", "sass": "^1.29.0", "sass-lint": "^1.13.1", "sinon": "^9.2.1" diff --git a/ui/desktop/tests/unit/services/origin-test.js b/ui/desktop/tests/unit/services/origin-test.js new file mode 100644 index 000000000..c133a53a5 --- /dev/null +++ b/ui/desktop/tests/unit/services/origin-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Service | origin', function(hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function(assert) { + let service = this.owner.lookup('service:origin'); + assert.ok(service); + }); +}); diff --git a/yarn.lock b/yarn.lock index 5f1a09cf2..7841d1b9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14044,6 +14044,11 @@ longest@^2.0.1: resolved "https://registry.yarnpkg.com/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g= +lookpath@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/lookpath/-/lookpath-1.1.0.tgz#932d68371a2f0b4a5644f03d6a2b4728edba96d2" + integrity sha512-B9NM7XpVfkyWqfOBI/UW0kVhGw7pJztsduch+1wkbYDi90mYK6/InFul3lG0hYko/VEcVMARVBJ5daFRc5aKCw== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -15332,15 +15337,15 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -npm-user-validate@^1.0.1, npm-user-validate@~1.0.0: +npm-user-validate@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561" integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw== npm@5.1.0, npm@~6.14.6: - version "6.14.8" - resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.8.tgz#64ef754345639bc035982ec3f609353c8539033c" - integrity sha512-HBZVBMYs5blsj94GTeQZel7s9odVuuSUHy1+AlZh7rPVux1os2ashvEGLy/STNK7vUjbrCg5Kq9/GXisJgdf6A== + version "6.14.9" + resolved "https://registry.yarnpkg.com/npm/-/npm-6.14.9.tgz#d2b4237562bfd95689249e2c2874700ed952ed82" + integrity sha512-yHi1+i9LyAZF1gAmgyYtVk+HdABlLy94PMIDoK1TRKWvmFQAt5z3bodqVwKvzY0s6dLqQPVsRLiwhJfNtiHeCg== dependencies: JSONStream "^1.3.5" abbrev "~1.1.1" @@ -15414,7 +15419,7 @@ npm@5.1.0, npm@~6.14.6: npm-pick-manifest "^3.0.2" npm-profile "^4.0.4" npm-registry-fetch "^4.0.7" - npm-user-validate "~1.0.0" + npm-user-validate "^1.0.1" npmlog "~4.1.2" once "~1.4.0" opener "^1.5.1"