diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index 58115593a..82c9828f4 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -81,6 +81,32 @@ export default class RoleModel extends GeneratedRoleModel { return grantScopes; } + /** + * Helper for retrieving grant scope ids that are keywords. + * @type {string[]} + */ + get grantScopeKeywords() { + return this.grant_scope_ids.filter((id) => + GRANT_SCOPE_KEYWORDS.includes(id), + ); + } + + /** + * Helper for retrieving grant_scope_ids that are orgs. + * @type {string[]} + */ + get grantScopeOrgIDs() { + return this.grant_scope_ids.filter((id) => id.startsWith('o_')); + } + + /** + * Helper for retrieving grant_scope_ids that are projects. + * @type {string[]} + */ + get grantScopeProjectIDs() { + return this.grant_scope_ids.filter((id) => id.startsWith('p_')); + } + /** * A list of IDs for principals of type `user`. * @type {string[]} diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js index edafa28e4..663675302 100644 --- a/addons/api/tests/unit/models/role-test.js +++ b/addons/api/tests/unit/models/role-test.js @@ -317,4 +317,64 @@ module('Unit | Model | role', function (hooks) { assert.strictEqual(grantScopes[2].id, orgScope.id); assert.strictEqual(grantScopes[3].id, projectScope.id); }); + + test('grantScopeKeywords returns an array with only keywords in grant_scope_ids', async function (assert) { + const store = this.owner.lookup('service:store'); + const orgScope = store.createRecord('scope', { id: 'o_123' }); + const projectScope = store.createRecord('scope', { id: 'p_123' }); + const globalScope = store.createRecord('scope', { id: 'global' }); + const role = store.createRecord('role', { + grant_scope_ids: [ + projectScope.id, + orgScope.id, + GRANT_SCOPE_THIS, + globalScope.id, + ], + }); + + const grantScopeKeywords = role.grantScopeKeywords; + + assert.strictEqual(grantScopeKeywords.length, 1); + assert.strictEqual(grantScopeKeywords[0], GRANT_SCOPE_THIS); + }); + + test('grantScopeOrgIDs returns an array with only org ids in grant_scope_ids', async function (assert) { + const store = this.owner.lookup('service:store'); + const orgScope = store.createRecord('scope', { id: 'o_123' }); + const projectScope = store.createRecord('scope', { id: 'p_123' }); + const globalScope = store.createRecord('scope', { id: 'global' }); + const role = store.createRecord('role', { + grant_scope_ids: [ + projectScope.id, + orgScope.id, + GRANT_SCOPE_THIS, + globalScope.id, + ], + }); + + const grantScopeOrgIDs = role.grantScopeOrgIDs; + + assert.strictEqual(grantScopeOrgIDs.length, 1); + assert.strictEqual(grantScopeOrgIDs[0], orgScope.id); + }); + + test('grantScopeProjectIDs returns an array with only project ids in grant_scope_ids', async function (assert) { + const store = this.owner.lookup('service:store'); + const orgScope = store.createRecord('scope', { id: 'o_123' }); + const projectScope = store.createRecord('scope', { id: 'p_123' }); + const globalScope = store.createRecord('scope', { id: 'global' }); + const role = store.createRecord('role', { + grant_scope_ids: [ + projectScope.id, + orgScope.id, + GRANT_SCOPE_THIS, + globalScope.id, + ], + }); + + const grantScopeProjectIDs = role.grantScopeProjectIDs; + + assert.strictEqual(grantScopeProjectIDs.length, 1); + assert.strictEqual(grantScopeProjectIDs[0], projectScope.id); + }); }); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 68d20e91a..ff864c25a 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -570,6 +570,8 @@ role: titles: parent_scope: Parent scope resource_type: Resource type + projects_selected: Projects selected + selected: '{items} selected out of {total}' messages: none: title: No scopes added @@ -581,6 +583,12 @@ role: keywords-selected: description: Just a heads up, you can't select both add all descendants and add all children. You can only choose one or the other. link: You can read more about it here + manage-custom-scopes: + title: Manage Custom Scopes + description: Select which specific orgs you want to apply to this role's grant sets. + no-org-scopes: + title: No Org Scopes Available + description: Org scopes are not available for assignment. keywords: this: sub-title: This scope diff --git a/addons/rose/addon/components/rose/header/brand/index.hbs b/addons/rose/addon/components/rose/header/brand/index.hbs index 88b5162d2..3a4078c5e 100644 --- a/addons/rose/addon/components/rose/header/brand/index.hbs +++ b/addons/rose/addon/components/rose/header/brand/index.hbs @@ -7,7 +7,8 @@ ...attributes class='rose-header-brand' title={{@text}} - @route={{if @route @route 'index'}} + @route={{if @route @route 'scopes.scope'}} + @model={{if @model @model 'global'}} > {{#if @logo}} diff --git a/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs new file mode 100644 index 000000000..a36217400 --- /dev/null +++ b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs @@ -0,0 +1,109 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + +{{#if @model.totalItemsCount}} + + + + + {{#if @model.subScopes}} + {{#if this.selectedItems.length}} + + + {{t + 'resources.role.scope.titles.selected' + items=this.selectedItems.length + total=@model.totalItemsCount + }} + + + {{/if}} + + <:body as |B|> + + + + {{B.data.displayName}} + + + + + + + {{! TODO: Add link to projects selected page}} + + + + + + + + {{else}} + + + + + {{/if}} + +{{else}} + + + + + + + +{{/if}} \ No newline at end of file diff --git a/ui/admin/app/components/form/role/manage-custom-scopes/index.js b/ui/admin/app/components/form/role/manage-custom-scopes/index.js new file mode 100644 index 000000000..c8db41f32 --- /dev/null +++ b/ui/admin/app/components/form/role/manage-custom-scopes/index.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class FormRoleManageCustomScopesIndexComponent extends Component { + // =attributes + + @tracked selectedItems = [...this.args.model.role.grantScopeOrgIDs]; + + get allGrantScopes() { + return [ + ...this.args.model.role.grantScopeKeywords, + ...this.args.model.role.grantScopeProjectIDs, + ...this.selectedItems, + ]; + } + + // =actions + + /** + * Handles the selection changes for the paginated table. + * @param {object} selectableRowsStates + */ + @action + selectionChange({ selectableRowsStates }) { + selectableRowsStates.forEach((row) => { + const { isSelected, selectionKey: key } = row; + const includesId = this.selectedItems.includes(key); + if (isSelected) { + if (!includesId) this.selectedItems = [...this.selectedItems, key]; + } else if (includesId) { + this.selectedItems = this.selectedItems.filter( + (item) => item !== key, + ); + } + }); + } +} diff --git a/ui/admin/app/components/form/role/manage-scopes/index.hbs b/ui/admin/app/components/form/role/manage-scopes/index.hbs index e49f03980..b5e7afdb3 100644 --- a/ui/admin/app/components/form/role/manage-scopes/index.hbs +++ b/ui/admin/app/components/form/role/manage-scopes/index.hbs @@ -1,3 +1,8 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + message, { catch: true }) + @notifySuccess('notifications.add-success') + async setGrantScopes(role, grantScopeIDs) { + await role.setGrantScopes(grantScopeIDs); + await this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); + } + + /** + * Redirect to manage scopes as if nothing ever happened. + */ + @action + async cancel() { + await this.router.replaceWith('scopes.scope.roles.role.manage-scopes'); + } +} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index a46f8ddd2..960a1802e 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -53,7 +53,9 @@ Router.map(function () { this.route('principals'); this.route('add-principals'); this.route('scopes'); - this.route('manage-scopes'); + this.route('manage-scopes', function () { + this.route('manage-custom-scopes'); + }); }); this.route('new'); }); diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/index.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/index.js new file mode 100644 index 000000000..1536b9229 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/index.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Route from '@ember/routing/route'; + +export default class ScopesScopeRolesRoleManageScopesIndexRoute extends Route {} diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js new file mode 100644 index 000000000..070c975c8 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route { + // =attributes + + queryParams = { + search: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, + }; + + // =services + + @service store; + + // =methods + + /** + * Loads sub scopes for the current scope. + * @returns {Promise<{role: RoleModel, subScopes: [ScopeModel], totalItems: number, totalItemsCount: number}> } + */ + async model({ search, page, pageSize }) { + const role = this.modelFor('scopes.scope.roles.role'); + const currentScope = this.modelFor('scopes.scope'); + const { id: scope_id } = currentScope; + const filters = { + scope_id: [{ equals: scope_id }], + }; + + const subScopes = await this.store.query('scope', { + scope_id, + query: { search, filters }, + page, + pageSize, + }); + const totalItems = subScopes.meta?.totalItems; + const totalItemsCount = await this.getTotalItemsCount( + scope_id, + search, + totalItems, + ); + + return { + role, + subScopes, + totalItems, + totalItemsCount, + }; + } + + /** + * Sets scopesExist to true if there exists any scopes and + * sets totalItemsCount to the number of scopes that exist. + * @param {string} scope_id + * @param {string} search + * @param {number} totalItems + * @returns {Promise} + */ + async getTotalItemsCount(scope_id, search, totalItems) { + if (!search) { + return totalItems; + } + const options = { pushToStore: false, peekIndexedDB: true }; + const scopes = await this.store.query( + 'scope', + { + scope_id, + query: { + filters: { + scope_id: [{ equals: scope_id }], + }, + }, + page: 1, + pageSize: 1, + }, + options, + ); + return scopes.meta?.totalItems; + } +} diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 1a007cc41..88fb5deaa 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -1054,3 +1054,15 @@ margin-bottom: 2rem; } } + +.role-manage-custom-scopes-form { + .table-selected-items-text { + display: flex; + justify-content: end; + margin-bottom: 0.5rem; + } + + .hds-pagination { + margin-bottom: 2rem; + } +} diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes.hbs index 1bb84d12d..9cd415a2e 100644 --- a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes.hbs +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes.hbs @@ -9,26 +9,4 @@ @route='scopes.scope.roles.role.manage-scopes' /> - - - - - - - - - {{t 'resources.role.scope.messages.manage-scopes.title'}} - {{! TODO: Add doclink }} - - {{t 'resources.role.scope.messages.manage-scopes.description'}} - - - - - - - \ No newline at end of file +{{outlet}} \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs new file mode 100644 index 000000000..c9870bad5 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/index.hbs @@ -0,0 +1,29 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + + + + + + + + + {{t 'resources.role.scope.messages.manage-scopes.title'}} + {{! TODO: Add doclink }} + + {{t 'resources.role.scope.messages.manage-scopes.description'}} + + + + + + + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs new file mode 100644 index 000000000..c8e2f0a87 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.hbs @@ -0,0 +1,40 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + +{{page-title (t 'resources.role.scope.messages.manage-custom-scopes.title')}} + + + + + + + + + + + {{t 'resources.role.scope.messages.manage-custom-scopes.title'}} + {{! TODO: Add doclink }} + + {{t + 'resources.role.scope.messages.manage-custom-scopes.description' + }} + + + + + + + \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/roles/role/scopes.hbs b/ui/admin/app/templates/scopes/scope/roles/role/scopes.hbs index 1208d4cd1..9477db3e6 100644 --- a/ui/admin/app/templates/scopes/scope/roles/role/scopes.hbs +++ b/ui/admin/app/templates/scopes/scope/roles/role/scopes.hbs @@ -153,7 +153,7 @@ {{#if (can 'setGrantScopes role' @model)}} - `.hds-form-toggle input[name="${name}"]`; + const SCOPE_CHECKBOX_SELECTOR = (id) => + `tbody [data-test-org-scopes-table-row="${id}"] input`; const TABLE_ROW_SELECTOR = 'tbody tr'; const SAVE_BTN_SELECTOR = 'form [type="submit"]'; const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]'; const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]'; const TOAST_SELECTOR = '[role="alert"]'; + const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]'; + const NO_RESULTS_MSG_SELECTOR = '[data-test-no-scope-results]'; const instances = { scopes: { global: null, org: null, }, - role: null, + globalRole: null, + orgRole: null, }; const urls = { orgScope: null, - roles: null, - role: null, - roleScopes: null, - manageScopes: null, + globalRoles: null, + globalRole: null, + globalRoleScopes: null, + globalManageScopes: null, + orgRoles: null, + orgRole: null, + orgRoleScopes: null, + orgManageScopes: null, + manageCustomScopes: null, }; hooks.beforeEach(function () { @@ -47,30 +68,43 @@ module('Acceptance | roles | scopes', function (hooks) { type: 'org', scope: { id: 'global', type: 'global' }, }); - instances.role = this.server.create( + instances.globalRole = this.server.create( + 'role', + { + scope: instances.scopes.global, + }, + 'withScopes', + ); + instances.orgRole = this.server.create( 'role', { scope: instances.scopes.org, }, 'withScopes', ); - urls.roles = `/scopes/${instances.scopes.org.id}/roles`; - urls.role = `${urls.roles}/${instances.role.id}`; - urls.roleScopes = `${urls.role}/scopes`; - urls.manageScopes = `${urls.role}/manage-scopes`; + + urls.globalRoles = `/scopes/global/roles`; + urls.globalRole = `${urls.globalRoles}/${instances.globalRole.id}`; + urls.globalRoleScopes = `${urls.globalRole}/scopes`; + urls.globalManageScopes = `${urls.globalRole}/manage-scopes`; + urls.manageCustomScopes = `${urls.globalManageScopes}/manage-custom-scopes`; + urls.orgRoles = `/scopes/${instances.scopes.org.id}/roles`; + urls.orgRole = `${urls.orgRoles}/${instances.orgRole.id}`; + urls.orgRoleScopes = `${urls.orgRole}/scopes`; + urls.orgManageScopes = `${urls.orgRole}/manage-scopes`; }); test('visiting role scopes', async function (assert) { - await visit(urls.roleScopes); + await visit(urls.orgRoleScopes); await a11yAudit(); - assert.strictEqual(currentURL(), urls.roleScopes); + assert.strictEqual(currentURL(), urls.orgRoleScopes); assert .dom(TABLE_ROW_SELECTOR) - .exists({ count: instances.role.grant_scope_ids.length }); + .exists({ count: instances.orgRole.grant_scope_ids.length }); }); test('user can naviage to scope from role grant scopes', async function (assert) { - await visit(urls.roleScopes); + await visit(urls.orgRoleScopes); await click('tbody tr:nth-child(2) a'); assert.strictEqual( currentURL(), @@ -79,7 +113,7 @@ module('Acceptance | roles | scopes', function (hooks) { }); test('user can naviage to parent scope from role grant scopes', async function (assert) { - await visit(urls.roleScopes); + await visit(urls.orgRoleScopes); await click('tbody tr:nth-child(2) td:nth-child(3) a'); assert.strictEqual( currentURL(), @@ -88,34 +122,34 @@ module('Acceptance | roles | scopes', function (hooks) { }); test('user sees no scopes message and action when role has no grant scopes', async function (assert) { - instances.role.update({ grant_scope_ids: [] }); - await visit(urls.roleScopes); + instances.orgRole.update({ grant_scope_ids: [] }); + await visit(urls.orgRoleScopes); assert.dom('.role-grant-scopes div').includesText('No scopes added'); assert.dom('.role-grant-scopes div div:nth-child(3) a').isVisible(); }); test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) { - instances.role.update({ + instances.orgRole.update({ grant_scope_ids: [], - authorized_actions: instances.role.authorized_actions.filter( + authorized_actions: instances.orgRole.authorized_actions.filter( (action) => action !== 'set-grant-scopes', ), }); - await visit(urls.roleScopes); + await visit(urls.orgRoleScopes); assert.dom('.role-grant-scopes div div:nth-child(3) a').doesNotExist(); }); - test('toggle and save scope keywords to add', async function (assert) { - instances.role.update({ grant_scope_ids: [] }); - await visit(urls.role); + test('user can toggle and save scope keywords to add on manage scopes page', async function (assert) { + instances.orgRole.update({ grant_scope_ids: [] }); + await visit(urls.orgRole); - await click(`[href="${urls.roleScopes}"]`); + await click(`[href="${urls.orgRoleScopes}"]`); assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); await click(MANAGE_SCOPES_SELECTOR); - assert.strictEqual(currentURL(), urls.manageScopes); + assert.strictEqual(currentURL(), urls.orgManageScopes); // Click three times to select, unselect, then reselect (for coverage) await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); @@ -123,21 +157,43 @@ module('Acceptance | roles | scopes', function (hooks) { await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); await click(SAVE_BTN_SELECTOR); - assert.strictEqual(currentURL(), urls.roleScopes); + assert.strictEqual(currentURL(), urls.orgRoleScopes); assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1); }); - test('toggle and cancel scope keywords to add', async function (assert) { - instances.role.update({ grant_scope_ids: [] }); - await visit(urls.role); + test('correct toggles are visible for global level role on manage scopes page', async function (assert) { + await visit(urls.globalRole); - await click(`[href="${urls.roleScopes}"]`); + await click(MANAGE_SCOPES_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalManageScopes); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible(); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible(); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).isVisible(); + }); + + test('correct toggles are visible for org level role on manage scopes page', async function (assert) { + await visit(urls.orgRole); + + await click(MANAGE_SCOPES_SELECTOR); + + assert.strictEqual(currentURL(), urls.orgManageScopes); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible(); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible(); + assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).doesNotExist(); + }); + + test('user can toggle and cancel scope keywords to add on manage scopes page', async function (assert) { + instances.orgRole.update({ grant_scope_ids: [] }); + await visit(urls.orgRole); + + await click(`[href="${urls.orgRoleScopes}"]`); assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); await click(MANAGE_SCOPES_SELECTOR); - assert.strictEqual(currentURL(), urls.manageScopes); + assert.strictEqual(currentURL(), urls.orgManageScopes); // Click three times to select, unselect, then reselect (for coverage) await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); @@ -145,11 +201,11 @@ module('Acceptance | roles | scopes', function (hooks) { await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); await click(CANCEL_BTN_SELECTOR); - assert.strictEqual(currentURL(), urls.roleScopes); + assert.strictEqual(currentURL(), urls.orgRoleScopes); assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); }); - test('shows error message on scope save', async function (assert) { + test('shows error message on scope save on manage scopes page', async function (assert) { this.server.post('/roles/:idMethod', () => { return new Response( 400, @@ -162,8 +218,8 @@ module('Acceptance | roles | scopes', function (hooks) { }, ); }); - instances.role.update({ grant_scope_ids: [] }); - await visit(urls.role); + instances.orgRole.update({ grant_scope_ids: [] }); + await visit(urls.orgRole); await click(MANAGE_SCOPES_SELECTOR); await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)); @@ -171,4 +227,125 @@ module('Acceptance | roles | scopes', function (hooks) { assert.dom(TOAST_SELECTOR).isVisible(); }); + + test('user can select and save custom scopes to add on manage custom scopes page', async function (assert) { + instances.globalRole.update({ grant_scope_ids: [] }); + await visit(urls.globalRole); + + await click(`[href="${urls.globalRoleScopes}"]`); + + assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + + await click(MANAGE_SCOPES_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalManageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + + // Click three times to select, unselect, then reselect (for coverage) + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalManageScopes); + + await click(SAVE_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalRoleScopes); + assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1); + }); + + test('user can select and cancel custom scopes to add on manage custom scopes page', async function (assert) { + instances.globalRole.update({ grant_scope_ids: [] }); + await visit(urls.globalRole); + + await click(`[href="${urls.globalRoleScopes}"]`); + + assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + + await click(MANAGE_SCOPES_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalManageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + + // Click three times to select, unselect, then reselect (for coverage) + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(CANCEL_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalManageScopes); + + await click(CANCEL_BTN_SELECTOR); + + assert.strictEqual(currentURL(), urls.globalRoleScopes); + assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0); + }); + + test('shows error message on custom scope save on manage custom scopes page', async function (assert) { + this.server.post('/roles/:idMethod', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: {}, + }, + ); + }); + instances.globalRole.update({ grant_scope_ids: [] }); + await visit(urls.globalRole); + + await click(MANAGE_SCOPES_SELECTOR); + await click(`[href="${urls.manageCustomScopes}"]`); + await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)); + await click(SAVE_BTN_SELECTOR); + + assert.dom(TOAST_SELECTOR).isVisible(); + }); + + test('user can search for a specifc org scope by id on manage custom scopes page', async function (assert) { + const anotherOrg = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + await visit(urls.globalManageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + + assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists(); + assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org.id); + await waitUntil( + () => findAll(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).length === 0, + ); + + assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists(); + assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).doesNotExist(); + }); + + test('user can search for org scopes and get no results on manage custom scopes page', async function (assert) { + const anotherOrg = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + await visit(urls.globalManageScopes); + + await click(`[href="${urls.manageCustomScopes}"]`); + + assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists(); + assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist'); + await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1); + + assert.dom(`[href="${urls.role1}"]`).doesNotExist(); + assert.dom(`[href="${urls.role2}"]`).doesNotExist(); + assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found'); + }); }); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js similarity index 89% rename from ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes-test.js rename to ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js index 417499630..75e363f2f 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/index-test.js @@ -1,3 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { visit, currentURL } from '@ember/test-helpers'; @@ -5,7 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { authenticateSession } from 'ember-simple-auth/test-support'; module( - 'Unit | Controller | scopes/scope/roles/role/manage-scopes', + 'Unit | Controller | scopes/scope/roles/role/manage-scopes/index', function (hooks) { setupTest(hooks); setupMirage(hooks); @@ -30,7 +35,7 @@ module( authenticateSession({}); store = this.owner.lookup('service:store'); controller = this.owner.lookup( - 'controller:scopes/scope/roles/role/manage-scopes', + 'controller:scopes/scope/roles/role/manage-scopes/index', ); instances.scopes.global = this.server.create('scope', { id: 'global' }); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js new file mode 100644 index 000000000..76d398462 --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { visit, currentURL, waitUntil } from '@ember/test-helpers'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; +import { authenticateSession } from 'ember-simple-auth/test-support'; + +module( + 'Unit | Controller | scopes/scope/roles/role/manage-scopes/manage-custom-scopes', + function (hooks) { + setupTest(hooks); + setupMirage(hooks); + setupIndexedDb(hooks); + + let store; + let controller; + + const instances = { + scopes: { + global: null, + org: null, + }, + role: null, + }; + + const urls = { + globalScope: null, + manageScopes: null, + manageCustomScopes: null, + }; + + hooks.beforeEach(async function () { + authenticateSession({}); + store = this.owner.lookup('service:store'); + controller = this.owner.lookup( + 'controller:scopes/scope/roles/role/manage-scopes/manage-custom-scopes', + ); + + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + instances.role = this.server.create('role', { + scopeId: 'global', + grant_scope_ids: [], + }); + + urls.globalScope = `/scopes/global`; + urls.manageScopes = `${urls.globalScope}/roles/${instances.role.id}/manage-scopes`; + urls.manageCustomScopes = `${urls.manageScopes}/manage-custom-scopes`; + }); + + test('it exists', function (assert) { + assert.ok(controller); + }); + + test('handleSearchInput action sets expected values correctly', async function (assert) { + const searchValue = 'test'; + controller.handleSearchInput({ target: { value: searchValue } }); + await waitUntil(() => controller.search === searchValue); + + assert.strictEqual(controller.page, 1); + assert.strictEqual(controller.search, searchValue); + }); + + test('setGrantScopes action adds scopes to specified model', async function (assert) { + await visit(urls.manageCustomScopes); + const role = await store.findRecord('role', instances.role.id); + + assert.deepEqual(role.grant_scope_ids, []); + + await controller.setGrantScopes(role, [instances.scopes.org.id]); + + assert.deepEqual(role.grant_scope_ids, [instances.scopes.org.id]); + }); + + test('cancel action causes transition to expected route', async function (assert) { + await visit(urls.manageCustomScopes); + + await controller.cancel(); + + assert.strictEqual(currentURL(), urls.manageScopes); + }); + }, +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes-test.js b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes-test.js index 1aadf2e87..5c2b055e2 100644 --- a/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes-test.js +++ b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes-test.js @@ -1,3 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; diff --git a/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/index-test.js b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/index-test.js new file mode 100644 index 000000000..ea87aa146 --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/index-test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/roles/role/manage-scopes/index', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/roles/role/manage-scopes/index', + ); + assert.ok(route); + }); + }, +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js new file mode 100644 index 000000000..6e2841011 --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes-test.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/roles/role/manage-scopes/manage-custom-scopes', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/roles/role/manage-scopes/manage-custom-scopes', + ); + assert.ok(route); + }); + }, +);
{{t 'resources.role.scope.messages.manage-scopes.description'}}
{{t + 'resources.role.scope.messages.manage-custom-scopes.description' + }}