From 786e6130cd61cdd44d23bca9c554c6ca9dc8d092 Mon Sep 17 00:00:00 2001 From: Dharini Jeeva Date: Tue, 13 Feb 2024 13:26:23 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20allow=20session=20record?= =?UTF-8?q?ing=20deletion=20(#2132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 allow session recording deletion --- .../api/addon/abilities/session-recording.js | 8 ++++ addons/api/mirage/config.js | 2 + .../api/mirage/factories/session-recording.js | 1 + .../unit/abilities/session-recording-test.js | 37 ++++++++++++++++ addons/core/translations/resources/en-us.yaml | 3 ++ ui/admin/app/abilities/session-recording.js | 10 +++++ .../routes/scopes/scope/session-recordings.js | 19 ++++++++ .../channels-by-connection/index.hbs | 17 +++++++- .../channels-by-connection/channel-test.js | 43 ++++++++++++++++++- 9 files changed, 136 insertions(+), 4 deletions(-) diff --git a/addons/api/addon/abilities/session-recording.js b/addons/api/addon/abilities/session-recording.js index d0ae91f84..1da5a094e 100644 --- a/addons/api/addon/abilities/session-recording.js +++ b/addons/api/addon/abilities/session-recording.js @@ -21,6 +21,14 @@ export default class SessionRecordingAbility extends ModelAbility { return !this.model.isUnknown && super.canRead; } + /** + * Only "known" session recording types may be deleted. + * @type {boolean} + */ + get canDelete() { + return !this.model.isUnknown && super.canDelete && this.model.isAvailable; + } + get canDownload() { return this.hasAuthorizedAction('download'); } diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 951956a46..cd4de1f31 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -798,6 +798,8 @@ function routes() { }, ); + this.del('/session-recordings/:id'); + /* Uncomment the following line and the Response import above * Then change the response code to simulate error responses. * this.get('/scopes', () => new Response(505)); diff --git a/addons/api/mirage/factories/session-recording.js b/addons/api/mirage/factories/session-recording.js index ea81b3c8c..2c3effa94 100644 --- a/addons/api/mirage/factories/session-recording.js +++ b/addons/api/mirage/factories/session-recording.js @@ -60,6 +60,7 @@ export default factory.extend({ 'read', 'download', 'reapply-storage-policy', + 'delete', ], withNonExistingUserAndTarget: trait({ diff --git a/addons/api/tests/unit/abilities/session-recording-test.js b/addons/api/tests/unit/abilities/session-recording-test.js index 48e8c0fc5..81f9d1354 100644 --- a/addons/api/tests/unit/abilities/session-recording-test.js +++ b/addons/api/tests/unit/abilities/session-recording-test.js @@ -5,6 +5,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import { + STATE_SESSION_RECORDING_AVAILABLE, + STATE_SESSION_RECORDING_STARTED, + TYPE_SESSION_RECORDING_SSH, +} from 'api/models/session-recording'; module('Unit | Ability | session-recording', function (hooks) { setupTest(hooks); @@ -49,4 +54,36 @@ module('Unit | Ability | session-recording', function (hooks) { ), ); }); + + test('can delete a session with proper authorization and in available state', function (assert) { + const recordingNotinAvaialableState = store.createRecord( + 'session-recording', + { + authorized_actions: ['delete'], + type: TYPE_SESSION_RECORDING_SSH, + state: STATE_SESSION_RECORDING_STARTED, + }, + ); + const recordingInAvailableState = store.createRecord('session-recording', { + authorized_actions: ['delete'], + type: TYPE_SESSION_RECORDING_SSH, + state: STATE_SESSION_RECORDING_AVAILABLE, + }); + assert.false( + canService.can('delete session-recording', recordingNotinAvaialableState), + ); + assert.true( + canService.can('delete session-recording', recordingInAvailableState), + ); + }); + + test('can re-apply Storage policy with proper authorization', function (assert) { + const recording = store.createRecord('session', { + authorized_actions: ['reapply-storage-policy'], + }); + + assert.true( + canService.can('reapplyStoragePolicy session-recording', recording), + ); + }); }); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 2796706f2..0f23d9b57 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -354,6 +354,8 @@ session-recording: up: '{bytes} up' down: '{bytes} down' duration: '{time} duration' + questions: + delete: Are you sure you want to delete this recording? connection: title: Connection title_index: Connection {index} @@ -949,6 +951,7 @@ policy: apply: Apply the storage policy to this scope and its children new: Add a new storage policy update: Re-apply storage policy + delete_recording: Delete recording questions: detach: Are you sure you want to detach this storage policy? messages: diff --git a/ui/admin/app/abilities/session-recording.js b/ui/admin/app/abilities/session-recording.js index 130e68c66..40fcbbf6b 100644 --- a/ui/admin/app/abilities/session-recording.js +++ b/ui/admin/app/abilities/session-recording.js @@ -29,4 +29,14 @@ export default class OverrideSessionRecordingAbility extends SessionRecordingAbi ? this.hasAuthorizedAction('reapply-storage-policy') : false; } + + /** + * This override ensures that session recordings may be deleted only if the + * session-recording feature flag is enabled. + */ + get canDelete() { + return this.features.isEnabled('ssh-session-recording') + ? super.canDelete + : false; + } } diff --git a/ui/admin/app/routes/scopes/scope/session-recordings.js b/ui/admin/app/routes/scopes/scope/session-recordings.js index 7d81c6d93..b34974dce 100644 --- a/ui/admin/app/routes/scopes/scope/session-recordings.js +++ b/ui/admin/app/routes/scopes/scope/session-recordings.js @@ -6,6 +6,10 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import orderBy from 'lodash/orderBy'; +import { loading } from 'ember-loading'; +import { confirm } from 'core/decorators/confirm'; +import { action } from '@ember/object'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeSessionRecordingsRoute extends Route { // =services @@ -68,4 +72,19 @@ export default class ScopesScopeSessionRecordingsRoute extends Route { }; } } + + /** + * Deletes the session recording + * @param {SessionRecording} recording + */ + @action + @loading + @confirm('resources.session-recording.questions.delete') + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.delete-success') + async delete(recording) { + await recording.destroyRecord(); + await this.router.replaceWith('scopes.scope.session-recordings'); + this.router.refresh(); + } } diff --git a/ui/admin/app/templates/scopes/scope/session-recordings/session-recording/channels-by-connection/index.hbs b/ui/admin/app/templates/scopes/scope/session-recordings/session-recording/channels-by-connection/index.hbs index 4056d8805..0a73c4ee4 100644 --- a/ui/admin/app/templates/scopes/scope/session-recordings/session-recording/channels-by-connection/index.hbs +++ b/ui/admin/app/templates/scopes/scope/session-recordings/session-recording/channels-by-connection/index.hbs @@ -16,8 +16,12 @@ {{#if (can 'reapplyStoragePolicy session-recording' @model.sessionRecording) }} - - + + + {{#if (can 'delete session-recording' @model.sessionRecording)}} + + + {{/if}} {{/if}} diff --git a/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js b/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js index cdf927993..b8c2618fe 100644 --- a/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js +++ b/ui/admin/tests/acceptance/session-recordings/session-recording/channels-by-connection/channel-test.js @@ -17,7 +17,10 @@ module( setupMirage(hooks); let featuresService; + let getRecordingCount; + const DELETE_DROPDOWN_SELECTOR = '[data-test-manage-dropdown-delete]'; + const DROPDOWN_SELECTOR = '[data-test-manage-dropdown]'; // Instances const instances = { scopes: { @@ -43,8 +46,14 @@ module( type: 'org', scope: { id: 'global', type: 'global' }, }); + instances.scopes.targetModel = this.server.create('target', { + scope: instances.scopes.global, + }); instances.sessionRecording = this.server.create('session-recording', { scope: instances.scopes.global, + create_time_values: { + target: instances.scopes.targetModel.attrs, + }, }); instances.connectionRecording = this.server.create( 'connection-recording', @@ -59,6 +68,9 @@ module( urls.sessionRecordings = `${urls.globalScope}/session-recordings`; urls.sessionRecording = `${urls.sessionRecordings}/${instances.sessionRecording.id}/channels-by-connection`; urls.channelRecording = `${urls.sessionRecording}/${instances.channelRecording.id}`; + getRecordingCount = () => + this.server.schema.sessionRecordings.all().models.length; + featuresService = this.owner.lookup('service:features'); authenticateSession({}); }); @@ -157,7 +169,7 @@ module( ); await visit(urls.sessionRecording); - assert.dom('[data-test-manage-dropdown]').doesNotExist(); + assert.dom(DROPDOWN_SELECTOR).doesNotExist(); }); test('user can view manage dropdown with proper authorization', async function (assert) { @@ -166,7 +178,34 @@ module( await visit(urls.sessionRecording); - assert.dom('[data-test-manage-dropdown]').isVisible(); + assert.dom(DROPDOWN_SELECTOR).isVisible(); + }); + + test('user can delete a recording with proper authorization', async function (assert) { + // Visit channel + const count = getRecordingCount(); + featuresService.enable('ssh-session-recording'); + assert.true( + instances.sessionRecording.authorized_actions.includes('delete'), + ); + await visit(urls.sessionRecording); + await click(DROPDOWN_SELECTOR); + + await click(DELETE_DROPDOWN_SELECTOR); + assert.strictEqual(currentURL(), urls.sessionRecordings); + + assert.strictEqual(getRecordingCount(), count - 1); + }); + + test('user cannot delete a recording without proper authorization', async function (assert) { + // Visit channel + featuresService.enable('ssh-session-recording'); + instances.sessionRecording.authorized_actions = + instances.sessionRecording.authorized_actions.filter( + (item) => item !== 'delete', + ); + await visit(urls.sessionRecording); + assert.dom(DELETE_DROPDOWN_SELECTOR).doesNotExist(); }); test('both retain until and delete after can be seen with proper authorization', async function (assert) {