diff --git a/ui/desktop/app/controllers/scopes/scope/projects/sessions/session.js b/ui/desktop/app/controllers/scopes/scope/projects/sessions/session.js index 3292e6745..2f65d56b6 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/sessions/session.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/sessions/session.js @@ -4,7 +4,43 @@ */ import Controller, { inject as controller } from '@ember/controller'; +import { TYPE_TARGET_RDP } from 'api/models/target'; +import { service } from '@ember/service'; +import { action } from '@ember/object'; export default class ScopesScopeProjectsSessionsSessionController extends Controller { @controller('scopes/scope/projects/sessions/index') sessions; + + // =services + + @service rdp; + @service confirm; + + // =getters + + /** + * Whether to show the "Open" button for launching the RDP client. + * @returns {boolean} + */ + get showOpenButton() { + return ( + this.model.target?.type === TYPE_TARGET_RDP && + this.rdp.isPreferredRdpClientSet && + this.model.id + ); + } + + // =methods + + @action + async launchRdpClient() { + try { + await this.rdp.launchRdpClient(this.model.id); + } catch (error) { + this.confirm + .confirm(error.message, { isConnectError: true }) + // Retry + .then(() => this.launchRdpClient()); + } + } } diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/session.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/session.hbs index 19e27bd83..3df918ab4 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/session.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/session.hbs @@ -19,15 +19,27 @@ @icon='loading' /> - {{#if (can 'cancel session' @model)}} + {{#if (or (can 'cancel session' @model) this.showOpenButton)}} - + {{#if (can 'cancel session' @model)}} + + {{/if}} + {{#if this.showOpenButton}} + + {{/if}} {{/if}} diff --git a/ui/desktop/tests/acceptance/projects/sessions/session-test.js b/ui/desktop/tests/acceptance/projects/sessions/session-test.js index 61e43a90c..486ef4bb3 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/session-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/session-test.js @@ -14,6 +14,7 @@ import { STATUS_SESSION_ACTIVE } from 'api/models/session'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import sinon from 'sinon'; +import { TYPE_TARGET_RDP } from 'api/models/target'; module('Acceptance | projects | sessions | session', function (hooks) { setupApplicationTest(hooks); @@ -24,6 +25,8 @@ module('Acceptance | projects | sessions | session', function (hooks) { const TOAST_DO_NOT_SHOW_AGAIN_BUTTON = '[data-test-toast-notification] button'; const TOAST_DISMISS_BUTTON = '[aria-label="Dismiss"]'; + const RDP_OPEN_BUTTON = '[data-test-session-detail-open-button]'; + const CANCEL_SESSION_BUTTON = '[data-test-session-detail-cancel-button]'; const instances = { scopes: { @@ -36,6 +39,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, user: null, session: null, + rdpSession: null, }; const urls = { @@ -139,8 +143,8 @@ module('Acceptance | projects | sessions | session', function (hooks) { this.ipcStub.withArgs('isCacheDaemonRunning').returns(false); // mock RDP service calls - let rdpService = this.owner.lookup('service:rdp'); - sinon.stub(rdpService, 'initialize').resolves(); + this.rdpService = this.owner.lookup('service:rdp'); + sinon.stub(this.rdpService, 'initialize').resolves(); }); hooks.afterEach(function () { @@ -486,7 +490,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - assert.dom('[data-test-session-detail-cancel-button]').isVisible(); + assert.dom(CANCEL_SESSION_BUTTON).isVisible(); }); test('cannot cancel a session without cancel permissions', async function (assert) { @@ -504,7 +508,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - assert.dom('[data-test-session-detail-cancel-button]').isNotVisible(); + assert.dom(CANCEL_SESSION_BUTTON).isNotVisible(); }); test('cancelling a session shows success alert', async function (assert) { @@ -521,7 +525,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - await click('[data-test-session-detail-cancel-button]'); + await click(CANCEL_SESSION_BUTTON); assert .dom('[data-test-toast-notification].hds-alert--color-success') @@ -542,7 +546,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - await click('[data-test-session-detail-cancel-button]'); + await click(CANCEL_SESSION_BUTTON); assert.strictEqual(currentURL(), urls.targets); }); @@ -562,7 +566,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - await click('[data-test-session-detail-cancel-button]'); + await click(CANCEL_SESSION_BUTTON); assert .dom('[data-test-toast-notification].hds-alert--color-critical') @@ -584,10 +588,109 @@ module('Acceptance | projects | sessions | session', function (hooks) { await visit(urls.session); - await click('[data-test-session-detail-cancel-button]'); + await click(CANCEL_SESSION_BUTTON); assert .dom('[data-test-toast-notification].hds-alert--color-critical') .isVisible(); }); + + test('visiting an RDP session should display "open" button when preferred client is set', async function (assert) { + this.ipcStub.withArgs('cliExists').returns(true); + + this.rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.ipcStub.withArgs('connect').returns({ + session_id: instances.rdpSession.id, + host_id: 'h_123', + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + + await visit(urls.rdpTarget); + await click(TARGET_CONNECT_BUTTON); + + assert.strictEqual(currentURL(), urls.rdpSession); + assert.dom(RDP_OPEN_BUTTON).isVisible(); + + await click(RDP_OPEN_BUTTON); + + assert.ok( + this.ipcStub.calledWith('launchRdpClient', instances.rdpSession.id), + ); + }); + + test('visiting an RDP session should not display "open" button when preferred client is set to none', async function (assert) { + this.ipcStub.withArgs('cliExists').returns(true); + + this.rdpService.preferredRdpClient = 'none'; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.ipcStub.withArgs('connect').returns({ + session_id: instances.rdpSession.id, + host_id: 'h_123', + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + + await visit(urls.rdpTarget); + await click(TARGET_CONNECT_BUTTON); + + assert.strictEqual(currentURL(), urls.rdpSession); + assert.dom(RDP_OPEN_BUTTON).doesNotExist(); + }); + + test('it shows confirm modal when connection error occurs on launching rdp client', async function (assert) { + this.ipcStub.withArgs('cliExists').returns(true); + + this.rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.ipcStub.withArgs('connect').returns({ + session_id: instances.rdpSession.id, + host_id: 'h_123', + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + this.ipcStub.withArgs('launchRdpClient', instances.rdpSession.id).rejects(); + + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + await visit(urls.rdpTarget); + await click(TARGET_CONNECT_BUTTON); + + assert.strictEqual(currentURL(), urls.rdpSession); + assert.dom(RDP_OPEN_BUTTON).isVisible(); + + await click(RDP_OPEN_BUTTON); + + assert.dom('.hds-modal').isVisible(); + }); + + test('it displays open button without cancel session permission', async function (assert) { + this.ipcStub.withArgs('cliExists').returns(true); + this.rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + instances.rdpSession.update({ authorized_actions: [] }); + + this.ipcStub.withArgs('connect').returns({ + session_id: instances.rdpSession.id, + host_id: 'h_123', + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + + await visit(urls.rdpTarget); + await click(TARGET_CONNECT_BUTTON); + + assert.strictEqual(currentURL(), urls.rdpSession); + assert.dom(RDP_OPEN_BUTTON).isVisible(); + assert.dom(CANCEL_SESSION_BUTTON).isNotVisible(); + }); }); diff --git a/ui/desktop/tests/acceptance/projects/targets/index-test.js b/ui/desktop/tests/acceptance/projects/targets/index-test.js index d75cf1aa5..6008e5a4c 100644 --- a/ui/desktop/tests/acceptance/projects/targets/index-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/index-test.js @@ -165,8 +165,8 @@ module('Acceptance | projects | targets | index', function (hooks) { this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); // mock RDP service calls - let rdpService = this.owner.lookup('service:rdp'); - sinon.stub(rdpService, 'initialize').resolves(); + this.rdpService = this.owner.lookup('service:rdp'); + sinon.stub(this.rdpService, 'initialize').resolves(); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { @@ -719,8 +719,7 @@ module('Acceptance | projects | targets | index', function (hooks) { }); test('shows `Open` button for RDP target with preferred client', async function (assert) { - let rdpService = this.owner.lookup('service:rdp'); - rdpService.preferredRdpClient = 'windows-app'; + this.rdpService.preferredRdpClient = 'windows-app'; instances.target.update({ type: TYPE_TARGET_RDP, }); @@ -732,8 +731,7 @@ module('Acceptance | projects | targets | index', function (hooks) { }); test('shows `Connect` button for RDP target with no preferred client', async function (assert) { - let rdpService = this.owner.lookup('service:rdp'); - rdpService.preferredRdpClient = 'none'; + this.rdpService.preferredRdpClient = 'none'; instances.target.update({ type: TYPE_TARGET_RDP, }); @@ -754,8 +752,7 @@ module('Acceptance | projects | targets | index', function (hooks) { test('clicking `Open` button for RDP target calls launchRdpClient IPC', async function (assert) { this.ipcStub.withArgs('cliExists').returns(true); - const rdpService = this.owner.lookup('service:rdp'); - rdpService.preferredRdpClient = 'windows-app'; + this.rdpService.preferredRdpClient = 'windows-app'; instances.target.update({ type: TYPE_TARGET_RDP }); this.ipcStub.withArgs('connect').returns({ @@ -779,8 +776,7 @@ module('Acceptance | projects | targets | index', function (hooks) { test('clicking `Connect` button for RDP target without preferred client calls connect IPC', async function (assert) { this.ipcStub.withArgs('cliExists').returns(true); - const rdpService = this.owner.lookup('service:rdp'); - rdpService.preferredRdpClient = 'none'; + this.rdpService.preferredRdpClient = 'none'; instances.target.update({ type: TYPE_TARGET_RDP }); this.ipcStub.withArgs('connect').returns({ @@ -801,8 +797,7 @@ module('Acceptance | projects | targets | index', function (hooks) { }); test('shows confirm modal when connection error occurs on launching rdp client', async function (assert) { - let rdpService = this.owner.lookup('service:rdp'); - rdpService.preferredRdpClient = 'windows-app'; + this.rdpService.preferredRdpClient = 'windows-app'; instances.target.update({ type: TYPE_TARGET_RDP }); this.ipcStub.withArgs('cliExists').returns(true);