diff --git a/addons/api/addon/abilities/session-recording.js b/addons/api/addon/abilities/session-recording.js
index 102d48aa6..d0ae91f84 100644
--- a/addons/api/addon/abilities/session-recording.js
+++ b/addons/api/addon/abilities/session-recording.js
@@ -24,4 +24,11 @@ export default class SessionRecordingAbility extends ModelAbility {
get canDownload() {
return this.hasAuthorizedAction('download');
}
+
+ /* Permission that checks whether a policy can be reapplied to a session recording
+ * @type {boolean}
+ */
+ get canReapplyStoragePolicy() {
+ return this.hasAuthorizedAction('reapply-storage-policy');
+ }
}
diff --git a/addons/api/addon/generated/models/channel-recording.js b/addons/api/addon/generated/models/channel-recording.js
index 18a29db9d..4ea51d1c5 100644
--- a/addons/api/addon/generated/models/channel-recording.js
+++ b/addons/api/addon/generated/models/channel-recording.js
@@ -31,7 +31,7 @@ export default class GeneratedChannelRecordingModel extends BaseModel {
'The total number of errors that occurred during the use of this Channel.',
readOnly: true,
})
- errors;
+ errors_number;
@attr('date', {
description: 'The time the Channel started.',
diff --git a/addons/api/addon/generated/models/connection-recording.js b/addons/api/addon/generated/models/connection-recording.js
index bb015f7c5..907f20ab2 100644
--- a/addons/api/addon/generated/models/connection-recording.js
+++ b/addons/api/addon/generated/models/connection-recording.js
@@ -38,7 +38,7 @@ export default class GeneratedConnectionRecordingModel extends BaseModel {
'The total number of errors that occurred during the use of this Connection.',
readOnly: true,
})
- errors;
+ errors_number;
@attr('date', {
description: 'The time the Connection started.',
diff --git a/addons/api/addon/generated/models/session-recording.js b/addons/api/addon/generated/models/session-recording.js
index 391e494a6..d78325880 100644
--- a/addons/api/addon/generated/models/session-recording.js
+++ b/addons/api/addon/generated/models/session-recording.js
@@ -70,7 +70,7 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
'The total number of errors that occurred during the use of the Session.',
readOnly: true,
})
- errors;
+ errors_number;
@attr('date', {
description: 'The time the Session started.',
@@ -109,4 +109,15 @@ export default class GeneratedSessionRecordingModel extends BaseModel {
readOnly: true,
})
error_details;
+
+ @attr('date', {
+ description: 'The time until a session recording is required to be stored.',
+ })
+ retain_until;
+
+ @attr('date', {
+ description:
+ 'The time a session recording is scheduled to be automatically deleted.',
+ })
+ delete_after;
}
diff --git a/addons/api/addon/models/session-recording.js b/addons/api/addon/models/session-recording.js
index 33720091e..df818693c 100644
--- a/addons/api/addon/models/session-recording.js
+++ b/addons/api/addon/models/session-recording.js
@@ -69,4 +69,23 @@ export default class SessionRecordingModel extends GeneratedSessionRecordingMode
this.create_time_values?.user?.name || this.create_time_values?.user?.id
);
}
+
+ /**
+ * Reapply policy dates via the `reapply-staorage-policy` method.
+ * @param {object} options
+ * @param {object} options.adapterOptions
+ * @return {Promise}
+ */
+ reapplyStoragePolicy(options = { adapterOptions: {} }) {
+ const defaultAdapterOptions = {
+ method: 'reapply-storage-policy',
+ };
+ return this.save({
+ ...options,
+ adapterOptions: {
+ ...defaultAdapterOptions,
+ ...options.adapterOptions,
+ },
+ });
+ }
}
diff --git a/addons/api/addon/serializers/channel-recording.js b/addons/api/addon/serializers/channel-recording.js
new file mode 100644
index 000000000..18ffc0743
--- /dev/null
+++ b/addons/api/addon/serializers/channel-recording.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import ApplicationSerializer from './application';
+import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
+
+export default class ChannelSerializer extends ApplicationSerializer.extend(
+ EmbeddedRecordsMixin,
+) {
+ normalize(typeClass, hash, ...rest) {
+ const normalizedHash = structuredClone(hash);
+ const err = normalizedHash.errors;
+ delete normalizedHash.errors;
+ const normalized = super.normalize(typeClass, normalizedHash, ...rest);
+ // we should not override the ember models errors field,
+ // so we rename the errors field from the api
+ normalized.data.attributes.errors_number = err;
+
+ return normalized;
+ }
+}
diff --git a/addons/api/addon/serializers/connection-recording.js b/addons/api/addon/serializers/connection-recording.js
index 4429ed509..ae1711d85 100644
--- a/addons/api/addon/serializers/connection-recording.js
+++ b/addons/api/addon/serializers/connection-recording.js
@@ -12,4 +12,16 @@ export default class ConnectionSerializer extends ApplicationSerializer.extend(
attrs = {
channel_recordings: { embedded: 'always' },
};
+
+ normalize(typeClass, hash, ...rest) {
+ const normalizedHash = structuredClone(hash);
+ const err = normalizedHash.errors;
+ delete normalizedHash.errors;
+ const normalized = super.normalize(typeClass, normalizedHash, ...rest);
+ // we should not override the ember models errors field,
+ // so we rename the errors field from the api
+ normalized.data.attributes.errors_number = err;
+
+ return normalized;
+ }
}
diff --git a/addons/api/addon/serializers/session-recording.js b/addons/api/addon/serializers/session-recording.js
index 434d92951..85f8578cc 100644
--- a/addons/api/addon/serializers/session-recording.js
+++ b/addons/api/addon/serializers/session-recording.js
@@ -11,4 +11,26 @@ export default class RecordingSerializer extends ApplicationSerializer.extend(
attrs = {
connection_recordings: { embedded: 'always' },
};
+
+ serialize(snapshot) {
+ let serialized = super.serialize(...arguments);
+ // The post request for reapply-storage-policy doesn't expect a payload,
+ // so we return nothing
+ if (snapshot?.adapterOptions?.method === 'reapply-storage-policy') {
+ return {};
+ }
+ return serialized;
+ }
+
+ normalize(typeClass, hash, ...rest) {
+ const normalizedHash = structuredClone(hash);
+ const err = normalizedHash.errors;
+ delete normalizedHash.errors;
+ const normalized = super.normalize(typeClass, normalizedHash, ...rest);
+ // we should not override the ember models errors field,
+ // so we rename the errors field from the api
+ normalized.data.attributes.errors_number = err;
+
+ return normalized;
+ }
}
diff --git a/addons/api/app/serializers/channel-recording.js b/addons/api/app/serializers/channel-recording.js
new file mode 100644
index 000000000..52fdc72ab
--- /dev/null
+++ b/addons/api/app/serializers/channel-recording.js
@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+export { default } from 'api/serializers/channel-recording';
diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index 1e57fb989..0fd3244a9 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -780,6 +780,24 @@ function routes() {
},
);
+ this.post(
+ '/session-recordings/:idMethod',
+ function ({ sessionRecordings }, { params: { idMethod } }) {
+ const attrs = this.normalizedRequestAttrs();
+ const id = idMethod.split(':')[0];
+ const method = idMethod.split(':')[1];
+ const record = sessionRecordings.find(id);
+ const updatedAttrs = {
+ version: attrs.version,
+ };
+ if (method === 'reapply-storage-policy') {
+ updatedAttrs.retain_until = faker.date.recent();
+ updatedAttrs.delete_after = faker.date.recent();
+ }
+ return record.update(updatedAttrs);
+ },
+ );
+
/* 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 d198fecb0..ea81b3c8c 100644
--- a/addons/api/mirage/factories/session-recording.js
+++ b/addons/api/mirage/factories/session-recording.js
@@ -43,11 +43,23 @@ export default factory.extend({
: null;
},
+ retain_until() {
+ return this.state === STATE_SESSION_RECORDING_AVAILABLE
+ ? faker.date.recent()
+ : null;
+ },
+ delete_after() {
+ return this.state === STATE_SESSION_RECORDING_AVAILABLE
+ ? faker.date.recent()
+ : null;
+ },
+
authorized_actions: () =>
permissions.authorizedActionsFor('session-recording') || [
'no-op',
'read',
'download',
+ 'reapply-storage-policy',
],
withNonExistingUserAndTarget: trait({
diff --git a/addons/api/tests/unit/serializers/session-recording-test.js b/addons/api/tests/unit/serializers/session-recording-test.js
index e95f84f2b..f7e47b867 100644
--- a/addons/api/tests/unit/serializers/session-recording-test.js
+++ b/addons/api/tests/unit/serializers/session-recording-test.js
@@ -25,4 +25,45 @@ module('Unit | Serializer | session-recording', function (hooks) {
assert.ok(serializedRecord);
});
+
+ test('it serializes only version when `adapterOptions.method` is `reapply-storage-policy`', function (assert) {
+ const store = this.owner.lookup('service:store');
+ const serializer = store.serializerFor('session-recording');
+ const record = store.createRecord('session-recording', {
+ version: 1,
+ retain_until: 1,
+ delete_after: 2,
+ });
+ const snapshot = record._createSnapshot();
+ snapshot.adapterOptions = {
+ method: 'reapply-storage-policy',
+ };
+ const serializedRecord = serializer.serialize(snapshot);
+ assert.deepEqual(serializedRecord, {});
+ });
+
+ test('it normalizes correctly', function (assert) {
+ let store = this.owner.lookup('service:store');
+ const serializer = store.serializerFor('session-recording');
+
+ let record = store.createRecord('session-recording').constructor;
+ const payload = {
+ id: '1',
+ type: 'session-recording',
+ errors: 2,
+ };
+ const normalized = serializer.normalize(record, payload);
+
+ assert.deepEqual(normalized, {
+ data: {
+ id: '1',
+ type: 'session-recording',
+ attributes: {
+ errors_number: 2,
+ type: 'session-recording',
+ },
+ relationships: {},
+ },
+ });
+ });
});
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 7e9325c6f..28ad32546 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -937,15 +937,18 @@ policy:
days: days
policy_description: Store recorded sessions for at least this long.
create_policy_description: Enable session recording for projects in this org.
+ retain_until: Retain until
actions:
delete: Delete Storage Policy
add: Add Storage Policy
apply: Apply the storage policy to this scope and its children
new: Add a new storage policy
+ update: Re-apply storage policy
questions:
detach: Are you sure you want to detach this storage policy?
messages:
detach: Successfully detached
+ reapply: Successfully reapplied
none:
title: No storage policies yet
description: Storage policies define how long to store recordings before they are automatically deleted. We recommend checking your data retention policies for compliance requirements.
diff --git a/addons/rose/app/styles/rose/components/metadata-list/_index.scss b/addons/rose/app/styles/rose/components/metadata-list/_index.scss
index 82273979c..e4745947d 100644
--- a/addons/rose/app/styles/rose/components/metadata-list/_index.scss
+++ b/addons/rose/app/styles/rose/components/metadata-list/_index.scss
@@ -18,5 +18,6 @@
display: flex;
align-items: center;
column-gap: 0.5rem;
+ font-size: var(--token-typography-body-200-font-size);
}
}
diff --git a/ui/admin/app/abilities/session-recording.js b/ui/admin/app/abilities/session-recording.js
index 6f5164444..130e68c66 100644
--- a/ui/admin/app/abilities/session-recording.js
+++ b/ui/admin/app/abilities/session-recording.js
@@ -19,4 +19,14 @@ export default class OverrideSessionRecordingAbility extends SessionRecordingAbi
? super.canRead
: false;
}
+
+ /**
+ * Reapplying policy is allowed only if the feature flag is enabled
+ * @type {boolean}
+ */
+ get canReapplyStoragePolicy() {
+ return this.features.isEnabled('ssh-session-recording')
+ ? this.hasAuthorizedAction('reapply-storage-policy')
+ : false;
+ }
}
diff --git a/ui/admin/app/components/form/scope/add-storage-policy/create.hbs b/ui/admin/app/components/form/scope/add-storage-policy/create.hbs
new file mode 100644
index 000000000..e5709b1c2
--- /dev/null
+++ b/ui/admin/app/components/form/scope/add-storage-policy/create.hbs
@@ -0,0 +1,31 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: MPL-2.0
+}}
+
+{{page-title (t 'titles.new')}}
+