{{#if @model.role.grantScopeOrgIDs.length}}
{{t
- 'resources.role.scope.titles.selected'
+ 'states.selected-items'
items=@model.role.grantScopeOrgIDs.length
total=@model.totalItemsCount
}}
@@ -103,7 +103,7 @@
{{#if @model.role.grantScopeProjectIDs.length}}
{{t
- 'resources.role.scope.titles.selected'
+ 'states.selected-items'
items=@model.role.grantScopeProjectIDs.length
total=@model.totalItemsCount
}}
diff --git a/ui/admin/app/components/form/role/manage-org-projects/index.hbs b/ui/admin/app/components/form/role/manage-org-projects/index.hbs
index 47bfa6fdb..e24b15957 100644
--- a/ui/admin/app/components/form/role/manage-org-projects/index.hbs
+++ b/ui/admin/app/components/form/role/manage-org-projects/index.hbs
@@ -5,7 +5,7 @@
{{#if @model.totalItemsCount}}
{{#if @model.projectScopes}}
-
+
{{#if this.selectedProjectsCount}}
{{t
- 'resources.role.scope.titles.selected'
+ 'states.selected-items'
items=this.selectedProjectsCount
total=@model.totalItemsCount
}}
diff --git a/ui/admin/app/components/roles/role/actions/index.hbs b/ui/admin/app/components/roles/role/actions/index.hbs
index a72fda28d..d9323602e 100644
--- a/ui/admin/app/components/roles/role/actions/index.hbs
+++ b/ui/admin/app/components/roles/role/actions/index.hbs
@@ -7,11 +7,19 @@
(hash
canAddPrincipals=(can 'addPrincipals role' @model)
canSetGrantScopes=(can 'setGrantScopes role' @model)
+ canSetGrants=(can 'setGrants role' @model)
canDelete=(can 'delete model' @model)
)
as |perms|
}}
- {{#if (or perms.canAddPrincipals perms.canSetGrantScopes perms.canDelete)}}
+ {{#if
+ (or
+ perms.canAddPrincipals
+ perms.canSetGrantScopes
+ perms.canSetGrants
+ perms.canDelete
+ )
+ }}
{{#if perms.canAddPrincipals}}
@@ -30,8 +38,18 @@
{{t 'resources.role.scope.actions.manage-scopes'}}
{{/if}}
+ {{#if perms.canSetGrants}}
+
+ {{t 'resources.role.grant-templates.actions.add-grant-templates'}}
+
+ {{/if}}
{{#if perms.canDelete}}
- {{#if (or perms.canAddPrincipals perms.canSetGrantScopes)}}
+ {{#if
+ (or perms.canAddPrincipals perms.canSetGrantScopes perms.canSetGrants)
+ }}
{{/if}}
div {
diff --git a/ui/admin/app/templates/scopes/scope/roles/role/add-grant-templates.hbs b/ui/admin/app/templates/scopes/scope/roles/role/add-grant-templates.hbs
new file mode 100644
index 000000000..6b0cde25b
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/roles/role/add-grant-templates.hbs
@@ -0,0 +1,44 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{page-title (t 'resources.role.grant-templates.actions.add-grant-templates')}}
+
+
+
+
+
+
+
+
+
+
+ {{t
+ 'resources.role.grant-templates.messages.add-grant-templates.title'
+ }}
+
+
+ {{t
+ 'resources.role.grant-templates.messages.add-grant-templates.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
index defc41134..881a035c3 100644
--- 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
@@ -34,7 +34,7 @@
-
+
-
+
item !== 'set-grants',
+ );
+ instances.role.update({ authorized_actions });
+ await visit(urls.role);
+
+ await click(selectors.MANAGE_DROPDOWN_ROLES);
+
+ assert.dom(selectors.MANAGE_DROPDOWN_ADD_GRANT_TEMPLATES).doesNotExist();
+ });
+
+ test('can search grant templates', async function (assert) {
+ await visit(urls.addGrantTemplates);
+
+ const initialRowCount = findAll(commonSelectors.TABLE_ROWS).length;
+
+ await fillIn(commonSelectors.SEARCH_INPUT, 'admin');
+ await waitUntil(() => findAll(commonSelectors.TABLE_ROWS).length === 3);
+
+ assert.strictEqual(initialRowCount, 10);
+ assert.strictEqual(findAll(commonSelectors.TABLE_ROWS).length, 3);
+ });
+
+ test('can search for grant templates and get no results', async function (assert) {
+ await visit(urls.addGrantTemplates);
+ assert.dom(commonSelectors.TABLE_ROWS).isVisible();
+
+ await fillIn(commonSelectors.SEARCH_INPUT, 'nonexistentgranttemplate');
+ await waitUntil(() => findAll(commonSelectors.TABLE_ROWS).length === 0);
+
+ assert.dom(commonSelectors.TABLE_ROWS).doesNotExist();
+ assert
+ .dom(selectors.NO_GRANT_TEMPLATES_MSG)
+ .includesText('No results found');
+ });
+
+ test('can select and add grant templates', async function (assert) {
+ await visit(urls.addGrantTemplates);
+ const initialGrantCount = instances.role.grant_strings.length;
+
+ await click(commonSelectors.TABLE_ROW_CHECKBOX);
+ await click(commonSelectors.SAVE_BTN);
+
+ assert.strictEqual(currentURL(), urls.grants);
+
+ assert.strictEqual(initialGrantCount, 0);
+ assert.dom(selectors.FIELD_GRANT).isVisible({ count: 1 });
+ assert.dom(selectors.FIELD_GRANT).hasValue('ids=*;type=*;actions=*');
+ });
+
+ test('cancel navigates back to grants without changes', async function (assert) {
+ await visit(urls.addGrantTemplates);
+ const initialGrantCount = instances.role.grant_strings.length;
+
+ await click(commonSelectors.TABLE_ROW_CHECKBOX);
+ await click(commonSelectors.CANCEL_BTN);
+
+ assert.strictEqual(initialGrantCount, 0);
+ assert.dom(selectors.FIELD_GRANT).isVisible({ count: 0 });
+ });
+});
diff --git a/ui/admin/tests/acceptance/roles/selectors.js b/ui/admin/tests/acceptance/roles/selectors.js
index ba19085b3..033c34aa2 100644
--- a/ui/admin/tests/acceptance/roles/selectors.js
+++ b/ui/admin/tests/acceptance/roles/selectors.js
@@ -14,6 +14,8 @@ export const MANAGE_DROPDOWN_ROLES_REMOVE =
'[data-test-manage-roles-dropdown] div:last-child button';
export const MANAGE_DROPDOWN_ADD_PRINCIPALS =
'[data-test-manage-role-principals]';
+export const MANAGE_DROPDOWN_ADD_GRANT_TEMPLATES =
+ '[data-test-manage-role-grant-templates]';
export const ROLE_BADGE = (id) =>
`tbody [data-test-role-row="${id}"] td:nth-child(2) .hds-badge__text`;
@@ -33,6 +35,8 @@ export const NO_RESULTS_GRANT_SCOPE_MSG = '[data-test-no-grant-scope-results]';
export const NO_SCOPES_MSG = '.role-grant-scopes div';
export const NO_SCOPES_MSG_LINK = '.role-grant-scopes div div:nth-child(3) a';
+export const NO_GRANT_TEMPLATES_MSG = '[data-test-no-grant-template-results]';
+
// Manage scopes selectors
export const SCOPE_TOGGLE = (name) => `.hds-form-toggle input[name="${name}"]`;
export const SCOPE_CHECKBOX = (type, id) =>
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-grant-templates-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-grant-templates-test.js
new file mode 100644
index 000000000..3952dc7b0
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/add-grant-templates-test.js
@@ -0,0 +1,77 @@
+/**
+ * Copyright IBM Corp. 2021, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'admin/tests/helpers';
+import { setupMirage } from 'admin/tests/helpers/mirage';
+import { setupSqlite } from 'api/test-support/helpers/sqlite';
+import sinon from 'sinon';
+
+module(
+ 'Unit | Controller | scopes/scope/roles/role/add-grant-templates',
+ function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+ setupSqlite(hooks);
+
+ let store;
+ let controller;
+ let router;
+ let routerStub;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ role: null,
+ };
+
+ hooks.beforeEach(async function () {
+ store = this.owner.lookup('service:store');
+ controller = this.owner.lookup(
+ 'controller:scopes/scope/roles/role/add-grant-templates',
+ );
+ router = this.owner.lookup('service:router');
+ routerStub = sinon.stub(router, 'replaceWith');
+
+ instances.scopes.global = this.server.create(
+ 'scope',
+ { id: 'global' },
+ 'withGlobalAuth',
+ );
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.role = this.server.create('role', {
+ scope: instances.scopes.org,
+ grant_strings: ['ids=*;type=user;actions=read'],
+ });
+ });
+
+ test('it exists', function (assert) {
+ assert.ok(controller);
+ });
+
+ test('addGrantTemplates action adds grant templates to role', async function (assert) {
+ const role = await store.findRecord('role', instances.role.id);
+
+ const newGrantTemplates = [
+ 'ids=*;type=*;actions=*',
+ 'ids=*;type=session;actions=list,read',
+ ];
+
+ await controller.addGrantTemplates(role, newGrantTemplates);
+
+ assert.ok(routerStub.calledOnceWith('scopes.scope.roles.role.grants'));
+ assert.deepEqual(role.grant_strings, [
+ 'ids=*;type=user;actions=read',
+ 'ids=*;type=*;actions=*',
+ 'ids=*;type=session;actions=list,read',
+ ]);
+ });
+ },
+);
diff --git a/ui/admin/tests/unit/routes/scopes/scope/roles/role/add-grant-templates-test.js b/ui/admin/tests/unit/routes/scopes/scope/roles/role/add-grant-templates-test.js
new file mode 100644
index 000000000..6c350e7a6
--- /dev/null
+++ b/ui/admin/tests/unit/routes/scopes/scope/roles/role/add-grant-templates-test.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright IBM Corp. 2021, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Route | scopes/scope/roles/role/add-grant-templates',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup(
+ 'route:scopes/scope/roles/role/add-grant-templates',
+ );
+ assert.ok(route);
+ });
+ },
+);