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:
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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user