mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-18 18:38:08 -05:00
* removes unused model hook from issuer import route * updates pki roles route to use api service * updates pki role details route to use api service * removes Ember Data Model support from pki-not-valid-after-form component * updates pki role generate and sign workflows to use certificate form and api service * adds pki certificate form * updates pki certificates routes to use api service * adds pki role form * removes Ember Data Model support from pki-key-parameters component * removes Ember Data Model support from pki-key-usage component * updates pki role create and edit views to use api service and form class * fixes tests * fixes a11y violations Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
parent
10d28ee713
commit
5ff894f494
51 changed files with 1276 additions and 1208 deletions
58
ui/app/forms/secrets/pki/certificate.ts
Normal file
58
ui/app/forms/secrets/pki/certificate.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import OpenApiForm from 'vault/forms/open-api';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
|
||||
import type { PkiIssueWithRoleRequest, PkiSignWithRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
|
||||
type PkiCertificateFormData = PkiIssueWithRoleRequest | PkiSignWithRoleRequest;
|
||||
|
||||
export default class PkiCertificateForm extends OpenApiForm<PkiCertificateFormData> {
|
||||
constructor(...args: ConstructorParameters<typeof OpenApiForm>) {
|
||||
super(...args);
|
||||
|
||||
const sansKeys = ['exclude_cn_from_sans', 'alt_names', 'ip_sans', 'uri_sans', 'other_sans'];
|
||||
const excludeKeys = ['ttl', 'issuer_ref']; // issuer_ref is not editable, ttl is set via customTtl
|
||||
const primaryKeys = ['common_name', 'csr'];
|
||||
const defaultGroup = this.formFieldGroups[0]?.['default'] as FormField[];
|
||||
|
||||
const fields = defaultGroup.reduce(
|
||||
(fields: { default: FormField[]; sans: FormField[] }, field: FormField) => {
|
||||
// better UX if csr is textarea
|
||||
if (field.name === 'csr') {
|
||||
field.options.editType = 'textarea';
|
||||
}
|
||||
// move sans related fields to their own group
|
||||
if (sansKeys.includes(field.name)) {
|
||||
fields.sans.push(field);
|
||||
} else if (field.name === 'not_after') {
|
||||
// customTtl is a convenience field that sets ttl and notAfter via one input <PkiNotValidAfterForm>
|
||||
// remove not_after and ttl fields and replace with customTtl
|
||||
const customTtlField = new FormField('customTtl', undefined, {
|
||||
label: 'Not valid after',
|
||||
subText:
|
||||
'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.',
|
||||
editType: 'yield',
|
||||
});
|
||||
fields.default.push(customTtlField);
|
||||
} else if (primaryKeys.includes(field.name)) {
|
||||
// move common_name and csr (sign only) to the top of the default group
|
||||
fields.default.unshift(field);
|
||||
} else if (!excludeKeys.includes(field.name)) {
|
||||
fields.default.push(field);
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
{ default: [], sans: [] }
|
||||
);
|
||||
|
||||
this.formFieldGroups = [
|
||||
new FormFieldGroup('default', fields.default),
|
||||
new FormFieldGroup('Subject Alternative Name (SAN) Options', fields.sans),
|
||||
];
|
||||
}
|
||||
}
|
||||
151
ui/app/forms/secrets/pki/role.ts
Normal file
151
ui/app/forms/secrets/pki/role.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import OpenApiForm from 'vault/forms/open-api';
|
||||
import FormFieldGroup from 'vault/utils/forms/field-group';
|
||||
|
||||
import type { PkiWriteRoleRequest } from '@hashicorp/vault-client-typescript';
|
||||
import type Form from 'vault/forms/form';
|
||||
import FormField from 'vault/utils/forms/field';
|
||||
import type { Validations } from 'vault/vault/app-types';
|
||||
|
||||
type PkiRoleFormData = PkiWriteRoleRequest & { name: string };
|
||||
|
||||
export default class PkiRoleForm extends OpenApiForm<PkiRoleFormData> {
|
||||
constructor(...args: ConstructorParameters<typeof Form>) {
|
||||
super('PkiWriteRoleRequest', ...args);
|
||||
|
||||
this.formFields.push(
|
||||
// add name field since it's not part of the OpenAPI spec
|
||||
new FormField('name', 'string', {
|
||||
label: 'Role Name',
|
||||
editDisabled: true,
|
||||
}),
|
||||
// add customTtl which is a convenience field that sets ttl and notAfter via one input <PkiNotValidAfterForm>
|
||||
new FormField('customTtl', undefined, {
|
||||
label: 'Not valid after',
|
||||
subText:
|
||||
'The time after which this certificate will no longer be valid. This can be a TTL (a range of time from now) or a specific date.',
|
||||
editType: 'yield',
|
||||
})
|
||||
);
|
||||
// setup form field groups
|
||||
this.formFieldGroups = [];
|
||||
for (const group in this.fieldGroupKeys) {
|
||||
const fieldKeys = this.fieldGroupKeys[group as keyof typeof this.fieldGroupKeys];
|
||||
this.formFieldGroups.push(
|
||||
new FormFieldGroup(
|
||||
group,
|
||||
fieldKeys.map((key) => this.findAndTransform(key))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validations: Validations = {
|
||||
name: [{ type: 'presence', message: 'Name is required.' }],
|
||||
};
|
||||
|
||||
fieldGroupKeys = {
|
||||
default: [
|
||||
'name',
|
||||
'issuer_ref',
|
||||
'customTtl',
|
||||
'not_before_duration',
|
||||
'max_ttl',
|
||||
'generate_lease',
|
||||
'no_store',
|
||||
'no_store_metadata',
|
||||
'basic_constraints_valid_for_non_ca',
|
||||
],
|
||||
'Domain handling': [
|
||||
'allowed_domains',
|
||||
'allowed_domains_template',
|
||||
'allow_bare_domains',
|
||||
'allow_subdomains',
|
||||
'allow_glob_domains',
|
||||
'allow_wildcard_certificates',
|
||||
'allow_localhost',
|
||||
'allow_any_name',
|
||||
'enforce_hostnames',
|
||||
],
|
||||
'Key parameters': ['key_type', 'key_bits', 'signature_bits'],
|
||||
'Key usage': ['key_usage', 'ext_key_usage', 'ext_key_usage_oids'],
|
||||
'Policy identifiers': ['policy_identifiers'],
|
||||
'Subject Alternative Name (SAN) Options': [
|
||||
'allow_ip_sans',
|
||||
'allowed_uri_sans',
|
||||
'allowed_uri_sans_template',
|
||||
'allowed_other_sans',
|
||||
],
|
||||
'Additional subject fields': [
|
||||
'allowed_user_ids',
|
||||
'allowed_serial_numbers',
|
||||
'serial_number_source',
|
||||
'require_cn',
|
||||
'use_csr_common_name',
|
||||
'use_csr_sans',
|
||||
'ou',
|
||||
'organization',
|
||||
'country',
|
||||
'locality',
|
||||
'province',
|
||||
'street_address',
|
||||
'postal_code',
|
||||
],
|
||||
};
|
||||
|
||||
fieldGroupsInfo = {
|
||||
'Domain handling': {
|
||||
footer: {
|
||||
text: 'These options can interact intricately with one another. For more information,',
|
||||
docText: 'learn more here.',
|
||||
docLink: '/vault/api-docs/secret/pki#allowed_domains',
|
||||
},
|
||||
},
|
||||
'Key parameters': {
|
||||
header: {
|
||||
text: `These are the parameters for generating or validating the certificate's key material.`,
|
||||
},
|
||||
},
|
||||
'Subject Alternative Name (SAN) Options': {
|
||||
header: {
|
||||
text: `Subject Alternative Names (SANs) are identities (domains, IP addresses, and URIs) Vault attaches to the requested certificates.`,
|
||||
},
|
||||
},
|
||||
'Additional subject fields': {
|
||||
header: {
|
||||
text: `Additional identity metadata Vault can attach to the requested certificates.`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
findAndTransform(key: string) {
|
||||
const field = this.formFields.find((field) => field.name === key) as FormField;
|
||||
if (key === 'key_usage' && !Array.isArray(this.data.key_usage)) {
|
||||
// default value for key_usage needs to be array
|
||||
// in the spec there is a default param that has the correct array but also a value in the x-vault-displayAttrs which is taking precedence
|
||||
// these should ideally align but perhaps we need to look at the default value over the value in x-vault-displayAttrs
|
||||
const keyUsage = (this.data.key_usage as unknown as string) || '';
|
||||
this.data.key_usage = keyUsage?.split(',');
|
||||
} else {
|
||||
const label = {
|
||||
not_before_duration: 'Backdate validity',
|
||||
no_store: 'Do not store certificates in storage backend',
|
||||
no_store_metadata: 'Do not store certificate metadata in storage backend',
|
||||
}[key];
|
||||
|
||||
if (label) {
|
||||
field.options.label = label;
|
||||
}
|
||||
if (key === 'not_before_duration') {
|
||||
field.options.subText =
|
||||
'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.';
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
}}
|
||||
|
||||
<nav class="toolbar" aria-label="toolbar" ...attributes>
|
||||
<div class="toolbar-scroller">
|
||||
<div class="toolbar-scroller" tabindex="0">
|
||||
{{yield}}
|
||||
</div>
|
||||
</nav>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
{{on "click" this.downloadCert}}
|
||||
data-test-pki-cert-download-button
|
||||
/>
|
||||
{{#if @model.canRevoke}}
|
||||
{{#if @canRevoke}}
|
||||
<ConfirmAction
|
||||
@buttonText="Revoke certificate"
|
||||
class="toolbar-button"
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#if @model.privateKey}}
|
||||
{{#if @certData.private_key}}
|
||||
<div class="has-top-margin-m">
|
||||
<Hds::Alert data-test-cert-detail-next-steps @type="inline" @color="highlight" class="has-bottom-margin-s" as |A|>
|
||||
<A.Title>Next steps</A.Title>
|
||||
|
|
@ -39,27 +39,29 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each @model.formFields as |field|}}
|
||||
{{#if field.options.isCertificate}}
|
||||
<InfoTableRow @label={{or field.options.label (capitalize (humanize (dasherize field.name)))}}>
|
||||
<CertificateCard @data={{get @model field.name}} />
|
||||
</InfoTableRow>
|
||||
{{else if (eq field.name "serialNumber")}}
|
||||
<InfoTableRow @label="Serial number">
|
||||
<code class="has-text-black">{{@model.serialNumber}}</code>
|
||||
</InfoTableRow>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (humanize (dasherize field.name))}}
|
||||
{{! formatDate fields can be 0 which will cause them to always render -- pass null instead }}
|
||||
@value={{or (get @model field.name) null}}
|
||||
@formatDate={{if field.options.formatDate "MMM dd yyyy hh:mm:ss a"}}
|
||||
@alwaysRender={{false}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#each this.displayFields as |field|}}
|
||||
{{#let (or (get @certData field) (get this.parsedCertificate field)) as |value|}}
|
||||
{{#if value}}
|
||||
{{#if (this.isCertificate field)}}
|
||||
<InfoTableRow @label={{this.label field}}>
|
||||
<CertificateCard @data={{value}} />
|
||||
</InfoTableRow>
|
||||
{{else if (eq field "serial_number")}}
|
||||
<InfoTableRow @label="Serial number">
|
||||
<code class="has-text-black">{{value}}</code>
|
||||
</InfoTableRow>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{this.label field}}
|
||||
@value={{value}}
|
||||
@formatDate={{if (eq field "revocation_time") "MMM dd yyyy hh:mm:ss a"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
<ParsedCertificateInfoRows @model={{@model.parsedCertificate}} />
|
||||
<ParsedCertificateInfoRows @model={{this.parsedCertificate}} />
|
||||
|
||||
{{#if @onBack}}
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless">
|
||||
|
|
|
|||
|
|
@ -4,17 +4,36 @@
|
|||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { toLabel } from 'core/helpers/to-label';
|
||||
import { parseCertificate } from 'vault/utils/parse-pki-cert';
|
||||
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type DownloadService from 'vault/services/download';
|
||||
import type PkiCertificateBaseModel from 'vault/models/pki/certificate/base';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type { ParsedCertificateData } from 'vault/utils/parse-pki-cert';
|
||||
import type Owner from '@ember/owner';
|
||||
|
||||
type CertificateDetails = {
|
||||
certificate?: string;
|
||||
common_name?: string;
|
||||
revocation_time?: number;
|
||||
serial_number?: string;
|
||||
ca_chain?: string[];
|
||||
issuing_ca?: string;
|
||||
private_key?: string;
|
||||
private_key_type?: string;
|
||||
};
|
||||
|
||||
interface Args {
|
||||
model: PkiCertificateBaseModel;
|
||||
certData: CertificateDetails;
|
||||
canRevoke: boolean;
|
||||
onRevoke?: CallableFunction;
|
||||
onBack?: CallableFunction;
|
||||
}
|
||||
|
|
@ -22,32 +41,81 @@ interface Args {
|
|||
export default class PkiCertificateDetailsComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly download: DownloadService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
|
||||
parsedCertificate: ParsedCertificateData;
|
||||
@tracked didRevoke = false;
|
||||
|
||||
constructor(owner: Owner, args: Args) {
|
||||
super(owner, args);
|
||||
this.parsedCertificate = parseCertificate(this.args.certData.certificate || '');
|
||||
}
|
||||
|
||||
get displayFields() {
|
||||
const fields = [
|
||||
'certificate',
|
||||
'common_name',
|
||||
'serial_number',
|
||||
'ca_chain',
|
||||
'issuing_ca',
|
||||
'private_key',
|
||||
'private_key_type',
|
||||
];
|
||||
// insert revocation_time after common_name if revoked
|
||||
if (this.args.certData.revocation_time || this.didRevoke) {
|
||||
fields.splice(2, 0, 'revocation_time');
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
isCertificate = (field: string) => ['certificate', 'issuing_ca', 'ca_chain', 'private_key'].includes(field);
|
||||
|
||||
label = (field: string) => {
|
||||
const label = toLabel([field]);
|
||||
return (
|
||||
{
|
||||
ca_chain: 'CA chain',
|
||||
issuing_ca: 'Issuing CA',
|
||||
}[field] || label
|
||||
);
|
||||
};
|
||||
|
||||
@action
|
||||
downloadCert() {
|
||||
try {
|
||||
const formattedSerial = this.args.model.serialNumber?.replace(/(\s|:)+/g, '-');
|
||||
this.download.pem(formattedSerial, this.args.model.certificate);
|
||||
const { certificate, serial_number } = this.args.certData;
|
||||
const formattedSerial = serial_number?.replace(/(\s|:)+/g, '-') || '';
|
||||
this.download.pem(formattedSerial, certificate as string);
|
||||
this.flashMessages.info('Your download has started.');
|
||||
} catch (err) {
|
||||
this.flashMessages.danger(errorMessage(err, 'Unable to prepare certificate for download.'));
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*revoke() {
|
||||
try {
|
||||
// the adapter updateRecord method calls the revoke endpoint since it is the only way to update a cert
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success('The certificate has been revoked.');
|
||||
if (this.args.onRevoke) {
|
||||
this.args.onRevoke();
|
||||
revoke = task(
|
||||
waitFor(async () => {
|
||||
try {
|
||||
const { certificate, serial_number } = this.args.certData;
|
||||
// either serial_number or certificate must be provided to revoke but not both
|
||||
const payload = serial_number ? { serial_number } : { certificate };
|
||||
const { revocation_time } = await this.api.secrets.pkiRevoke(
|
||||
this.secretMountPath.currentPath,
|
||||
payload
|
||||
);
|
||||
this.args.certData.revocation_time = revocation_time;
|
||||
this.didRevoke = true; // triggers a re-render by adding revocation_time to displayFields
|
||||
this.flashMessages.success('The certificate has been revoked.');
|
||||
if (this.args.onRevoke) {
|
||||
this.args.onRevoke();
|
||||
}
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(
|
||||
error,
|
||||
'Could not revoke certificate. See Vault logs for details.'
|
||||
);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.flashMessages.danger(
|
||||
errorMessage(error, 'Could not revoke certificate. See Vault logs for details.')
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if @role.canDelete}}
|
||||
{{#if @capabilities.canDelete}}
|
||||
<ConfirmAction
|
||||
class="toolbar-button"
|
||||
@buttonColor="secondary"
|
||||
|
|
@ -16,33 +16,33 @@
|
|||
/>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
{{#if @role.canGenerateCert}}
|
||||
{{#if @capabilities.canGenerateCert}}
|
||||
<LinkTo
|
||||
class="toolbar-link"
|
||||
@route="roles.role.generate"
|
||||
@models={{array @role.backend @role.id}}
|
||||
@models={{array this.secretMountPath.currentPath @role.name}}
|
||||
data-test-pki-role-generate-cert
|
||||
>
|
||||
Generate Certificate
|
||||
<Icon @name="chevron-right" />
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{#if @role.canSign}}
|
||||
{{#if @capabilities.canSign}}
|
||||
<LinkTo
|
||||
class="toolbar-link"
|
||||
@route="roles.role.sign"
|
||||
@models={{array @role.backend @role.id}}
|
||||
@models={{array this.secretMountPath.currentPath @role.name}}
|
||||
data-test-pki-role-sign-cert
|
||||
>
|
||||
Sign Certificate
|
||||
<Icon @name="chevron-right" />
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{#if @role.canEdit}}
|
||||
{{#if @capabilities.canEdit}}
|
||||
<LinkTo
|
||||
class="toolbar-link"
|
||||
@route="roles.role.edit"
|
||||
@models={{array @role.backend @role.id}}
|
||||
@models={{array this.secretMountPath.currentPath @role.name}}
|
||||
data-test-pki-role-edit-link
|
||||
>
|
||||
Edit
|
||||
|
|
@ -51,52 +51,46 @@
|
|||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{#each @role.formFieldGroups as |fg|}}
|
||||
|
||||
{{#each this.displayGroups as |fg|}}
|
||||
{{#each-in fg as |group fields|}}
|
||||
{{#if (not-eq group "default")}}
|
||||
<h3 class="is-size-4 has-text-weight-semibold has-top-margin-m">{{group}}</h3>
|
||||
{{/if}}
|
||||
{{#each fields as |attr|}}
|
||||
{{#let (get @role attr.name) as |val|}}
|
||||
{{#if (eq attr.name "issuerRef")}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{val}}
|
||||
@alwaysRender={{true}}
|
||||
>
|
||||
<LinkTo @route="issuers.issuer.details" @models={{array @role.backend val}}>{{val}}</LinkTo>
|
||||
</InfoTableRow>
|
||||
{{else if (includes attr.name this.arrayAttrs)}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{val}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
{{else if (includes attr.name (array "noStore" "noStoreMetadata"))}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{not val}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
{{else if (eq attr.name "customTtl")}}
|
||||
{{! Show either notAfter or ttl }}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{or @role.notAfter @role.ttl}}
|
||||
@alwaysRender={{true}}
|
||||
@formatDate={{if @role.notAfter "MMM d yyyy HH:mm zzzz"}}
|
||||
@formatTtl={{@role.ttl}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{val}}
|
||||
@alwaysRender={{true}}
|
||||
@type={{or attr.type attr.options.type}}
|
||||
@defaultShown={{attr.options.defaultShown}}
|
||||
@formatTtl={{eq attr.options.editType "ttl"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#each fields as |field|}}
|
||||
{{#let (get @role field) as |val|}}
|
||||
{{#let (this.label field) as |label|}}
|
||||
{{#if (eq field "issuer_ref")}}
|
||||
<InfoTableRow @label={{label}} @value={{val}} @alwaysRender={{true}}>
|
||||
<LinkTo @route="issuers.issuer.details" @models={{array this.secretMountPath.currentPath val}}>
|
||||
{{val}}
|
||||
</LinkTo>
|
||||
</InfoTableRow>
|
||||
{{else if (this.isArrayField field)}}
|
||||
<InfoTableRow @label={{label}} @value={{val}} @alwaysRender={{true}} />
|
||||
{{else if (includes field (array "no_store" "no_store_metadata"))}}
|
||||
<InfoTableRow @label={{label}} @value={{not val}} @alwaysRender={{true}} />
|
||||
{{else if (eq field "custom_ttl")}}
|
||||
{{! Show either notAfter or ttl }}
|
||||
<InfoTableRow
|
||||
@label={{label}}
|
||||
@value={{or @role.not_after @role.ttl}}
|
||||
@alwaysRender={{true}}
|
||||
@formatDate={{if @role.not_after "MMM d yyyy HH:mm zzzz"}}
|
||||
@formatTtl={{@role.ttl}}
|
||||
/>
|
||||
{{else}}
|
||||
<InfoTableRow
|
||||
@label={{label}}
|
||||
@value={{val}}
|
||||
@alwaysRender={{true}}
|
||||
@type={{if (this.isArrayField field) "array"}}
|
||||
@defaultShown={{this.defaultShown field}}
|
||||
@formatTtl={{includes field (array "not_before_duration" "max_ttl")}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
|
|
|
|||
|
|
@ -6,43 +6,142 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { toLabel } from 'core/helpers/to-label';
|
||||
|
||||
import type { PkiReadRoleResponse } from '@hashicorp/vault-client-typescript';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type PkiRoleModel from 'vault/models/pki/role';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type ApiService from 'vault/services/api';
|
||||
|
||||
interface Args {
|
||||
role: PkiRoleModel;
|
||||
role: PkiReadRoleResponse & { name: string };
|
||||
capabilities: { canDelete: boolean; canEdit: boolean; canGenerateCert: boolean; canSign: boolean };
|
||||
}
|
||||
|
||||
export default class DetailsPage extends Component<Args> {
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly version: VersionService;
|
||||
@service declare readonly api: ApiService;
|
||||
|
||||
get breadcrumbs() {
|
||||
return [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview' },
|
||||
{ label: 'Roles', route: 'roles.index' },
|
||||
{ label: this.args.role.id },
|
||||
label = (field: string) => {
|
||||
const label = toLabel([field]);
|
||||
return (
|
||||
{
|
||||
name: 'Role name',
|
||||
issuer_ref: 'Issuer',
|
||||
custom_ttl: 'Issued certificates expire after',
|
||||
not_before_duration: 'Issued certificate backdating',
|
||||
no_store: 'Store in storage backend',
|
||||
no_store_metadata: 'Store metadata in storage backend',
|
||||
max_ttl: 'Max TTL',
|
||||
generate_lease: 'Generate lease with certificate',
|
||||
basic_constraints_valid_for_non_ca: 'Add basic constraints',
|
||||
allowed_domains_template: 'Allow templates in allowed domains',
|
||||
ext_key_usage: 'Extended key usage',
|
||||
ext_key_usage_oids: 'Extended key usage OIDs',
|
||||
allow_ip_sans: 'Allow IP SANs',
|
||||
allowed_uri_sans: 'URI Subject Alternative Names (URI SANs)',
|
||||
allowed_uri_sans_template: 'Allow URI SANs template',
|
||||
allowed_other_sans: 'Other SANs',
|
||||
require_cn: 'Require common name',
|
||||
use_csr_common_name: 'Use CSR common name',
|
||||
use_csr_sans: 'Use CSR SANs',
|
||||
ou: 'Organizational units (OU)',
|
||||
locality: 'Locality/City',
|
||||
province: 'Province/State',
|
||||
}[field] || label
|
||||
);
|
||||
};
|
||||
|
||||
isArrayField = (field: string) => ['key_usage', 'ext_key_usage', 'ext_key_usage_oids'].includes(field);
|
||||
|
||||
defaultShown = (field: string) => {
|
||||
if (this.isArrayField(field)) {
|
||||
return 'None';
|
||||
} else if (field === 'max_ttl') {
|
||||
return 'System default';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
get displayGroups() {
|
||||
const defaultArray = [
|
||||
'name',
|
||||
'issuer_ref',
|
||||
'custom_ttl',
|
||||
'not_before_duration',
|
||||
'max_ttl',
|
||||
'generate_lease',
|
||||
'no_store',
|
||||
'basic_constraints_valid_for_non_ca',
|
||||
];
|
||||
if (this.version.isEnterprise) {
|
||||
// insert no_store_metadata after no_store for Enterprise versions
|
||||
defaultArray.splice(defaultArray.length - 1, 0, 'no_store_metadata');
|
||||
}
|
||||
return [
|
||||
{ default: defaultArray },
|
||||
{
|
||||
'Domain handling': [
|
||||
'allowed_domains',
|
||||
'allowed_domains_template',
|
||||
'allow_bare_domains',
|
||||
'allow_subdomains',
|
||||
'allow_glob_domains',
|
||||
'allow_wildcard_certificates',
|
||||
'allow_localhost',
|
||||
'allow_any_name',
|
||||
'enforce_hostnames',
|
||||
],
|
||||
},
|
||||
{
|
||||
'Key parameters': ['key_type', 'key_bits', 'signature_bits'],
|
||||
},
|
||||
{
|
||||
'Key usage': ['key_usage', 'ext_key_usage', 'ext_key_usage_oids'],
|
||||
},
|
||||
{ 'Policy identifiers': ['policy_identifiers'] },
|
||||
{
|
||||
'Subject Alternative Name (SAN) Options': [
|
||||
'allow_ip_sans',
|
||||
'allowed_uri_sans',
|
||||
'allowed_uri_sans_template',
|
||||
'allowed_other_sans',
|
||||
],
|
||||
},
|
||||
{
|
||||
'Additional subject fields': [
|
||||
'allowed_user_ids',
|
||||
'allowed_serial_numbers',
|
||||
'serial_number_source',
|
||||
'require_cn',
|
||||
'use_csr_common_name',
|
||||
'use_csr_sans',
|
||||
'ou',
|
||||
'organization',
|
||||
'country',
|
||||
'locality',
|
||||
'province',
|
||||
'street_address',
|
||||
'postal_code',
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get arrayAttrs() {
|
||||
return ['keyUsage', 'extKeyUsage', 'extKeyUsageOids'];
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteRole() {
|
||||
try {
|
||||
await this.args.role.destroyRecord();
|
||||
await this.api.secrets.pkiDeleteRole(this.args.role.name, this.secretMountPath.currentPath);
|
||||
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));
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.flashMessages.danger(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
<FormField @attr={{field}} @model={{this.form}} @modelValidations={{this.modelValidations}} data-test-field>
|
||||
{{#if (eq fieldName "customTtl")}}
|
||||
{{! custom_ttl field has editType yield, which will render this }}
|
||||
<PkiNotValidAfterForm @attr={{field}} @form={{this.form}} />
|
||||
<PkiNotValidAfterForm @form={{this.form}} />
|
||||
{{/if}}
|
||||
</FormField>
|
||||
{{/let}}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
{{/if}}
|
||||
</p>
|
||||
{{#if this.keyParamFields}}
|
||||
<PkiKeyParameters @model={{@form}} @fields={{this.keyParamFields}} @modelValidations={{@modelValidations}} />
|
||||
<PkiKeyParameters @form={{@form}} @fields={{this.keyParamFields}} @modelValidations={{@modelValidations}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p class="has-bottom-margin-m" data-test-toggle-group-description>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
{{#each @form.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
{{#if (eq group "Key parameters")}}
|
||||
<PkiKeyParameters @model={{@form}} @fields={{fields}} @modelValidations={{this.modelValidations}} />
|
||||
<PkiKeyParameters @form={{@form}} @fields={{fields}} @modelValidations={{this.modelValidations}} />
|
||||
{{else}}
|
||||
{{#each fields as |field|}}
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{{yield}}
|
||||
{{#each @fields as |field|}}
|
||||
{{#if (eq (camelize field.name) "keyBits")}}
|
||||
{{#if (eq field.name "key_bits")}}
|
||||
<div class="field" data-test-field="key_bits">
|
||||
<FormFieldLabel
|
||||
for={{field.name}}
|
||||
|
|
@ -17,22 +17,22 @@
|
|||
class="select is-fullwidth"
|
||||
{{! these extra parentheses are necessary to conditionally apply the modifier }}
|
||||
{{! TODO: refactor this component and avoid this conditional modifier pattern }}
|
||||
{{(unless (this.getValue "keyType") (modifier "hds-tooltip" "Choose a key type before specifying bit length."))}}
|
||||
{{(unless @form.data.key_type (modifier "hds-tooltip" "Choose a key type before specifying bit length."))}}
|
||||
>
|
||||
<select
|
||||
id={{field.name}}
|
||||
name={{field.name}}
|
||||
data-test-input={{field.name}}
|
||||
disabled={{unless (this.getValue "keyType") true}}
|
||||
disabled={{unless @form.data.key_type true}}
|
||||
{{on "change" this.onKeyBitsChange}}
|
||||
>
|
||||
{{#if (and field.options.noDefault (not (this.getValue "keyType")))}}
|
||||
{{#if (and field.options.noDefault (not @form.data.key_type))}}
|
||||
<option value="">
|
||||
Select one
|
||||
</option>
|
||||
{{/if}}
|
||||
{{#each this.keyBitOptions as |val|}}
|
||||
<option selected={{loose-equal (this.getValue "keyBits") val}} value={{val}}>
|
||||
<option selected={{loose-equal @form.data.key_bits val}} value={{val}}>
|
||||
{{val}}
|
||||
</option>
|
||||
{{/each}}
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<FormField
|
||||
data-test-field={{field}}
|
||||
@attr={{field}}
|
||||
@model={{@model}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{@modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
@onChange={{this.handleSelection}}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@
|
|||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { underscore } from '@ember/string';
|
||||
import Form from 'vault/forms/form';
|
||||
|
||||
import type PkiRoleModel from 'vault/models/pki/role';
|
||||
import type PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
import type PkiConfigGenerateForm from 'vault/forms/secrets/pki/config/generate';
|
||||
import type PkiKeyForm from 'vault/forms/secrets/pki/key';
|
||||
import type { HTMLElementEvent } from 'forms';
|
||||
|
|
@ -16,15 +14,12 @@ import type { HTMLElementEvent } from 'forms';
|
|||
* @module PkiKeyParameters
|
||||
* PkiKeyParameters components are used to display a list of key bit options depending on the selected key type. The key bits field is disabled until a key type is selected.
|
||||
* If the component renders in a group, other attrs may be passed in and will be rendered using the <FormField> component
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiKeyParameters @model={{@model}} @fields={{fields}}/>
|
||||
* ```
|
||||
*
|
||||
* @param {class} model - The pki/role, pki/action, pki/key model.
|
||||
* @param {string} fields - Array of attributes from a formFieldGroup generated by the @withFormFields decorator ex: [{ name: 'attrName', type: 'string', options: {...} }]
|
||||
*/
|
||||
interface Args {
|
||||
model: PkiRoleModel | PkiKeyForm | PkiConfigGenerateForm;
|
||||
form: PkiRoleForm | PkiKeyForm | PkiConfigGenerateForm;
|
||||
}
|
||||
interface TypeOptions {
|
||||
rsa: string;
|
||||
|
|
@ -33,45 +28,31 @@ interface TypeOptions {
|
|||
any: string;
|
||||
}
|
||||
interface BitOptions {
|
||||
[key: string]: Array<string>;
|
||||
[key: string]: Array<number>;
|
||||
}
|
||||
|
||||
// first value in array is the default bits for that key type
|
||||
const KEY_BITS_OPTIONS: BitOptions = {
|
||||
rsa: ['2048', '3072', '4096', '0'],
|
||||
ec: ['256', '224', '384', '521', '0'],
|
||||
ed25519: ['0'],
|
||||
any: ['0'],
|
||||
rsa: [2048, 3072, 4096, 0],
|
||||
ec: [256, 224, 384, 521, 0],
|
||||
ed25519: [0],
|
||||
any: [0],
|
||||
};
|
||||
|
||||
export default class PkiKeyParameters extends Component<Args> {
|
||||
// shim to support both model and form types until all models can be migrated
|
||||
getValue = (key: string) => {
|
||||
const { model } = this.args;
|
||||
if (model instanceof Form) {
|
||||
return model.data[underscore(key) as keyof typeof model.data];
|
||||
}
|
||||
return model[key as keyof typeof model];
|
||||
};
|
||||
|
||||
setValue = (key: string, value: unknown) => {
|
||||
const { model } = this.args;
|
||||
const modelKey = model instanceof Form ? underscore(key) : key;
|
||||
model.set(modelKey, value);
|
||||
};
|
||||
|
||||
get keyBitOptions() {
|
||||
const keyType = this.getValue('keyType');
|
||||
const keyType = this.args.form.data.key_type as keyof typeof KEY_BITS_OPTIONS;
|
||||
return keyType ? KEY_BITS_OPTIONS[keyType] : [];
|
||||
}
|
||||
|
||||
@action handleSelection(name: string, selection: string) {
|
||||
this.setValue(name, selection);
|
||||
@action
|
||||
handleSelection(name: string, selection: string) {
|
||||
this.args.form.set(name, selection);
|
||||
|
||||
if (['keyType', 'key_type'].includes(name) && Object.keys(KEY_BITS_OPTIONS)?.includes(selection)) {
|
||||
if (name === 'key_type' && Object.keys(KEY_BITS_OPTIONS)?.includes(selection)) {
|
||||
const bitOptions = KEY_BITS_OPTIONS[selection as keyof TypeOptions];
|
||||
if (bitOptions) {
|
||||
this.setValue('keyBits', bitOptions[0]);
|
||||
this.args.form.data.key_bits = bitOptions[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@
|
|||
}}
|
||||
|
||||
<CheckboxGrid
|
||||
@name="keyUsage"
|
||||
@name="key_usage"
|
||||
@label="Key usage"
|
||||
@subText="Specifies the default key usage constraint on the issued certificate. To specify no default key_usage constraints, uncheck every item in this list."
|
||||
@fields={{this.keyUsageFields}}
|
||||
@value={{@model.keyUsage}}
|
||||
@value={{@form.data.key_usage}}
|
||||
@onChange={{this.checkboxChange}}
|
||||
data-test-key-usage-key-usage-checkboxes
|
||||
/>
|
||||
<CheckboxGrid
|
||||
@name="extKeyUsage"
|
||||
@name="ext_key_usage"
|
||||
@label="Extended key usage"
|
||||
@subText="Specifies the default key usage constraint on the issued certificate. To specify no default ext_key_usage constraints, uncheck every item in this list."
|
||||
@fields={{this.extKeyUsageFields}}
|
||||
@value={{@model.extKeyUsage}}
|
||||
@value={{@form.data.ext_key_usage}}
|
||||
@onChange={{this.checkboxChange}}
|
||||
class="has-top-margin-s"
|
||||
data-test-key-usage-ext-key-usage-checkboxes
|
||||
|
|
@ -31,19 +31,19 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{{#each this.keyUsageFlags as |field|}}
|
||||
{{#let (get @model.allByKey field) as |attr|}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
{{#each this.keyUsageFlags as |fieldName|}}
|
||||
{{#let (find-by "name" fieldName @form.formFields) as |field|}}
|
||||
<FormField data-test-field={{true}} @attr={{field}} @model={{@form}} />
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
<StringList
|
||||
class="is-shadowless"
|
||||
data-test-input="extKeyUsageOids"
|
||||
data-test-input="ext_key_usage_oids"
|
||||
@label="Extended key usage OIDs"
|
||||
@inputValue={{get @model "extKeyUsageOids"}}
|
||||
@inputValue={{get @form.data "ext_key_usage_oids"}}
|
||||
@onChange={{this.onStringListChange}}
|
||||
@attrName="extKeyUsageOids"
|
||||
@attrName="ext_key_usage_oids"
|
||||
@subText="A list of extended key usage OIDs. Add one item per row."
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -6,18 +6,17 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
import type PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
/**
|
||||
* @module PkiKeyUsage
|
||||
* PkiKeyUsage components are used to build out the toggle options for PKI's role create/update key_usage, ext_key_usage and ext_key_usage_oids model params.
|
||||
* Instead of having the user search on the following goLang pages for these options we present them in checkbox form and manually add them to the params as an array of strings.
|
||||
* key_usage options: https://pkg.go.dev/crypto/x509#KeyUsage
|
||||
* ext_key_usage options (not all are include on purpose): https://pkg.go.dev/crypto/x509#ExtKeyUsage
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiKeyUsage @model={@model} @group={group}/>
|
||||
* ```
|
||||
* @param {class} model - The pki/pki-role-engine model.
|
||||
* @param {string} group - The name of the group created in the model. In this case, it's the "Key usage" group.
|
||||
*
|
||||
* @param {class} form - PkiRoleForm.
|
||||
* @param {string} group - The name of the group created in the form. In this case, it's the "Key usage" group.
|
||||
*/
|
||||
|
||||
interface Field {
|
||||
|
|
@ -51,27 +50,25 @@ const EXT_KEY_USAGE_FIELDS: Field[] = [
|
|||
];
|
||||
|
||||
interface PkiKeyUsageArgs {
|
||||
group: string;
|
||||
model: {
|
||||
keyUsage: string[];
|
||||
extKeyUsageOids: string[];
|
||||
extKeyUsage: string[];
|
||||
};
|
||||
form: PkiRoleForm;
|
||||
}
|
||||
|
||||
export default class PkiKeyUsage extends Component<PkiKeyUsageArgs> {
|
||||
keyUsageFlags = ['clientFlag', 'serverFlag', 'codeSigningFlag', 'emailProtectionFlag'];
|
||||
keyUsageFlags = ['client_flag', 'server_flag', 'code_signing_flag', 'email_protection_flag'];
|
||||
keyUsageFields = KEY_USAGE_FIELDS;
|
||||
extKeyUsageFields = EXT_KEY_USAGE_FIELDS;
|
||||
|
||||
@action onStringListChange(value: string[]) {
|
||||
this.args.model.extKeyUsageOids = value;
|
||||
@action
|
||||
onStringListChange(value: string[]) {
|
||||
this.args.form.data.ext_key_usage_oids = value;
|
||||
}
|
||||
|
||||
@action checkboxChange(name: string, value: string[]) {
|
||||
// Make sure we can set this value type to this model key
|
||||
if (name === 'keyUsage' || name === 'extKeyUsage') {
|
||||
this.args.model[name] = value;
|
||||
@action
|
||||
checkboxChange(name: string, value: string[]) {
|
||||
if (name === 'key_usage') {
|
||||
this.args.form.data.key_usage = value;
|
||||
} else if (name === 'ext_key_usage') {
|
||||
this.args.form.data.ext_key_usage = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@
|
|||
data-test-input="ttl"
|
||||
@onChange={{this.setAndBroadcastTtl}}
|
||||
@label="TTL"
|
||||
@helperTextEnabled={{@attr.options.helperTextEnabled}}
|
||||
@description={{@attr.helpText}}
|
||||
@initialValue={{@model.ttl}}
|
||||
@initialValue={{@form.data.ttl}}
|
||||
@hideToggle={{true}}
|
||||
>
|
||||
<label class="sr-only" for="ttl">Set relative certificate expiry with TTL</label>
|
||||
|
|
|
|||
|
|
@ -9,18 +9,15 @@ import { tracked } from '@glimmer/tracking';
|
|||
import { format } from 'date-fns';
|
||||
|
||||
import type { HTMLElementEvent } from 'forms';
|
||||
import PkiConfigGenerateForm from 'vault/forms/secrets/pki/config/generate';
|
||||
import type PkiConfigGenerateForm from 'vault/forms/secrets/pki/config/generate';
|
||||
import type PkiCertificateIssueForm from 'vault/forms/secrets/pki/certificate';
|
||||
import type PkiIssuersSignIntermediateForm from 'vault/forms/secrets/pki/issuers/sign-intermediate';
|
||||
|
||||
/**
|
||||
* <PkiNotValidAfterForm /> components are used to manage two mutually exclusive role options in the form.
|
||||
*/
|
||||
interface Args {
|
||||
model: {
|
||||
ttl: string | number;
|
||||
notAfter?: string;
|
||||
not_after?: string;
|
||||
set: (key: string, value: string | number) => void;
|
||||
};
|
||||
form: PkiConfigGenerateForm | PkiIssuersSignIntermediateForm | PkiCertificateIssueForm;
|
||||
}
|
||||
|
||||
export default class PkiNotValidAfterForm extends Component<Args> {
|
||||
|
|
@ -31,19 +28,15 @@ export default class PkiNotValidAfterForm extends Component<Args> {
|
|||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
const { model } = this.args;
|
||||
this.cachedNotAfter = model[this.notAfterKey] || '';
|
||||
const { not_after, ttl } = this.args.form.data;
|
||||
this.cachedNotAfter = not_after || '';
|
||||
this.formDate = this.calculateFormDate(this.cachedNotAfter);
|
||||
this.cachedTtl = model.ttl || '';
|
||||
this.cachedTtl = ttl || '';
|
||||
if (this.cachedNotAfter) {
|
||||
this.groupValue = 'specificDate';
|
||||
}
|
||||
}
|
||||
|
||||
get notAfterKey() {
|
||||
return this.args.model instanceof PkiConfigGenerateForm ? 'not_after' : 'notAfter';
|
||||
}
|
||||
|
||||
calculateFormDate(value: string) {
|
||||
// API expects and returns full ISO string
|
||||
// but the form input only accepts yyyy-MM-dd format
|
||||
|
|
@ -56,15 +49,15 @@ export default class PkiNotValidAfterForm extends Component<Args> {
|
|||
@action onRadioButtonChange(selection: string) {
|
||||
this.groupValue = selection;
|
||||
// Clear the previous selection if they have clicked the other radio button.
|
||||
const { model } = this.args;
|
||||
const { data } = this.args.form;
|
||||
if (selection === 'specificDate') {
|
||||
model.ttl = '';
|
||||
model[this.notAfterKey] = this.cachedNotAfter;
|
||||
data.ttl = '';
|
||||
data.not_after = this.cachedNotAfter;
|
||||
this.formDate = this.calculateFormDate(this.cachedNotAfter);
|
||||
}
|
||||
if (selection === 'ttl') {
|
||||
model[this.notAfterKey] = '';
|
||||
model.ttl = `${this.cachedTtl}`;
|
||||
data.not_after = '';
|
||||
data.ttl = `${this.cachedTtl}`;
|
||||
this.formDate = '';
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +70,7 @@ export default class PkiNotValidAfterForm extends Component<Args> {
|
|||
}
|
||||
const ttlVal = enabled === true ? goSafeTimeString : 0;
|
||||
this.cachedTtl = ttlVal;
|
||||
this.args.model.ttl = ttlVal;
|
||||
this.args.form.data.ttl = ttlVal.toString();
|
||||
}
|
||||
|
||||
@action setAndBroadcastInput(evt: HTMLElementEvent<HTMLInputElement>) {
|
||||
|
|
@ -85,7 +78,7 @@ export default class PkiNotValidAfterForm extends Component<Args> {
|
|||
if (!setDate) return;
|
||||
|
||||
this.cachedNotAfter = setDate;
|
||||
this.args.model[this.notAfterKey] = setDate;
|
||||
this.args.form.data.not_after = setDate;
|
||||
this.formDate = this.calculateFormDate(setDate);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,22 +6,23 @@
|
|||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
<NamespaceReminder @mode={{if @role.isNew "create" "update"}} @noun="PKI role" />
|
||||
{{#each @role.formFieldGroups as |fieldGroup|}}
|
||||
<NamespaceReminder @mode={{if @form.isNew "create" "update"}} @noun="PKI role" />
|
||||
|
||||
{{#each @form.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
{{! DEFAULT VIEW }}
|
||||
{{#if (eq group "default")}}
|
||||
{{#each fields as |attr|}}
|
||||
{{#if (and (eq attr.name "issuerRef") @issuers)}}
|
||||
{{#each fields as |field|}}
|
||||
{{#if (and (eq field.name "issuer_ref") @issuers)}}
|
||||
<div class="has-top-margin-m {{unless this.showDefaultIssuer 'has-bottom-margin-xs' 'has-bottom-margin-m'}}">
|
||||
<FormFieldLabel
|
||||
for="select-{{attr.name}}"
|
||||
for="select-{{field.name}}"
|
||||
@label="Issuer ref"
|
||||
@helpText={{(if this.showHelpText attr.options.helpText)}}
|
||||
@subText={{attr.options.subText}}
|
||||
@helpText={{(if this.showHelpText field.options.helpText)}}
|
||||
@subText={{field.options.subText}}
|
||||
/>
|
||||
<Toggle
|
||||
@name={{concat attr.name "-toggle"}}
|
||||
@name={{concat field.name "-toggle"}}
|
||||
@checked={{this.showDefaultIssuer}}
|
||||
@onChange={{this.toggleShowDefaultIssuer}}
|
||||
>
|
||||
|
|
@ -32,95 +33,93 @@
|
|||
<div class="has-top-margin-xs has-bottom-margin-l">
|
||||
<div class="select is-fullwidth">
|
||||
<Select
|
||||
@name={{attr.name}}
|
||||
@name={{field.name}}
|
||||
@options={{this.issuers}}
|
||||
@valueAttribute={{"issuerDisplayName"}}
|
||||
@labelAttribute={{"issuerDisplayName"}}
|
||||
@isFullwidth={{true}}
|
||||
@selectedValue={{@role.issuerRef}}
|
||||
@onChange={{action (mut @role.issuerRef)}}
|
||||
aria-labelledby={{attr.name}}
|
||||
@selectedValue={{@form.data.issuer_ref}}
|
||||
@onChange={{action (mut @form.data.issuer_ref)}}
|
||||
aria-labelledby={{field.name}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{else if (this.showField field.name)}}
|
||||
<FormField
|
||||
data-test-field={{attr}}
|
||||
@attr={{attr}}
|
||||
@model={{@role}}
|
||||
data-test-field={{field.name}}
|
||||
@attr={{field}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
>
|
||||
<PkiNotValidAfterForm @attr={{attr}} @model={{@role}} />
|
||||
<PkiNotValidAfterForm @form={{@form}} />
|
||||
</FormField>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#let (camelize (concat "show" group)) as |prop|}}
|
||||
<ToggleButton
|
||||
@isOpen={{get @role prop}}
|
||||
@openLabel={{concat "Hide " group}}
|
||||
@closedLabel={{group}}
|
||||
@onClick={{fn (mut (get @role prop))}}
|
||||
class="is-block"
|
||||
data-test-button={{group}}
|
||||
/>
|
||||
{{#if (get @role prop)}}
|
||||
<div class="box is-tall is-marginless" data-test-toggle-div={{group}}>
|
||||
{{#let (get @role.fieldGroupsInfo group) as |toggleGroup|}}
|
||||
{{! HEADER }}
|
||||
{{#if toggleGroup.header}}
|
||||
<div class="has-bottom-margin-s">
|
||||
<FormFieldLabel
|
||||
for={{toggleGroup.header.name}}
|
||||
@label={{toggleGroup.header.label}}
|
||||
@subText={{toggleGroup.header.text}}
|
||||
@docLink={{toggleGroup.header.docLink}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{! FIELDS }}
|
||||
{{#if (eq group "Key usage")}}
|
||||
<PkiKeyUsage @model={{@role}} />
|
||||
{{else if (eq group "Key parameters")}}
|
||||
<PkiKeyParameters @model={{@role}} @fields={{fields}} />
|
||||
{{else}}
|
||||
{{#each fields as |attr|}}
|
||||
<FormField
|
||||
data-test-field={{true}}
|
||||
@attr={{attr}}
|
||||
@model={{@role}}
|
||||
@modelValidations={{@roleValidations}}
|
||||
@showHelpText={{false}}
|
||||
>
|
||||
{{yield attr}}
|
||||
</FormField>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{! FOOTER }}
|
||||
{{#if toggleGroup.footer}}
|
||||
<p class="sub-text">
|
||||
<Icon @name="info" />
|
||||
{{toggleGroup.footer.text}}
|
||||
{{#if toggleGroup.footer.docLink}}
|
||||
<DocLink @path={{toggleGroup.footer.docLink}}>
|
||||
{{toggleGroup.footer.docText}}
|
||||
</DocLink>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
<ToggleButton
|
||||
@isOpen={{includes group this.openGroups}}
|
||||
@openLabel={{concat "Hide " group}}
|
||||
@closedLabel={{group}}
|
||||
@onClick={{fn this.toggleGroup group}}
|
||||
class="is-block"
|
||||
data-test-button={{group}}
|
||||
/>
|
||||
{{#if (includes group this.openGroups)}}
|
||||
<div class="box is-tall is-marginless" data-test-toggle-div={{group}}>
|
||||
{{#let (get @form.fieldGroupsInfo group) as |toggleGroup|}}
|
||||
{{! HEADER }}
|
||||
{{#if toggleGroup.header}}
|
||||
<div class="has-bottom-margin-s">
|
||||
<FormFieldLabel
|
||||
for={{toggleGroup.header.name}}
|
||||
@label={{toggleGroup.header.label}}
|
||||
@subText={{toggleGroup.header.text}}
|
||||
@docLink={{toggleGroup.header.docLink}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{! FIELDS }}
|
||||
{{#if (eq group "Key usage")}}
|
||||
<PkiKeyUsage @form={{@form}} />
|
||||
{{else if (eq group "Key parameters")}}
|
||||
<PkiKeyParameters @form={{@form}} @fields={{fields}} />
|
||||
{{else}}
|
||||
{{#each fields as |field|}}
|
||||
<FormField
|
||||
data-test-field={{field.name}}
|
||||
@attr={{field}}
|
||||
@model={{@form}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
>
|
||||
{{yield field}}
|
||||
</FormField>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{! FOOTER }}
|
||||
{{#if toggleGroup.footer}}
|
||||
<p class="sub-text">
|
||||
<Icon @name="info" />
|
||||
{{toggleGroup.footer.text}}
|
||||
{{#if toggleGroup.footer.docLink}}
|
||||
<DocLink @path={{toggleGroup.footer.docLink}}>
|
||||
{{toggleGroup.footer.docText}}
|
||||
</DocLink>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<Hds::ButtonSet class="has-top-padding-s">
|
||||
<Hds::Button
|
||||
@text={{if @role.isNew "Create" "Update"}}
|
||||
@text={{if @form.isNew "Create" "Update"}}
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
|
|
|
|||
|
|
@ -6,86 +6,99 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
import type Store from '@ember-data/store';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type SecretMountPathService from 'vault/services/secret-mount-path';
|
||||
import type PkiRoleModel from 'vault/models/pki/role';
|
||||
import type PkiIssuerModel from 'vault/models/pki/issuer';
|
||||
import type VersionService from 'vault/services/version';
|
||||
import type PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
import type { ValidationMap } from 'vault/app-types';
|
||||
|
||||
/**
|
||||
* @module PkiRoleForm
|
||||
* PkiRoleForm components are used to create and update PKI roles.
|
||||
* @module PkiRoleFormComponent
|
||||
* PkiRoleFormComponent is used to create and update PKI roles.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiRoleForm @model={{this.model}}/>
|
||||
* ```
|
||||
* @callback onCancel
|
||||
* @callback onSave
|
||||
* @param {Object} role - pki/role model.
|
||||
* @param {Array} issuers - pki/issuer model.
|
||||
* @param {Object} role - pki role form class.
|
||||
* @param {Array} issuers - pki issuers list key info.
|
||||
* @param {onCancel} onCancel - Callback triggered when cancel button is clicked.
|
||||
* @param {onSave} onSave - Callback triggered on save success.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
role: PkiRoleModel;
|
||||
issuers: PkiIssuerModel[];
|
||||
form: PkiRoleForm;
|
||||
issuers: { issuer_name?: string; issuer_id: string }[];
|
||||
onSave: CallableFunction;
|
||||
onCancel: CallableFunction;
|
||||
}
|
||||
|
||||
export default class PkiRoleForm extends Component<Args> {
|
||||
@service declare readonly store: Store;
|
||||
export default class PkiRoleFormComponent extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly secretMountPath: SecretMountPathService;
|
||||
@service declare readonly version: VersionService;
|
||||
|
||||
@tracked errorBanner = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
@tracked modelValidations: ValidationMap | null = null;
|
||||
@tracked showDefaultIssuer = true;
|
||||
@tracked openGroups: string[] = [];
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
||||
this.showDefaultIssuer = this.args.role.issuerRef === 'default';
|
||||
this.showDefaultIssuer = this.args.form.data.issuer_ref === 'default';
|
||||
}
|
||||
|
||||
// hide no_store_metadata field for community edition
|
||||
showField = (fieldName: string) => (fieldName === 'no_store_metadata' ? this.version.isEnterprise : true);
|
||||
|
||||
get issuers() {
|
||||
return this.args.issuers?.map((issuer) => {
|
||||
return { issuerDisplayName: issuer.issuerName || issuer.issuerId };
|
||||
return this.args.issuers?.map(({ issuer_name, issuer_id }) => {
|
||||
return { issuerDisplayName: issuer_name || issuer_id };
|
||||
});
|
||||
}
|
||||
|
||||
@task
|
||||
*save(event: Event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state, invalidFormMessage } = this.args.role.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
if (isValid) {
|
||||
const { isNew, name } = this.args.role;
|
||||
yield this.args.role.save();
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'created' : 'updated'} the role ${name}.`);
|
||||
this.args.onSave();
|
||||
save = task(
|
||||
waitFor(async (event: Event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state, invalidFormMessage, data } = this.args.form.toJSON();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
if (isValid) {
|
||||
const { name, ...payload } = data;
|
||||
await this.api.secrets.pkiWriteRole(name, this.secretMountPath.currentPath, payload);
|
||||
this.flashMessages.success(`Successfully saved the role ${name}.`);
|
||||
this.args.onSave(name);
|
||||
}
|
||||
} catch (error) {
|
||||
const { message } = await this.api.parseError(error);
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorBanner = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action
|
||||
toggleShowDefaultIssuer() {
|
||||
this.showDefaultIssuer = !this.showDefaultIssuer;
|
||||
|
||||
if (this.showDefaultIssuer) {
|
||||
this.args.role.issuerRef = 'default';
|
||||
this.args.form.data.issuer_ref = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleGroup(group: string) {
|
||||
if (this.openGroups.includes(group)) {
|
||||
this.openGroups = this.openGroups.filter((g) => g !== group);
|
||||
} else {
|
||||
this.openGroups = [...this.openGroups, group];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,31 +3,35 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if @model.serialNumber}}
|
||||
<Page::PkiCertificateDetails @model={{@model}} @onBack={{this.cancel}} />
|
||||
{{#if this.certData}}
|
||||
<Page::PkiCertificateDetails @certData={{this.certData}} @canRevoke={{this.canRevoke}} @onBack={{this.cancel}} />
|
||||
{{else}}
|
||||
<form {{on "submit" (perform this.save)}} data-test-pki-generate-cert-form>
|
||||
<div class="box is-bottomless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} />
|
||||
<NamespaceReminder @mode="create" @noun="certificate" />
|
||||
{{#let (get @model.formFieldGroups "0") as |defaultGroup|}}
|
||||
{{#each defaultGroup.default as |attr|}}
|
||||
<FormField @model={{@model}} @attr={{attr}} @modelValidations={{this.modelValidations}}>
|
||||
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
|
||||
|
||||
{{#let (get @form.formFieldGroups "0") as |defaultGroup|}}
|
||||
{{#each defaultGroup.default as |field|}}
|
||||
<FormField @model={{@form}} @attr={{field}} @modelValidations={{this.modelValidations}}>
|
||||
<PkiNotValidAfterForm @form={{@form}} />
|
||||
</FormField>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
|
||||
<FormFieldGroups
|
||||
@model={{@model}}
|
||||
@model={{@form}}
|
||||
@renderGroup="Subject Alternative Name (SAN) Options"
|
||||
@groupName="formFieldGroups"
|
||||
@showHelpText={{false}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr class="has-background-gray-100" />
|
||||
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button
|
||||
@text={{capitalize this.verb}}
|
||||
@text={{capitalize @mode}}
|
||||
@icon={{if this.save.isRunning "loading"}}
|
||||
type="submit"
|
||||
disabled={{this.save.isRunning}}
|
||||
|
|
|
|||
|
|
@ -8,50 +8,83 @@ import { action } from '@ember/object';
|
|||
import { task } from 'ember-concurrency';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
|
||||
import type RouterService from '@ember/routing/router';
|
||||
import type Store from '@ember-data/store';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type DownloadService from 'vault/services/download';
|
||||
import type PkiCertificateGenerateModel from 'vault/models/pki/certificate/generate';
|
||||
import type PkiCertificateSignModel from 'vault/models/pki/certificate/sign';
|
||||
import type SecretMountPath from 'vault/services/secret-mount-path';
|
||||
import type PkiCertificateForm from 'vault/forms/secrets/pki/certificate';
|
||||
import type CapabilitiesService from 'vault/services/capabilities';
|
||||
import type {
|
||||
PkiIssueWithRoleRequest,
|
||||
PkiIssueWithRoleResponse,
|
||||
PkiSignWithRoleRequest,
|
||||
PkiSignWithRoleResponse,
|
||||
} from '@hashicorp/vault-client-typescript';
|
||||
|
||||
interface Args {
|
||||
role: string;
|
||||
form: PkiCertificateForm;
|
||||
onSuccess: CallableFunction;
|
||||
model: PkiCertificateGenerateModel | PkiCertificateSignModel;
|
||||
type: string;
|
||||
mode: 'generate' | 'sign';
|
||||
}
|
||||
|
||||
export default class PkiRoleGenerate extends Component<Args> {
|
||||
@service declare readonly download: DownloadService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly store: Store;
|
||||
@service declare readonly api: ApiService;
|
||||
@service('app-router') declare readonly router: RouterService;
|
||||
@service declare readonly secretMountPath: SecretMountPath;
|
||||
@service declare readonly capabilities: CapabilitiesService;
|
||||
|
||||
@tracked errorBanner = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
@tracked declare certData: PkiIssueWithRoleResponse | PkiSignWithRoleResponse;
|
||||
@tracked canRevoke = false;
|
||||
|
||||
get verb() {
|
||||
return this.args.type === 'sign' ? 'sign' : 'generate';
|
||||
}
|
||||
save = task(
|
||||
waitFor(async (evt: Event) => {
|
||||
evt.preventDefault();
|
||||
this.errorBanner = '';
|
||||
const { role, form, mode, onSuccess } = this.args;
|
||||
|
||||
@task
|
||||
*save(evt: Event) {
|
||||
evt.preventDefault();
|
||||
this.errorBanner = '';
|
||||
const { model, onSuccess } = this.args;
|
||||
try {
|
||||
yield model.save();
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
this.errorBanner = errorMessage(err, `Could not ${this.verb} certificate. See Vault logs for details.`);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { data } = form.toJSON();
|
||||
if (mode === 'generate') {
|
||||
this.certData = await this.api.secrets.pkiIssueWithRole(
|
||||
role,
|
||||
this.secretMountPath.currentPath,
|
||||
data as PkiIssueWithRoleRequest
|
||||
);
|
||||
} else {
|
||||
this.certData = await this.api.secrets.pkiSignWithRole(
|
||||
role,
|
||||
this.secretMountPath.currentPath,
|
||||
data as PkiSignWithRoleRequest
|
||||
);
|
||||
}
|
||||
// check for revoke capabilities for certificate details component
|
||||
const { canCreate } = await this.capabilities.for('pkiRevoke', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
});
|
||||
this.canRevoke = canCreate;
|
||||
onSuccess();
|
||||
// since we are staying on the same page to display cert details scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
} catch (err) {
|
||||
const { message } = await this.api.parseError(
|
||||
err,
|
||||
`Could not ${mode} certificate. See Vault logs for details.`
|
||||
);
|
||||
this.errorBanner = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@action cancel() {
|
||||
this.args.model.unloadRecord();
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.role.details');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
@showHelpText={{false}}
|
||||
>
|
||||
{{! attr customTtl has editType yield and will show this component }}
|
||||
<PkiNotValidAfterForm @attr={{formField}} @model={{@form}} />
|
||||
<PkiNotValidAfterForm @form={{@form}} />
|
||||
</FormField>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
|
|
|||
|
|
@ -7,20 +7,30 @@ import Route from '@ember/routing/route';
|
|||
import { service } from '@ember/service';
|
||||
|
||||
export default class PkiCertificateDetailsRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service secretMountPath;
|
||||
@service capabilities;
|
||||
|
||||
model() {
|
||||
const id = this.paramsFor('certificates/certificate').serial;
|
||||
return this.store.queryRecord('pki/certificate/base', { backend: this.secretMountPath.currentPath, id });
|
||||
async model() {
|
||||
const { serial } = this.paramsFor('certificates/certificate');
|
||||
const certificate = await this.api.secrets.pkiReadCert(serial, this.secretMountPath.currentPath);
|
||||
const { canCreate } = await this.capabilities.for('pkiRevoke', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: { serial_number: serial, ...certificate },
|
||||
canRevoke: canCreate,
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
super.setupController(controller, model);
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview', model: this.secretMountPath.currentPath },
|
||||
{ label: 'Certificates', route: 'certificates.index', model: this.secretMountPath.currentPath },
|
||||
{ label: model.id },
|
||||
{ label: model.certificate.serial_number },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,15 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'pki/decorators/check-issuers';
|
||||
import { hash } from 'rsvp';
|
||||
import { getCliMessage } from 'pki/routes/overview';
|
||||
import { PkiListCertsListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
|
||||
@withConfig()
|
||||
export default class PkiCertificatesIndexRoute extends Route {
|
||||
@service pagination;
|
||||
@service secretMountPath;
|
||||
@service store; // used by @withConfig decorator
|
||||
@service api;
|
||||
|
||||
queryParams = {
|
||||
page: {
|
||||
|
|
@ -21,30 +22,28 @@ export default class PkiCertificatesIndexRoute extends Route {
|
|||
},
|
||||
};
|
||||
|
||||
async fetchCertificates(params) {
|
||||
try {
|
||||
const page = Number(params.page) || 1;
|
||||
return await this.pagination.lazyPaginatedQuery('pki/certificate/base', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
responsePath: 'data.keys',
|
||||
page,
|
||||
skipCache: page === 1,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 404) {
|
||||
return { parentModel: this.modelFor('certificates') };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
model(params) {
|
||||
return hash({
|
||||
async model(params) {
|
||||
const model = {
|
||||
hasConfig: this.pkiMountHasConfig,
|
||||
certificates: this.fetchCertificates(params),
|
||||
parentModel: this.modelFor('certificates'),
|
||||
pageFilter: params.pageFilter,
|
||||
});
|
||||
certificates: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const page = Number(params.page) || 1;
|
||||
const { keys: certificates } = await this.api.secrets.pkiListCerts(
|
||||
this.secretMountPath.currentPath,
|
||||
PkiListCertsListEnum.TRUE
|
||||
);
|
||||
model.certificates = paginate(certificates, { page });
|
||||
} catch (e) {
|
||||
if (e.response.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ export default class PkiIssuersImportRoute extends Route {
|
|||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('pki/action');
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs = [
|
||||
|
|
|
|||
|
|
@ -5,26 +5,28 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
import { hash } from 'rsvp';
|
||||
import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
@withConfirmLeave('model.role', ['model.issuers'])
|
||||
export default class PkiRolesCreateRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
async model() {
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
return hash({
|
||||
role: this.store.createRecord('pki/role', { backend }),
|
||||
issuers: this.store.query('pki/issuer', { backend }).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
});
|
||||
let issuers = [];
|
||||
try {
|
||||
const response = await this.api.secrets.pkiListIssuers(backend, PkiListIssuersListEnum.TRUE);
|
||||
issuers = this.api.keyInfoToArray(response, 'issuer_id');
|
||||
} catch (error) {
|
||||
if (error.response.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return {
|
||||
form: new PkiRoleForm({}, { isNew: true }),
|
||||
issuers,
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
|
|
@ -36,10 +38,4 @@ export default class PkiRolesCreateRoute extends Route {
|
|||
{ label: 'Create' },
|
||||
];
|
||||
}
|
||||
|
||||
willTransition() {
|
||||
// after upgrading to Ember Data 5.3.2 we saw duplicate records in the store after creating and saving a new role
|
||||
// it's unclear why this ghost record is persisting, manually unloading refreshes the store
|
||||
this.store.unloadAll('pki/role');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfig } from 'pki/decorators/check-issuers';
|
||||
import { hash } from 'rsvp';
|
||||
import { getCliMessage } from 'pki/routes/overview';
|
||||
import { PkiListRolesListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import { paginate } from 'core/utils/paginate-list';
|
||||
|
||||
@withConfig()
|
||||
export default class PkiRolesIndexRoute extends Route {
|
||||
@service store; // used by @withConfig decorator
|
||||
@service api;
|
||||
@service secretMountPath;
|
||||
@service pagination;
|
||||
|
||||
queryParams = {
|
||||
page: {
|
||||
|
|
@ -20,38 +21,33 @@ export default class PkiRolesIndexRoute extends Route {
|
|||
},
|
||||
};
|
||||
|
||||
async fetchRoles(params) {
|
||||
try {
|
||||
const page = Number(params.page) || 1;
|
||||
return await this.pagination.lazyPaginatedQuery('pki/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
responsePath: 'data.keys',
|
||||
page,
|
||||
skipCache: page === 1,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 404) {
|
||||
return { parentModel: this.modelFor('roles') };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
model(params) {
|
||||
return hash({
|
||||
async model(params) {
|
||||
const model = {
|
||||
hasConfig: this.pkiMountHasConfig,
|
||||
roles: this.fetchRoles(params),
|
||||
parentModel: this.modelFor('roles'),
|
||||
pageFilter: params.pageFilter,
|
||||
});
|
||||
roles: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const page = Number(params.page) || 1;
|
||||
const { keys: roles } = await this.api.secrets.pkiListRoles(
|
||||
this.secretMountPath.currentPath,
|
||||
PkiListRolesListEnum.TRUE
|
||||
);
|
||||
model.roles = paginate(roles, { page });
|
||||
} catch (e) {
|
||||
if (e.response.status !== 404) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const roles = resolvedModel.roles;
|
||||
|
||||
if (roles?.length) controller.notConfiguredMessage = getCliMessage('roles');
|
||||
else controller.notConfiguredMessage = getCliMessage();
|
||||
controller.notConfiguredMessage = resolvedModel.roles?.length ? getCliMessage('roles') : getCliMessage();
|
||||
}
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
|
|
|
|||
|
|
@ -7,25 +7,46 @@ import Route from '@ember/routing/route';
|
|||
import { service } from '@ember/service';
|
||||
|
||||
export default class RolesRoleDetailsRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service secretMountPath;
|
||||
@service capabilities;
|
||||
|
||||
model() {
|
||||
const { role } = this.paramsFor('roles/role');
|
||||
return this.store.queryRecord('pki/role', {
|
||||
backend: this.secretMountPath.currentPath,
|
||||
id: role,
|
||||
});
|
||||
async fetchCapabilities(id) {
|
||||
const { pathFor } = this.capabilities;
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
|
||||
const pathMap = {
|
||||
role: pathFor('pkiRole', { backend, id }),
|
||||
issue: pathFor('pkiIssue', { backend, id }),
|
||||
sign: pathFor('pkiSign', { backend, id }),
|
||||
};
|
||||
const perms = await this.capabilities.fetch(Object.values(pathMap));
|
||||
|
||||
return {
|
||||
canEdit: perms[pathMap.role].canUpdate,
|
||||
canDelete: perms[pathMap.role].canDelete,
|
||||
canGenerateCert: perms[pathMap.issue].canUpdate,
|
||||
canSign: perms[pathMap.sign].canUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
async model() {
|
||||
const { role: name } = this.paramsFor('roles/role');
|
||||
return {
|
||||
role: await this.api.secrets
|
||||
.pkiReadRole(name, this.secretMountPath.currentPath)
|
||||
.then((role) => ({ name, ...role })),
|
||||
capabilities: await this.fetchCapabilities(name),
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const { id } = resolvedModel;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview', model: this.secretMountPath.currentPath },
|
||||
{ label: 'Roles', route: 'roles.index', model: this.secretMountPath.currentPath },
|
||||
{ label: id },
|
||||
{ label: resolvedModel.role.name },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,43 +5,44 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
import { hash } from 'rsvp';
|
||||
import { PkiListIssuersListEnum } from '@hashicorp/vault-client-typescript';
|
||||
import PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
@withConfirmLeave('model.role', ['model.issuers'])
|
||||
export default class PkiRoleEditRoute extends Route {
|
||||
@service store;
|
||||
@service api;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
const { role } = this.paramsFor('roles/role');
|
||||
async model() {
|
||||
const { role: name } = this.paramsFor('roles/role');
|
||||
const backend = this.secretMountPath.currentPath;
|
||||
|
||||
return hash({
|
||||
role: this.store.queryRecord('pki/role', {
|
||||
backend,
|
||||
id: role,
|
||||
}),
|
||||
issuers: this.store.query('pki/issuer', { backend }).catch((err) => {
|
||||
if (err.httpStatus === 404) {
|
||||
return [];
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
});
|
||||
const role = await this.api.secrets.pkiReadRole(name, backend).then((role) => ({ name, ...role }));
|
||||
|
||||
let issuers = [];
|
||||
try {
|
||||
const response = await this.api.secrets.pkiListIssuers(backend, PkiListIssuersListEnum.TRUE);
|
||||
issuers = this.api.keyInfoToArray(response, 'issuer_id');
|
||||
} catch (error) {
|
||||
if (error.response.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
form: new PkiRoleForm(role),
|
||||
issuers,
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const {
|
||||
role: { id },
|
||||
} = resolvedModel;
|
||||
const { form } = resolvedModel;
|
||||
const { name } = form.data;
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.secretMountPath.currentPath, route: 'overview', model: this.secretMountPath.currentPath },
|
||||
{ label: 'Roles', route: 'roles.index', model: this.secretMountPath.currentPath },
|
||||
{ label: id, route: 'roles.role.details', models: [this.secretMountPath.currentPath, id] },
|
||||
{ label: name, route: 'roles.role.details', models: [this.secretMountPath.currentPath, name] },
|
||||
{ label: 'Edit' },
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
import PkiCertificateForm from 'vault/forms/secrets/pki/certificate';
|
||||
|
||||
withConfirmLeave();
|
||||
export default class PkiRoleGenerateRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
async model() {
|
||||
model() {
|
||||
const { role } = this.paramsFor('roles/role');
|
||||
return this.store.createRecord('pki/certificate/generate', {
|
||||
return {
|
||||
role,
|
||||
});
|
||||
form: new PkiCertificateForm('PkiIssueWithRoleRequest', {}, { isNew: true }),
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
|
|
|
|||
|
|
@ -5,18 +5,18 @@
|
|||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
import PkiCertificateForm from 'vault/forms/secrets/pki/certificate';
|
||||
|
||||
withConfirmLeave();
|
||||
export default class PkiRoleSignRoute extends Route {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
const { role } = this.paramsFor('roles/role');
|
||||
return this.store.createRecord('pki/certificate/sign', {
|
||||
return {
|
||||
role,
|
||||
});
|
||||
form: new PkiCertificateForm('PkiSignWithRoleRequest', {}, { isNew: true }),
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@
|
|||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<Page::PkiCertificateDetails @model={{this.model}} />
|
||||
|
||||
<Page::PkiCertificateDetails @certData={{this.model.certificate}} @canRevoke={{this.model.canRevoke}} />
|
||||
|
|
@ -12,10 +12,10 @@
|
|||
@hasConfig={{this.model.hasConfig}}
|
||||
>
|
||||
<:list as |certs|>
|
||||
{{#each certs as |pkiCertificate|}}
|
||||
{{#each certs as |cert|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "certificates.certificate.details" this.model.parentModel.id pkiCertificate.id}}
|
||||
@params={{array "certificates.certificate.details" this.model.parentModel.id cert}}
|
||||
@linkPrefix={{this.mountPoint}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div>
|
||||
<Icon @name="certificate" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{pkiCertificate.id}}
|
||||
{{cert}}
|
||||
</span>
|
||||
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
||||
</div>
|
||||
|
|
@ -38,10 +38,7 @@
|
|||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route="certificates.certificate.details"
|
||||
@model={{pkiCertificate.id}}
|
||||
>Details</dd.Interactive>
|
||||
<dd.Interactive @route="certificates.certificate.details" @model={{cert}}>Details</dd.Interactive>
|
||||
</Hds::Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Page::PkiIssuerImport @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
||||
<Page::PkiIssuerImport @breadcrumbs={{this.breadcrumbs}} />
|
||||
|
|
@ -15,9 +15,8 @@
|
|||
</PageHeader>
|
||||
|
||||
<PkiRoleForm
|
||||
@role={{this.model.role}}
|
||||
@form={{this.model.form}}
|
||||
@issuers={{this.model.issuers}}
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.index"}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.role.id}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.form.data.name}}
|
||||
/>
|
||||
|
|
@ -19,10 +19,10 @@
|
|||
{{/if}}
|
||||
</:actions>
|
||||
<:list as |roles|>
|
||||
{{#each roles as |pkiRole|}}
|
||||
{{#each roles as |role|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "roles.role.details" this.model.parentModel.id pkiRole.id}}
|
||||
@params={{array "roles.role.details" this.model.parentModel.id role}}
|
||||
@linkPrefix={{this.mountPoint}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<div>
|
||||
<Icon @name="user" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{pkiRole.id}}
|
||||
{{role}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -43,14 +43,12 @@
|
|||
@hasChevron={{false}}
|
||||
data-test-popup-menu-trigger
|
||||
/>
|
||||
<dd.Interactive
|
||||
@route="roles.role.details"
|
||||
@models={{array this.model.parentModel.id pkiRole.id}}
|
||||
>Details</dd.Interactive>
|
||||
<dd.Interactive
|
||||
@route="roles.role.edit"
|
||||
@models={{array this.model.parentModel.id pkiRole.id}}
|
||||
>Edit</dd.Interactive>
|
||||
<dd.Interactive @route="roles.role.details" @models={{array this.model.parentModel.id role}}>
|
||||
Details
|
||||
</dd.Interactive>
|
||||
<dd.Interactive @route="roles.role.edit" @models={{array this.model.parentModel.id role}}>
|
||||
Edit
|
||||
</dd.Interactive>
|
||||
</Hds::Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@
|
|||
<h1 class="title is-3" data-test-page-title>
|
||||
<Icon @name="file-text" @size="24" class="has-text-grey-light" />
|
||||
PKI Role
|
||||
<code>{{this.model.name}}</code>
|
||||
<code>{{this.model.role.name}}</code>
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
<Page::PkiRoleDetails @role={{this.model.role}} @capabilities={{this.model.capabilities}} />
|
||||
|
|
@ -13,9 +13,10 @@
|
|||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiRoleForm
|
||||
@role={{this.model.role}}
|
||||
@form={{this.model.form}}
|
||||
@issuers={{this.model.issuers}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.role.id}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.role.id}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.form.data.name}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.form.data.name}}
|
||||
/>
|
||||
|
|
@ -15,4 +15,4 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiRoleGenerate @onSuccess={{this.toggleTitle}} @model={{this.model}} />
|
||||
<PkiRoleGenerate @form={{this.model.form}} @role={{this.model.role}} @mode="generate" @onSuccess={{this.toggleTitle}} />
|
||||
|
|
@ -15,4 +15,4 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiRoleGenerate @model={{this.model}} @type="sign" @onSuccess={{this.toggleTitle}} />
|
||||
<PkiRoleGenerate @form={{this.model.form}} @role={{this.model.role}} @mode="sign" @onSuccess={{this.toggleTitle}} />
|
||||
|
|
@ -111,7 +111,7 @@ module('Acceptance | pki/pki cross sign', function (hooks) {
|
|||
max_ttl="720h"`,
|
||||
]);
|
||||
await visit(`vault/secrets-engines/${this.intMountPath}/pki/roles/${myRole}/generate`);
|
||||
await fillIn(GENERAL.inputByAttr('commonName'), 'my-leaf');
|
||||
await fillIn(GENERAL.inputByAttr('common_name'), 'my-leaf');
|
||||
await fillIn('[data-test-ttl-value="TTL"]', '3600');
|
||||
await click(GENERAL.submitButton);
|
||||
await click(PKI_CROSS_SIGN.copyButton('Certificate'));
|
||||
|
|
|
|||
|
|
@ -1,176 +0,0 @@
|
|||
/**
|
||||
* Copyright IBM Corp. 2016, 2025
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { click, currentURL, fillIn, visit } from '@ember/test-helpers';
|
||||
import { runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import {
|
||||
PKI_CONFIGURE_CREATE,
|
||||
PKI_ISSUER_LIST,
|
||||
PKI_ROLE_DETAILS,
|
||||
} from 'vault/tests/helpers/pki/pki-selectors';
|
||||
|
||||
const OVERVIEW_BREADCRUMB = '[data-test-breadcrumbs] li:nth-of-type(2) > a';
|
||||
/**
|
||||
* This test module should test that dirty route models are cleaned up when
|
||||
* the user leaves the page via cancel or breadcrumb navigation
|
||||
*/
|
||||
module('Acceptance | pki engine route cleanup test', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
await login();
|
||||
// Setup PKI engine
|
||||
const mountPath = `pki-workflow-${uuidv4()}`;
|
||||
await enablePage.enable('pki', mountPath);
|
||||
this.mountPath = mountPath;
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
await login();
|
||||
// Cleanup engine
|
||||
await runCmd([`delete sys/mounts/${this.mountPath}`]);
|
||||
});
|
||||
|
||||
module('role routes', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
await login();
|
||||
// Configure PKI
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(`${GENERAL.emptyStateActions} a`);
|
||||
await click(PKI_CONFIGURE_CREATE.optionByKey('generate-root'));
|
||||
await fillIn(GENERAL.inputByAttr('type'), 'internal');
|
||||
await fillIn(GENERAL.inputByAttr('common_name'), 'my-root-cert');
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('create role exit via cancel', async function (assert) {
|
||||
let roles;
|
||||
await login();
|
||||
// Create PKI
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(GENERAL.secretTab('Roles'));
|
||||
roles = this.store.peekAll('pki/role');
|
||||
assert.strictEqual(roles.length, 0, 'No roles exist yet');
|
||||
await click(PKI_ROLE_DETAILS.createRoleLink);
|
||||
roles = this.store.peekAll('pki/role');
|
||||
const role = roles.at(0);
|
||||
assert.strictEqual(roles.length, 1, 'New role exists');
|
||||
assert.true(role.isNew, 'Role is new model');
|
||||
await click(GENERAL.cancelButton);
|
||||
roles = this.store.peekAll('pki/role');
|
||||
assert.strictEqual(roles.length, 0, 'Role is removed from store');
|
||||
});
|
||||
test('create role exit via breadcrumb', async function (assert) {
|
||||
let roles;
|
||||
await login();
|
||||
// Create PKI
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(GENERAL.secretTab('Roles'));
|
||||
roles = this.store.peekAll('pki/role');
|
||||
assert.strictEqual(roles.length, 0, 'No roles exist yet');
|
||||
await click(PKI_ROLE_DETAILS.createRoleLink);
|
||||
roles = this.store.peekAll('pki/role');
|
||||
const role = roles.at(0);
|
||||
assert.strictEqual(roles.length, 1, 'New role exists');
|
||||
assert.true(role.isNew, 'Role is new model');
|
||||
await click(OVERVIEW_BREADCRUMB);
|
||||
roles = this.store.peekAll('pki/role');
|
||||
assert.strictEqual(roles.length, 0, 'Role is removed from store');
|
||||
});
|
||||
test('edit role', async function (assert) {
|
||||
let roles, role;
|
||||
const roleId = 'workflow-edit-role';
|
||||
await login();
|
||||
// Create PKI
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(GENERAL.secretTab('Roles'));
|
||||
roles = this.store.peekAll('pki/role');
|
||||
assert.strictEqual(roles.length, 0, 'No roles exist yet');
|
||||
await click(PKI_ROLE_DETAILS.createRoleLink);
|
||||
await fillIn(GENERAL.inputByAttr('name'), roleId);
|
||||
await click(GENERAL.submitButton);
|
||||
assert.dom(GENERAL.infoRowValue('Role name')).hasText(roleId, 'Shows correct role after create');
|
||||
roles = this.store.peekAll('pki/role');
|
||||
role = roles.at(0);
|
||||
assert.strictEqual(roles.length, 1, 'Role is created');
|
||||
assert.false(role.hasDirtyAttributes, 'Role no longer has dirty attributes');
|
||||
|
||||
// Edit role
|
||||
await click(PKI_ROLE_DETAILS.editRoleLink);
|
||||
await click(GENERAL.ttl.toggle('issuerRef-toggle'));
|
||||
await fillIn(GENERAL.selectByAttr('issuerRef'), 'foobar');
|
||||
role = this.store.peekRecord('pki/role', roleId);
|
||||
assert.true(role.hasDirtyAttributes, 'Role has dirty attrs');
|
||||
// Exit page via cancel button
|
||||
await click(GENERAL.cancelButton);
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets-engines/${this.mountPath}/pki/roles/${roleId}/details`
|
||||
);
|
||||
role = this.store.peekRecord('pki/role', roleId);
|
||||
assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back');
|
||||
|
||||
// Edit again
|
||||
await click(PKI_ROLE_DETAILS.editRoleLink);
|
||||
await click(GENERAL.ttl.toggle('issuerRef-toggle'));
|
||||
await fillIn(GENERAL.selectByAttr('issuerRef'), 'foobar2');
|
||||
role = this.store.peekRecord('pki/role', roleId);
|
||||
assert.true(role.hasDirtyAttributes, 'Role has dirty attrs');
|
||||
// Exit page via breadcrumbs
|
||||
await click(OVERVIEW_BREADCRUMB);
|
||||
role = this.store.peekRecord('pki/role', roleId);
|
||||
assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back');
|
||||
});
|
||||
});
|
||||
|
||||
module('issuer routes', function () {
|
||||
test('import issuer exit via cancel', async function (assert) {
|
||||
let issuers;
|
||||
await login();
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(GENERAL.secretTab('Issuers'));
|
||||
issuers = this.store.peekAll('pki/issuer');
|
||||
assert.strictEqual(issuers.length, 0, 'No issuer models exist yet');
|
||||
await click(PKI_ISSUER_LIST.importIssuerLink);
|
||||
issuers = this.store.peekAll('pki/action');
|
||||
assert.strictEqual(issuers.length, 1, 'Action model created');
|
||||
const issuer = issuers.at(0);
|
||||
assert.true(issuer.hasDirtyAttributes, 'Action has dirty attrs');
|
||||
assert.true(issuer.isNew, 'Action is new');
|
||||
// Exit
|
||||
await click('[data-test-pki-ca-cert-cancel]');
|
||||
assert.strictEqual(currentURL(), `/vault/secrets-engines/${this.mountPath}/pki/issuers`);
|
||||
issuers = this.store.peekAll('pki/action');
|
||||
assert.strictEqual(issuers.length, 0, 'Action is removed from store');
|
||||
});
|
||||
test('import issuer exit via breadcrumb', async function (assert) {
|
||||
let issuers;
|
||||
await login();
|
||||
await visit(`/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
await click(GENERAL.secretTab('Issuers'));
|
||||
issuers = this.store.peekAll('pki/issuer');
|
||||
assert.strictEqual(issuers.length, 0, 'No issuers exist yet');
|
||||
await click(PKI_ISSUER_LIST.importIssuerLink);
|
||||
issuers = this.store.peekAll('pki/action');
|
||||
assert.strictEqual(issuers.length, 1, 'Action model created');
|
||||
const issuer = issuers.at(0);
|
||||
assert.true(issuer.hasDirtyAttributes, 'Action model has dirty attrs');
|
||||
assert.true(issuer.isNew, 'Action model is new');
|
||||
// Exit
|
||||
await click(OVERVIEW_BREADCRUMB);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets-engines/${this.mountPath}/pki/overview`);
|
||||
issuers = this.store.peekAll('pki/action');
|
||||
assert.strictEqual(issuers.length, 0, 'Issuer is removed from store');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -246,7 +246,7 @@ module('Acceptance | pki workflow', function (hooks) {
|
|||
await click(GENERAL.submitButton);
|
||||
assert.strictEqual(
|
||||
flash.latestMessage,
|
||||
`Successfully created the role ${roleName}.`,
|
||||
`Successfully saved the role ${roleName}.`,
|
||||
'renders success flash upon creation'
|
||||
);
|
||||
assert.strictEqual(
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export const PKI_ROLE_DETAILS = {
|
|||
noStoreValue: GENERAL.infoRowValue('Store in storage backend'),
|
||||
noStoreMetadataValue: GENERAL.infoRowValue('Store metadata in storage backend'),
|
||||
keyUsageValue: GENERAL.infoRowValue('Key usage'),
|
||||
extKeyUsageValue: GENERAL.infoRowValue('Ext key usage'),
|
||||
extKeyUsageValue: GENERAL.infoRowValue('Extended key usage'),
|
||||
customTtlValue: GENERAL.infoRowValue('Issued certificates expire after'),
|
||||
deleteRoleButton: '[data-test-pki-role-delete]',
|
||||
generateCertLink: '[data-test-pki-role-generate-cert]',
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CERTIFICATES } from 'vault/tests/helpers/pki/pki-helpers';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | pki | Page::PkiCertificateDetails', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
const downloadService = this.owner.lookup('service:download');
|
||||
|
|
@ -24,120 +23,96 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function (
|
|||
const routerService = this.owner.lookup('service:router');
|
||||
this.routerSpy = sinon.stub(routerService, 'transitionTo');
|
||||
|
||||
this.owner.lookup('service:secretMountPath').update('pki');
|
||||
this.backend = 'pki';
|
||||
this.owner.lookup('service:secretMountPath').update(this.backend);
|
||||
|
||||
const store = this.owner.lookup('service:store');
|
||||
const id = '4d:b6:ed:90:d6:b0:d4:bb:8e:5d:73:6a:6f:32:dc:8c:71:7c:db:5f';
|
||||
store.pushPayload('pki/certificate/base', {
|
||||
modelName: 'pki/certificate/base',
|
||||
data: {
|
||||
certificate: '-----BEGIN CERTIFICATE-----',
|
||||
common_name: 'example.com Intermediate Authority',
|
||||
issue_date: 1673540867000,
|
||||
serial_number: id,
|
||||
parsed_certificate: {
|
||||
not_valid_after: 1831220897000,
|
||||
not_valid_before: 1673540867000,
|
||||
},
|
||||
},
|
||||
});
|
||||
store.pushPayload('pki/certificate/generate', {
|
||||
modelName: 'pki/certificate/generate',
|
||||
data: {
|
||||
certificate: '-----BEGIN CERTIFICATE-----',
|
||||
ca_chain: '-----BEGIN CERTIFICATE-----',
|
||||
issuer_ca: '-----BEGIN CERTIFICATE-----',
|
||||
private_key: '-----BEGIN PRIVATE KEY-----',
|
||||
private_key_type: 'rsa',
|
||||
common_name: 'example.com Intermediate Authority',
|
||||
issue_date: 1673540867000,
|
||||
serial_number: id,
|
||||
parsed_certificate: {
|
||||
not_valid_after: 1831220897000,
|
||||
not_valid_before: 1673540867000,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.model = store.peekRecord('pki/certificate/base', id);
|
||||
this.generatedModel = store.peekRecord('pki/certificate/generate', id);
|
||||
this.revokeResponse = {
|
||||
revocation_time: 1673972804,
|
||||
revocation_time_rfc3339: '2023-01-17T16:26:44.960933411Z',
|
||||
};
|
||||
this.revokeStub = sinon
|
||||
.stub(this.owner.lookup('service:api').secrets, 'pkiRevoke')
|
||||
.resolves(this.revokeResponse);
|
||||
|
||||
this.server.post('/sys/capabilities-self', () => ({
|
||||
data: {
|
||||
capabilities: ['root'],
|
||||
'pki/revoke': ['root'],
|
||||
},
|
||||
}));
|
||||
const sn = '4d:b6:ed:90:d6:b0:d4:bb:8e:5d:73:6a:6f:32:dc:8c:71:7c:db:5f';
|
||||
this.cert = {
|
||||
certificate: CERTIFICATES.rootPem,
|
||||
serial_number: sn,
|
||||
revocation_time: 0,
|
||||
revocation_time_rfc3339: '',
|
||||
};
|
||||
this.generatedCert = {
|
||||
certificate: CERTIFICATES.rootPem,
|
||||
ca_chain: ['-----BEGIN CERTIFICATE-----'],
|
||||
issuing_ca: '-----BEGIN CERTIFICATE-----',
|
||||
private_key: '-----BEGIN PRIVATE KEY-----',
|
||||
private_key_type: 'rsa',
|
||||
serial_number: sn,
|
||||
};
|
||||
this.certData = this.cert;
|
||||
this.canRevoke = true;
|
||||
this.onBack = sinon.spy();
|
||||
this.onRevoke = sinon.spy();
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`
|
||||
<Page::PkiCertificateDetails
|
||||
@certData={{this.certData}}
|
||||
@canRevoke={{this.canRevoke}}
|
||||
@onBack={{this.onBack}}
|
||||
@onRevoke={{this.onRevoke}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render actions and fields for base cert', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.server.post('/pki/revoke', (schema, req) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
data.serial_number,
|
||||
this.model.serialNumber,
|
||||
'Revoke request made with serial number'
|
||||
);
|
||||
return {
|
||||
data: {
|
||||
revocation_time: 1673972804,
|
||||
revocation_time_rfc3339: '2023-01-17T16:26:44.960933411Z',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await render(hbs`<Page::PkiCertificateDetails @model={{this.model}} />`, { owner: this.engine });
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom('[data-test-component="info-table-row"]')
|
||||
.exists({ count: 5 }, 'Correct number of fields render when certificate has not been revoked');
|
||||
.exists({ count: 8 }, 'Correct number of fields render when certificate has not been revoked');
|
||||
assert
|
||||
.dom(`${GENERAL.infoRowValue('Certificate')} [data-test-certificate-card]`)
|
||||
.exists('Certificate card renders for certificate');
|
||||
assert.dom(`${GENERAL.infoRowValue('Serial number')} code`).exists('Serial number renders as monospace');
|
||||
|
||||
await click('[data-test-pki-cert-download-button]');
|
||||
const { serialNumber, certificate } = this.model;
|
||||
const { serial_number, certificate } = this.certData;
|
||||
assert.ok(
|
||||
this.downloadSpy.calledWith(serialNumber.replace(/(\s|:)+/g, '-'), certificate),
|
||||
this.downloadSpy.calledWith(serial_number.replace(/(\s|:)+/g, '-'), certificate),
|
||||
'Download pem method called with correct args'
|
||||
);
|
||||
|
||||
await click(GENERAL.confirmTrigger);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
this.revokeStub.calledWith(this.backend, { serial_number: this.certData.serial_number }),
|
||||
'Revoke request called with correct params'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Revocation time')).exists('Revocation time is displayed');
|
||||
});
|
||||
|
||||
test('it should render actions and fields for generated cert', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
this.server.post('/pki/revoke', (schema, req) => {
|
||||
const data = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(
|
||||
data.serial_number,
|
||||
this.model.serialNumber,
|
||||
'Revoke request made with serial number'
|
||||
);
|
||||
return {
|
||||
data: {
|
||||
revocation_time: 1673972804,
|
||||
revocation_time_rfc3339: '2023-01-17T16:26:44.960933411Z',
|
||||
},
|
||||
};
|
||||
});
|
||||
this.certData = this.generatedCert;
|
||||
await this.renderComponent();
|
||||
|
||||
await render(hbs`<Page::PkiCertificateDetails @model={{this.generatedModel}} />`, { owner: this.engine });
|
||||
assert.dom('[data-test-cert-detail-next-steps]').exists('Private key next steps warning shows');
|
||||
assert
|
||||
.dom('[data-test-component="info-table-row"]')
|
||||
.exists({ count: 9 }, 'Correct number of fields render when certificate has not been revoked');
|
||||
.exists({ count: 12 }, 'Correct number of fields render when certificate has not been revoked');
|
||||
assert
|
||||
.dom(`${GENERAL.infoRowValue('Certificate')} [data-test-certificate-card]`)
|
||||
.exists('Certificate card renders for certificate');
|
||||
assert.dom(`${GENERAL.infoRowValue('Serial number')} code`).exists('Serial number renders as monospace');
|
||||
assert
|
||||
.dom(`${GENERAL.infoRowValue('CA Chain')} [data-test-certificate-card]`)
|
||||
.dom(`${GENERAL.infoRowValue('CA chain')} [data-test-certificate-card]`)
|
||||
.exists('Certificate card renders for CA Chain');
|
||||
assert
|
||||
.dom(`${GENERAL.infoRowValue('Issuing CA')} [data-test-certificate-card]`)
|
||||
|
|
@ -147,45 +122,36 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function (
|
|||
.exists('Certificate card renders for private key');
|
||||
|
||||
await click('[data-test-pki-cert-download-button]');
|
||||
const { serialNumber, certificate } = this.model;
|
||||
const { serial_number, certificate } = this.certData;
|
||||
assert.ok(
|
||||
this.downloadSpy.calledWith(serialNumber.replace(/(\s|:)+/g, '-'), certificate),
|
||||
this.downloadSpy.calledWith(serial_number.replace(/(\s|:)+/g, '-'), certificate),
|
||||
'Download pem method called with correct args'
|
||||
);
|
||||
|
||||
await click(GENERAL.confirmTrigger);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
this.revokeStub.calledWith(this.backend, { serial_number: this.certData.serial_number }),
|
||||
'Revoke request called with correct params'
|
||||
);
|
||||
assert.dom(GENERAL.infoRowValue('Revocation time')).exists('Revocation time is displayed');
|
||||
});
|
||||
|
||||
test('it should render back button', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.cancel = () => assert.ok('onBack action is triggered');
|
||||
|
||||
await render(hbs`<Page::PkiCertificateDetails @model={{this.model}} @onBack={{this.cancel}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click('[data-test-pki-cert-details-back]');
|
||||
assert.true(this.onBack.calledOnce, 'onBack action is called when back button is clicked');
|
||||
});
|
||||
|
||||
test('it should send action on revoke if provided', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.server.post('/pki/revoke', () => ({
|
||||
data: {
|
||||
revocation_time: 1673972804,
|
||||
revocation_time_rfc3339: '2023-01-17T16:26:44.960933411Z',
|
||||
},
|
||||
}));
|
||||
|
||||
this.revoked = () => assert.ok('onRevoke action is triggered');
|
||||
|
||||
await render(hbs`<Page::PkiCertificateDetails @model={{this.model}} @onRevoke={{this.revoked}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.confirmTrigger);
|
||||
await click(GENERAL.confirmButton);
|
||||
assert.true(this.onRevoke.calledOnce, 'onRevoke action is called when certificate is revoked');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,35 +5,49 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { PKI_ROLE_DETAILS } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | pki role details page', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/role', {
|
||||
this.backend = 'pki';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
this.role = {
|
||||
name: 'Foobar',
|
||||
backend: 'pki',
|
||||
noStore: false,
|
||||
noStoreMetadata: true,
|
||||
keyUsage: [],
|
||||
extKeyUsage: ['bar', 'baz'],
|
||||
no_store: false,
|
||||
no_store_metadata: true,
|
||||
key_usage: [],
|
||||
ext_key_usage: ['bar', 'baz'],
|
||||
ttl: 600,
|
||||
});
|
||||
issuer_ref: 'issuer-1',
|
||||
};
|
||||
this.capabilities = {
|
||||
canEdit: true,
|
||||
canDelete: true,
|
||||
canGenerateCert: true,
|
||||
canSign: true,
|
||||
};
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`
|
||||
<Page::PkiRoleDetails @role={{this.role}} @capabilities={{this.capabilities}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render the page component', async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PKI_ROLE_DETAILS.issuerLabel).hasText('Issuer', 'Label is');
|
||||
assert
|
||||
.dom(`${PKI_ROLE_DETAILS.keyUsageValue} [data-test-icon="minus"]`)
|
||||
|
|
@ -53,12 +67,8 @@ module('Integration | Component | pki role details page', function (hooks) {
|
|||
test('it should render the enterprise-only values in enterprise edition', async function (assert) {
|
||||
const version = this.owner.lookup('service:version');
|
||||
version.type = 'enterprise';
|
||||
await render(
|
||||
hbs`
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(PKI_ROLE_DETAILS.noStoreMetadataValue)
|
||||
.containsText('No', 'noStoreMetadata shows opposite of what the value is');
|
||||
|
|
@ -66,22 +76,54 @@ module('Integration | Component | pki role details page', function (hooks) {
|
|||
|
||||
test('it should render the notAfter date if present', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.model = this.store.createRecord('pki/role', {
|
||||
|
||||
this.role = {
|
||||
name: 'Foobar',
|
||||
backend: 'pki',
|
||||
noStore: false,
|
||||
keyUsage: [],
|
||||
extKeyUsage: ['bar', 'baz'],
|
||||
notAfter: '2030-05-04T12:00:00.000Z',
|
||||
});
|
||||
await render(
|
||||
hbs`
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
no_store: false,
|
||||
key_usage: [],
|
||||
ext_key_usage: ['bar', 'baz'],
|
||||
not_after: '2030-05-04T12:00:00.000Z',
|
||||
issuer_ref: 'issuer-1',
|
||||
};
|
||||
|
||||
await this.renderComponent();
|
||||
assert
|
||||
.dom(PKI_ROLE_DETAILS.customTtlValue)
|
||||
.containsText('May', 'Formats the notAfter date instead of TTL');
|
||||
});
|
||||
|
||||
test('it should hide actions when user does not have capabilities', async function (assert) {
|
||||
this.capabilities = {
|
||||
canEdit: false,
|
||||
canDelete: false,
|
||||
canGenerateCert: false,
|
||||
canSign: false,
|
||||
};
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PKI_ROLE_DETAILS.editRoleLink).doesNotExist('Edit link is not rendered');
|
||||
assert.dom(PKI_ROLE_DETAILS.deleteRoleButton).doesNotExist('Delete button is not rendered');
|
||||
assert.dom(PKI_ROLE_DETAILS.generateCertLink).doesNotExist('Generate Cert link is not rendered');
|
||||
assert.dom(PKI_ROLE_DETAILS.signCertLink).doesNotExist('Sign link is not rendered');
|
||||
});
|
||||
|
||||
test('it should render actions when user has capabilities and delete role', async function (assert) {
|
||||
const deleteStub = sinon.stub(this.owner.lookup('service:api').secrets, 'pkiDeleteRole');
|
||||
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(PKI_ROLE_DETAILS.editRoleLink).exists('Edit link renders');
|
||||
assert.dom(PKI_ROLE_DETAILS.deleteRoleButton).exists('Delete button renders');
|
||||
assert.dom(PKI_ROLE_DETAILS.generateCertLink).exists('Generate Cert link renders');
|
||||
assert.dom(PKI_ROLE_DETAILS.signCertLink).exists('Sign link renders');
|
||||
|
||||
await click(PKI_ROLE_DETAILS.deleteRoleButton);
|
||||
await click(GENERAL.confirmButton);
|
||||
|
||||
assert.true(
|
||||
deleteStub.calledWith(this.role.name, this.backend),
|
||||
'Delete API called with correct parameters'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ module('Integration | Component | pki key form', function (hooks) {
|
|||
this.genExportedStub.calledWith(this.backend, {
|
||||
key_name: 'test-key',
|
||||
key_type: 'rsa',
|
||||
key_bits: '2048',
|
||||
key_bits: 2048,
|
||||
}),
|
||||
'generates exported key with correct params'
|
||||
);
|
||||
|
|
@ -111,7 +111,7 @@ module('Integration | Component | pki key form', function (hooks) {
|
|||
this.genInternalStub.calledWith(this.backend, {
|
||||
key_name: 'test-key',
|
||||
key_type: 'rsa',
|
||||
key_bits: '2048',
|
||||
key_bits: 2048,
|
||||
}),
|
||||
'generates internal key with correct params'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,94 +9,83 @@ import { render, fillIn } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
module('Integration | Component | pki key parameters', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/role', { backend: 'pki' });
|
||||
[this.fields] = Object.values(this.model.formFieldGroups.find((g) => g['Key parameters']));
|
||||
this.data = { key_type: 'rsa', key_bits: 2048, signature_bits: 0 };
|
||||
this.form = new PkiRoleForm(this.data, { isNew: true });
|
||||
this.fields = this.form.formFieldGroups.find((g) => g['Key parameters'])['Key parameters'];
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<PkiKeyParameters @form={{this.form}} @fields={{this.fields}} @modelValidations={{this.modelValidations}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render the component and display the correct defaults', async function (assert) {
|
||||
test('it should render the component and display the correct values', async function (assert) {
|
||||
assert.expect(3);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiKeyParameters
|
||||
@model={{this.model}}
|
||||
@fields={{this.fields}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(GENERAL.inputByAttr('keyType')).hasValue('rsa');
|
||||
assert.dom(GENERAL.inputByAttr('keyBits')).hasValue('2048');
|
||||
assert.dom(GENERAL.inputByAttr('signatureBits')).hasValue('0');
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('key_type')).hasValue('rsa');
|
||||
assert.dom(GENERAL.inputByAttr('key_bits')).hasValue('2048');
|
||||
assert.dom(GENERAL.inputByAttr('signature_bits')).hasValue('0');
|
||||
});
|
||||
|
||||
test('it should set the model properties of key_type and key_bits when key_type changes', async function (assert) {
|
||||
assert.expect(11);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiKeyParameters
|
||||
@model={{this.model}}
|
||||
@fields={{this.fields}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.strictEqual(this.model.keyType, 'rsa', 'sets the default value for key_type on the model.');
|
||||
assert.strictEqual(this.model.keyBits, '2048', 'sets the default value for key_bits on the model.');
|
||||
test('it should set values of key_type and key_bits when key_type changes', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('key_type'), 'ec');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
'0',
|
||||
'sets the default value for signature_bits on the model.'
|
||||
this.form.data.key_type,
|
||||
'ec',
|
||||
'sets the new selected value for key_type on the model.'
|
||||
);
|
||||
await fillIn(GENERAL.inputByAttr('keyType'), 'ec');
|
||||
assert.strictEqual(this.model.keyType, 'ec', 'sets the new selected value for key_type on the model.');
|
||||
assert.strictEqual(
|
||||
this.model.keyBits,
|
||||
'256',
|
||||
this.form.data.key_bits,
|
||||
256,
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('keyType'), 'ed25519');
|
||||
await fillIn(GENERAL.inputByAttr('key_type'), 'ed25519');
|
||||
assert.strictEqual(
|
||||
this.model.keyType,
|
||||
this.form.data.key_type,
|
||||
'ed25519',
|
||||
'sets the new selected value for key_type on the model.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.model.keyBits,
|
||||
'0',
|
||||
this.form.data.key_bits,
|
||||
0,
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('keyType'), 'ec');
|
||||
await fillIn(GENERAL.inputByAttr('keyBits'), '384');
|
||||
assert.strictEqual(this.model.keyType, 'ec', 'sets the new selected value for key_type on the model.');
|
||||
await fillIn(GENERAL.inputByAttr('key_type'), 'ec');
|
||||
await fillIn(GENERAL.inputByAttr('key_bits'), '384');
|
||||
assert.strictEqual(
|
||||
this.model.keyBits,
|
||||
this.form.data.key_type,
|
||||
'ec',
|
||||
'sets the new selected value for key_type on the model.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.form.data.key_bits,
|
||||
'384',
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('signatureBits'), '384');
|
||||
await fillIn(GENERAL.inputByAttr('signature_bits'), '384');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
this.form.data.signature_bits,
|
||||
'384',
|
||||
'sets the new selected value for signature_bits on the model.'
|
||||
);
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('signatureBits'), '0');
|
||||
await fillIn(GENERAL.inputByAttr('signature_bits'), '0');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
this.form.data.signature_bits,
|
||||
'0',
|
||||
'sets the default value for signature_bits on the model.'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,113 +10,50 @@ import { hbs } from 'ember-cli-htmlbars';
|
|||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { PKI_ROLE_FORM } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
module('Integration | Component | pki-key-usage', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/role');
|
||||
// add fields that openapi normally hydrates
|
||||
// ideally we pull this from the openapi schema in the future
|
||||
const openapifields = [
|
||||
{
|
||||
name: 'clientFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for client auth use. Defaults to true. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'serverFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for server auth use. Defaults to true. See also, RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'codeSigningFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for code signing use. Defaults to false. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'emailProtectionFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for email protection use. Defaults to false. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
},
|
||||
];
|
||||
this.model._allByKey = {};
|
||||
openapifields.forEach((f) => {
|
||||
this.model._allByKey[f.name] = f;
|
||||
this.model[f.name] = f.options.defaultValue;
|
||||
});
|
||||
this.model.backend = 'pki';
|
||||
this.owner.lookup('service:secret-mount-path').update('pki');
|
||||
this.form = new PkiRoleForm({}, { isNew: true });
|
||||
|
||||
this.renderComponent = () => render(hbs`<PkiKeyUsage @form={{this.form}} />`, { owner: this.engine });
|
||||
});
|
||||
|
||||
test('it should render the component', async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiKeyUsage
|
||||
@model={{this.model}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
assert.strictEqual(findAll('.b-checkbox').length, 19, 'it render 19 checkboxes');
|
||||
assert.dom(PKI_ROLE_FORM.digitalSignature).isChecked('Digital Signature is true by default');
|
||||
assert.dom(PKI_ROLE_FORM.keyAgreement).isChecked('Key Agreement is true by default');
|
||||
assert.dom(PKI_ROLE_FORM.keyEncipherment).isChecked('Key Encipherment is true by default');
|
||||
assert.dom(PKI_ROLE_FORM.any).isNotChecked('Any is false by default');
|
||||
assert.dom(GENERAL.inputByAttr('clientFlag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('serverFlag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('codeSigningFlag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('emailProtectionFlag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('extKeyUsageOids')).exists('Extended Key usage oids renders');
|
||||
assert.dom(GENERAL.inputByAttr('client_flag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('server_flag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('code_signing_flag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('email_protection_flag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('ext_key_usage_oids')).exists('Extended Key usage oids renders');
|
||||
});
|
||||
|
||||
test('it should set the model properties of key_usage and ext_key_usage based on the checkbox selections', async function (assert) {
|
||||
test('it should set values of key_usage and ext_key_usage based on the checkbox selections', async function (assert) {
|
||||
assert.expect(2);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiKeyUsage
|
||||
@model={{this.model}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
await click(PKI_ROLE_FORM.digitalSignature);
|
||||
await click(PKI_ROLE_FORM.any);
|
||||
await click(PKI_ROLE_FORM.serverAuth);
|
||||
|
||||
assert.deepEqual(
|
||||
this.model.keyUsage,
|
||||
this.form.data.key_usage,
|
||||
['KeyAgreement', 'KeyEncipherment'],
|
||||
'removes digitalSignature from the model when unchecked.'
|
||||
'removes DigitalSignature from key_usage when unchecked.'
|
||||
);
|
||||
assert.deepEqual(
|
||||
this.form.data.ext_key_usage,
|
||||
['Any', 'ServerAuth'],
|
||||
'adds new checkboxes to when checked'
|
||||
);
|
||||
assert.deepEqual(this.model.extKeyUsage, ['Any', 'ServerAuth'], 'adds new checkboxes to when checked');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,52 +9,29 @@ import { render, click, fillIn } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { PKI_NOT_VALID_AFTER } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import PkiCertificateForm from 'vault/forms/secrets/pki/certificate';
|
||||
|
||||
module('Integration | Component | pki-not-valid-after-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/role', { backend: 'pki' });
|
||||
this.attr = {
|
||||
helpText: '',
|
||||
options: {
|
||||
helperTextEnabled: 'toggled on and shows text',
|
||||
},
|
||||
};
|
||||
this.form = new PkiCertificateForm('PkiIssueWithRoleRequest', {}, { isNew: true });
|
||||
this.renderComponent = () =>
|
||||
render(hbs`<PkiNotValidAfterForm @form={{this.form}} />`, { owner: this.engine });
|
||||
});
|
||||
|
||||
test('it should render the component with ttl selected by default', async function (assert) {
|
||||
assert.expect(3);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiNotValidAfterForm
|
||||
@model={{this.model}}
|
||||
@attr={{this.attr}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(PKI_NOT_VALID_AFTER.ttlForm).exists('shows the TTL picker');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.ttlTimeInput).hasValue('', 'default TTL is empty');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioTtl).isChecked('ttl is selected by default');
|
||||
});
|
||||
|
||||
test('it clears and resets model properties from cache when changing radio selection', async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiNotValidAfterForm
|
||||
@model={{this.model}}
|
||||
@attr={{this.attr}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioTtl).isChecked('notBeforeDate radio is selected');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.ttlForm).exists({ count: 1 }, 'shows TTL form');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioDate).isNotChecked('NotAfter selection not checked');
|
||||
|
|
@ -71,41 +48,38 @@ module('Integration | Component | pki-not-valid-after-form', function (hooks) {
|
|||
const notAfterExpected = '1994-11-05T00:00:00.000Z';
|
||||
const ttlDate = 1;
|
||||
await fillIn('[data-test-input="not_after"]', utcDate);
|
||||
|
||||
assert.strictEqual(
|
||||
this.model.notAfter,
|
||||
this.form.data.not_after,
|
||||
notAfterExpected,
|
||||
'sets the model property notAfter when this value is selected and filled in.'
|
||||
);
|
||||
await click('[data-test-radio-button="ttl"]');
|
||||
assert.strictEqual(
|
||||
this.model.notAfter,
|
||||
this.form.data.not_after,
|
||||
'',
|
||||
'The notAfter is cleared on the model because the radio button was selected.'
|
||||
);
|
||||
await fillIn('[data-test-ttl-value="TTL"]', ttlDate);
|
||||
assert.strictEqual(
|
||||
this.model.ttl,
|
||||
this.form.data.ttl,
|
||||
'1s',
|
||||
'The ttl is now saved on the model because the radio button was selected.'
|
||||
);
|
||||
|
||||
await click('[data-test-radio-button="not_after"]');
|
||||
assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.');
|
||||
assert.strictEqual(this.model.notAfter, notAfterExpected, 'notAfter gets populated from local cache');
|
||||
});
|
||||
test('Form renders properly for edit when TTL present', async function (assert) {
|
||||
this.model = this.store.createRecord('pki/role', { backend: 'pki', ttl: 6000 });
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiNotValidAfterForm
|
||||
@model={{this.model}}
|
||||
@attr={{this.attr}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
assert.strictEqual(this.form.data.ttl, '', 'TTL is cleared after radio select.');
|
||||
assert.strictEqual(
|
||||
this.form.data.not_after,
|
||||
notAfterExpected,
|
||||
'notAfter gets populated from local cache'
|
||||
);
|
||||
});
|
||||
|
||||
test('Form renders properly for edit when TTL present', async function (assert) {
|
||||
this.form.data.ttl = 6000;
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioTtl).isChecked('notBeforeDate radio is selected');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.ttlForm).exists({ count: 1 }, 'shows TTL form');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioDate).isNotChecked('NotAfter selection not checked');
|
||||
|
|
@ -114,20 +88,12 @@ module('Integration | Component | pki-not-valid-after-form', function (hooks) {
|
|||
assert.dom(PKI_NOT_VALID_AFTER.ttlTimeInput).hasValue('100', 'TTL value is correctly shown');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.ttlUnitInput).hasValue('m', 'TTL unit is correctly shown');
|
||||
});
|
||||
|
||||
test('Form renders properly for edit when notAfter present', async function (assert) {
|
||||
const utcDate = '1994-11-05T00:00:00.000Z';
|
||||
this.model = this.store.createRecord('pki/role', { backend: 'pki', notAfter: utcDate });
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiNotValidAfterForm
|
||||
@model={{this.model}}
|
||||
@attr={{this.attr}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
this.form.data.not_after = utcDate;
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioDate).isChecked('notAfter radio is selected');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.dateInput).exists({ count: 1 }, 'shows date picker');
|
||||
assert.dom(PKI_NOT_VALID_AFTER.radioTtl).isNotChecked('ttl radio not selected');
|
||||
|
|
|
|||
|
|
@ -9,23 +9,40 @@ import { render, click, fillIn } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import sinon from 'sinon';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import PkiRoleForm from 'vault/forms/secrets/pki/role';
|
||||
|
||||
module('Integration | Component | pki-role-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupEngine(hooks, 'pki'); // https://github.com/ember-engines/ember-engines/pull/653
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.role = this.store.createRecord('pki/role');
|
||||
this.store.createRecord('pki/issuer', { issuerName: 'issuer-0', issuerId: 'abcd-efgh' });
|
||||
this.store.createRecord('pki/issuer', { issuerName: 'issuer-1', issuerId: 'ijkl-mnop' });
|
||||
this.issuers = this.store.peekAll('pki/issuer');
|
||||
this.role.backend = 'pki';
|
||||
this.backend = 'pki';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
|
||||
this.writeStub = sinon.stub(this.owner.lookup('service:api').secrets, 'pkiWriteRole');
|
||||
|
||||
this.issuers = [
|
||||
{ issuer_name: 'issuer-0', issuer_id: 'abcd-efgh' },
|
||||
{ issuer_name: 'issuer-1', issuer_id: 'ijkl-mnop' },
|
||||
];
|
||||
this.onCancel = sinon.spy();
|
||||
this.onSave = sinon.spy();
|
||||
|
||||
this.formDefaults = {
|
||||
allow_ip_sans: true,
|
||||
allow_localhost: true,
|
||||
client_flag: true,
|
||||
enforce_hostnames: true,
|
||||
key_usage: ['DigitalSignature', 'KeyAgreement', 'KeyEncipherment'],
|
||||
not_before_duration: 30,
|
||||
serial_number_source: 'json-csr',
|
||||
server_flag: true,
|
||||
use_csr_common_name: true,
|
||||
use_csr_sans: true,
|
||||
};
|
||||
|
||||
setRunOptions({
|
||||
rules: {
|
||||
// TODO: fix RadioCard component (replace with HDS)
|
||||
|
|
@ -33,30 +50,29 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
|||
'nested-interactive': { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
this.renderComponent = () => {
|
||||
this.form = new PkiRoleForm(this.role, { isNew: !this.role });
|
||||
return render(
|
||||
hbs`<PkiRoleForm @form={{this.form}} @issuers={{this.issuers}} @onCancel={{this.onCancel}} @onSave={{this.onSave}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it should render default fields and toggle groups', async function (assert) {
|
||||
await render(
|
||||
hbs`
|
||||
<PkiRoleForm
|
||||
@role={{this.role}}
|
||||
@issuers={{this.issuers}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(GENERAL.ttl.toggle('issuerRef-toggle')).exists();
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.ttl.toggle('issuer_ref-toggle')).exists();
|
||||
assert.dom(GENERAL.ttl.input('Backdate validity')).exists();
|
||||
assert.dom(GENERAL.fieldByAttr('customTtl')).exists();
|
||||
assert.dom(GENERAL.ttl.toggle('Max TTL')).exists();
|
||||
assert.dom(GENERAL.fieldByAttr('generateLease')).exists();
|
||||
assert.dom(GENERAL.fieldByAttr('noStore')).exists();
|
||||
assert.dom(GENERAL.fieldByAttr('generate_lease')).exists();
|
||||
assert.dom(GENERAL.fieldByAttr('no_store')).exists();
|
||||
assert
|
||||
.dom(GENERAL.fieldByAttr('noStoreMetadata'))
|
||||
.doesNotExist('noStoreMetadata is not shown b/c not enterprise');
|
||||
assert.dom(GENERAL.inputByAttr('basicConstraintsValidForNonCa')).exists();
|
||||
.dom(GENERAL.fieldByAttr('no_store_metadata'))
|
||||
.doesNotExist('no_store_metadata is not shown for community edition');
|
||||
assert.dom(GENERAL.inputByAttr('basic_constraints_valid_for_non_ca')).exists();
|
||||
assert.dom(GENERAL.button('Domain handling')).exists('shows form-field group add domain handling');
|
||||
assert.dom(GENERAL.button('Key parameters')).exists('shows form-field group key params');
|
||||
assert.dom(GENERAL.button('Key usage')).exists('shows form-field group key usage');
|
||||
|
|
@ -68,57 +84,16 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
|||
});
|
||||
|
||||
test('it renders enterprise-only values in enterprise edition', async function (assert) {
|
||||
const version = this.owner.lookup('service:version');
|
||||
version.type = 'enterprise';
|
||||
await render(
|
||||
hbs`
|
||||
<PkiRoleForm
|
||||
@role={{this.role}}
|
||||
@issuers={{this.issuers}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(GENERAL.fieldByAttr('noStoreMetadata')).exists();
|
||||
this.owner.lookup('service:version').type = 'enterprise';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.fieldByAttr('no_store_metadata')).exists();
|
||||
});
|
||||
|
||||
test('it should save a new pki role with various options selected', async function (assert) {
|
||||
// Key usage, Key params and Not valid after options are tested in their respective component tests
|
||||
assert.expect(7);
|
||||
this.server.post(`/${this.role.backend}/roles/test-role`, (schema, req) => {
|
||||
assert.ok(true, 'Request made to save role');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
const allowedDomainsTemplate = request.allowed_domains_template;
|
||||
const policyIdentifiers = request.policy_identifiers;
|
||||
const allowedUriSansTemplate = request.allow_uri_sans_template;
|
||||
const allowedSerialNumbers = request.allowed_serial_numbers;
|
||||
assert.expect(3);
|
||||
|
||||
assert.true(allowedDomainsTemplate, 'correctly sends allowed_domains_template');
|
||||
assert.strictEqual(policyIdentifiers[0], 'some-oid', 'correctly sends policy_identifiers');
|
||||
assert.true(allowedUriSansTemplate, 'correctly sends allowed_uri_sans_template');
|
||||
assert.strictEqual(
|
||||
allowedSerialNumbers[0],
|
||||
'some-serial-number',
|
||||
'correctly sends allowed_serial_numbers'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiRoleForm
|
||||
@role={{this.role}}
|
||||
@issuers={{this.issuers}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
await this.renderComponent();
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
|
|
@ -126,124 +101,84 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
|||
.dom(GENERAL.validationErrorByAttr('name'))
|
||||
.includesText('Name is required.', 'show correct error message');
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'test-role');
|
||||
await click('[data-test-input="basicConstraintsValidForNonCa"]');
|
||||
const name = 'test-role';
|
||||
await fillIn(GENERAL.inputByAttr('name'), name);
|
||||
await click('[data-test-input="basic_constraints_valid_for_non_ca"]');
|
||||
await click(GENERAL.button('Domain handling'));
|
||||
await click('[data-test-input="allowedDomainsTemplate"]');
|
||||
await click('[data-test-input="allowed_domains_template"]');
|
||||
await click(GENERAL.button('Policy identifiers'));
|
||||
await fillIn('[data-test-input="policyIdentifiers"] [data-test-string-list-input="0"]', 'some-oid');
|
||||
await fillIn('[data-test-input="policy_identifiers"] [data-test-string-list-input="0"]', 'some-oid');
|
||||
await click(GENERAL.button('Subject Alternative Name (SAN) Options'));
|
||||
await click('[data-test-input="allowUriSansTemplate"]');
|
||||
await click('[data-test-input="allowed_uri_sans_template"]');
|
||||
await click(GENERAL.button('Additional subject fields'));
|
||||
await fillIn(
|
||||
'[data-test-input="allowedSerialNumbers"] [data-test-string-list-input="0"]',
|
||||
'[data-test-input="allowed_serial_numbers"] [data-test-string-list-input="0"]',
|
||||
'some-serial-number'
|
||||
);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('it should update attributes on the model on update', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.store.pushPayload('pki/role', {
|
||||
modelName: 'pki/role',
|
||||
name: 'test-role',
|
||||
backend: 'pki-test',
|
||||
id: 'role-id',
|
||||
});
|
||||
|
||||
this.role = this.store.peekRecord('pki/role', 'role-id');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiRoleForm
|
||||
@role={{this.role}}
|
||||
@issuers={{this.issuers}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
const payload = {
|
||||
...this.formDefaults,
|
||||
allowed_domains_template: true,
|
||||
basic_constraints_valid_for_non_ca: true,
|
||||
policy_identifiers: ['some-oid'],
|
||||
allowed_uri_sans_template: true,
|
||||
allowed_serial_numbers: ['some-serial-number'],
|
||||
};
|
||||
assert.true(
|
||||
this.writeStub.calledWith(name, this.backend, payload),
|
||||
'Correct endpoint is called to save role'
|
||||
);
|
||||
await click(GENERAL.ttl.toggle('issuerRef-toggle'));
|
||||
await fillIn(GENERAL.selectByAttr('issuerRef'), 'issuer-1');
|
||||
await click(GENERAL.submitButton);
|
||||
assert.strictEqual(this.role.issuerRef, 'issuer-1', 'Issuer Ref correctly saved on create');
|
||||
assert.true(this.onSave.calledWith(name), 'onSave called with role name after successful save');
|
||||
});
|
||||
|
||||
test('it should edit a role', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.server.post(`/pki-test/roles/test-role`, (schema, req) => {
|
||||
assert.ok(true, 'Request made to correct endpoint to update role');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
assert.propEqual(
|
||||
request,
|
||||
{
|
||||
allow_ip_sans: true,
|
||||
issuer_ref: 'issuer-1',
|
||||
key_bits: '224',
|
||||
key_type: 'ec',
|
||||
key_usage: ['DigitalSignature', 'KeyAgreement', 'KeyEncipherment'],
|
||||
not_before_duration: '30s',
|
||||
require_cn: true,
|
||||
serial_number_source: 'json-csr',
|
||||
signature_bits: '384',
|
||||
use_csr_common_name: true,
|
||||
use_csr_sans: true,
|
||||
},
|
||||
'sends role params in correct type'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
assert.expect(7);
|
||||
|
||||
this.store.pushPayload('pki/role', {
|
||||
modelName: 'pki/role',
|
||||
this.role = {
|
||||
name: 'test-role',
|
||||
backend: 'pki-test',
|
||||
id: 'role-id',
|
||||
issuer_ref: 'default',
|
||||
key_type: 'rsa',
|
||||
key_bits: 3072, // string type in dropdown, API returns as numbers
|
||||
signature_bits: 512, // string type in dropdown, API returns as numbers
|
||||
});
|
||||
};
|
||||
await this.renderComponent();
|
||||
|
||||
this.role = this.store.peekRecord('pki/role', 'role-id');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiRoleForm
|
||||
@role={{this.role}}
|
||||
@issuers={{this.issuers}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await click(GENERAL.ttl.toggle('issuerRef-toggle'));
|
||||
await fillIn(GENERAL.selectByAttr('issuerRef'), 'issuer-1');
|
||||
await click(GENERAL.ttl.toggle('issuer_ref-toggle'));
|
||||
await fillIn(GENERAL.selectByAttr('issuer_ref'), 'issuer-1');
|
||||
|
||||
await click(GENERAL.button('Key parameters'));
|
||||
assert.dom(GENERAL.inputByAttr('keyType')).hasValue('rsa');
|
||||
assert.dom(GENERAL.inputByAttr('key_type')).hasValue('rsa');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('keyBits'))
|
||||
.dom(GENERAL.inputByAttr('key_bits'))
|
||||
.hasValue('3072', 'dropdown has model value, not default value (2048)');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('signatureBits'))
|
||||
.dom(GENERAL.inputByAttr('signature_bits'))
|
||||
.hasValue('512', 'dropdown has model value, not default value (0)');
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('keyType'), 'ec');
|
||||
await fillIn(GENERAL.inputByAttr('keyBits'), '224');
|
||||
await fillIn(GENERAL.inputByAttr('key_type'), 'ec');
|
||||
await fillIn(GENERAL.inputByAttr('key_bits'), '224');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('keyBits'))
|
||||
.dom(GENERAL.inputByAttr('key_bits'))
|
||||
.hasValue('224', 'dropdown has selected value, not default value (256)');
|
||||
await fillIn(GENERAL.inputByAttr('signatureBits'), '384');
|
||||
await fillIn(GENERAL.inputByAttr('signature_bits'), '384');
|
||||
assert
|
||||
.dom(GENERAL.inputByAttr('signatureBits'))
|
||||
.dom(GENERAL.inputByAttr('signature_bits'))
|
||||
.hasValue('384', 'dropdown has selected value, not default value (0)');
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
assert.strictEqual(this.role.issuerRef, 'issuer-1', 'Issuer Ref correctly saved on create');
|
||||
|
||||
const payload = {
|
||||
...this.formDefaults,
|
||||
issuer_ref: 'issuer-1',
|
||||
key_bits: '224',
|
||||
key_type: 'ec',
|
||||
signature_bits: '384',
|
||||
};
|
||||
assert.true(
|
||||
this.writeStub.calledWith('test-role', this.backend, payload),
|
||||
'Correct endpoint is called to save role'
|
||||
);
|
||||
assert.true(this.onSave.calledWith('test-role'), 'onSave called with role name after successful save');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,70 +5,75 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, fillIn } from '@ember/test-helpers';
|
||||
import { render, fillIn, click } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import Sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { PKI_ROLE_GENERATE } from 'vault/tests/helpers/pki/pki-selectors';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import PkiCertificateForm from 'vault/forms/secrets/pki/certificate';
|
||||
|
||||
module('Integration | Component | pki-role-generate', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.secretMountPath.currentPath = 'pki-test';
|
||||
this.model = this.store.createRecord('pki/certificate/generate', {
|
||||
role: 'my-role',
|
||||
});
|
||||
this.role = 'my-role';
|
||||
this.backend = 'pki-test';
|
||||
this.owner.lookup('service:secret-mount-path').update(this.backend);
|
||||
this.form = new PkiCertificateForm('PkiIssueWithRoleRequest', {}, { isNew: true });
|
||||
this.mode = 'generate';
|
||||
this.onSuccess = Sinon.spy();
|
||||
const { secrets } = this.owner.lookup('service:api');
|
||||
const response = { serial_number: 'abcd-efgh-ijkl', certificate: '---CERT---' };
|
||||
this.issueStub = Sinon.stub(secrets, 'pkiIssueWithRole').resolves(response);
|
||||
this.signStub = Sinon.stub(secrets, 'pkiSignWithRole').resolves(response);
|
||||
|
||||
this.renderComponent = () =>
|
||||
render(
|
||||
hbs`<PkiRoleGenerate @form={{this.form}} @role={{this.role}} @mode={{this.mode}} @onSuccess={{this.onSuccess}} />`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
});
|
||||
|
||||
test('it should render the component with the form by default', async function (assert) {
|
||||
assert.expect(4);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiRoleGenerate
|
||||
@model={{this.model}}
|
||||
@onSuccess={{this.onSuccess}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(PKI_ROLE_GENERATE.form).exists('shows the cert generate form');
|
||||
assert.dom(GENERAL.inputByAttr('commonName')).exists('shows the common name field');
|
||||
assert.dom(GENERAL.inputByAttr('common_name')).exists('shows the common name field');
|
||||
assert.dom(GENERAL.button('Subject Alternative Name (SAN) Options')).exists('toggle exists');
|
||||
await fillIn(GENERAL.inputByAttr('commonName'), 'example.com');
|
||||
assert.strictEqual(this.model.commonName, 'example.com', 'Filling in the form updates the model');
|
||||
await fillIn(GENERAL.inputByAttr('common_name'), 'example.com');
|
||||
assert.strictEqual(this.form.data.common_name, 'example.com', 'Filling in the form updates the model');
|
||||
});
|
||||
|
||||
test('it should render the component displaying the cert', async function (assert) {
|
||||
assert.expect(5);
|
||||
const record = this.store.createRecord('pki/certificate/generate', {
|
||||
role: 'my-role',
|
||||
serialNumber: 'abcd-efgh-ijkl',
|
||||
certificate: 'my-very-cool-certificate',
|
||||
});
|
||||
this.set('model', record);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
<PkiRoleGenerate
|
||||
@model={{this.model}}
|
||||
@onSuccess={{this.onSuccess}}
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
test('it should render correctly for each mode', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.mode = 'generate';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.submitButton).hasText('Generate', 'shows correct text for submit button');
|
||||
await click(GENERAL.submitButton);
|
||||
assert.true(
|
||||
this.issueStub.calledWith(this.role, this.backend),
|
||||
'makes request to issue endpoint in generate mode'
|
||||
);
|
||||
|
||||
this.mode = 'sign';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.submitButton).hasText('Sign', 'shows correct text for submit button');
|
||||
await click(GENERAL.submitButton);
|
||||
assert.true(
|
||||
this.signStub.calledWith(this.role, this.backend),
|
||||
'makes request to sign endpoint in sign mode'
|
||||
);
|
||||
});
|
||||
|
||||
test('it should generate cert and display details', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.submitButton);
|
||||
assert.dom(PKI_ROLE_GENERATE.form).doesNotExist('Does not show the form');
|
||||
assert.dom(PKI_ROLE_GENERATE.downloadButton).exists('shows the download button');
|
||||
assert.dom(PKI_ROLE_GENERATE.revokeButton).exists('shows the revoke button');
|
||||
|
|
|
|||
Loading…
Reference in a new issue