diff --git a/ui/app/forms/secrets/pki/certificate.ts b/ui/app/forms/secrets/pki/certificate.ts new file mode 100644 index 0000000000..48847e2158 --- /dev/null +++ b/ui/app/forms/secrets/pki/certificate.ts @@ -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 { + constructor(...args: ConstructorParameters) { + 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 + // 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), + ]; + } +} diff --git a/ui/app/forms/secrets/pki/role.ts b/ui/app/forms/secrets/pki/role.ts new file mode 100644 index 0000000000..ef6548d110 --- /dev/null +++ b/ui/app/forms/secrets/pki/role.ts @@ -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 { + constructor(...args: ConstructorParameters) { + 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 + 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; + } +} diff --git a/ui/lib/core/addon/components/toolbar.hbs b/ui/lib/core/addon/components/toolbar.hbs index 144da87e18..b3ddce692d 100644 --- a/ui/lib/core/addon/components/toolbar.hbs +++ b/ui/lib/core/addon/components/toolbar.hbs @@ -4,7 +4,7 @@ }} \ No newline at end of file diff --git a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs index 1c17503a8c..c47a040f44 100644 --- a/ui/lib/pki/addon/components/page/pki-certificate-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-certificate-details.hbs @@ -13,7 +13,7 @@ {{on "click" this.downloadCert}} data-test-pki-cert-download-button /> - {{#if @model.canRevoke}} + {{#if @canRevoke}} -{{#if @model.privateKey}} +{{#if @certData.private_key}}
Next steps @@ -39,27 +39,29 @@
{{/if}} -{{#each @model.formFields as |field|}} - {{#if field.options.isCertificate}} - - - - {{else if (eq field.name "serialNumber")}} - - {{@model.serialNumber}} - - {{else}} - - {{/if}} +{{#each this.displayFields as |field|}} + {{#let (or (get @certData field) (get this.parsedCertificate field)) as |value|}} + {{#if value}} + {{#if (this.isCertificate field)}} + + + + {{else if (eq field "serial_number")}} + + {{value}} + + {{else}} + + {{/if}} + {{/if}} + {{/let}} {{/each}} - + {{#if @onBack}}
diff --git a/ui/lib/pki/addon/components/page/pki-certificate-details.ts b/ui/lib/pki/addon/components/page/pki-certificate-details.ts index b78cf2f7a6..87f235ec5d 100644 --- a/ui/lib/pki/addon/components/page/pki-certificate-details.ts +++ b/ui/lib/pki/addon/components/page/pki-certificate-details.ts @@ -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 { @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.') - ); - } - } + }) + ); } diff --git a/ui/lib/pki/addon/components/page/pki-role-details.hbs b/ui/lib/pki/addon/components/page/pki-role-details.hbs index 6ff03a75a8..abc22b9e16 100644 --- a/ui/lib/pki/addon/components/page/pki-role-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-role-details.hbs @@ -5,7 +5,7 @@ - {{#if @role.canDelete}} + {{#if @capabilities.canDelete}}
{{/if}} - {{#if @role.canGenerateCert}} + {{#if @capabilities.canGenerateCert}} Generate Certificate {{/if}} - {{#if @role.canSign}} + {{#if @capabilities.canSign}} Sign Certificate {{/if}} - {{#if @role.canEdit}} + {{#if @capabilities.canEdit}} Edit @@ -51,52 +51,46 @@ {{/if}}
-{{#each @role.formFieldGroups as |fg|}} + +{{#each this.displayGroups as |fg|}} {{#each-in fg as |group fields|}} {{#if (not-eq group "default")}}

{{group}}

{{/if}} - {{#each fields as |attr|}} - {{#let (get @role attr.name) as |val|}} - {{#if (eq attr.name "issuerRef")}} - - {{val}} - - {{else if (includes attr.name this.arrayAttrs)}} - - {{else if (includes attr.name (array "noStore" "noStoreMetadata"))}} - - {{else if (eq attr.name "customTtl")}} - {{! Show either notAfter or ttl }} - - {{else}} - - {{/if}} + + {{#each fields as |field|}} + {{#let (get @role field) as |val|}} + {{#let (this.label field) as |label|}} + {{#if (eq field "issuer_ref")}} + + + {{val}} + + + {{else if (this.isArrayField field)}} + + {{else if (includes field (array "no_store" "no_store_metadata"))}} + + {{else if (eq field "custom_ttl")}} + {{! Show either notAfter or ttl }} + + {{else}} + + {{/if}} + {{/let}} {{/let}} {{/each}} {{/each-in}} diff --git a/ui/lib/pki/addon/components/page/pki-role-details.ts b/ui/lib/pki/addon/components/page/pki-role-details.ts index 0298710b64..4bd203aaba 100644 --- a/ui/lib/pki/addon/components/page/pki-role-details.ts +++ b/ui/lib/pki/addon/components/page/pki-role-details.ts @@ -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 { @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); } } } diff --git a/ui/lib/pki/addon/components/pki-generate-root.hbs b/ui/lib/pki/addon/components/pki-generate-root.hbs index 22c02d344c..c09cbd348f 100644 --- a/ui/lib/pki/addon/components/pki-generate-root.hbs +++ b/ui/lib/pki/addon/components/pki-generate-root.hbs @@ -74,7 +74,7 @@ {{#if (eq fieldName "customTtl")}} {{! custom_ttl field has editType yield, which will render this }} - + {{/if}} {{/let}} diff --git a/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs b/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs index 24dc9a6f91..2f12820238 100644 --- a/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs +++ b/ui/lib/pki/addon/components/pki-generate-toggle-groups.hbs @@ -34,7 +34,7 @@ {{/if}}

{{#if this.keyParamFields}} - + {{/if}} {{else}}

diff --git a/ui/lib/pki/addon/components/pki-key-form.hbs b/ui/lib/pki/addon/components/pki-key-form.hbs index 93cddb0d12..67c0e4502d 100644 --- a/ui/lib/pki/addon/components/pki-key-form.hbs +++ b/ui/lib/pki/addon/components/pki-key-form.hbs @@ -20,7 +20,7 @@ {{#each @form.formFieldGroups as |fieldGroup|}} {{#each-in fieldGroup as |group fields|}} {{#if (eq group "Key parameters")}} - + {{else}} {{#each fields as |field|}}

{{/unless}} - {{else}} + {{else if (this.showField field.name)}} - + {{/if}} {{/each}} {{else}} - {{#let (camelize (concat "show" group)) as |prop|}} - - {{#if (get @role prop)}} -
- {{#let (get @role.fieldGroupsInfo group) as |toggleGroup|}} - {{! HEADER }} - {{#if toggleGroup.header}} -
- -
- {{/if}} - {{! FIELDS }} - {{#if (eq group "Key usage")}} - - {{else if (eq group "Key parameters")}} - - {{else}} - {{#each fields as |attr|}} - - {{yield attr}} - - {{/each}} - {{/if}} - {{! FOOTER }} - {{#if toggleGroup.footer}} -

- - {{toggleGroup.footer.text}} - {{#if toggleGroup.footer.docLink}} - - {{toggleGroup.footer.docText}} - - {{/if}} -

- {{/if}} - {{/let}} -
- {{/if}} - {{/let}} + + {{#if (includes group this.openGroups)}} +
+ {{#let (get @form.fieldGroupsInfo group) as |toggleGroup|}} + {{! HEADER }} + {{#if toggleGroup.header}} +
+ +
+ {{/if}} + {{! FIELDS }} + {{#if (eq group "Key usage")}} + + {{else if (eq group "Key parameters")}} + + {{else}} + {{#each fields as |field|}} + + {{yield field}} + + {{/each}} + {{/if}} + {{! FOOTER }} + {{#if toggleGroup.footer}} +

+ + {{toggleGroup.footer.text}} + {{#if toggleGroup.footer.docLink}} + + {{toggleGroup.footer.docText}} + + {{/if}} +

+ {{/if}} + {{/let}} +
+ {{/if}} {{/if}} {{/each-in}} {{/each}} - * ``` * @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 { - @service declare readonly store: Store; +export default class PkiRoleFormComponent extends Component { + @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]; } } } diff --git a/ui/lib/pki/addon/components/pki-role-generate.hbs b/ui/lib/pki/addon/components/pki-role-generate.hbs index aaf8a7cf43..772a99d6ec 100644 --- a/ui/lib/pki/addon/components/pki-role-generate.hbs +++ b/ui/lib/pki/addon/components/pki-role-generate.hbs @@ -3,31 +3,35 @@ SPDX-License-Identifier: BUSL-1.1 }} -{{#if @model.serialNumber}} - +{{#if this.certData}} + {{else}}
- {{#let (get @model.formFieldGroups "0") as |defaultGroup|}} - {{#each defaultGroup.default as |attr|}} - - + + {{#let (get @form.formFieldGroups "0") as |defaultGroup|}} + {{#each defaultGroup.default as |field|}} + + {{/each}} {{/let}} +
+
+ { @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'); } } diff --git a/ui/lib/pki/addon/components/pki-sign-intermediate-form.hbs b/ui/lib/pki/addon/components/pki-sign-intermediate-form.hbs index 56e60e8e56..faf894f5b3 100644 --- a/ui/lib/pki/addon/components/pki-sign-intermediate-form.hbs +++ b/ui/lib/pki/addon/components/pki-sign-intermediate-form.hbs @@ -49,7 +49,7 @@ @showHelpText={{false}} > {{! attr customTtl has editType yield and will show this component }} - + {{/let}} {{/each}} diff --git a/ui/lib/pki/addon/routes/certificates/certificate/details.js b/ui/lib/pki/addon/routes/certificates/certificate/details.js index cdd1d243e1..9408cab9ef 100644 --- a/ui/lib/pki/addon/routes/certificates/certificate/details.js +++ b/ui/lib/pki/addon/routes/certificates/certificate/details.js @@ -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 }, ]; } } diff --git a/ui/lib/pki/addon/routes/certificates/index.js b/ui/lib/pki/addon/routes/certificates/index.js index 8605f257cd..e4f3cf4963 100644 --- a/ui/lib/pki/addon/routes/certificates/index.js +++ b/ui/lib/pki/addon/routes/certificates/index.js @@ -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) { diff --git a/ui/lib/pki/addon/routes/issuers/import.js b/ui/lib/pki/addon/routes/issuers/import.js index 5b21ebd58f..89a18deb46 100644 --- a/ui/lib/pki/addon/routes/issuers/import.js +++ b/ui/lib/pki/addon/routes/issuers/import.js @@ -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 = [ diff --git a/ui/lib/pki/addon/routes/roles/create.js b/ui/lib/pki/addon/routes/roles/create.js index 3964c85237..61a2938175 100644 --- a/ui/lib/pki/addon/routes/roles/create.js +++ b/ui/lib/pki/addon/routes/roles/create.js @@ -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'); - } } diff --git a/ui/lib/pki/addon/routes/roles/index.js b/ui/lib/pki/addon/routes/roles/index.js index ebf2bd9109..b1da121667 100644 --- a/ui/lib/pki/addon/routes/roles/index.js +++ b/ui/lib/pki/addon/routes/roles/index.js @@ -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) { diff --git a/ui/lib/pki/addon/routes/roles/role/details.js b/ui/lib/pki/addon/routes/roles/role/details.js index 08b66fb2e0..4aa19b28f9 100644 --- a/ui/lib/pki/addon/routes/roles/role/details.js +++ b/ui/lib/pki/addon/routes/roles/role/details.js @@ -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 }, ]; } } diff --git a/ui/lib/pki/addon/routes/roles/role/edit.js b/ui/lib/pki/addon/routes/roles/role/edit.js index ea005f5d0a..504f6db489 100644 --- a/ui/lib/pki/addon/routes/roles/role/edit.js +++ b/ui/lib/pki/addon/routes/roles/role/edit.js @@ -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' }, ]; } diff --git a/ui/lib/pki/addon/routes/roles/role/generate.js b/ui/lib/pki/addon/routes/roles/role/generate.js index cb0dbc9344..6e15052a13 100644 --- a/ui/lib/pki/addon/routes/roles/role/generate.js +++ b/ui/lib/pki/addon/routes/roles/role/generate.js @@ -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) { diff --git a/ui/lib/pki/addon/routes/roles/role/sign.js b/ui/lib/pki/addon/routes/roles/role/sign.js index 30266ae258..91a494db1e 100644 --- a/ui/lib/pki/addon/routes/roles/role/sign.js +++ b/ui/lib/pki/addon/routes/roles/role/sign.js @@ -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) { diff --git a/ui/lib/pki/addon/templates/certificates/certificate/details.hbs b/ui/lib/pki/addon/templates/certificates/certificate/details.hbs index 22a7e46450..230bef5c44 100644 --- a/ui/lib/pki/addon/templates/certificates/certificate/details.hbs +++ b/ui/lib/pki/addon/templates/certificates/certificate/details.hbs @@ -14,4 +14,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/certificates/index.hbs b/ui/lib/pki/addon/templates/certificates/index.hbs index 2e80cbd95c..ff0ee6bfa0 100644 --- a/ui/lib/pki/addon/templates/certificates/index.hbs +++ b/ui/lib/pki/addon/templates/certificates/index.hbs @@ -12,10 +12,10 @@ @hasConfig={{this.model.hasConfig}} > <:list as |certs|> - {{#each certs as |pkiCertificate|}} + {{#each certs as |cert|}}
@@ -23,7 +23,7 @@
- {{pkiCertificate.id}} + {{cert}}
@@ -38,10 +38,7 @@ @hasChevron={{false}} data-test-popup-menu-trigger /> - Details + Details
diff --git a/ui/lib/pki/addon/templates/issuers/import.hbs b/ui/lib/pki/addon/templates/issuers/import.hbs index 88f2d74c0a..b4a56ccbda 100644 --- a/ui/lib/pki/addon/templates/issuers/import.hbs +++ b/ui/lib/pki/addon/templates/issuers/import.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: BUSL-1.1 }} - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/create.hbs b/ui/lib/pki/addon/templates/roles/create.hbs index 41370d2e2c..58ade326b3 100644 --- a/ui/lib/pki/addon/templates/roles/create.hbs +++ b/ui/lib/pki/addon/templates/roles/create.hbs @@ -15,9 +15,8 @@ \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/index.hbs b/ui/lib/pki/addon/templates/roles/index.hbs index d5f85784be..499b25413f 100644 --- a/ui/lib/pki/addon/templates/roles/index.hbs +++ b/ui/lib/pki/addon/templates/roles/index.hbs @@ -19,10 +19,10 @@ {{/if}} <:list as |roles|> - {{#each roles as |pkiRole|}} + {{#each roles as |role|}}
@@ -30,7 +30,7 @@
- {{pkiRole.id}} + {{role}}
@@ -43,14 +43,12 @@ @hasChevron={{false}} data-test-popup-menu-trigger /> - Details - Edit + + Details + + + Edit + diff --git a/ui/lib/pki/addon/templates/roles/role/details.hbs b/ui/lib/pki/addon/templates/roles/role/details.hbs index 45ca95502d..e8952ebcfd 100644 --- a/ui/lib/pki/addon/templates/roles/role/details.hbs +++ b/ui/lib/pki/addon/templates/roles/role/details.hbs @@ -11,8 +11,8 @@

PKI Role - {{this.model.name}} + {{this.model.role.name}}

- \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/role/edit.hbs b/ui/lib/pki/addon/templates/roles/role/edit.hbs index 2693a509a3..a20521efbb 100644 --- a/ui/lib/pki/addon/templates/roles/role/edit.hbs +++ b/ui/lib/pki/addon/templates/roles/role/edit.hbs @@ -13,9 +13,10 @@ + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/role/generate.hbs b/ui/lib/pki/addon/templates/roles/role/generate.hbs index da6784da85..c5b980b414 100644 --- a/ui/lib/pki/addon/templates/roles/role/generate.hbs +++ b/ui/lib/pki/addon/templates/roles/role/generate.hbs @@ -15,4 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/role/sign.hbs b/ui/lib/pki/addon/templates/roles/role/sign.hbs index c4712fe35a..0f82c6f9b6 100644 --- a/ui/lib/pki/addon/templates/roles/role/sign.hbs +++ b/ui/lib/pki/addon/templates/roles/role/sign.hbs @@ -15,4 +15,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/tests/acceptance/pki/pki-cross-sign-test.js b/ui/tests/acceptance/pki/pki-cross-sign-test.js index 2fad561ad6..65334f57ac 100644 --- a/ui/tests/acceptance/pki/pki-cross-sign-test.js +++ b/ui/tests/acceptance/pki/pki-cross-sign-test.js @@ -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')); diff --git a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js b/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js deleted file mode 100644 index 2b46983b80..0000000000 --- a/ui/tests/acceptance/pki/pki-engine-route-cleanup-test.js +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index a3cfa5f902..6e1554df74 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -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( diff --git a/ui/tests/helpers/pki/pki-selectors.ts b/ui/tests/helpers/pki/pki-selectors.ts index 07d658a742..8e9056c691 100644 --- a/ui/tests/helpers/pki/pki-selectors.ts +++ b/ui/tests/helpers/pki/pki-selectors.ts @@ -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]', diff --git a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js index 1b764cc4c4..c174ab767d 100644 --- a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js @@ -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` + + `, + { 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``, { 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``, { 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``, { - 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``, { - 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'); }); }); diff --git a/ui/tests/integration/components/pki/page/pki-role-details-test.js b/ui/tests/integration/components/pki/page/pki-role-details-test.js index f549d6556d..f25711b870 100644 --- a/ui/tests/integration/components/pki/page/pki-role-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-role-details-test.js @@ -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` + + `, + { owner: this.engine } + ); }); test('it should render the page component', async function (assert) { - await render( - hbs` - - `, - { 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` - - `, - { 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` - - `, - { 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' + ); + }); }); diff --git a/ui/tests/integration/components/pki/pki-key-form-test.js b/ui/tests/integration/components/pki/pki-key-form-test.js index fc6c6d9e4a..5c62247c72 100644 --- a/ui/tests/integration/components/pki/pki-key-form-test.js +++ b/ui/tests/integration/components/pki/pki-key-form-test.js @@ -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' ); diff --git a/ui/tests/integration/components/pki/pki-key-parameters-test.js b/ui/tests/integration/components/pki/pki-key-parameters-test.js index c70fb41c5a..93ffff9247 100644 --- a/ui/tests/integration/components/pki/pki-key-parameters-test.js +++ b/ui/tests/integration/components/pki/pki-key-parameters-test.js @@ -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``, + { 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` -
- -
- `, - { 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` -
- -
- `, - { 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.' ); diff --git a/ui/tests/integration/components/pki/pki-key-usage-test.js b/ui/tests/integration/components/pki/pki-key-usage-test.js index 44e66da8e6..e03d7afa26 100644 --- a/ui/tests/integration/components/pki/pki-key-usage-test.js +++ b/ui/tests/integration/components/pki/pki-key-usage-test.js @@ -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``, { owner: this.engine }); }); test('it should render the component', async function (assert) { - await render( - hbs` -
- -
- `, - { 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` -
- -
- `, - { 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'); }); }); diff --git a/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js index bed14fb7fe..dbc846d866 100644 --- a/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js +++ b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js @@ -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``, { owner: this.engine }); }); test('it should render the component with ttl selected by default', async function (assert) { assert.expect(3); - await render( - hbs` -
- -
- `, - { 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` -
- -
- `, - { 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` -
- -
- `, - { 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` -
- -
- `, - { 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'); diff --git a/ui/tests/integration/components/pki/pki-role-form-test.js b/ui/tests/integration/components/pki/pki-role-form-test.js index 76d3376078..b67f651542 100644 --- a/ui/tests/integration/components/pki/pki-role-form-test.js +++ b/ui/tests/integration/components/pki/pki-role-form-test.js @@ -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``, + { owner: this.engine } + ); + }; }); test('it should render default fields and toggle groups', async function (assert) { - await render( - hbs` - - `, - { 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` - - `, - { 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` - - `, - { 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` - - `, - { 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` - - `, - { 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'); }); }); diff --git a/ui/tests/integration/components/pki/pki-role-generate-test.js b/ui/tests/integration/components/pki/pki-role-generate-test.js index 77221f097d..2e7108b5cd 100644 --- a/ui/tests/integration/components/pki/pki-role-generate-test.js +++ b/ui/tests/integration/components/pki/pki-role-generate-test.js @@ -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``, + { owner: this.engine } + ); }); test('it should render the component with the form by default', async function (assert) { assert.expect(4); - await render( - hbs` -
- -
- `, - { 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` -
- -
- `, - { 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');