Merge remote-tracking branch 'origin/session-recording' into feat-use-mirage-for-recordings-navigation
This commit is contained in:
commit
f2cd8ec742
26 files changed
+620
-69
No files matched your search
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import ModelAbility from './model';
|
||||
|
||||
/**
|
||||
* Provides abilities for session recordings.
|
||||
*/
|
||||
export default class RecordingAbility extends ModelAbility {
|
||||
// =services
|
||||
|
||||
// =permissions
|
||||
|
||||
/**
|
||||
* Only "known" session recording types may be read.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get canRead() {
|
||||
return !this.model.isUnknown && this.hasAuthorizedAction('read');
|
||||
}
|
||||
|
||||
get canList() {
|
||||
return (
|
||||
this.hasAuthorizedCollectionAction('list') &&
|
||||
(this.model.isGlobal || this.model.isOrg)
|
||||
);
|
||||
}
|
||||
|
||||
get canDownloadAsciiCast() {
|
||||
return this.model.isSSH && this.hasAuthorizedAction('download');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import ApplicationAdapter from './application';
|
||||
|
||||
export default class StorageBucketAdapter extends ApplicationAdapter {
|
||||
/**
|
||||
* Overrides the default `buildURL` such that on _create_, URLs for records of
|
||||
* type `plugin` get an extra query parameter `?plugin_name`. All other
|
||||
* request types behave as normal.
|
||||
*
|
||||
* @override
|
||||
* @param {string} modelName
|
||||
* @param {string} id
|
||||
* @return {string} url
|
||||
* @return {string} requestType
|
||||
*/
|
||||
buildURL(modelName, id, snapshot, requestType) {
|
||||
let url = super.buildURL(...arguments);
|
||||
const pluginName = snapshot?.attr('plugin')?.name;
|
||||
if (requestType === 'createRecord' && pluginName) {
|
||||
url = `${url}?plugin_name=${pluginName}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export default class GeneratedStorageBucketModel extends BaseModel {
|
||||
readOnly: true,
|
||||
isNestedAttribute: true,
|
||||
})
|
||||
client_secret_hmac;
|
||||
secrets_hmac;
|
||||
|
||||
@attr('string', {
|
||||
isNestedSecret: true,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class StorageBucketSerializer extends ApplicationSerializer {
|
||||
normalize(typeClass, hash, ...rest) {
|
||||
const normalizedHash = structuredClone(hash);
|
||||
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
|
||||
|
||||
// Remove secret fields
|
||||
delete normalized.data.attributes.access_key_id;
|
||||
delete normalized.data.attributes.secret_access_key;
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Ability } from 'ember-can';
|
||||
|
||||
export default class extends Ability {}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/adapters/storage-bucket';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/storage-bucket';
|
||||
@@ -14,6 +14,7 @@ import { targetHandler } from './route-handlers/target';
|
||||
import { pickRandomStatusString } from './factories/session';
|
||||
import initializeMockIPC from './scenarios/ipc';
|
||||
import makeBooleanFilter from './helpers/bexpr-filter';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
const isTesting = environmentConfig.environment === 'test';
|
||||
|
||||
@@ -668,7 +669,7 @@ function routes() {
|
||||
this.patch('/storage-buckets/:id');
|
||||
this.post(
|
||||
'/storage-buckets',
|
||||
({ storageBuckets }, { queryParams: { plugin_name } }) => {
|
||||
function ({ storageBuckets }, { queryParams: { plugin_name } }) {
|
||||
const attrs = this.normalizedRequestAttrs();
|
||||
if (plugin_name) {
|
||||
attrs.type = 'plugin';
|
||||
@@ -676,6 +677,11 @@ function routes() {
|
||||
name: plugin_name,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove the secrets and add the hmac in the response
|
||||
delete attrs.secrets;
|
||||
attrs.secrets_hmac = faker.git.commitSha();
|
||||
|
||||
return storageBuckets.create(attrs);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,7 +18,11 @@ export default factory.extend({
|
||||
type: (i) => types[i % types.length],
|
||||
|
||||
authorized_actions: () =>
|
||||
permissions.authorizedActionsFor('recording') || ['no-op', 'read', 'list'],
|
||||
permissions.authorizedActionsFor('recording') || [
|
||||
'no-op',
|
||||
'read',
|
||||
'download',
|
||||
],
|
||||
|
||||
withNonExistingUserAndTarget: trait({
|
||||
afterCreate(recording, server) {
|
||||
|
||||
@@ -37,8 +37,10 @@ export default factory.extend({
|
||||
collectionActions.workers = ['create:worker-led', 'list'];
|
||||
}
|
||||
|
||||
// Session recording resources are only available on global or org scope
|
||||
if (this.type === 'global' || this.type === 'org') {
|
||||
collectionActions['storage-buckets'] = ['create', 'list'];
|
||||
collectionActions.recordings = ['list'];
|
||||
}
|
||||
|
||||
return collectionActions;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Adapter | storage bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it generates correct createRecord URLs for storage buckets with extra query parameter ?plugin_name', function (assert) {
|
||||
assert.expect(1);
|
||||
const scopeID = 'o_1';
|
||||
const mockSnapshot = {
|
||||
adapterOptions: {
|
||||
scopeID,
|
||||
},
|
||||
attr() {
|
||||
return { name: 'aws_s3' };
|
||||
},
|
||||
};
|
||||
const adapter = this.owner.lookup('adapter:storage-bucket');
|
||||
const createRecordURL = adapter.buildURL(
|
||||
'storage-bucket',
|
||||
null,
|
||||
mockSnapshot,
|
||||
'createRecord'
|
||||
);
|
||||
assert.strictEqual(
|
||||
createRecordURL,
|
||||
'/v1/storage-buckets?plugin_name=aws_s3'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
} from 'api/models/storage-bucket';
|
||||
|
||||
module('Unit | Serializer | storage bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('storage-bucket', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
|
||||
test('it serializes a new aws plugin as expected', async function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const record = store.createRecord('storage-bucket', {
|
||||
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
region: 'eu-west-1',
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
secrets_hmac: 'hmac',
|
||||
});
|
||||
const expectedResult = {
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
secrets: {
|
||||
access_key_id: 'foobars',
|
||||
secret_access_key: 'testing',
|
||||
},
|
||||
};
|
||||
assert.deepEqual(record.serialize(), expectedResult);
|
||||
});
|
||||
|
||||
test('it normalizes correctly', function (assert) {
|
||||
assert.expect(1);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('storage-bucket');
|
||||
const hostSetModelClass = store.createRecord('storage-bucket').constructor;
|
||||
const payload = {
|
||||
id: '1',
|
||||
version: 1,
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
attributes: {
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
secrets_hmac: 'this is secret',
|
||||
};
|
||||
const normalized = serializer.normalizeSingleResponse(
|
||||
store,
|
||||
hostSetModelClass,
|
||||
payload
|
||||
);
|
||||
|
||||
assert.deepEqual(normalized, {
|
||||
included: [],
|
||||
data: {
|
||||
id: '1',
|
||||
attributes: {
|
||||
type: TYPE_STORAGE_BUCKET_PLUGIN,
|
||||
version: 1,
|
||||
authorized_actions: [],
|
||||
name: 'AWS',
|
||||
description: 'this has an aws plugin',
|
||||
bucket_name: 'bucketname',
|
||||
bucket_prefix: 'bucketprefix',
|
||||
worker_filter: 'workerfilter',
|
||||
secrets_hmac: 'this is secret',
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
type: 'storage-bucket',
|
||||
relationships: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import DurationUnitFormat from 'intl-unofficial-duration-unit-format';
|
||||
|
||||
export default class extends Helper {
|
||||
// =services
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
/**
|
||||
*
|
||||
* @param {number} durationInMs Duration in milliseconds
|
||||
* @returns localized human-readable duration string. For example, in English "1 hr 23 min 20 sec"
|
||||
*/
|
||||
compute([durationInMs]) {
|
||||
const durationInSeconds = durationInMs / 1000;
|
||||
const durationFormat = new DurationUnitFormat(this.intl.primaryLocale, {
|
||||
style: 'narrow',
|
||||
format: '{days} {hours} {minutes} {seconds}',
|
||||
round: true,
|
||||
});
|
||||
|
||||
return durationFormat.format(durationInSeconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/helpers/format-time-duration';
|
||||
@@ -40,7 +40,8 @@
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-intl": "^6.0.0-beta.4",
|
||||
"ember-loading": "^2.0.0",
|
||||
"ember-truth-helpers": "^3.0.0"
|
||||
"ember-truth-helpers": "^3.0.0",
|
||||
"intl-unofficial-duration-unit-format": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.18.2",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'dummy/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
|
||||
module('Integration | Helper | format-time-duration', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
test('It renders in human-readable duration strings given different durations in milliseconds', async function (assert) {
|
||||
// Set mock input (in milliseconds) durations
|
||||
this.set('inputDurationSeconds', 3000);
|
||||
this.set('inputDurationMinutes', 2700000);
|
||||
this.set('inputDurationHours', 5000000);
|
||||
this.set('inputDurationDays', 177800000);
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationSeconds}}`);
|
||||
assert.dom(this.element).hasText('3s');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationMinutes}}`);
|
||||
assert.dom(this.element).hasText('45m');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationHours}}`);
|
||||
assert.dom(this.element).hasText('1h 23m 20s');
|
||||
|
||||
await render(hbs`{{format-time-duration this.inputDurationDays}}`);
|
||||
assert.dom(this.element).hasText('2d 1h 23m 20s');
|
||||
});
|
||||
});
|
||||
@@ -16,3 +16,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hds-table {
|
||||
border-collapse: revert;
|
||||
}
|
||||
|
||||
.hds-dropdown {
|
||||
.hds-dropdown-toggle-icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,23 @@
|
||||
|
||||
@import 'color-tokens-tier-2';
|
||||
|
||||
// HDS Light Mode
|
||||
// This is the default HDS color theme. Only Boundary custom tokens
|
||||
// are included here for completeness.
|
||||
:root {
|
||||
// Tier-1 (Boundary custom)
|
||||
// Boundary custom first-tier color tokens with alpha.
|
||||
// In HDS, these correspond to the named color token,
|
||||
// with the alpha identifier. However, they do not have stand-alone tokens
|
||||
// associated with them and appear hard-coded alongside other styles.
|
||||
// They are pulled out here for easier theming.
|
||||
--token-color-palette-neutral-600-alpha-200: #3b3d4559;
|
||||
--token-color-palette-neutral-600-alpha-100: #3b3d4540;
|
||||
--token-color-palette-neutral-500-alpha-200: #656a7640;
|
||||
--token-color-palette-neutral-500-alpha-100: #656a7626;
|
||||
--token-color-palette-neutral-500-alpha-50: #656a760d;
|
||||
}
|
||||
|
||||
// HDS Dark Mode
|
||||
// This theme reverses the lightness order of shades per color for tier-1
|
||||
// color tokens. It also provides custom tier-2 mapping overrides that
|
||||
@@ -87,23 +104,6 @@
|
||||
--token-form-indicator-optional-color: var(--token-color-palette-neutral-200);
|
||||
}
|
||||
|
||||
// HDS Light Mode
|
||||
// This is the default HDS color theme. Only Boundary custom tokens
|
||||
// are included here for completeness.
|
||||
@mixin light-mode {
|
||||
// Tier-1 (Boundary custom)
|
||||
// Boundary custom first-tier color tokens with alpha.
|
||||
// In HDS, these correspond to the named color token,
|
||||
// with the alpha identifier. However, they do not have stand-alone tokens
|
||||
// associated with them and appear hard-coded alongside other styles.
|
||||
// They are pulled out here for easier theming.
|
||||
--token-color-palette-neutral-600-alpha-200: #3b3d4559;
|
||||
--token-color-palette-neutral-600-alpha-100: #3b3d4540;
|
||||
--token-color-palette-neutral-500-alpha-200: #656a7640;
|
||||
--token-color-palette-neutral-500-alpha-100: #656a7626;
|
||||
--token-color-palette-neutral-500-alpha-50: #656a760d;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ember-application:not(.rose-theme-light) {
|
||||
@include dark-mode;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import RecordingAbility from 'api/abilities/recording';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OverrideRecordingAbility extends RecordingAbility {
|
||||
// =service
|
||||
@service features;
|
||||
|
||||
/**
|
||||
* This override ensures that session recordings may be read only if the
|
||||
* session-recording feature flag is enabled.
|
||||
*/
|
||||
get canRead() {
|
||||
return this.features.isEnabled('session-recording') ? super.canRead : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This override ensures that session recordings can only be presented in the global scope
|
||||
* and if the session-recording feature flag is enabled. */
|
||||
get canList() {
|
||||
return this.features.isEnabled('session-recording')
|
||||
? this.hasAuthorizedCollectionAction('list') && this.model.isGlobal
|
||||
: false;
|
||||
}
|
||||
|
||||
get canNavigate() {
|
||||
return this.canList;
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,13 @@ export default class OverrideStorageBucketAbility extends StorageBucketAbility {
|
||||
}
|
||||
|
||||
/**
|
||||
* This override ensures that storage buckets can only be presented in the global scope.
|
||||
* This override ensures that storage buckets can only be presented in the global scope
|
||||
* and if the session-recording feature flag is enabled.
|
||||
*/
|
||||
get canList() {
|
||||
return this.hasAuthorizedCollectionAction('list') && this.model.isGlobal;
|
||||
return this.features.isEnabled('session-recording')
|
||||
? this.hasAuthorizedCollectionAction('list') && this.model.isGlobal
|
||||
: false;
|
||||
}
|
||||
|
||||
get canNavigate() {
|
||||
|
||||
@@ -776,7 +776,3 @@
|
||||
'sidebar';
|
||||
}
|
||||
}
|
||||
|
||||
.storage-bucket-list {
|
||||
border-collapse: revert;
|
||||
}
|
||||
@@ -4,15 +4,16 @@
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
{{#each @model as |recording|}}
|
||||
<p>
|
||||
<LinkTo
|
||||
@route='scopes.scope.session-recordings.session-recording'
|
||||
@model={{recording.id}}
|
||||
>
|
||||
{{recording.id}}
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/each}}
|
||||
<LinkTo
|
||||
@route='scopes.scope.session-recordings.session-recording'
|
||||
@model={{@model.session_recording_id}}
|
||||
>
|
||||
{{@model.session_recording_id}}
|
||||
</LinkTo>
|
||||
<br /><br />
|
||||
<p>{{format-time-duration 3000}}</p>
|
||||
<p>{{format-time-duration 2700000}}</p>
|
||||
<p>{{format-time-duration 5000000}}</p>
|
||||
<p>{{format-time-duration 177800000}}</p>
|
||||
</page.body>
|
||||
</Rose::Layout::Page>
|
||||
@@ -32,7 +32,6 @@
|
||||
<page.body>
|
||||
{{#if @model}}
|
||||
<Hds::Table
|
||||
class='storage-bucket-list'
|
||||
@valign='middle'
|
||||
@model={{@model}}
|
||||
@columns={{array
|
||||
@@ -93,7 +92,20 @@
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{!TODO: add dropdown for delete and edit actions}}
|
||||
{{!TODO: add dropdown for delete actions}}
|
||||
<Hds::Dropdown as |dd|>
|
||||
<dd.ToggleIcon
|
||||
@icon='more-horizontal'
|
||||
@text='Overflow Options'
|
||||
@hasChevron={{false}}
|
||||
@size='small'
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.storage-buckets.storage-bucket'
|
||||
@model={{B.data.id}}
|
||||
@text={{t 'actions.edit'}}
|
||||
/>
|
||||
</Hds::Dropdown>
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { TYPE_RECORDING_SSH } from 'api/models/recording';
|
||||
|
||||
module('Unit | Abilities | session-recording', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let features;
|
||||
let canService;
|
||||
let store;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
features = this.owner.lookup('service:features');
|
||||
features.enable('session-recording');
|
||||
canService = this.owner.lookup('service:can');
|
||||
store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('can read session recording feature is enabled', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const recordingWithAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: ['read'],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
const recordingWithoutAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: [],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can('read recording', recordingWithAuthorizedAction)
|
||||
);
|
||||
assert.false(
|
||||
canService.can('read recording', recordingWithoutAuthorizedAction)
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot read session recording when feature is disabled', function (assert) {
|
||||
assert.expect(2);
|
||||
features.disable('session-recording');
|
||||
|
||||
const recordingWithAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: ['read'],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
const recordingWithoutAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: [],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
|
||||
assert.false(
|
||||
canService.can('read recording', recordingWithAuthorizedAction)
|
||||
);
|
||||
assert.false(
|
||||
canService.can('read recording', recordingWithoutAuthorizedAction)
|
||||
);
|
||||
});
|
||||
|
||||
test('can list session recording when in global scope', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const scopeModelWithAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: ['list'] },
|
||||
type: 'global',
|
||||
id: 'first',
|
||||
});
|
||||
const scopeModelWithoutAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: [] },
|
||||
type: 'global',
|
||||
id: 'second',
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can('list recording', scopeModelWithAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
assert.false(
|
||||
canService.can('list recording', scopeModelWithoutAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot list session recording when in org scope', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const scopeModelWithAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: ['list'] },
|
||||
type: 'org',
|
||||
id: 'first',
|
||||
});
|
||||
const scopeModelWithoutAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: [] },
|
||||
type: 'org',
|
||||
id: 'second',
|
||||
});
|
||||
|
||||
assert.false(
|
||||
canService.can('list recording', scopeModelWithAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
assert.false(
|
||||
canService.can('list recording', scopeModelWithoutAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('can navigate to session recording when feature is enabled', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const scopeModelWithAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: ['list'] },
|
||||
type: 'global',
|
||||
id: 'first',
|
||||
});
|
||||
const scopeModelWithoutAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: [] },
|
||||
type: 'global',
|
||||
id: 'second',
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can('navigate recording', scopeModelWithAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
assert.false(
|
||||
canService.can('navigate recording', scopeModelWithoutAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot navigate to session recording when feature is disabled', function (assert) {
|
||||
assert.expect(2);
|
||||
features.disable('session-recording');
|
||||
|
||||
const scopeModelWithAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: ['list'] },
|
||||
type: 'global',
|
||||
id: 'first',
|
||||
});
|
||||
const scopeModelWithoutAuthorizedAction = store.createRecord('scope', {
|
||||
authorized_collection_actions: { recordings: [] },
|
||||
type: 'global',
|
||||
id: 'second',
|
||||
});
|
||||
|
||||
assert.false(
|
||||
canService.can('navigate recording', scopeModelWithAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
assert.false(
|
||||
canService.can('navigate recording', scopeModelWithoutAuthorizedAction, {
|
||||
collection: 'recordings',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('can download asciicast when an ssh recording has download authorization', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const recordingWithAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: ['download'],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
const recordingWithoutAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: [],
|
||||
type: TYPE_RECORDING_SSH,
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can(
|
||||
'downloadAsciiCast recording',
|
||||
recordingWithAuthorizedAction
|
||||
)
|
||||
);
|
||||
assert.false(
|
||||
canService.can(
|
||||
'downloadAsciiCast recording',
|
||||
recordingWithoutAuthorizedAction
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot download asciicast when a recording is not ssh type', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const recordingWithAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: ['download'],
|
||||
type: 'unknown',
|
||||
});
|
||||
const recordingWithoutAuthorizedAction = store.createRecord('recording', {
|
||||
authorized_actions: [],
|
||||
type: 'unknown',
|
||||
});
|
||||
|
||||
assert.false(
|
||||
canService.can(
|
||||
'downloadAsciiCast recording',
|
||||
recordingWithAuthorizedAction
|
||||
)
|
||||
);
|
||||
assert.false(
|
||||
canService.can(
|
||||
'downloadAsciiCast recording',
|
||||
recordingWithoutAuthorizedAction
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,16 +14,18 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let features;
|
||||
let canService;
|
||||
let store;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
features = this.owner.lookup('service:features');
|
||||
features.enable('session-recording');
|
||||
canService = this.owner.lookup('service:can');
|
||||
store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('can read storage bucket when authorized and feature is enabled', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const storageBucket = store.createRecord('storage-bucket', {
|
||||
authorized_actions: ['read'],
|
||||
@@ -36,8 +38,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot read storage bucket when unauthorized and feature is enabled', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const storageBucket = store.createRecord('storage-bucket', {
|
||||
authorized_actions: [],
|
||||
@@ -50,8 +50,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot read storage bucket when authorized and feature is disabled', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
features.disable('session-recording');
|
||||
|
||||
const storageBucket = store.createRecord('storage-bucket', {
|
||||
@@ -65,8 +63,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot read storage bucket when unauthorized and feature is disabled', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
features.disable('session-recording');
|
||||
|
||||
const storageBucket = store.createRecord('storage-bucket', {
|
||||
@@ -80,8 +76,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('can list storage bucket when authorized and in global scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['list'] },
|
||||
@@ -98,8 +92,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot list storage bucket when unauthorized and in global scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': [] },
|
||||
@@ -116,8 +108,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot list storage bucket when authorized and in org scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['list'] },
|
||||
@@ -134,8 +124,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('can create storage bucket when authorized and in global scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['create'] },
|
||||
@@ -152,8 +140,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('can create storage bucket when authorized and in org scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['create'] },
|
||||
@@ -170,8 +156,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot create storage bucket when authorized and in project scope', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['create'] },
|
||||
@@ -188,8 +172,6 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('cannot create storage bucket when unauthorized', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': [] },
|
||||
@@ -206,8 +188,23 @@ module('Unit | Abilities | storage-bucket', function (hooks) {
|
||||
|
||||
test('can navigate to storage bucket when authorized', function (assert) {
|
||||
assert.expect(1);
|
||||
const canService = this.owner.lookup('service:can');
|
||||
const store = this.owner.lookup('service:store');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['create'] },
|
||||
type: 'global',
|
||||
id: 'global',
|
||||
});
|
||||
|
||||
assert.true(
|
||||
canService.can('navigate storage-bucket', scopeModel, {
|
||||
collection: 'storage-buckets',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot navigate to storage bucket when authorized and feature is disabled', function (assert) {
|
||||
assert.expect(1);
|
||||
features.disable('session-recording');
|
||||
|
||||
const scopeModel = store.createRecord('scope', {
|
||||
authorized_collection_actions: { 'storage-buckets': ['create'] },
|
||||
|
||||
@@ -10551,9 +10551,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001449:
|
||||
version "1.0.30001458"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz#871e35866b4654a7d25eccca86864f411825540c"
|
||||
integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==
|
||||
version "1.0.30001466"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001466.tgz#c1e6197c540392e09709ecaa9e3e403428c53375"
|
||||
integrity sha512-ewtFBSfWjEmxUgNBSZItFSmVtvk9zkwkl1OfRZlKA8slltRN+/C/tuGVrF9styXkN36Yu3+SeJ1qkXxDEyNZ5w==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -17269,6 +17269,11 @@ intl-messageformat@10.2.1, intl-messageformat@^10.1.0:
|
||||
"@formatjs/icu-messageformat-parser" "2.1.10"
|
||||
tslib "2.4.0"
|
||||
|
||||
intl-unofficial-duration-unit-format@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/intl-unofficial-duration-unit-format/-/intl-unofficial-duration-unit-format-3.1.0.tgz#994feefc8d903d9e5044dc7e4e1d38ebad59394b"
|
||||
integrity sha512-yjvUDwV63zSGkbeUqpDRJmOgMQoJA8OFhNeo+1KY20q+lFCtcvRWszJNsq/ssrUJRy8g+Mt3BYNfwFvRRSAkow==
|
||||
|
||||
invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
||||
@@ -25706,9 +25711,9 @@ terser-webpack-plugin@^5.1.3:
|
||||
terser "^5.7.2"
|
||||
|
||||
terser@^4.1.2, terser@^4.6.3, terser@^5.14.2, terser@^5.3.0, terser@^5.3.4, terser@^5.7.2:
|
||||
version "5.16.5"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a"
|
||||
integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg==
|
||||
version "5.16.6"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.6.tgz#f6c7a14a378ee0630fbe3ac8d1f41b4681109533"
|
||||
integrity sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
|
||||
Reference in new issue
Block a user