From 0223755b2ae090befd4911d65332fc451ebe401c Mon Sep 17 00:00:00 2001 From: Priya Patel Date: Tue, 24 Jun 2025 14:00:52 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20rdp=20target=20form-=20r?= =?UTF-8?q?ead,=20list=20and=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/scopes/scope/targets/index.js | 55 +++++++++++-- .../templates/scopes/scope/targets/index.hbs | 82 +++++++++---------- .../tests/acceptance/targets/delete-test.js | 27 ++++++ .../tests/acceptance/targets/list-test.js | 60 +++++++++++++- .../tests/acceptance/targets/read-test.js | 41 +++++++++- .../scopes/scope/targets/index-test.js | 4 + 6 files changed, 216 insertions(+), 53 deletions(-) diff --git a/ui/admin/app/controllers/scopes/scope/targets/index.js b/ui/admin/app/controllers/scopes/scope/targets/index.js index 594707a4c..1ef0d70e4 100644 --- a/ui/admin/app/controllers/scopes/scope/targets/index.js +++ b/ui/admin/app/controllers/scopes/scope/targets/index.js @@ -10,7 +10,11 @@ import { action } from '@ember/object'; import { loading } from 'ember-loading'; import { confirm } from 'core/decorators/confirm'; import { notifySuccess, notifyError } from 'core/decorators/notify'; -import { TYPES_TARGET } from 'api/models/target'; +import { + TYPE_TARGET_SSH, + TYPE_TARGET_RDP, + TYPE_TARGET_TCP, +} from 'api/models/target'; export default class ScopesScopeTargetsIndexController extends Controller { // =services @@ -19,6 +23,7 @@ export default class ScopesScopeTargetsIndexController extends Controller { @service can; @service router; @service confirm; + @service features; // =attributes @@ -68,13 +73,6 @@ export default class ScopesScopeTargetsIndexController extends Controller { }; } - get targetTypeOptions() { - return TYPES_TARGET.map((type) => ({ - id: type, - name: this.intl.t(`resources.target.types.${type}`), - })); - } - /** * If can list (at least): return default welcome message. * If can create (only): return create-but-not-list welcome message. @@ -100,6 +98,47 @@ export default class ScopesScopeTargetsIndexController extends Controller { return this.intl.t(description, { resource }); } + get isRDPEnabled() { + return this.features.isEnabled('rdp-target'); + } + + get isSSHEnabled() { + return this.features.isEnabled('ssh-target'); + } + + /** + * Returns enabled target types based on feature flags. + * @returns {Array} + */ + get enabledTargetTypes() { + const types = [TYPE_TARGET_TCP]; + if (this.isSSHEnabled) types.push(TYPE_TARGET_SSH); + if (this.isRDPEnabled) types.push(TYPE_TARGET_RDP); + return types; + } + + get targetTypeOptions() { + const enabledTypes = this.enabledTargetTypes; + return enabledTypes.map((type) => ({ + id: type, + name: this.intl.t(`resources.target.types.${type}`), + })); + } + + get showTargetTypeOptions() { + return this.isRDPEnabled || this.isSSHEnabled; + } + + /** + * Returns filtered targets based on enabled features. + */ + get targetsData() { + const enabledTypes = this.enabledTargetTypes; + return this.model.targets.filter((target) => + enabledTypes.includes(target.type), + ); + } + // =actions /** diff --git a/ui/admin/app/templates/scopes/scope/targets/index.hbs b/ui/admin/app/templates/scopes/scope/targets/index.hbs index f72f2dabc..3b970e030 100644 --- a/ui/admin/app/templates/scopes/scope/targets/index.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/index.hbs @@ -54,7 +54,7 @@ /> {{/if}} - {{#if (feature-flag 'ssh-target')}} + {{#if this.showTargetTypeOptions}} <:body as |B|> - {{#each @model.targets as |data|}} - {{#if (or (feature-flag 'ssh-target') data.isTCP)}} - - - {{#if (can 'read model' data)}} - - {{data.displayName}} - - {{else}} + {{#each this.targetsData as |data|}} + + + {{#if (can 'read model' data)}} + {{data.displayName}} - {{/if}} - - {{data.description}} - - - - {{#if data.type}} - - {{/if}} - - - {{#if data.isActive}} - - {{/if}} - - - + {{else}} + {{data.displayName}} + {{/if}} + + {{data.description}} + + + + {{#if data.type}} + - - - {{/if}} + {{/if}} + + + {{#if data.isActive}} + + {{/if}} + + + + + {{/each}} diff --git a/ui/admin/tests/acceptance/targets/delete-test.js b/ui/admin/tests/acceptance/targets/delete-test.js index 339992178..982616824 100644 --- a/ui/admin/tests/acceptance/targets/delete-test.js +++ b/ui/admin/tests/acceptance/targets/delete-test.js @@ -14,6 +14,7 @@ import { authenticateSession } from 'ember-simple-auth/test-support'; import * as commonSelectors from 'admin/tests/helpers/selectors'; import * as selectors from './selectors'; import { setRunOptions } from 'ember-a11y-testing/test-support'; +import { TYPE_TARGET_RDP } from 'api/models/target'; module('Acceptance | targets | delete', function (hooks) { setupApplicationTest(hooks); @@ -22,6 +23,8 @@ module('Acceptance | targets | delete', function (hooks) { setupIntl(hooks, 'en-us'); let getTargetCount; + let getRDPTargetCount; + let featuresService; const instances = { scopes: { @@ -30,6 +33,7 @@ module('Acceptance | targets | delete', function (hooks) { project: null, }, target: null, + rdpTarget: null, }; const urls = { orgScope: null, @@ -37,6 +41,7 @@ module('Acceptance | targets | delete', function (hooks) { targets: null, target: null, newTarget: null, + rdpTarget: null, }; hooks.beforeEach(async function () { @@ -53,14 +58,24 @@ module('Acceptance | targets | delete', function (hooks) { instances.target = this.server.create('target', { scope: instances.scopes.project, }); + instances.rdpTarget = this.server.create('target', { + type: TYPE_TARGET_RDP, + scope: instances.scopes.project, + }); // Generate route URLs for resources urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.targets = `${urls.projectScope}/targets`; urls.target = `${urls.targets}/${instances.target.id}`; urls.newTarget = `${urls.targets}/new`; + urls.rdpTarget = `${urls.targets}/${instances.rdpTarget.id}`; // Generate resource counter getTargetCount = () => this.server.schema.targets.all().models.length; + getRDPTargetCount = () => + this.server.schema.targets.where({ type: TYPE_TARGET_RDP }).models.length; + + featuresService = this.owner.lookup('service:features'); + await authenticateSession({}); }); @@ -183,4 +198,16 @@ module('Acceptance | targets | delete', function (hooks) { assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText('Oops.'); }); + + test('can delete rdp target', async function (assert) { + featuresService.enable('rdp-target'); + const rdpTargetCount = getRDPTargetCount(); + await visit(urls.targets); + + await click(commonSelectors.HREF(urls.rdpTarget)); + await click(selectors.MANAGE_DROPDOWN); + await click(selectors.MANAGE_DROPDOWN_DELETE); + + assert.strictEqual(getRDPTargetCount(), rdpTargetCount - 1); + }); }); diff --git a/ui/admin/tests/acceptance/targets/list-test.js b/ui/admin/tests/acceptance/targets/list-test.js index e4af7aa9d..2b5fd8d7d 100644 --- a/ui/admin/tests/acceptance/targets/list-test.js +++ b/ui/admin/tests/acceptance/targets/list-test.js @@ -16,7 +16,11 @@ import { setupApplicationTest } from 'admin/tests/helpers'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { setupSqlite } from 'api/test-support/helpers/sqlite'; import { authenticateSession } from 'ember-simple-auth/test-support'; -import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target'; +import { + TYPE_TARGET_TCP, + TYPE_TARGET_SSH, + TYPE_TARGET_RDP, +} from 'api/models/target'; import { STATUS_SESSION_ACTIVE } from 'api/models/session'; import * as commonSelectors from 'admin/tests/helpers/selectors'; import * as selectors from './selectors'; @@ -28,6 +32,7 @@ module('Acceptance | targets | list', function (hooks) { setupMirage(hooks); setupSqlite(hooks); + let featuresService; const NAME_VALUES_ARRAY = ['Alpha', 'Beta', 'Delta', 'Epsilon', 'Gamma']; const ID_VALUES_ARRAY = ['i_0001', 'i_0010', 'i_0100', 'i_1000', 'i_10000']; const CREATED_TIME_VALUES_ARRAY = [ @@ -47,6 +52,7 @@ module('Acceptance | targets | list', function (hooks) { tcpTarget: null, sshTarget: null, session: null, + rdpTarget: null, }; const urls = { @@ -55,6 +61,7 @@ module('Acceptance | targets | list', function (hooks) { targets: null, tcpTarget: null, sshTarget: null, + rdpTarget: null, }; hooks.beforeEach(async function () { @@ -81,14 +88,20 @@ module('Acceptance | targets | list', function (hooks) { scope: instances.scopes.project, status: STATUS_SESSION_ACTIVE, }); + instances.rdpTarget = this.server.create('target', { + type: TYPE_TARGET_RDP, + scope: instances.scopes.project, + }); urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`; urls.projectScope = `/scopes/${instances.scopes.project.id}`; urls.targets = `${urls.projectScope}/targets`; urls.tcpTarget = `${urls.targets}/${instances.tcpTarget.id}`; urls.sshTarget = `${urls.targets}/${instances.sshTarget.id}`; + urls.rdpTarget = `${urls.targets}/${instances.rdpTarget.id}`; - const featuresService = this.owner.lookup('service:features'); + featuresService = this.owner.lookup('service:features'); featuresService.enable('ssh-target'); + featuresService.enable('rdp-target'); await authenticateSession({}); }); @@ -219,12 +232,15 @@ module('Acceptance | targets | list', function (hooks) { assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); assert.dom(commonSelectors.HREF(urls.sshTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).isVisible(); await fillIn(commonSelectors.SEARCH_INPUT, instances.sshTarget.id); await waitFor(commonSelectors.HREF(urls.tcpTarget), { count: 0 }); + await waitFor(commonSelectors.HREF(urls.rdpTarget), { count: 0 }); assert.dom(commonSelectors.HREF(urls.sshTarget)).isVisible(); assert.dom(commonSelectors.HREF(urls.tcpTarget)).doesNotExist(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).doesNotExist(); }); test('user can search for targets and get no results', async function (assert) { @@ -243,6 +259,7 @@ module('Acceptance | targets | list', function (hooks) { assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); assert.dom(commonSelectors.HREF(urls.sshTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).isVisible(); await fillIn( commonSelectors.SEARCH_INPUT, @@ -252,6 +269,7 @@ module('Acceptance | targets | list', function (hooks) { assert.dom(commonSelectors.HREF(urls.sshTarget)).doesNotExist(); assert.dom(commonSelectors.HREF(urls.tcpTarget)).doesNotExist(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).doesNotExist(); assert.dom(selectors.NO_RESULTS_MSG).includesText('No results found'); }); @@ -271,12 +289,14 @@ module('Acceptance | targets | list', function (hooks) { assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); assert.dom(commonSelectors.HREF(urls.sshTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).isVisible(); await click(commonSelectors.FILTER_DROPDOWN('type')); await click(commonSelectors.FILTER_DROPDOWN_ITEM('tcp')); await click(commonSelectors.FILTER_DROPDOWN_ITEM_APPLY_BTN('type')); assert.dom(commonSelectors.HREF(urls.sshTarget)).doesNotExist(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).doesNotExist(); assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); }); @@ -464,4 +484,40 @@ module('Acceptance | targets | list', function (hooks) { }); }, ); + + test('user can only see RDP and TCP targets when rdp-target feature is enabled', async function (assert) { + featuresService.disable('ssh-target'); + + await visit(urls.projectScope); + + await click(commonSelectors.HREF(urls.targets)); + + assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.sshTarget)).doesNotExist(); + + await click(commonSelectors.FILTER_DROPDOWN('type')); + + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('tcp')).isVisible(); + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('rdp')).isVisible(); + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('ssh')).doesNotExist(); + }); + + test('user can only see SSH and TCP targets when ssh-target feature is enabled', async function (assert) { + featuresService.disable('rdp-target'); + + await visit(urls.projectScope); + + await click(commonSelectors.HREF(urls.targets)); + + assert.dom(commonSelectors.HREF(urls.tcpTarget)).isVisible(); + assert.dom(commonSelectors.HREF(urls.rdpTarget)).doesNotExist(); + assert.dom(commonSelectors.HREF(urls.sshTarget)).isVisible(); + + await click(commonSelectors.FILTER_DROPDOWN('type')); + + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('tcp')).isVisible(); + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('rdp')).doesNotExist(); + assert.dom(commonSelectors.FILTER_DROPDOWN_ITEM('ssh')).isVisible(); + }); }); diff --git a/ui/admin/tests/acceptance/targets/read-test.js b/ui/admin/tests/acceptance/targets/read-test.js index 77d6ccb11..88e230a13 100644 --- a/ui/admin/tests/acceptance/targets/read-test.js +++ b/ui/admin/tests/acceptance/targets/read-test.js @@ -10,7 +10,11 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { setupSqlite } from 'api/test-support/helpers/sqlite'; import { setupIntl } from 'ember-intl/test-support'; import { authenticateSession } from 'ember-simple-auth/test-support'; -import { TYPE_TARGET_SSH, TYPE_TARGET_TCP } from 'api/models/target'; +import { + TYPE_TARGET_TCP, + TYPE_TARGET_SSH, + TYPE_TARGET_RDP, +} from 'api/models/target'; import * as commonSelectors from 'admin/tests/helpers/selectors'; import * as selectors from './selectors'; import { setRunOptions } from 'ember-a11y-testing/test-support'; @@ -33,6 +37,7 @@ module('Acceptance | targets | read', function (hooks) { sshTarget: null, tcpTarget: null, alias: null, + rdpTarget: null, }; const urls = { globalScope: null, @@ -43,6 +48,7 @@ module('Acceptance | targets | read', function (hooks) { tcpTarget: null, alias: null, aliases: null, + rdpTarget: null, }; hooks.beforeEach(async function () { @@ -65,6 +71,10 @@ module('Acceptance | targets | read', function (hooks) { type: TYPE_TARGET_TCP, scope: instances.scopes.project, }); + instances.rdpTarget = this.server.create('target', { + type: TYPE_TARGET_RDP, + scope: instances.scopes.project, + }); instances.alias = this.server.createList('alias', 1, { scope: instances.scopes.global, @@ -83,6 +93,7 @@ module('Acceptance | targets | read', function (hooks) { urls.unknownTarget = `${urls.targets}/foo`; urls.aliases = `${urls.globalScope}/aliases`; urls.alias = `${urls.tcpTarget}/${aliasResource.id}`; + urls.rdpTarget = `${urls.targets}/${instances.rdpTarget.id}`; await authenticateSession({ username: 'admin' }); }); @@ -318,4 +329,32 @@ module('Acceptance | targets | read', function (hooks) { assert.dom(selectors.ALIASES_FLYOUT).isVisible(); }); + + test('cannot navigate to a rdp target form without proper authorization', async function (assert) { + featuresService.enable('rdp-target'); + instances.rdpTarget.authorized_actions = + instances.rdpTarget.authorized_actions.filter((item) => item !== 'read'); + + await visit(urls.projectScope); + + assert.dom(commonSelectors.TABLE_RESOURCE_LINK(urls.tcpTarget)).isVisible(); + assert + .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.rdpTarget)) + .doesNotExist(); + }); + + test('visiting rdp target', async function (assert) { + featuresService.enable('rdp-target'); + await visit(urls.projectScope); + + await click(commonSelectors.HREF(urls.targets)); + await a11yAudit(); + + assert.strictEqual(currentURL(), urls.targets); + + await click(commonSelectors.HREF(urls.rdpTarget)); + await a11yAudit(); + + assert.strictEqual(currentURL(), urls.rdpTarget); + }); }); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js index 676cb36d2..14d900cd6 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js @@ -27,6 +27,7 @@ module('Unit | Controller | scopes/scope/targets/index', function (hooks) { let controller; let getTargetCount; let getAliasCount; + let features; const instances = { scopes: { @@ -53,6 +54,7 @@ module('Unit | Controller | scopes/scope/targets/index', function (hooks) { intl = this.owner.lookup('service:intl'); store = this.owner.lookup('service:store'); controller = this.owner.lookup('controller:scopes/scope/targets/index'); + features = this.owner.lookup('service:features'); instances.scopes.global = this.server.create('scope', { id: 'global' }); instances.scopes.org = this.server.create('scope', { @@ -99,6 +101,8 @@ module('Unit | Controller | scopes/scope/targets/index', function (hooks) { }); test('targetTypeOptions returns expected object', function (assert) { + features.enable('rdp-target'); + features.enable('ssh-target'); let controller = this.owner.lookup('controller:scopes/scope/targets/index'); assert.deepEqual(controller.targetTypeOptions, [ { id: TYPE_TARGET_TCP, name: 'Generic TCP' },