UI: Add policy flyout to PKI (#12335) (#12373)

* make router-lookup helper

* add policyPaths arg to flyout and update route cache to map

* update kv flyouts and test coverage

* round out test coverage, rename method from get to lookup

* alphabetize PATH_MAP

* support other change events for inputSearch to allow copy/pasting items

* update overview requests and improve ux for limited permissions

* request each key permissions

* add flyout to pki page header

* update changelog

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Vault Automation 2026-02-17 22:28:47 -05:00 committed by GitHub
parent d8c0e831e2
commit 504334f8bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 289 additions and 125 deletions

View file

@ -1,3 +1,3 @@
```release-note:feature
**UI Policy Generator (Enterprise)**: Adds policy generator flyout to KV V2 secrets engine prepopulated with relevant API requests for each page.
**UI Policy Generator (Enterprise)**: Adds policy generator flyout to KV V2 and PKI secrets engines prepopulated with relevant API requests for each page.
```

View file

@ -16,51 +16,56 @@ export const SUDO_PATHS = [
export const SUDO_PATH_PREFIXES = ['sys/leases/revoke-prefix', 'sys/leases/revoke-force'];
export const PATH_MAP = {
customLogin: apiPath`sys/config/ui/login/default-auth/${'id'}`,
customMessages: apiPath`sys/config/ui/custom-messages/${'id'}`,
syncActivate: apiPath`sys/activation-flags/secrets-sync/activate`,
syncDestination: apiPath`sys/sync/destinations/${'type'}/${'name'}`,
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
syncRemoveAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/remove`,
kvConfig: apiPath`${'path'}/config`,
kvMetadata: apiPath`${'backend'}/metadata/${'path'}`,
authMethodConfig: apiPath`auth/${'path'}/config`,
authMethodConfigAws: apiPath`auth/${'path'}/config/client`,
authMethodDelete: apiPath`sys/auth/${'path'}`,
pkiRevoke: apiPath`${'backend'}/revoke`,
clientsActivityExport: apiPath`${'namespace'}/sys/internal/counters/activity/export`,
clientsConfig: apiPath`sys/internal/counters/config`,
customLogin: apiPath`sys/config/ui/login/default-auth/${'id'}`,
customMessages: apiPath`sys/config/ui/custom-messages/${'id'}`,
kmipCredentialsRevoke: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
kubernetesCreds: apiPath`${'backend'}/creds/${'name'}`,
kubernetesRole: apiPath`${'backend'}/role/${'name'}`,
kvConfig: apiPath`${'path'}/config`,
kvMetadata: apiPath`${'backend'}/metadata/${'path'}`,
ldapDynamicRole: apiPath`${'backend'}/role/${'name'}`,
ldapDynamicRoleCreds: apiPath`${'backend'}/creds/${'name'}`,
ldapLibrary: apiPath`${'backend'}/library/${'name'}`,
ldapLibraryCheckIn: apiPath`${'backend'}/library/${'name'}/check-in`,
ldapLibraryCheckOut: apiPath`${'backend'}/library/${'name'}/check-out`,
ldapRotateStaticRole: apiPath`${'backend'}/rotate-role/${'name'}`,
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
pkiCertificates: apiPath`${'backend'}/certificates`,
pkiConfigAcme: apiPath`${'backend'}/config/acme`,
pkiConfigAutoTidy: apiPath`${'backend'}/config/auto-tidy`,
pkiConfigCluster: apiPath`${'backend'}/config/cluster`,
pkiConfigCrl: apiPath`${'backend'}/config/crl`,
pkiConfigUrls: apiPath`${'backend'}/config/urls`,
pkiIssuersImportBundle: apiPath`${'backend'}/issuers/import/bundle`,
pkiIssuersGenerateRoot: apiPath`${'backend'}/issuers/generate/root/${'type'}`,
pkiIssuersGenerateIntermediate: apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`,
pkiIssuersCrossSign: apiPath`${'backend'}/issuers/cross-sign`,
pkiIssuer: apiPath`${'backend'}/issuer/${'issuerId'}`,
pkiIssuerSignIntermediate: apiPath`${'backend'}/issuer/${'issuerId'}/sign-intermediate`,
pkiRoot: apiPath`${'backend'}/root`,
pkiRootRotate: apiPath`${'backend'}/root/rotate/${'type'}`,
pkiIntermediateCrossSign: apiPath`${'backend'}/intermediate/cross-sign`,
pkiIssue: apiPath`${'backend'}/issue/${'id'}`,
pkiIssuer: apiPath`${'backend'}/issuer/${'issuerId'}`,
pkiIssuersCrossSign: apiPath`${'backend'}/issuers/cross-sign`,
pkiIssuersGenerateIntermediate: apiPath`${'backend'}/issuers/generate/intermediate/${'type'}`,
pkiIssuersGenerateRoot: apiPath`${'backend'}/issuers/generate/root/${'type'}`,
pkiIssuerSignIntermediate: apiPath`${'backend'}/issuer/${'issuerId'}/sign-intermediate`,
pkiIssuersImportBundle: apiPath`${'backend'}/issuers/import/bundle`,
pkiKey: apiPath`${'backend'}/key/${'keyId'}`,
pkiKeysGenerate: apiPath`${'backend'}/keys/generate`,
pkiKeysImport: apiPath`${'backend'}/keys/import`,
pkiRevoke: apiPath`${'backend'}/revoke`,
pkiRole: apiPath`${'backend'}/roles/${'id'}`,
pkiIssue: apiPath`${'backend'}/issue/${'id'}`,
pkiRoles: apiPath`${'backend'}/roles`,
pkiRoot: apiPath`${'backend'}/root`,
pkiRootRotate: apiPath`${'backend'}/root/rotate/${'type'}`,
pkiSign: apiPath`${'backend'}/sign/${'id'}`,
pkiSignVerbatim: apiPath`${'backend'}/sign-verbatim/${'id'}`,
ldapStaticRole: apiPath`${'backend'}/static-role/${'name'}`,
ldapDynamicRole: apiPath`${'backend'}/role/${'name'}`,
ldapRotateStaticRole: apiPath`${'backend'}/rotate-role/${'name'}`,
ldapStaticRoleCreds: apiPath`${'backend'}/static-cred/${'name'}`,
ldapDynamicRoleCreds: apiPath`${'backend'}/creds/${'name'}`,
ldapLibrary: apiPath`${'backend'}/library/${'name'}`,
ldapLibraryCheckOut: apiPath`${'backend'}/library/${'name'}/check-out`,
ldapLibraryCheckIn: apiPath`${'backend'}/library/${'name'}/check-in`,
kubernetesRole: apiPath`${'backend'}/role/${'name'}`,
kubernetesCreds: apiPath`${'backend'}/creds/${'name'}`,
kmipScope: apiPath`${'backend'}/scopes/${'name'}`,
kmipRole: apiPath`${'backend'}/scopes/${'scope'}/roles/${'name'}`,
kmipCredentialsRevoke: apiPath`${'backend'}/scope/${'scope'}/role/${'role'}/credentials/revoke`,
clientsConfig: apiPath`sys/internal/counters/config`,
clientsActivityExport: apiPath`${'namespace'}/sys/internal/counters/activity/export`,
pkiTidy: apiPath`${'backend'}/tidy`,
pkiTidyStatus: apiPath`${'backend'}/tidy/status`,
syncActivate: apiPath`sys/activation-flags/secrets-sync/activate`,
syncDestination: apiPath`sys/sync/destinations/${'type'}/${'name'}`,
syncRemoveAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/remove`,
syncSetAssociation: apiPath`sys/sync/destinations/${'type'}/${'name'}/associations/set`,
};

View file

@ -8,7 +8,7 @@
@type="text"
@id={{@id}}
@value={{this.searchInput}}
{{on "keyup" this.inputChanged}}
{{on (or @changeEvent "keyup") this.inputChanged}}
placeholder={{@placeholder}}
autocomplete="off"
data-test-input-search={{@id}}

View file

@ -9,13 +9,14 @@ import { tracked } from '@glimmer/tracking';
/**
* @module InputSearch
* This component renders an input that fires a callback on "keyup" containing the input's value
* This component renders an input that fires a callback on "keyup" or the passed change event containing the input's value
*
* @example
* <InputSearch @initialValue="secret/path/" @onChange={{this.handleSearch}} @placeholder="search..." />
*
* @param {string} [id] - unique id for the input
* @param {string} [initialValue] - initial search value, i.e. a secret path prefix, that pre-fills the input field
* @param {string} [changeEvent="keyup"] - the input change event for which to fire the onChange callback
* @param {string} [placeholder] - placeholder text for the input
* @param {string} [label] - label for the input
* @param {string} [subtext] - displays below the label

View file

@ -44,34 +44,36 @@
</div>
<div class="level-right is-flex is-paddingless is-marginless">
<div class="level-item">
{{#if (or @canRead @canEdit)}}
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage key"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if @canRead}}
<dd.Interactive
@route="keys.key.details"
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="details"
>
Details
</dd.Interactive>
{{/if}}
{{#if @canEdit}}
<dd.Interactive
@route="keys.key.edit"
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="edit"
>
Edit
</dd.Interactive>
{{/if}}
</Hds::Dropdown>
{{/if}}
{{#let (get @keyPermsById pkiKey.key_id) as |perms|}}
{{#if (or perms.canRead perms.canUpdate)}}
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="Manage key"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#if perms.canRead}}
<dd.Interactive
@route="keys.key.details"
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="details"
>
Details
</dd.Interactive>
{{/if}}
{{#if perms.canUpdate}}
<dd.Interactive
@route="keys.key.edit"
@models={{array @backend pkiKey.key_id}}
data-test-key-menu-link="edit"
>
Edit
</dd.Interactive>
{{/if}}
</Hds::Dropdown>
{{/if}}
{{/let}}
</div>
</div>
</div>

View file

@ -19,11 +19,11 @@
</:action>
<:content>
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
{{format-number @issuers.length}}
</Hds::Text::Display>
</:content>
</OverviewCard>
{{#if (not-eq @roles 403)}}
{{#if @canListRoles}}
<OverviewCard
@cardTitle="Roles"
@subText="The total number of roles in this PKI mount that have been created to generate certificates."
@ -39,24 +39,37 @@
</:action>
<:content>
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{format-number (if (eq @roles 404) 0 @roles.length)}}
{{format-number @roles.length}}
</Hds::Text::Display>
</:content>
</OverviewCard>
{{/if}}
<OverviewCard @cardTitle="Issue certificate" @subText="Begin issuing a certificate by choosing a role.">
<OverviewCard
@cardTitle="Issue certificate"
@subText="Begin issuing a certificate by {{if @canListRoles 'choosing' 'entering'}} a role."
>
<:content>
<div class="has-top-margin-m is-flex">
<SearchSelect
class="is-flex-grow-1"
@ariaLabel="Role"
@selectLimit="1"
@options={{this.searchSelectOptions.roles}}
@placeholder="Type to find a role..."
@disallowNewItems={{true}}
@onChange={{this.handleRolesInput}}
data-test-issue-certificate-input
/>
{{#if @canListRoles}}
<SearchSelect
class="is-flex-grow-1"
@ariaLabel="Role"
@selectLimit="1"
@options={{this.searchSelectOptions.roles}}
@placeholder="Type to find a role..."
@disallowNewItems={{true}}
@onChange={{this.handleRolesInput}}
data-test-issue-certificate-input
/>
{{else}}
<InputSearch
class="is-flex-grow-1"
@placeholder="Input a role.."
@onChange={{this.handleRolesInput}}
@changeEvent="input"
@id="role"
/>
{{/if}}
<Hds::Button
@text="Issue"
@color="secondary"
@ -70,19 +83,32 @@
</:content>
</OverviewCard>
<OverviewCard @cardTitle="View certificate" @subText="Quickly view a certificate by typing its serial number.">
<OverviewCard
@cardTitle="View certificate"
@subText="Quickly view a certificate by {{if @canListCertificates 'looking up' 'providing'}} its serial number."
>
<:content>
<div class="has-top-margin-m {{unless this.certificateValue 'is-flex'}}">
<SearchSelect
class="is-flex-grow-1"
@ariaLabel="Certificate serial number"
@selectLimit="1"
@options={{this.searchSelectOptions.certificates}}
@placeholder="33:a3:..."
@disallowNewItems={{true}}
@onChange={{this.handleCertificateInput}}
data-test-view-certificate-input
/>
{{#if @canListCertificates}}
<SearchSelect
class="is-flex-grow-1"
@ariaLabel="Certificate serial number"
@selectLimit="1"
@options={{this.searchSelectOptions.certificates}}
@placeholder="33:a3:..."
@disallowNewItems={{true}}
@onChange={{this.handleCertificateInput}}
data-test-view-certificate-input
/>
{{else}}
<InputSearch
class="is-flex-grow-1"
@placeholder="33:a3:..."
@onChange={{this.handleCertificateInput}}
@changeEvent="input"
@id="certificate"
/>
{{/if}}
<Hds::Button
@text="View"
@color="secondary"

View file

@ -13,6 +13,7 @@
</:breadcrumbs>
<:actions>
<CodeGenerator::Policy::Flyout @policyPaths={{this.policyPaths}} />
{{#if @configRoute}}
<Hds::Button @color="secondary" @route="overview" @text="Exit configuration" data-test-button="Exit configuration" />
{{else}}

View file

@ -8,10 +8,12 @@ import { tracked } from '@glimmer/tracking';
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
import type RouterService from '@ember/routing/router-service';
import type FlashMessageService from 'vault/services/flash-messages';
import type { PATH_MAP } from 'vault/utils/constants/capabilities';
import type ApiService from 'vault/services/api';
import type CapabilitiesService from 'vault/services/capabilities';
import type FlashMessageService from 'vault/services/flash-messages';
import type RouterService from '@ember/routing/router-service';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
/**
* @module PkiPageHeader
@ -26,22 +28,40 @@ interface Args {
backend: { id: string };
}
const ROUTE_PATH_MAP = {
'vault.cluster.secrets.backend.pki.certificates.index': ['pkiCertificates'],
'vault.cluster.secrets.backend.pki.roles.index': ['pkiRoles'],
'vault.cluster.secrets.backend.pki.tidy.index': ['pkiTidy', 'pkiTidyStatus', 'pkiConfigAutoTidy'],
} satisfies Record<string, readonly (keyof typeof PATH_MAP)[]>;
export default class PkiPageHeader extends Component<Args> {
@service('app-router') declare readonly router: RouterService;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly capabilities: CapabilitiesService;
@tracked engineToDisable = undefined;
get breadcrumbs() {
return [
{ label: 'Vault', route: 'vault', icon: 'vault', linkExternal: true },
{ label: 'Secrets engines', route: 'secrets', linkExternal: true },
{
label: this.args?.backend?.id,
},
{ label: this.args?.backend?.id },
];
}
// PKI does not make capability requests for these routes
// so manually pass the relevant paths for each route.
get policyPaths() {
const backend = this.args?.backend?.id;
const { currentRouteName } = this.router;
const paths = ROUTE_PATH_MAP[currentRouteName as keyof typeof ROUTE_PATH_MAP];
if (paths) {
return this.capabilities.pathsForList(paths, { backend });
}
return null;
}
@task
*disableEngine(engine: SecretsEngineResource) {
const { engineType, id, path } = engine;

View file

@ -22,21 +22,24 @@ export default class PkiKeysIndexRoute extends Route {
},
};
async fetchCapabilities(keyId) {
async fetchCapabilities(keys) {
const { pathFor } = this.capabilities;
const backend = this.secretMountPath.currentPath;
const keyPathsById = this.keyPathsById(backend, keys);
const pathMap = {
import: pathFor('pkiKeysImport', { backend }),
generate: pathFor('pkiKeysGenerate', { backend }),
key: pathFor('pkiKey', { backend, keyId }),
...keyPathsById,
};
const perms = await this.capabilities.fetch(Object.values(pathMap));
const apiPaths = Object.values(pathMap);
const perms = await this.capabilities.fetch(apiPaths, {
routeForCache: 'vault.cluster.secrets.backend.pki.keys',
});
return {
canImportKeys: perms[pathMap.import].canUpdate,
canGenerateKeys: perms[pathMap.generate].canUpdate,
canRead: perms[pathMap.key].canRead,
canEdit: perms[pathMap.key].canUpdate,
keyPermsById: this.keyCapabilitiesById(keyPathsById, perms),
};
}
@ -53,7 +56,7 @@ export default class PkiKeysIndexRoute extends Route {
PkiListKeysListEnum.TRUE
);
const keys = this.api.keyInfoToArray(response, 'key_id');
const capabilities = await this.fetchCapabilities(keys[0].key_id);
const capabilities = await this.fetchCapabilities(keys);
Object.assign(model, { ...capabilities, keys: paginate(keys, { page }) });
} catch (e) {
if (e.response.status === 404) {
@ -82,4 +85,20 @@ export default class PkiKeysIndexRoute extends Route {
controller.set('page', undefined);
}
}
keyPathsById(backend, keys) {
// Construct API path for each key in the list
return Object.fromEntries(
keys.map(({ key_id: keyId }) => [keyId, this.capabilities.pathFor('pkiKey', { backend, keyId })])
);
}
keyCapabilitiesById(keyPathsById, perms) {
// Iterate over key ids and return an object with Capabilities as their value
return Object.fromEntries(
Object.entries(keyPathsById)
.filter(([, apiPath]) => apiPath in perms)
.map(([keyId, apiPath]) => [keyId, perms[apiPath]])
);
}
}

View file

@ -24,9 +24,9 @@ export const getCliMessage = (msg) => {
@withConfig()
export default class PkiOverviewRoute extends Route {
@service secretMountPath;
@service auth;
@service api;
@service capabilities;
@service secretMountPath;
async fetchAllCertificates() {
try {
@ -36,7 +36,10 @@ export default class PkiOverviewRoute extends Route {
);
return keys;
} catch (e) {
return e.response.status;
const { status } = await this.api.parseError(e);
// If there was a permissions (403) or some other error
// swallow because this data is for rendering overview cards
return status === 404 ? [] : null;
}
}
@ -48,7 +51,10 @@ export default class PkiOverviewRoute extends Route {
);
return keys;
} catch (e) {
return e.response.status;
const { status } = await this.api.parseError(e);
// If there was a permissions (403) or some other error
// swallow because this data is for rendering overview cards
return status === 404 ? [] : null;
}
}
@ -60,17 +66,39 @@ export default class PkiOverviewRoute extends Route {
);
return keys;
} catch (e) {
return e.response.status;
const { status } = await this.api.parseError(e);
return status === 404 ? [] : null;
}
}
async fetchCapabilities() {
const { pathFor } = this.capabilities;
const backend = this.secretMountPath.currentPath;
// the issuers list endpoint is unauthenticated so we do not need to check capabilities for it
const pathMap = {
certificates: pathFor('pkiCertificates', { backend }),
roles: pathFor('pkiRoles', { backend }),
};
const apiPaths = Object.values(pathMap);
const perms = await this.capabilities.fetch(apiPaths, {
routeForCache: 'vault.cluster.secrets.backend.pki.overview',
});
return {
canListCertificates: perms[pathMap.certificates].canList,
canListRoles: perms[pathMap.roles].canList,
};
}
async model() {
const { canListCertificates, canListRoles } = await this.fetchCapabilities();
return hash({
hasConfig: this.pkiMountHasConfig,
engine: this.modelFor('application'),
roles: this.fetchAllRoles(),
roles: canListRoles ? this.fetchAllRoles() : null,
issuers: this.fetchAllIssuers(),
certificates: this.fetchAllCertificates(),
certificates: canListCertificates ? this.fetchAllCertificates() : null,
canListCertificates,
canListRoles,
});
}

View file

@ -11,7 +11,6 @@
@backend={{this.model.parentModel.id}}
@canImportKeys={{this.model.canImportKeys}}
@canGenerateKeys={{this.model.canGenerateKeys}}
@canRead={{this.model.canRead}}
@canEdit={{this.model.canEdit}}
@keyPermsById={{this.model.keyPermsById}}
@hasConfig={{this.model.hasConfig}}
/>

View file

@ -13,6 +13,8 @@
@roles={{this.model.roles}}
@certificates={{this.model.certificates}}
@engine={{this.model.engine}}
@canListCertificates={{this.model.canListCertificates}}
@canListRoles={{this.model.canListRoles}}
/>
{{else}}
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>

View file

@ -80,10 +80,11 @@ module('Acceptance | pki overview', function (hooks) {
assert.dom(`${overviewCard.container('Roles')} p`).hasText('1');
});
test('hides roles card if user does not have permissions', async function (assert) {
test('hides roles and certificates card if user does not have permissions', async function (assert) {
await login(this.pkiIssuersList);
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
assert.dom(overviewCard.title('Roles')).doesNotExist('Roles card does not exist');
assert.dom(overviewCard.title('Certificates')).doesNotExist('Certificates card does not exist');
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
});

View file

@ -34,8 +34,26 @@ module('Integration | Component | pki key list page', function (hooks) {
this.keys.meta = STANDARD_META;
this.canImportKeys = true;
this.canGenerateKeys = true;
this.canRead = true;
this.canEdit = true;
this.keyPermsById = {
'724862ff-6438-bad0-b598-77a6c7f4e934': {
canCreate: true,
canDelete: true,
canList: true,
canPatch: true,
canRead: true,
canSudo: true,
canUpdate: true,
},
'9fdddf12-9ce3-0268-6b34-dc1553b00175': {
canCreate: true,
canDelete: true,
canList: true,
canPatch: true,
canRead: true,
canSudo: true,
canUpdate: true,
},
};
this.renderComponent = () =>
render(
@ -45,8 +63,7 @@ module('Integration | Component | pki key list page', function (hooks) {
@mountPoint="vault.cluster.secrets.backend.pki"
@canImportKeys={{this.canImportKeys}}
@canGenerateKeys={{this.canGenerateKeys}}
@canRead={{this.canRead}}
@canEdit={{this.canEdit}}
@keyPermsById={{this.keyPermsById}}
/>,
`,
{ owner: this.engine }
@ -92,9 +109,10 @@ module('Integration | Component | pki key list page', function (hooks) {
this.canImportKeys = false;
this.canGenerateKeys = false;
this.canRead = false;
this.canEdit = false;
this.keyPermsById = {
'724862ff-6438-bad0-b598-77a6c7f4e934': { canRead: false, canUpdate: false },
'9fdddf12-9ce3-0268-6b34-dc1553b00175': { canRead: false, canUpdate: false },
};
await this.renderComponent();
assert.dom(PKI_KEYS.importKey).doesNotExist('renders import action');

View file

@ -24,10 +24,19 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
this.roles = ['role-0', 'role-1', 'role-2'];
this.certificates = ['22:2222:22222:2222', '33:3333:33333:3333'];
this.engineId = 'pki';
this.canListCertificates = true;
this.canListRoles = true;
this.renderComponent = () =>
render(
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @cretificates={{this.certificates}} @engine={{this.engineId}} />`,
hbs`<Page::PkiOverview
@issuers={{this.issuers}}
@roles={{this.roles}}
@certificates={{this.certificates}}
@engine={{this.engineId}}
@canListCertificates={{this.canListCertificates}}
@canListRoles={{this.canListRoles}}
/>`,
{ owner: this.engine }
);
});
@ -39,6 +48,14 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
.hasText(
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2'
);
this.issuers = [];
await this.renderComponent();
assert
.dom(overviewCard.container('Issuers'))
.hasText(
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 0'
);
});
test('shows the correct information on roles card', async function (assert) {
@ -48,7 +65,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
.hasText(
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 3'
);
this.roles = 404;
this.roles = [];
await this.renderComponent();
assert
.dom(overviewCard.container('Roles'))
@ -57,17 +74,42 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
);
});
test('shows the input search fields for View Certificates card', async function (assert) {
test('shows the search select dropdown for View Certificates card', async function (assert) {
await this.renderComponent();
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
assert
.dom(overviewCard.description('View certificate'))
.hasText('Quickly view a certificate by looking up its serial number.');
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
assert.dom(GENERAL.inputSearch('certificate')).doesNotExist('it does not render certificate input');
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
});
test('shows the search select dropdown for Issue Certificates card', async function (assert) {
await this.renderComponent();
assert.dom(overviewCard.title('Issue certificate')).hasText('Issue certificate');
assert
.dom(overviewCard.description('Issue certificate'))
.hasText('Begin issuing a certificate by choosing a role.');
assert.dom(PKI_OVERVIEW.issueCertificateInput).exists();
assert.dom(GENERAL.inputSearch('role')).doesNotExist('it does not render role input');
assert.dom(PKI_OVERVIEW.issueCertificateButton).hasText('Issue');
});
test('shows the input search fields for Issue Certificates card', async function (assert) {
test('it renders manual search inputs when no list permission', async function (assert) {
this.canListCertificates = false;
this.canListRoles = false;
await this.renderComponent();
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
assert.dom(overviewCard.container('Roles')).doesNotExist();
assert
.dom(overviewCard.description('View certificate'))
.hasText('Quickly view a certificate by providing its serial number.');
assert
.dom(overviewCard.description('Issue certificate'))
.hasText('Begin issuing a certificate by entering a role.');
assert.dom(PKI_OVERVIEW.issueCertificateInput).doesNotExist('role search select does not render');
assert.dom(GENERAL.inputSearch('role')).exists('it renders input instead of search select');
assert.dom(PKI_OVERVIEW.viewCertificateInput).doesNotExist('certificate search select does not render');
assert.dom(GENERAL.inputSearch('certificate')).exists('it renders input instead of search selects');
});
});