diff --git a/addons/api/addon/abilities/storage-bucket.js b/addons/api/addon/abilities/storage-bucket.js new file mode 100644 index 000000000..d521b8ad4 --- /dev/null +++ b/addons/api/addon/abilities/storage-bucket.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import ModelAbility from './model'; + +/** + * Provides abilities for storage buckets. + */ +export default class StorageBucketAbility extends ModelAbility { + // =services + + // =permissions + + /** + * Only "known" storage bucket types may be read. + * @type {boolean} + */ + get canRead() { + return !this.model.isUnknown && this.hasAuthorizedAction('read'); + } + + /** + * Storage buckets can only exist within global and org scopes. + * @type {boolean} + */ + get canCreate() { + return ( + this.hasAuthorizedCollectionAction('create') && + (this.model.isGlobal || this.model.isOrg) + ); + } + + get canList() { + return ( + this.hasAuthorizedCollectionAction('list') && + (this.model.isGlobal || this.model.isOrg) + ); + } +} diff --git a/addons/api/app/abilities/storage-bucket.js b/addons/api/app/abilities/storage-bucket.js new file mode 100644 index 000000000..ed343c8d4 --- /dev/null +++ b/addons/api/app/abilities/storage-bucket.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'api/abilities/storage-bucket'; diff --git a/ui/admin/app/abilities/storage-bucket.js b/ui/admin/app/abilities/storage-bucket.js new file mode 100644 index 000000000..a34d0b9d3 --- /dev/null +++ b/ui/admin/app/abilities/storage-bucket.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import StorageBucketAbility from 'api/abilities/storage-bucket'; +import { inject as service } from '@ember/service'; + +export default class OverrideStorageBucketAbility extends StorageBucketAbility { + // =service + @service features; + + /** + * This override ensures that storage buckets 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 storage buckets can only be presented in the global scope. + */ + get canList() { + return this.hasAuthorizedCollectionAction('list') && this.model.isGlobal; + } + + get canNavigate() { + return this.canCreate || this.canList; + } +} diff --git a/ui/admin/tests/unit/abilities/storage-bucket-test.js b/ui/admin/tests/unit/abilities/storage-bucket-test.js new file mode 100644 index 000000000..3961c60dd --- /dev/null +++ b/ui/admin/tests/unit/abilities/storage-bucket-test.js @@ -0,0 +1,224 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { + TYPE_STORAGE_BUCKET_PLUGIN, + TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, +} from 'api/models/storage-bucket'; + +module('Unit | Abilities | storage-bucket', function (hooks) { + setupTest(hooks); + + let features; + + hooks.beforeEach(function () { + features = this.owner.lookup('service:features'); + features.enable('session-recording'); + }); + + 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'], + type: TYPE_STORAGE_BUCKET_PLUGIN, + compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, + }); + + assert.true(canService.can('read storage-bucket', storageBucket)); + }); + + 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: [], + type: TYPE_STORAGE_BUCKET_PLUGIN, + compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, + }); + + assert.false(canService.can('read storage-bucket', storageBucket)); + }); + + 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', { + authorized_actions: ['read'], + type: TYPE_STORAGE_BUCKET_PLUGIN, + compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, + }); + + assert.false(canService.can('read storage-bucket', storageBucket)); + }); + + 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', { + authorized_actions: [], + type: TYPE_STORAGE_BUCKET_PLUGIN, + compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3, + }); + + assert.false(canService.can('read storage-bucket', storageBucket)); + }); + + 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'] }, + type: 'global', + id: 'global', + }); + + assert.true( + canService.can('list storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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': [] }, + type: 'global', + id: 'global', + }); + + assert.false( + canService.can('list storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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'] }, + type: 'org', + id: 'o_123', + }); + + assert.false( + canService.can('list storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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'] }, + type: 'global', + id: 'global', + }); + + assert.true( + canService.can('create storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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'] }, + type: 'org', + id: 'o_123', + }); + + assert.true( + canService.can('create storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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'] }, + type: 'project', + id: 'p_123', + }); + + assert.false( + canService.can('create storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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': [] }, + type: 'global', + id: 'global', + }); + + assert.false( + canService.can('create storage-bucket', scopeModel, { + collection: 'storage-buckets', + }) + ); + }); + + 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', + }) + ); + }); +});