Icu 13374 add permission checks for filtering resources in sessions and targets (#2226)

* refactor: 💡 hide active sessions filter if no list permissions

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* test: 💍 add active sessions filter permissions test

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 add permissions check for targets and users

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 address PR feedback

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 move user permission check to a helper func

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374

* refactor: 💡 move permissions logic to user and target helper

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-13374
This commit is contained in:
Cameron Perera authored and Zhihe Li committed 2024-04-12 14:55:34 -04:00
1 parent edba33bbda
commit 626bdc2c92
6 files changed
+173 -100

No files matched your search

@@ -10,6 +10,7 @@ import { action } from '@ember/object';
export default class ScopesScopeSessionsIndexRoute extends Route {
// =services
@service can;
@service store;
// =attributes
@@ -40,8 +41,8 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
};
allSessions;
associatedUsers;
associatedTargets;
associatedUsers = [];
associatedTargets = [];
/**
* Loads all sessions under the current scope and encapsulates them into
@@ -49,7 +50,9 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
* @return {Promise{[{sessions: [SessionModel], allSessions: [SessionModel], associatedUsers: [UserModel], associatedTargets: [TargetModel], totalItems: number}]}}
*/
async model({ search, users, targets, status, page, pageSize }) {
const { id: scope_id } = this.modelFor('scopes.scope');
const scope = this.modelFor('scopes.scope');
const { id: scope_id } = scope;
const filters = {
scope_id: [{ equals: scope_id }],
status: [],
@@ -81,11 +84,11 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
if (!this.allSessions) {
await this.getAllSessions(scope_id);
}
if (!this.associatedUsers) {
await this.getAssociatedUsers();
if (this.associatedUsers.length === 0) {
await this.getAssociatedUsers(scope);
}
if (!this.associatedTargets) {
await this.getAssociatedTargets(scope_id);
if (this.associatedTargets.length === 0) {
await this.getAssociatedTargets(scope);
}
return {
@@ -113,53 +116,77 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
});
}
// async canListUsers(orgId) {
// const orgScope = await this.store.findRecord('scope', orgId);
// const globalScope = await this.store.findRecord('scope', 'global');
// return (
// this.can.can('list model', globalScope, { collection: 'users' }) &&
// this.can.can('list model', orgScope, { collection: 'users' })
// );
// }
/**
* Get all the users but only load them once when entering the route.
* @param scope
* @returns {Promise<void>}
*/
async getAssociatedUsers() {
const uniqueSessionUserIds = new Set(
this.allSessions
.filter((session) => session.user_id)
.map((session) => session.user_id),
);
const filters = {
id: { values: [] },
};
uniqueSessionUserIds.forEach((userId) => {
filters.id.values.push({ equals: userId });
});
const associatedUsersQuery = {
scope_id: 'global',
recursive: true,
query: { filters },
};
this.associatedUsers = await this.store.query('user', associatedUsersQuery);
async getAssociatedUsers(scope) {
const orgScope = await this.store.findRecord('scope', scope.scope.id);
const globalScope = await this.store.findRecord('scope', 'global');
if (
this.can.can('list model', globalScope, { collection: 'users' }) &&
this.can.can('list model', orgScope, { collection: 'users' })
) {
const uniqueSessionUserIds = new Set(
this.allSessions
.filter((session) => session.user_id)
.map((session) => session.user_id),
);
const filters = {
id: { values: [] },
};
uniqueSessionUserIds.forEach((userId) => {
filters.id.values.push({ equals: userId });
});
const associatedUsersQuery = {
scope_id: 'global',
recursive: true,
query: { filters },
};
this.associatedUsers = await this.store.query(
'user',
associatedUsersQuery,
);
}
}
/**
* Get all the targets but only load them once when entering the route.
* @param scope_id
* @param scope
* @returns {Promise<void>}
*/
async getAssociatedTargets(scope_id) {
const uniqueSessionTargetIds = new Set(
this.allSessions
.filter((session) => session.target_id)
.map((session) => session.target_id),
);
const filters = { id: { values: [] } };
uniqueSessionTargetIds.forEach((targetId) => {
filters.id.values.push({ equals: targetId });
});
const associatedTargetsQuery = {
scope_id,
query: { filters },
};
this.associatedTargets = await this.store.query(
'target',
associatedTargetsQuery,
);
async getAssociatedTargets(scope) {
if (this.can.can('list model', scope, { collection: 'targets' })) {
const uniqueSessionTargetIds = new Set(
this.allSessions
.filter((session) => session.target_id)
.map((session) => session.target_id),
);
const filters = { id: { values: [] } };
uniqueSessionTargetIds.forEach((targetId) => {
filters.id.values.push({ equals: targetId });
});
const associatedTargetsQuery = {
scope_id: scope.id,
query: { filters },
};
this.associatedTargets = await this.store.query(
'target',
associatedTargetsQuery,
);
}
}
// =actions
@@ -169,11 +196,11 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
*/
@action
async refreshAll() {
const { id: scope_id } = this.modelFor('scopes.scope');
const scope = this.modelFor('scopes.scope');
await this.getAllSessions(scope_id);
await this.getAssociatedUsers();
await this.getAssociatedTargets();
await this.getAllSessions(scope.id);
await this.getAssociatedUsers(scope);
await this.getAssociatedTargets(scope);
return super.refresh(...arguments);
}
@@ -60,19 +60,21 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
filters.type.push({ equals: type });
});
const sessions = await this.store.query('session', {
scope_id,
query: {
filters: {
scope_id: [{ equals: scope_id }],
status: [
{ equals: STATUS_SESSION_ACTIVE },
{ equals: STATUS_SESSION_PENDING },
],
if (this.can.can('list model', scope, { collection: 'sessions' })) {
const sessions = await this.store.query('session', {
scope_id,
query: {
filters: {
scope_id: [{ equals: scope_id }],
status: [
{ equals: STATUS_SESSION_ACTIVE },
{ equals: STATUS_SESSION_PENDING },
],
},
},
},
});
this.addActiveSessionFilters(filters, availableSessions, sessions);
});
this.addActiveSessionFilters(filters, availableSessions, sessions);
}
let targets;
let totalItems = 0;
@@ -28,37 +28,41 @@
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
<S.Generic>
<Dropdown
name='user'
@toggleText={{t 'resources.user.title'}}
@itemOptions={{@model.associatedUsers}}
@checkedItems={{this.users}}
@applyFilter={{fn this.applyFilter 'users'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#each itemOptions as |itemOption|}}
<FD.Checkbox
@value={{itemOption.id}}
checked={{includes itemOption.id this.users}}
{{on 'change' selectItem}}
>
{{itemOption.name}}
</FD.Checkbox>
{{/each}}
</Dropdown>
</S.Generic>
<S.Generic>
<Dropdown
name='target'
@toggleText={{t 'resources.target.title'}}
@itemOptions={{@model.associatedTargets}}
@checkedItems={{this.targets}}
@applyFilter={{fn this.applyFilter 'targets'}}
@isSearchable={{true}}
/>
</S.Generic>
{{#if @model.associatedUsers}}
<S.Generic>
<Dropdown
name='user'
@toggleText={{t 'resources.user.title'}}
@itemOptions={{@model.associatedUsers}}
@checkedItems={{this.users}}
@applyFilter={{fn this.applyFilter 'users'}}
@isSearchable={{true}}
as |FD selectItem itemOptions|
>
{{#each itemOptions as |itemOption|}}
<FD.Checkbox
@value={{itemOption.id}}
checked={{includes itemOption.id this.users}}
{{on 'change' selectItem}}
>
{{itemOption.name}}
</FD.Checkbox>
{{/each}}
</Dropdown>
</S.Generic>
{{/if}}
{{#if @model.associatedTargets}}
<S.Generic>
<Dropdown
name='target'
@toggleText={{t 'resources.target.title'}}
@itemOptions={{@model.associatedTargets}}
@checkedItems={{this.targets}}
@applyFilter={{fn this.applyFilter 'targets'}}
@isSearchable={{true}}
/>
</S.Generic>
{{/if}}
<S.Generic>
<Dropdown
name='status'
@@ -38,15 +38,17 @@
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
<S.Generic>
<Dropdown
name='active-sessions'
@toggleText={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
{{#if (can 'list model' this.scope collection='sessions')}}
<S.Generic>
<Dropdown
name='active-sessions'
@toggleText={{t 'resources.target.titles.active-sessions'}}
@itemOptions={{this.availableSessionOptions}}
@checkedItems={{this.availableSessions}}
@applyFilter={{fn this.applyFilter 'availableSessions'}}
/>
</S.Generic>
{{/if}}
{{#if (feature-flag 'ssh-target')}}
<S.Generic>
<Dropdown
@@ -229,6 +229,21 @@ module('Acceptance | sessions | list', function (hooks) {
assert.dom('tbody tr').exists({ count: 1 });
});
test('users filter is hidden if no users returned or no list permissions', async function (assert) {
instances.scopes.global.authorized_collection_actions.users =
instances.scopes.global.authorized_collection_actions.users.filter(
(item) => item !== 'list',
);
instances.scopes.org.authorized_collection_actions.users =
instances.scopes.org.authorized_collection_actions.users.filter(
(item) => item !== 'list',
);
await visit(urls.projectScope);
await click(`[href="${urls.sessions}"]`);
assert.dom(FILTER_TOGGLE_SELECTOR('user')).doesNotExist();
});
test('user can filter for sessions by target', async function (assert) {
await visit(urls.projectScope);
instances.sessions[2].update({
@@ -244,6 +259,17 @@ module('Acceptance | sessions | list', function (hooks) {
assert.dom('tbody tr').exists({ count: 1 });
});
test('targets filter is hidden if no targets returned or no list permissions', async function (assert) {
instances.scopes.project.authorized_collection_actions.targets =
instances.scopes.project.authorized_collection_actions.targets.filter(
(item) => item !== 'list',
);
await visit(urls.projectScope);
await click(`[href="${urls.sessions}"]`);
assert.dom(FILTER_TOGGLE_SELECTOR('target')).doesNotExist();
});
test('user can filter for sessions by status', async function (assert) {
await visit(urls.projectScope);
@@ -239,6 +239,18 @@ module('Acceptance | targets | list', function (hooks) {
assert.dom(`[href="${urls.tcpTarget}"]`).doesNotExist();
});
test('active sessions filter is hidden if user does not have permission to list sessions', async function (assert) {
instances.scopes.project.authorized_collection_actions.sessions =
instances.scopes.project.authorized_collection_actions.sessions.filter(
(item) => item !== 'list',
);
await visit(urls.projectScope);
await click(`[href="${urls.targets}"]`);
assert.dom(FILTER_DROPDOWN_SELECTOR('active-sessions')).doesNotExist();
});
test('user can navigate to active sessions from targets table', async function (assert) {
await visit(urls.projectScope);