Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions addons/api/mirage/scenarios/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,20 @@ export default function initializeMockIPC(server, config) {
resumeClientAgent() {}
hasRunningSessions() {}
stopAll() {}
getRdpClients() {
return ['windows-app', 'none', 'mstsc'];
}
getPreferredRdpClient() {
return 'windows-app';
}
getRecommendedRdpClient() {
return {
name: 'windows-app',
link: 'https://apps.apple.com/us/app/windows-app/id1295203466',
};
}
setPreferredRdpClient() {}
launchRdpClient() {}
}

/**
Expand Down
1 change: 1 addition & 0 deletions addons/core/translations/actions/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ show-errors: Show Errors
hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
open: Open
15 changes: 15 additions & 0 deletions addons/core/translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ settings:
alerts:
cache-daemon: There may be a problem with the cache daemon
client-agent: There may be a problem with the client agent
preferred-clients:
title: Preferred Clients
description: Select the applications for Boundary to use when opening these target types.
table:
header:
protocol: Protocol
client: Client
data:
protocols:
windows-rdp: Windows RDP
clients:
none: None
mstsc: Remote Desktop Connection (mstsc)
windows-app: Windows App
none-detected: 'None detected. We recommend <a href={rdpClientLink} target="_blank" rel="noopener noreferrer">{rdpClientName}</a>.'
worker-filter-generator:
title: Filter generator
description: Choose what you want to format into a filter.
Expand Down
40 changes: 39 additions & 1 deletion e2e-tests/desktop/tests/targets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
import { expect, test } from '../fixtures/baseTest.js';
import * as boundaryHttp from '../../helpers/boundary-http.js';
import { textToMatch } from '../fixtures/tesseractTest.js';
import {
isRdpClientInstalled,
isRdpRunning,
killRdpProcesses,
isOSForRdpSupported,
} from '../../helpers/rdp.js';

const hostName = 'Host name for test';
let org;
Expand Down Expand Up @@ -117,7 +123,6 @@ test.beforeEach(
});

// Create an RDP target and add host source and credential sources
// TODO: A test for RDP target connection will be added later when the Proxy is in place.
rdpTarget = await boundaryHttp.createTarget(request, {
scopeId: project.id,
type: 'rdp',
Expand Down Expand Up @@ -285,4 +290,37 @@ test.describe('Targets tests', () => {
authedPage.getByRole('link', { name: targetWithHost.name }),
).toBeVisible();
});

test('Launches RDP client when connecting to an RDP target', async ({
authedPage,
}) => {
const isRdpClientInstalledCheck = await isRdpClientInstalled();
const isOSForRdpSupportedCheck = await isOSForRdpSupported();

test.skip(
!isRdpClientInstalledCheck || !isOSForRdpSupportedCheck,
'RDP client is not installed/supported on this system',
);

const beforeLaunchRunning = await isRdpRunning();
expect(beforeLaunchRunning).toBe(false);

await authedPage.getByRole('link', { name: rdpTarget.name }).click();
await authedPage.getByRole('button', { name: 'Open' }).click();

await expect(
authedPage.getByRole('heading', { name: 'Sessions' }),
).toBeVisible();

const afterLaunchRunning = await isRdpRunning();
expect(afterLaunchRunning).toBe(true);

await authedPage.getByRole('button', { name: 'End Session' }).click();
await expect(authedPage.getByText('Canceled successfully.')).toBeVisible();

killRdpProcesses();

const afterKillRunning = await isRdpRunning();
expect(afterKillRunning).toBe(false);
});
});
60 changes: 60 additions & 0 deletions e2e-tests/helpers/rdp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { execSync } from 'node:child_process';

export async function isOSForRdpSupported() {
return process.platform === 'win32' || process.platform === 'darwin';
}

export async function isRdpRunning() {
try {
let result;
if (process.platform === 'win32') {
result = execSync('tasklist /FI "IMAGENAME eq mstsc.exe" /FO CSV /NH', {
encoding: 'utf-8',
});
return result && result.includes('mstsc.exe');
} else if (process.platform === 'darwin') {
result = execSync('pgrep -x "Windows App"', {
encoding: 'utf-8',
});
return result && result.trim().length > 0;
}
return false;
} catch {
return false;
}
}

export async function isRdpClientInstalled() {
try {
if (process.platform === 'win32') {
execSync('where mstsc', { stdio: 'ignore' });
return true;
} else if (process.platform === 'darwin') {
const result = execSync(
'mdfind "kMDItemCFBundleIdentifier == \'com.microsoft.rdc.macos\'"',
{ encoding: 'utf-8' },
);
return result && result.trim().length > 0;
}
return false;
} catch {
return false;
}
}

export function killRdpProcesses() {
try {
if (process.platform === 'win32') {
execSync('taskkill /F /IM mstsc.exe', { stdio: 'ignore' });
} else if (process.platform === 'darwin') {
execSync('pkill -x "Windows App"', { stdio: 'ignore' });
}
} catch {
// no op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}

<SettingsCard
@header={{t 'settings.preferred-clients.title'}}
@icon='external-link'
@description={{t 'settings.preferred-clients.description'}}
>
<:body>
<Hds::Table
class='full-width'
@model={{this.protocolClients}}
@columns={{array
(hash
label=(t 'settings.preferred-clients.table.header.protocol')
width='50%'
)
(hash
label=(t 'settings.preferred-clients.table.header.client') width='50%'
)
}}
>
<:body as |B|>
<B.Tr>
<B.Td>{{t
(concat
'settings.preferred-clients.table.data.protocols.'
B.data.protocolType
)
}}
</B.Td>
<B.Td>
{{#if this.showRecommendedRdpClient}}
<Hds::Text::Body data-test-recommended-rdp-client>
{{t
'settings.preferred-clients.table.data.clients.none-detected'
rdpClientLink=this.rdp.recommendedRdpClient.link
rdpClientName=(t
(concat
'settings.preferred-clients.table.data.clients.'
this.rdp.recommendedRdpClient.name
)
)
htmlSafe=true
}}
</Hds::Text::Body>
{{else}}
<Hds::Form::Select::Field
@width='100%'
{{on 'change' B.data.updateClient}}
data-test-select-preferred-rdp-client
as |F|
>
<F.Options>
{{#each B.data.clients as |client|}}
<option
value={{client}}
selected={{eq B.data.preferredClient client}}
>
{{t
(concat
'settings.preferred-clients.table.data.clients.'
client
)
}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
</:body>
</SettingsCard>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { RDP_CLIENT_NONE } from 'desktop/services/rdp';

const PROTOCOL_WINDOWS_RDP = 'windows-rdp';

export default class SettingsCardPreferredClientsComponent extends Component {
// =services
@service rdp;

// =getters

/**
* Returns the list of protocols and their available clients
* @returns {Array<Object>}
*/
get protocolClients() {
return [
{
protocolType: PROTOCOL_WINDOWS_RDP,
clients: this.rdp.rdpClients,
preferredClient: this.rdp.preferredRdpClient,
updateClient: this.updatePreferredRDPClient,
},
];
}

get showRecommendedRdpClient() {
return (
this.rdp.rdpClients.length === 1 &&
this.rdp.rdpClients[0] === RDP_CLIENT_NONE
);
}

// =methods

/**
* Updates the preferred RDP client
* @param value
* @return {Promise<void>}
*/
@action
async updatePreferredRDPClient({ target: { value } }) {
await this.rdp.setPreferredRdpClient(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
@service store;
@service can;
@service intl;
@service rdp;

// =attributes

Expand Down Expand Up @@ -244,6 +245,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
'scopes.scope.projects.sessions.session',
session_id,
);

return session;
}

/**
Expand Down Expand Up @@ -311,4 +314,23 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
async refresh() {
await this.currentRoute.refreshAll();
}

/**
* Quick connect method used to call main connect method and
* then launch RDP client
* @param {TargetModel} target
*/
@action
async quickConnectAndLaunchRdp(target) {
try {
const session = await this.connect(target);
// Launch RDP client
await this.rdp.launchRdpClient(session.id);
} catch (error) {
this.confirm
.confirm(error.message, { isConnectError: true })
// Retry
.then(() => this.quickConnectAndLaunchRdp(target));
}
}
}
Loading
Loading