UI: PKI Role toolbar (#18229)

This commit is contained in:
Chelsea Shaw
2022-12-06 14:34:43 -06:00
committed by GitHub
parent 986f42678b
commit 724e3fd2f7
24 changed files with 432 additions and 149 deletions

View File

@@ -55,4 +55,8 @@ export default class PkiRoleAdapter extends ApplicationAdapter {
queryRecord(store, type, query) {
return this.fetchByQuery(store, query);
}
deleteRecord(store, type, snapshot) {
const { id, record } = snapshot;
return this.ajax(this._urlForRole(record.backend, id), 'DELETE');
}
}

View File

@@ -1,13 +1,71 @@
import Model, { attr } from '@ember-data/model';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import { withModelValidations } from 'vault/decorators/model-validations';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { withFormFields } from 'vault/decorators/model-form-fields';
const validations = {
name: [{ type: 'presence', message: 'Name is required.' }],
};
const fieldGroups = [
{
default: [
'name',
'issuerRef',
'customTtl',
'notBeforeDuration',
'maxTtl',
'generateLease',
'noStore',
'addBasicConstraints',
],
},
{
'Domain handling': [
'allowedDomains',
'allowedDomainsTemplate',
'allowBareDomains',
'allowSubdomains',
'allowGlobDomains',
'allowWildcardCertificates',
'allowLocalhost', // default: true (returned true by OpenApi)
'allowAnyName',
'enforceHostnames', // default: true (returned true by OpenApi)
],
},
{
'Key parameters': ['keyType', 'keyBits', 'signatureBits'],
},
{
'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'],
},
{ 'Policy identifiers': ['policyIdentifiers'] },
{
'Subject Alternative Name (SAN) Options': [
'allowIpSans',
'allowedUriSans',
'allowUriSansTemplate',
'allowedOtherSans',
],
},
{
'Additional subject fields': [
'allowedSerialNumbers',
'requireCn',
'useCsrCommonName',
'useCsrSans',
'ou',
'organization',
'country',
'locality',
'province',
'streetAddress',
'postalCode',
],
},
];
@withFormFields(null, fieldGroups)
@withModelValidations(validations)
export default class PkiRoleModel extends Model {
get useOpenAPI() {
@@ -242,34 +300,33 @@ export default class PkiRoleModel extends Model {
@attr({ hideFormSection: true }) postalCode;
/* End of overriding Additional subject field options */
/* CAPABILITIES */
/* CAPABILITIES
* Default to show UI elements unless we know they can't access the given path
*/
@lazyCapabilities(apiPath`${'backend'}/roles/${'id'}`, 'backend', 'id') updatePath;
get canDelete() {
return this.updatePath.get('canCreate');
return this.updatePath.get('isLoading') || this.updatePath.get('canCreate') !== false;
}
get canEdit() {
return this.updatePath.get('canEdit');
return this.updatePath.get('isLoading') || this.updatePath.get('canUpdate') !== false;
}
get canRead() {
return this.updatePath.get('canRead');
return this.updatePath.get('isLoading') || this.updatePath.get('canRead') !== false;
}
@lazyCapabilities(apiPath`${'backend'}/issue/${'id'}`, 'backend', 'id') generatePath;
get canReadIssue() {
// ARG TODO was duplicate name, added Issue
return this.generatePath.get('canUpdate');
get canGenerateCert() {
return this.generatePath.get('isLoading') || this.generatePath.get('canUpdate') !== false;
}
@lazyCapabilities(apiPath`${'backend'}/sign/${'id'}`, 'backend', 'id') signPath;
get canSign() {
return this.signPath.get('canUpdate');
return this.signPath.get('isLoading') || this.signPath.get('canUpdate') !== false;
}
@lazyCapabilities(apiPath`${'backend'}/sign-verbatim/${'id'}`, 'backend', 'id') signVerbatimPath;
get canSignVerbatim() {
return this.signVerbatimPath.get('canUpdate');
return this.signVerbatimPath.get('isLoading') || this.signVerbatimPath.get('canUpdate') !== false;
}
_fieldToAttrsGroups = null;
// Gets header/footer copy for specific toggle groups.
get fieldGroupsInfo() {
return {
@@ -297,67 +354,4 @@ export default class PkiRoleModel extends Model {
},
};
}
get fieldGroups() {
if (!this._fieldToAttrsGroups) {
this._fieldToAttrsGroups = fieldToAttrs(this, [
{
default: [
'name',
'issuerRef',
'customTtl',
'notBeforeDuration',
'maxTtl',
'generateLease',
'noStore',
'addBasicConstraints',
],
},
{
'Domain handling': [
'allowedDomains',
'allowedDomainsTemplate',
'allowBareDomains',
'allowSubdomains',
'allowGlobDomains',
'allowWildcardCertificates',
'allowLocalhost', // default: true (returned true by OpenApi)
'allowAnyName',
'enforceHostnames', // default: true (returned true by OpenApi)
],
},
{
'Key parameters': ['keyType', 'keyBits', 'signatureBits'],
},
{
'Key usage': ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'],
},
{ 'Policy identifiers': ['policyIdentifiers'] },
{
'Subject Alternative Name (SAN) Options': [
'allowIpSans',
'allowedUriSans',
'allowUriSansTemplate',
'allowedOtherSans',
],
},
{
'Additional subject fields': [
'allowedSerialNumbers',
'requireCn',
'useCsrCommonName',
'useCsrSans',
'ou',
'organization',
'country',
'locality',
'province',
'streetAddress',
'postalCode',
],
},
]);
}
return this._fieldToAttrsGroups;
}
}

View File

@@ -83,7 +83,7 @@
</form>
{{else}}
<div class="box is-sideless is-fullwidth is-marginless">
{{#each this.model.fieldGroups as |fieldGroup|}}
{{#each this.model.formFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (or (eq group "default") (eq group "Options"))}}
{{#each fields as |attr|}}

View File

@@ -1,6 +1,6 @@
{{#unless this.dontShowTab}}
{{#if @isEngine}}
<LinkTo @route={{@link}}>
<LinkTo @route={{@link}} data-test-secret-list-tab={{@label}}>
{{@label}}
</LinkTo>
{{else}}

View File

@@ -1,33 +1,38 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-role-details-title>
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
PKI Role
<code>{{@role.name}}</code>
</h1>
</p.levelLeft>
</PageHeader>
<Toolbar>
<ToolbarActions>
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.deleteRole}}
@confirmTitle="Delete role?"
@confirmButtonText="Delete"
data-test-pki-role-delete
>
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
<LinkTo class="toolbar-link" @route="overview">Generate Certificate <Icon @name="chevron-right" /></LinkTo>
<LinkTo class="toolbar-link" @route="overview">Sign Certificate <Icon @name="chevron-right" /></LinkTo>
<LinkTo class="toolbar-link" @route="roles.role.edit" @model={{@role.id}}>Edit <Icon @name="chevron-right" /></LinkTo>
{{#if @role.canDelete}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{this.deleteRole}}
@confirmTitle="Delete role?"
@confirmButtonText="Delete"
data-test-pki-role-delete
>
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
{{/if}}
{{#if @role.canGenerateCert}}
<LinkTo class="toolbar-link" @route="roles.role.generate" @model={{@role.id}} data-test-pki-role-generate-cert>
Generate Certificate
<Icon @name="chevron-right" />
</LinkTo>
{{/if}}
{{#if @role.canSign}}
<LinkTo class="toolbar-link" @route="roles.role.sign" @model={{@role.id}} data-test-pki-role-sign-cert>
Sign Certificate
<Icon @name="chevron-right" />
</LinkTo>
{{/if}}
{{#if @role.canEdit}}
<LinkTo class="toolbar-link" @route="roles.role.edit" @model={{@role.id}} data-test-pki-role-edit-link>
Edit
<Icon @name="chevron-right" />
</LinkTo>
{{/if}}
</ToolbarActions>
</Toolbar>
{{#each @role.fieldGroups as |fg|}}
{{#each @role.formFieldGroups as |fg|}}
{{#each-in fg as |group fields|}}
{{#if (not-eq group "default")}}
<h3 class="is-size-4 has-text-semibold has-top-margin-m">{{group}}</h3>

View File

@@ -1,22 +1,24 @@
import { action } from '@ember/object';
import RouterService from '@ember/routing/router-service';
import Component from '@glimmer/component';
// interface Attribute {
// name: string;
// options?: {
// label?: string;
// };
// }
import FlashMessageService from 'vault/services/flash-messages';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
// TODO: pull this in from route model once it's TS
interface Args {
role: {
backend: string;
id: string;
rollbackAttributes: () => void;
destroyRecord: () => void;
};
}
export default class DetailsPage extends Component<Args> {
@service declare readonly router: RouterService;
@service declare readonly flashMessages: FlashMessageService;
get breadcrumbs() {
return [
{ label: 'secrets', route: 'secrets', linkExternal: true },
@@ -30,7 +32,15 @@ export default class DetailsPage extends Component<Args> {
return ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'];
}
@action deleteRole() {
// TODO: delete role
@action
async deleteRole() {
try {
await this.args.role.destroyRecord();
this.flashMessages.success('Role deleted successfully');
this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.index');
} catch (error) {
this.args.role.rollbackAttributes();
this.flashMessages.danger(errorMessage(error));
}
}
}

View File

@@ -1,25 +1,9 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-role-details-title>
{{#if @model.isNew}}
Create a PKI role
{{else}}
Edit
{{@model.name}}
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
<form {{on "submit" (perform this.save)}}>
<div class="box is-sideless is-fullwidth is-marginless">
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
{{! ARG TODO write a test for namespace reminder }}
<NamespaceReminder @mode={{if @model.isNew "create" "update"}} @noun="PKI role" />
{{#each @model.fieldGroups as |fieldGroup|}}
{{#each @model.formFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{! DEFAULT VIEW }}
{{#if (eq group "default")}}

View File

@@ -8,7 +8,7 @@ export default class PkiCertificatesIndexRoute extends Route {
beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
return this.pathHelp.getNewModel('pki/certificate', 'pki');
return this.pathHelp.getNewModel('pki/certificate', this.secretMountPath.currentPath);
}
model() {

View File

@@ -8,7 +8,7 @@ export default class PkiIssuersIndexRoute extends Route {
beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
return this.pathHelp.getNewModel('pki/issuer', 'pki');
return this.pathHelp.getNewModel('pki/issuer', this.secretMountPath.currentPath);
}
model() {

View File

@@ -8,7 +8,7 @@ export default class PkiKeysIndexRoute extends Route {
beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
return this.pathHelp.getNewModel('pki/key', 'pki');
return this.pathHelp.getNewModel('pki/key', this.secretMountPath.currentPath);
}
model() {

View File

@@ -12,4 +12,15 @@ export default class PkiRolesCreateRoute extends PkiRolesIndexRoute {
backend: this.secretMountPath.currentPath,
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'roles', route: 'roles.index' },
{ label: 'create' },
];
}
}

View File

@@ -9,7 +9,7 @@ export default class PkiRolesIndexRoute extends Route {
beforeModel() {
// Must call this promise before the model hook otherwise
// the model doesn't hydrate from OpenAPI correctly.
return this.pathHelp.getNewModel('pki/role', 'pki');
return this.pathHelp.getNewModel('pki/role', this.secretMountPath.currentPath);
}
model() {

View File

@@ -8,4 +8,16 @@ export default class RolesRoleDetailsRoute extends PkiRolesIndexRoute {
id: role,
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const { id } = resolvedModel;
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'roles', route: 'roles.index' },
{ label: id },
];
}
}

View File

@@ -10,4 +10,17 @@ export default class PkiRoleEditRoute extends PkiRolesIndexRoute {
id: role,
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const { id } = resolvedModel;
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'roles', route: 'roles.index' },
{ label: id, route: 'roles.role.details' },
{ label: 'edit' },
];
}
}

View File

@@ -1,3 +1,14 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-role-page-title>
Create a PKI role
</h1>
</p.levelLeft>
</PageHeader>
<PkiRoleForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.index"}}

View File

@@ -10,7 +10,7 @@
/>
<Toolbar>
<ToolbarActions>
<ToolbarLink @type="add" @route="roles.create">
<ToolbarLink @type="add" @route="roles.create" data-test-pki-role-create-link>
Create role
</ToolbarLink>
</ToolbarActions>

View File

@@ -1 +1,13 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-role-page-title>
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
PKI Role
<code>{{this.model.name}}</code>
</h1>
</p.levelLeft>
</PageHeader>
<Page::PkiRoleDetails @role={{this.model}} />

View File

@@ -1,3 +1,13 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-role-page-title>
Edit role
</h1>
</p.levelLeft>
</PageHeader>
<PkiRoleForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}}

View File

@@ -0,0 +1,205 @@
import { create } from 'ember-cli-page-object';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import { click, currentURL, fillIn, visit } from '@ember/test-helpers';
import { SELECTORS } from 'vault/tests/helpers/pki/workflow';
const consoleComponent = create(consoleClass);
const tokenWithPolicy = async function (name, policy) {
await consoleComponent.runCommands([
`write sys/policies/acl/${name} policy=${btoa(policy)}`,
`write -field=client_token auth/token/create policies=${name}`,
]);
return consoleComponent.lastLogOutput;
};
const runCommands = async function (commands) {
try {
await consoleComponent.runCommands(commands);
const res = consoleComponent.lastLogOutput;
if (res.includes('Error')) {
throw new Error(res);
}
return res;
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`The following occurred when trying to run the command(s):\n ${commands.join('\n')} \n\n ${
consoleComponent.lastLogOutput
}`
);
throw error;
}
};
/**
* This test module should test the PKI workflow, including:
* - link between pages and confirm that the url is as expected
* - log in as user with a policy and ensure expected UI elements are shown/hidden
*/
module('Acceptance | pki workflow', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
await authPage.login();
// Setup PKI engine
const mountPath = `pki-workflow-${new Date().getTime()}`;
await enablePage.enable('pki', mountPath);
await runCommands([
`write ${mountPath}/roles/some-role \
issuer_ref="default" \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl="720h"`,
]);
this.mountPath = mountPath;
await logout.visit();
});
hooks.afterEach(async function () {
await logout.visit();
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('roles', function (hooks) {
hooks.beforeEach(async function () {
await authPage.login();
// Setup role-specific items
await runCommands([
`write ${this.mountPath}/roles/some-role \
issuer_ref="default" \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl="720h"`,
]);
const pki_admin_policy = `
path "${this.mountPath}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
},
`;
const pki_reader_policy = `
path "${this.mountPath}/roles" {
capabilities = ["read", "list"]
},
path "${this.mountPath}/roles/*" {
capabilities = ["read", "list"]
},
`;
const pki_editor_policy = `
path "${this.mountPath}/roles" {
capabilities = ["read", "list"]
},
path "${this.mountPath}/roles/*" {
capabilities = ["read", "update"]
},
`;
this.pkiRoleReader = await tokenWithPolicy('pki-reader', pki_reader_policy);
this.pkiRoleEditor = await tokenWithPolicy('pki-editor', pki_editor_policy);
this.pkiAdminToken = await tokenWithPolicy('pki-admin', pki_admin_policy);
await logout.visit();
});
test('shows correct items if user has all permissions', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(SELECTORS.rolesTab).exists('Roles tab is present');
await click(SELECTORS.rolesTab);
assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list');
await click('.linked-block');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.generateCertLink).exists('Generate cert link is shown');
await click(SELECTORS.generateCertLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/generate`);
// Go back to details and test all the links
await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.signCertLink).exists('Sign cert link is shown');
await click(SELECTORS.signCertLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/sign`);
await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown');
await click(SELECTORS.editRoleLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`);
await visit(`/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.deleteRoleButton).exists('Delete role button is shown');
await click(`${SELECTORS.deleteRoleButton} [data-test-confirm-action-trigger]`);
await click(`[data-test-confirm-button]`);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/roles`,
'redirects to roles list after deletion'
);
});
test('it does not show toolbar items the user does not have permission to see', async function (assert) {
await authPage.login(this.pkiRoleReader);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(SELECTORS.rolesTab).exists('Roles tab is present');
await click(SELECTORS.rolesTab);
assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list');
await click('.linked-block');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown');
assert.dom(SELECTORS.generateCertLink).doesNotExist('Generate cert link is not shown');
assert.dom(SELECTORS.signCertLink).doesNotExist('Sign cert link is not shown');
assert.dom(SELECTORS.editRoleLink).doesNotExist('Edit link is not shown');
});
test('it shows correct toolbar items for the user policy', async function (assert) {
await authPage.login(this.pkiRoleEditor);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(SELECTORS.rolesTab).exists('Roles tab is present');
await click(SELECTORS.rolesTab);
assert.dom(SELECTORS.createRoleLink).exists({ count: 1 }, 'Create role link is rendered');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
assert.dom('.linked-block').exists({ count: 1 }, 'One role is in list');
await click('.linked-block');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/details`);
assert.dom(SELECTORS.deleteRoleButton).doesNotExist('Delete role button is not shown');
assert.dom(SELECTORS.generateCertLink).doesNotExist('Generate cert link is not shown');
assert.dom(SELECTORS.signCertLink).doesNotExist('Sign cert link is not shown');
assert.dom(SELECTORS.editRoleLink).exists('Edit link is shown');
await click(SELECTORS.editRoleLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/some-role/edit`);
});
test('create role happy path', async function (assert) {
const roleName = 'another-role';
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(SELECTORS.rolesTab).exists('Roles tab is present');
await click(SELECTORS.rolesTab);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
await click(SELECTORS.createRoleLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/create`);
assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumbs are rendered');
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
assert.dom(SELECTORS.pageTitle).hasText('Create a PKI role');
await fillIn(SELECTORS.roleForm.roleName, roleName);
await click(SELECTORS.roleForm.roleCreateButton);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/${roleName}/details`);
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
assert.dom(SELECTORS.pageTitle).hasText(`PKI Role ${roleName}`);
});
});
});

View File

@@ -1,7 +1,4 @@
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
breadcrumbs: '[data-test-breadcrumbs] li',
title: '[data-test-role-details-title]',
issuerLabel: '[data-test-row-label="Issuer"]',
noStoreValue: '[data-test-value-div="Store in storage backend"]',
keyUsageValue: '[data-test-value-div="Key usage"]',

View File

@@ -0,0 +1,22 @@
import { SELECTORS as ROLEFORM } from './roles/form';
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
breadcrumbs: '[data-test-breadcrumbs] li',
pageTitle: '[data-test-pki-role-page-title]',
// TABS
overviewTab: '[data-test-secret-list-tab="Overview"]',
rolesTab: '[data-test-secret-list-tab="Roles"]',
issuersTab: '[data-test-secret-list-tab="Issuers"]',
certsTab: '[data-test-secret-list-tab="Certificates"]',
keysTab: '[data-test-secret-list-tab="Keys"]',
configTab: '[data-test-secret-list-tab="Configuration"]',
// ROLES
deleteRoleButton: '[data-test-pki-role-delete]',
generateCertLink: '[data-test-pki-role-generate-cert]',
signCertLink: '[data-test-pki-role-sign-cert]',
editRoleLink: '[data-test-pki-role-edit-link]',
createRoleLink: '[data-test-pki-role-create-link]',
roleForm: {
...ROLEFORM,
},
};

View File

@@ -27,7 +27,7 @@ module('Integration | Component | pki key details page', function (hooks) {
});
test('it renders the page component and deletes a key', async function (assert) {
assert.expect(9);
assert.expect(7);
this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => {
assert.ok(true, 'confirming delete fires off destroyRecord()');
});
@@ -39,8 +39,6 @@ module('Integration | Component | pki key details page', function (hooks) {
{ owner: this.engine }
);
assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumb containers exist');
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
assert.dom(SELECTORS.title).containsText('View key', 'title renders');
assert.dom(SELECTORS.keyIdValue).hasText(' 724862ff-6438-bad0-b598-77a6c7f4e934', 'key id renders');
assert.dom(SELECTORS.keyNameValue).hasText('test-key', 'key name renders');

View File

@@ -11,9 +11,8 @@ module('Integration | Component | pki-key-parameters', function (hooks) {
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role');
this.model.backend = 'pki';
[this.fields] = Object.values(this.model.fieldGroups.find((g) => g['Key parameters']));
this.model = this.store.createRecord('pki/role', { backend: 'pki' });
[this.fields] = Object.values(this.model.formFieldGroups.find((g) => g['Key parameters']));
});
test('it should render the component and display the correct defaults', async function (assert) {

View File

@@ -22,17 +22,13 @@ module('Integration | Component | pki role details page', function (hooks) {
});
test('it should render the page component', async function (assert) {
assert.expect(8);
assert.expect(5);
await render(
hbs`
<Page::PkiRoleDetails @role={{this.model}} />
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumb containers exist');
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
assert.dom(SELECTORS.title).containsText('PKI Role Foobar', 'Title includes type and name of role');
// Attribute-specific checks
assert.dom(SELECTORS.issuerLabel).hasText('Issuer', 'Label is');
assert.dom(SELECTORS.keyUsageValue).hasText('None', 'Key usage shows none when array is empty');
assert