feat: 🎸 Add grant templates for roles (#3115)
* feat: 🎸 Add grant templates for roles * test: 💍 Add a11y ignore * fix: 🐛 Add selected text * test: 💍 Update tests * refactor: 💡 Update manage scope styles to be consistent as well * Add missing copyright headers --------- Co-authored-by: ZedLi <5783847+ZedLi@users.noreply.github.com>
This commit is contained in:
1 parent
79b6360c30
commit
43c9389b69
19 files changed
+758
-32
No files matched your search
@@ -31,6 +31,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.selected-items-text {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
height: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
> div,
|
||||
table {
|
||||
margin-bottom: 1rem;
|
||||
@@ -40,6 +48,11 @@
|
||||
.hds-dropdown-toggle-button__text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Remove margin if the segmented group is followed by the selected items text
|
||||
&:has(+ .selected-items-text) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-filtering-toolbar {
|
||||
|
||||
@@ -613,6 +613,53 @@ role:
|
||||
description: Grants are permissions which allow roles to take actions and access resources.
|
||||
actions:
|
||||
create: New Grant
|
||||
grant-templates:
|
||||
title_plural: Grant Templates
|
||||
actions:
|
||||
add-grant-templates: Add Grant Templates
|
||||
messages:
|
||||
add-grant-templates:
|
||||
title: Add Grant Templates
|
||||
description: Select grant templates to add to this role.
|
||||
titles:
|
||||
grant-strings: Grant Strings
|
||||
templates:
|
||||
global-admin:
|
||||
name: Global Admin
|
||||
description: Full administrative access to all resources in Boundary
|
||||
org-admin:
|
||||
name: Org Admin
|
||||
description: Administrative access to all resources within an organization
|
||||
project-admin:
|
||||
name: Project Admin
|
||||
description: Administrative access to all resources within a project
|
||||
session-manager:
|
||||
name: Session Manager
|
||||
description: Can view and cancel sessions
|
||||
target-manager:
|
||||
name: Target Manager
|
||||
description: Can create and manage targets
|
||||
host-resource-manager:
|
||||
name: Host Resource Manager
|
||||
description: Can manage hosts and host catalogs
|
||||
credential-manager:
|
||||
name: Credential Manager
|
||||
description: Can manage credentials and credential stores
|
||||
user-manager:
|
||||
name: User Manager
|
||||
description: Can manage users, groups, and accounts
|
||||
auth-method-manager:
|
||||
name: Auth Method Manager
|
||||
description: Can manage auth methods and managed groups
|
||||
session-auditor:
|
||||
name: Session Auditor
|
||||
description: Can view session recordings
|
||||
target-user:
|
||||
name: Target User
|
||||
description: Can connect to targets but cannot manage anything
|
||||
read-only-user:
|
||||
name: Read-Only User
|
||||
description: Can view but not modify any resources
|
||||
scope:
|
||||
title: Scope
|
||||
title_plural: Scopes
|
||||
@@ -624,7 +671,6 @@ role:
|
||||
parent_scope: Parent scope
|
||||
resource_type: Resource type
|
||||
projects_selected: Projects selected
|
||||
selected: '{items} selected out of {total}'
|
||||
messages:
|
||||
none:
|
||||
title: No scopes added
|
||||
|
||||
@@ -29,3 +29,4 @@ incomplete-loading:
|
||||
refreshing:
|
||||
description: Updating cache...
|
||||
tooltip: Some items may not appear until the cache is finished updating
|
||||
selected-items: '{items} selected out of {total}'
|
||||
@@ -0,0 +1,97 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Rose::Form
|
||||
class='full-width search-filtering'
|
||||
@onSubmit={{this.submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
as |form|
|
||||
>
|
||||
<Hds::SegmentedGroup as |S|>
|
||||
<S.TextInput
|
||||
@value={{@search}}
|
||||
@type='search'
|
||||
placeholder={{t 'actions.search'}}
|
||||
aria-label={{t 'actions.search'}}
|
||||
{{on 'input' @handleSearchInput}}
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
|
||||
{{#if this.paginatedTemplates}}
|
||||
|
||||
<div class='selected-items-text'>
|
||||
{{#if this.selectedGrantTemplates}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'states.selected-items'
|
||||
items=this.selectedGrantTemplates.length
|
||||
total=this.totalCount
|
||||
}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Hds::Table
|
||||
@model={{this.paginatedTemplates}}
|
||||
@columns={{array
|
||||
(hash label=(t 'form.name.label'))
|
||||
(hash label=(t 'resources.role.grant-templates.titles.grant-strings'))
|
||||
}}
|
||||
@isSelectable={{true}}
|
||||
@onSelectionChange={{this.onSelectionChange}}
|
||||
@valign='middle'
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr
|
||||
@selectionKey={{B.data.id}}
|
||||
@isSelected={{includes B.data.id this.selectedGrantTemplates}}
|
||||
@selectionAriaLabelSuffix='row {{B.data.id}}'
|
||||
>
|
||||
<B.Td>
|
||||
<Hds::Text::Body @tag='p' @weight='semibold'>
|
||||
{{B.data.name}}
|
||||
</Hds::Text::Body>
|
||||
<Hds::Text::Body @tag='p'>
|
||||
{{B.data.description}}
|
||||
</Hds::Text::Body>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<ul>
|
||||
{{#each B.data.grantStrings as |grantString|}}
|
||||
<li>
|
||||
<Hds::Text::Code @tag='code'>{{grantString}}</Hds::Text::Code>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
@totalItems={{this.filteredTemplates.length}}
|
||||
@currentPage={{@page}}
|
||||
@currentPageSize={{@pageSize}}
|
||||
/>
|
||||
<form.actions
|
||||
@submitText={{t
|
||||
'resources.role.grant-templates.actions.add-grant-templates'
|
||||
}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState data-test-no-grant-template-results as |A|>
|
||||
<A.Header @title={{t 'titles.no-results-found'}} />
|
||||
<A.Body
|
||||
@text={{t
|
||||
'descriptions.no-search-results'
|
||||
query=@search
|
||||
resource=(t 'resources.role.grant-templates.title_plural')
|
||||
}}
|
||||
/>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
|
||||
</Rose::Form>
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
export default class FormRoleAddGrantTemplatesIndexComponent extends Component {
|
||||
// =services
|
||||
|
||||
@service intl;
|
||||
|
||||
// =properties
|
||||
|
||||
@tracked selectedGrantTemplates = [];
|
||||
|
||||
/**
|
||||
* Grant template definitions with their grant strings.
|
||||
* @type {Array<{id: string, grantStrings: Array<string>}>}
|
||||
*/
|
||||
grantTemplateDefinitions = [
|
||||
{
|
||||
id: 'global-admin',
|
||||
grantStrings: ['ids=*;type=*;actions=*'],
|
||||
},
|
||||
{
|
||||
id: 'org-admin',
|
||||
grantStrings: [
|
||||
'ids=<org-id>;type=scope;actions=*',
|
||||
'ids=*;type=auth-method;actions=*',
|
||||
'ids=*;type=user;actions=*',
|
||||
'ids=*;type=group;actions=*',
|
||||
'ids=*;type=role;actions=*',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'project-admin',
|
||||
grantStrings: [
|
||||
'ids=<project-id>;type=scope;actions=*',
|
||||
'ids=*;type=host-catalog;actions=*',
|
||||
'ids=*;type=target;actions=*',
|
||||
'ids=*;type=credential-store;actions=*',
|
||||
'ids=*;type=session;actions=*',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'session-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=session;actions=list,read,cancel',
|
||||
'ids=*;type=target;actions=read,list',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'target-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=target;actions=create,list,read,update,delete,add-host-sources,set-host-sources,remove-host-sources,add-credential-sources,set-credential-sources,remove-credential-sources',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'host-resource-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=host-catalog;actions=create,list,read,update,delete',
|
||||
'ids=*;type=host-set;actions=create,read,update,delete,add-hosts,set-hosts,remove-hosts',
|
||||
'ids=*;type=host;actions=create,read,update,delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'credential-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=credential;actions=create,read,update,delete',
|
||||
'ids=*;type=credential-store;actions=create,list,read,update,delete',
|
||||
'ids=*;type=credential-library;actions=create,read,update,delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'user-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=user;actions=create,list,read,update,delete,add-accounts,set-accounts,remove-accounts',
|
||||
'ids=*;type=group;actions=create,list,read,update,delete,add-members,set-members,remove-members',
|
||||
'ids=*;type=account;actions=create,read,update,delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'auth-method-manager',
|
||||
grantStrings: [
|
||||
'ids=*;type=auth-method;actions=create,list,read,update,delete,change-state',
|
||||
'ids=*;type=managed-group;actions=create,read,update,delete',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'session-auditor',
|
||||
grantStrings: [
|
||||
'ids=*;type=session;actions=list,read',
|
||||
'ids=*;type=session-recording;actions=list,read,download',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'target-user',
|
||||
grantStrings: [
|
||||
'ids=*;type=target;actions=authorize-session,read,list',
|
||||
'ids=*;type=session;actions=read:self,cancel:self',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'read-only-user',
|
||||
grantStrings: ['ids=*;type=session;actions=list,read,cancel'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Grant templates with translated names and descriptions.
|
||||
* @type {Array<{id: string, name: string, grantStrings: Array<string>, description: string}>}
|
||||
*/
|
||||
get grantTemplates() {
|
||||
return this.grantTemplateDefinitions.map((template) => ({
|
||||
id: template.id,
|
||||
name: this.intl.t(
|
||||
`resources.role.grant-templates.templates.${template.id}.name`,
|
||||
),
|
||||
grantStrings: template.grantStrings,
|
||||
description: this.intl.t(
|
||||
`resources.role.grant-templates.templates.${template.id}.description`,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtered grant templates based on search query.
|
||||
* @return {Array<{id: string, name: string, grantStrings: Array<string>, description: string}>}
|
||||
*/
|
||||
get filteredTemplates() {
|
||||
let filteredTemplates = this.grantTemplates;
|
||||
|
||||
if (this.args.search) {
|
||||
filteredTemplates = this.grantTemplates.filter((template) => {
|
||||
const searchTerm = this.args.search?.toLowerCase() ?? '';
|
||||
return (
|
||||
template.name.toLowerCase().includes(searchTerm) ||
|
||||
template.description.toLowerCase().includes(searchTerm) ||
|
||||
template.grantStrings.some((grant) => grant.includes(searchTerm))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredTemplates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated templates for current page.
|
||||
* @type {Array}
|
||||
*/
|
||||
get paginatedTemplates() {
|
||||
return this.filteredTemplates.slice(
|
||||
(this.args.page - 1) * this.args.pageSize,
|
||||
this.args.page * this.args.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
get totalCount() {
|
||||
return this.grantTemplateDefinitions.length;
|
||||
}
|
||||
// =actions
|
||||
|
||||
@action
|
||||
onSelectionChange({ selectionKey, selectedRowsKeys }) {
|
||||
const oldKeys = new Set(this.selectedGrantTemplates);
|
||||
if (selectionKey === 'all') {
|
||||
const newKeys = new Set(selectedRowsKeys);
|
||||
if (newKeys.size === 0) {
|
||||
this.selectedGrantTemplates = [...newKeys.difference(oldKeys)];
|
||||
} else {
|
||||
this.selectedGrantTemplates = [...newKeys.union(oldKeys)];
|
||||
}
|
||||
} else {
|
||||
if (oldKeys.has(selectionKey)) {
|
||||
oldKeys.delete(selectionKey);
|
||||
} else {
|
||||
oldKeys.add(selectionKey);
|
||||
}
|
||||
this.selectedGrantTemplates = [...oldKeys];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
submit() {
|
||||
// Convert selected template IDs to their grant strings
|
||||
const grantStrings = this.grantTemplates
|
||||
.filter((template) => this.selectedGrantTemplates.includes(template.id))
|
||||
.flatMap((template) => template.grantStrings);
|
||||
this.args.submit(grantStrings);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{{#if @model.totalItemsCount}}
|
||||
<Rose::Form
|
||||
class='full-width role-manage-custom-scopes-form'
|
||||
class='full-width search-filtering'
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.role.isSaving}}
|
||||
@@ -35,11 +35,11 @@
|
||||
|
||||
{{#if @model.scopes}}
|
||||
{{#if @model.canSelectOrgs}}
|
||||
<div class='table-selected-items-text'>
|
||||
<div class='selected-items-text'>
|
||||
{{#if @model.role.grantScopeOrgIDs.length}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{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}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'resources.role.scope.titles.selected'
|
||||
'states.selected-items'
|
||||
items=@model.role.grantScopeProjectIDs.length
|
||||
total=@model.totalItemsCount
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{{#if @model.totalItemsCount}}
|
||||
<Rose::Form
|
||||
class='full-width role-manage-custom-scopes-form'
|
||||
class='full-width search-filtering'
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.role.isSaving}}
|
||||
@@ -21,11 +21,11 @@
|
||||
/>
|
||||
</Hds::SegmentedGroup>
|
||||
{{#if @model.projectScopes}}
|
||||
<div class='table-selected-items-text'>
|
||||
<div class='selected-items-text'>
|
||||
{{#if this.selectedProjectsCount}}
|
||||
<Hds::Text::Body @tag='p' @color='faint'>
|
||||
{{t
|
||||
'resources.role.scope.titles.selected'
|
||||
'states.selected-items'
|
||||
items=this.selectedProjectsCount
|
||||
total=@model.totalItemsCount
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}}
|
||||
<Hds::Dropdown data-test-manage-roles-dropdown as |dd|>
|
||||
<dd.ToggleButton @text={{t 'actions.manage'}} @color='secondary' />
|
||||
{{#if perms.canAddPrincipals}}
|
||||
@@ -30,8 +38,18 @@
|
||||
{{t 'resources.role.scope.actions.manage-scopes'}}
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if perms.canSetGrants}}
|
||||
<dd.Interactive
|
||||
@route='scopes.scope.roles.role.add-grant-templates'
|
||||
data-test-manage-role-grant-templates
|
||||
>
|
||||
{{t 'resources.role.grant-templates.actions.add-grant-templates'}}
|
||||
</dd.Interactive>
|
||||
{{/if}}
|
||||
{{#if perms.canDelete}}
|
||||
{{#if (or perms.canAddPrincipals perms.canSetGrantScopes)}}
|
||||
{{#if
|
||||
(or perms.canAddPrincipals perms.canSetGrantScopes perms.canSetGrants)
|
||||
}}
|
||||
<dd.Separator />
|
||||
{{/if}}
|
||||
<dd.Interactive
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { debounce } from 'core/decorators/debounce';
|
||||
|
||||
export default class ScopesScopeRolesRoleAddGrantTemplatesController extends Controller {
|
||||
// =services
|
||||
|
||||
@service router;
|
||||
@service intl;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = ['search', 'page', 'pageSize'];
|
||||
@tracked search;
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Add grant templates to the current role.
|
||||
* @param {RoleModel} role
|
||||
* @param {[string]} grantTemplates
|
||||
*/
|
||||
@action
|
||||
async addGrantTemplates(role, grantTemplates) {
|
||||
// Add the grant templates to the role's grant_strings without saving yet
|
||||
// Note that we transition first to avoid a discard changes confirmation
|
||||
this.router.replaceWith('scopes.scope.roles.role.grants');
|
||||
role.grant_strings = [...role.grant_strings, ...grantTemplates];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to role grants as if nothing ever happened.
|
||||
*/
|
||||
@action
|
||||
cancel() {
|
||||
this.router.replaceWith('scopes.scope.roles.role.grants');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input on each keystroke and the search queryParam
|
||||
* @param {object} event
|
||||
*/
|
||||
@action
|
||||
@debounce(250)
|
||||
handleSearchInput(event) {
|
||||
const { value } = event.target;
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ Router.map(function () {
|
||||
this.route('grants');
|
||||
this.route('principals');
|
||||
this.route('add-principals');
|
||||
this.route('add-grant-templates');
|
||||
this.route('scopes');
|
||||
this.route('manage-scopes', function () {
|
||||
this.route('manage-custom-scopes');
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeRolesRoleAddGrantTemplatesRoute extends Route {
|
||||
// =attributes
|
||||
|
||||
queryParams = {
|
||||
search: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Returns the current role
|
||||
* @return {{role: RoleModel}}
|
||||
*/
|
||||
async model() {
|
||||
return this.modelFor('scopes.scope.roles.role');
|
||||
}
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
if (isExiting) {
|
||||
controller.set('search', '');
|
||||
controller.set('page', 1);
|
||||
controller.set('pageSize', 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -888,26 +888,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.role-manage-custom-scopes-form {
|
||||
.filters-applied {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.table-selected-items-text {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.hds-table {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hds-pagination {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// workers
|
||||
.workers {
|
||||
> div {
|
||||
|
||||
@@ -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')}}
|
||||
<Breadcrumbs::Item
|
||||
@text={{t 'resources.role.grant-templates.actions.add-grant-templates'}}
|
||||
@route='scopes.scope.roles.role.add-grant-templates'
|
||||
/>
|
||||
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<Hds::PageHeader as |PH|>
|
||||
<PH.Breadcrumb>
|
||||
<Breadcrumbs::Container />
|
||||
</PH.Breadcrumb>
|
||||
<PH.Title>
|
||||
{{t
|
||||
'resources.role.grant-templates.messages.add-grant-templates.title'
|
||||
}}
|
||||
</PH.Title>
|
||||
<PH.Description>
|
||||
{{t
|
||||
'resources.role.grant-templates.messages.add-grant-templates.description'
|
||||
}}
|
||||
</PH.Description>
|
||||
</Hds::PageHeader>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
<Form::Role::AddGrantTemplates
|
||||
@model={{@model}}
|
||||
@submit={{fn this.addGrantTemplates @model}}
|
||||
@cancel={{this.cancel}}
|
||||
@search={{this.search}}
|
||||
@handleSearchInput={{this.handleSearchInput}}
|
||||
@page={{this.page}}
|
||||
@pageSize={{this.pageSize}}
|
||||
/>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
</Hds::PageHeader>
|
||||
</page.header>
|
||||
|
||||
<page.body class='search-filtering'>
|
||||
<page.body>
|
||||
<Form::Role::ManageCustomScopes
|
||||
@model={{@model}}
|
||||
@search={{this.search}}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@
|
||||
</Hds::PageHeader>
|
||||
</page.header>
|
||||
|
||||
<page.body class='search-filtering'>
|
||||
<page.body>
|
||||
<Form::Role::ManageOrgProjects
|
||||
@model={{@model}}
|
||||
@search={{this.search}}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
fillIn,
|
||||
findAll,
|
||||
waitUntil,
|
||||
} from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'admin/tests/helpers';
|
||||
import * as commonSelectors from 'admin/tests/helpers/selectors';
|
||||
import * as selectors from './selectors';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
|
||||
module('Acceptance | roles | add-grant-templates', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
let instances = {
|
||||
scopes: {
|
||||
org: null,
|
||||
},
|
||||
role: null,
|
||||
};
|
||||
let urls = {
|
||||
roles: null,
|
||||
role: null,
|
||||
addGrantTemplates: null,
|
||||
grants: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
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: [],
|
||||
});
|
||||
urls.roles = `/scopes/${instances.scopes.org.id}/roles`;
|
||||
urls.role = `${urls.roles}/${instances.role.id}`;
|
||||
urls.addGrantTemplates = `${urls.role}/add-grant-templates`;
|
||||
urls.grants = `${urls.role}/grants`;
|
||||
});
|
||||
|
||||
test('visiting add-grant-templates route', async function (assert) {
|
||||
await visit(urls.addGrantTemplates);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.addGrantTemplates);
|
||||
});
|
||||
|
||||
test('cannot access add-grant-templates without proper authorization', async function (assert) {
|
||||
setRunOptions({
|
||||
rules: {
|
||||
'color-contrast': {
|
||||
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-12-19
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const authorized_actions = instances.role.authorized_actions.filter(
|
||||
(item) => 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 });
|
||||
});
|
||||
});
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user