UI: standardize display for type=exported (#19672)

This commit is contained in:
Chelsea Shaw 2023-03-23 10:49:24 -05:00 committed by GitHub
parent db31cf2da2
commit 55d18515c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1067 additions and 339 deletions

View File

@ -24,6 +24,13 @@ const validations = {
],
};
/**
* This model maps to multiple PKI endpoints, specifically the ones that make up the
* configuration/create workflow. These endpoints also share a nontypical behavior in that
* a POST request to the endpoints don't necessarily result in a single entity created --
* depending on the inputs, some number of issuers, keys, and certificates can be created
* from the API.
*/
@withModelValidations(validations)
@withFormFields()
export default class PkiActionModel extends Model {
@ -37,6 +44,7 @@ export default class PkiActionModel extends Model {
@attr importedIssuers;
@attr importedKeys;
@attr mapping;
@attr('string', { readOnly: true, masked: true }) certificate;
/* actionType generate-root */
@attr('string', {
@ -176,12 +184,12 @@ export default class PkiActionModel extends Model {
@attr('string') ttl;
@attr('date') notAfter;
@attr('string', { readOnly: true }) issuerId; // returned from generate-root action
@attr('string', { label: 'Issuer ID', readOnly: true, detailLinkTo: 'issuers.issuer.details' }) issuerId; // returned from generate-root action
// For generating and signing a CSR
@attr('string', { label: 'CSR', masked: true }) csr;
@attr caChain;
@attr('string', { label: 'Key ID' }) keyId;
@attr('string', { label: 'Key ID', detailLinkTo: 'keys.key.details' }) keyId;
@attr('string', { masked: true }) privateKey;
@attr('string') privateKeyType;

View File

@ -0,0 +1,84 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-engine-page-title>
{{this.title}}
</h1>
</p.levelLeft>
</PageHeader>
{{#unless @config.id}}
<div class="box is-bottomless is-fullwidth is-marginless">
<div class="columns">
{{#each this.configTypes as |option|}}
<div class="column is-flex">
<label for={{option.key}} class="box-label is-column {{if (eq @config.actionType option.key) 'is-selected'}}">
<div>
<h3 class="box-label-header title is-6">
<Icon @size="24" @name={{option.icon}} />
{{option.label}}
</h3>
<p class="help has-text-grey-dark">
{{option.description}}
</p>
</div>
<div>
<RadioButton
id={{option.key}}
name="pki-config-type"
@value={{option.key}}
@groupValue={{@config.actionType}}
@onChange={{fn (mut @config.actionType) option.key}}
data-test-pki-config-option={{option.key}}
/>
<label for={{option.key}}></label>
</div>
</label>
</div>
{{/each}}
</div>
</div>
{{/unless}}
{{#if (eq @config.actionType "import")}}
<PkiImportPemBundle
@model={{@config}}
@onCancel={{@onCancel}}
@onSave={{fn (mut this.title) "View imported items"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canImportBundle}}
/>
{{else if (eq @config.actionType "generate-root")}}
<PkiGenerateRoot
@model={{@config}}
@urls={{@urls}}
@onCancel={{@onCancel}}
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canGenerateIssuerRoot}}
@onSave={{fn (mut this.title) "View root certificate"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
/>
{{else if (eq @config.actionType "generate-csr")}}
<PkiGenerateCsr
@model={{@config}}
@onCancel={{@onCancel}}
@onSave={{fn (mut this.title) "View generated CSR"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
/>
{{else}}
<EmptyState
@title="Choose an option"
@message="To see configuration options, choose your desired output above."
data-test-configuration-empty-state
/>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="button" class="button is-primary" disabled={{true}} data-test-pki-config-save>
Done
</button>
<button type="button" class="button has-left-margin-s" {{on "click" @onCancel}} data-test-pki-config-cancel>
Cancel
</button>
</div>
</div>
{{/if}}

View File

@ -5,28 +5,34 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
// TYPES
import Store from '@ember-data/store';
import Router from '@ember/routing/router';
import FlashMessageService from 'vault/services/flash-messages';
import PkiActionModel from 'vault/models/pki/action';
import { Breadcrumb } from 'vault/vault/app-types';
interface Args {
config: PkiActionModel;
onCancel: CallableFunction;
breadcrumbs: Breadcrumb;
}
/**
* @module PkiConfigureForm
* PkiConfigureForm component is used to configure a PKI engine mount.
* @module PkiConfigureCreate
* Page::PkiConfigureCreate component is used to configure a PKI engine mount.
* The component shows three options for configuration and which form
* is shown. The sub-forms rendered handle rendering the form itself
* and form submission and cancel actions.
*/
export default class PkiConfigureForm extends Component<Args> {
export default class PkiConfigureCreate extends Component<Args> {
@service declare readonly store: Store;
@service declare readonly router: Router;
@service declare readonly flashMessages: FlashMessageService;
@tracked title = 'Configure PKI';
get configTypes() {
return [
{

View File

@ -0,0 +1,17 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-page-title>
{{this.title}}
</h1>
</p.levelLeft>
</PageHeader>
<PkiGenerateCsr
@model={{@model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onSave={{fn (mut this.title) "View generated CSR"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
/>

View File

@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class PagePkiIssuerGenerateIntermediateComponent extends Component {
@tracked title = 'Generate intermediate CSR';
}

View File

@ -0,0 +1,18 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-page-title>
{{this.title}}
</h1>
</p.levelLeft>
</PageHeader>
<PkiGenerateRoot
@model={{@model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onSave={{fn (mut this.title) "View generated root"}}
@adapterOptions={{hash actionType="generate-root" useIssuer=@model.canGenerateIssuerRoot}}
/>

View File

@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class PagePkiIssuerGenerateRootComponent extends Component {
@tracked title = 'Generate root';
}

View File

@ -0,0 +1,18 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-page-title>
{{this.title}}
</h1>
</p.levelLeft>
</PageHeader>
<PkiImportPemBundle
@model={{@model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onSave={{fn (mut this.title) "View imported items"}}
@adapterOptions={{hash actionType="import" useIssuer=true}}
/>

View File

@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class PagePkiIssuerImportComponent extends Component {
@tracked title = 'Import a CA';
}

View File

@ -1,69 +0,0 @@
<div class="box is-bottomless is-fullwidth is-marginless">
{{#unless @config.id}}
<div class="columns">
{{#each this.configTypes as |option|}}
<div class="column is-flex">
<label for={{option.key}} class="box-label is-column {{if (eq @config.actionType option.key) 'is-selected'}}">
<div>
<h3 class="box-label-header title is-6">
<Icon @size="24" @name={{option.icon}} />
{{option.label}}
</h3>
<p class="help has-text-grey-dark">
{{option.description}}
</p>
</div>
<div>
<RadioButton
id={{option.key}}
name="pki-config-type"
@value={{option.key}}
@groupValue={{@config.actionType}}
@onChange={{fn (mut @config.actionType) option.key}}
data-test-pki-config-option={{option.key}}
/>
<label for={{option.key}}></label>
</div>
</label>
</div>
{{/each}}
</div>
{{/unless}}
{{#if (eq @config.actionType "import")}}
<PkiImportPemBundle
@model={{@config}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canImportBundle}}
/>
{{else if (eq @config.actionType "generate-root")}}
<PkiGenerateRoot
@model={{@config}}
@urls={{@urls}}
@onCancel={{@onCancel}}
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canGenerateIssuerRoot}}
/>
{{else if (eq @config.actionType "generate-csr")}}
<PkiGenerateCsr
@model={{@config}}
@onCancel={{@onCancel}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
/>
{{else}}
<EmptyState
@title="Choose an option"
@message="To see configuration options, choose your desired output above."
data-test-configuration-empty-state
/>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="button" class="button is-primary" disabled={{true}} data-test-pki-config-save>
Done
</button>
<button type="button" class="button has-left-margin-s" {{on "click" @onCancel}} data-test-pki-config-cancel>
Cancel
</button>
</div>
</div>
{{/if}}
</div>

View File

@ -19,8 +19,28 @@ interface Args {
useIssuer: boolean;
onComplete: CallableFunction;
onCancel: CallableFunction;
onSave?: CallableFunction;
}
/**
* @module PkiGenerateCsrComponent
* PkiGenerateCsr shows only the fields valid for the generate CSR endpoint.
* This component renders the form, handles the model save and rollback actions,
* and shows the resulting data on success. onCancel is required for the cancel
* transition, and if onSave is provided it will call that after save for any
* side effects in the parent.
*
* @example
* ```js
* <PkiGenerateRoot @model={{this.model}} @onCancel={{transition-to "vault.cluster"}} @onSave={{fn (mut this.title) "Successful"}} @adapterOptions={{hash actionType="import" useIssuer=false}} />
* ```
*
* @param {Object} model - pki/action model.
* @callback onCancel - Callback triggered when cancel button is clicked, after model is unloaded
* @callback onSave - Optional - Callback triggered after model is saved, as a side effect. Results are shown on the same component
* @callback onComplete - Callback triggered when "Done" button clicked, on results view
* @param {Object} adapterOptions - object passed as adapterOptions on the model.save method
*/
export default class PkiGenerateCsrComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@ -64,12 +84,15 @@ export default class PkiGenerateCsrComponent extends Component<Args> {
*save(event: Event): Generator<Promise<boolean | PkiActionModel>> {
event.preventDefault();
try {
const { model } = this.args;
const { model, onSave } = this.args;
const { isValid, state, invalidFormMessage } = model.validate();
if (isValid) {
const useIssuer = yield this.getCapability();
yield model.save({ adapterOptions: { actionType: 'generate-csr', useIssuer } });
this.flashMessages.success('Successfully generated CSR.');
if (onSave) {
onSave();
}
} else {
this.modelValidations = state;
this.alert = invalidFormMessage;

View File

@ -1,69 +1,126 @@
<form {{on "submit" (perform this.save)}} data-test-pki-config-generate-root-form>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<h2 class="title is-size-5 has-border-bottom-light page-header" data-test-generate-root-title="Root parameters">
Root parameters
</h2>
{{#each this.defaultFields as |field|}}
{{#let (find-by "name" field @model.allFields) as |attr|}}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} data-test-field>
{{#if (eq field "customTtl")}}
{{! customTtl attr has editType yield, which will render this }}
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
{{/if}}
</FormField>
{{/let}}
{{/each}}
<PkiGenerateToggleGroups @model={{@model}} />
{{#if @urls}}
<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-urls-section>
<h2
class="title is-size-5 page-header {{if @urls.canCreate 'has-border-bottom-light' 'is-borderless'}}"
data-test-generate-root-title="Issuer URLs"
>
Issuer URLs
</h2>
{{#if @urls.canSet}}
{{#each @urls.allFields as |attr|}}
{{#if (not (eq attr.name "mountPath"))}}
<FormField
@attr={{attr}}
@mode="create"
@model={{@urls}}
@showHelpText={{attr.options.showHelpText}}
data-test-urls-field
/>
{{/if}}
{{/each}}
{{else}}
<EmptyState
@title="You do not have permissions to set URLs."
@message="These are not required but will need to be configured later. You can do this via the CLI or by changing your permissions and returning to this form."
/>
{{/if}}
</fieldset>
{{/if}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="submit" class="button is-primary" data-test-pki-generate-root-save>
Done
</button>
<button {{on "click" @onCancel}} type="button" class="button has-left-margin-s" data-test-pki-generate-root-cancel>
Cancel
</button>
{{! Show results if model has an ID, which is only generated after save }}
{{#if @model.id}}
<Toolbar />
{{#if @model.privateKey}}
<div class="has-top-margin-m">
<AlertBanner
@title="Next steps"
@type="warning"
@message="The private_key is only available once. Make sure you copy and save it now."
/>
</div>
{{#if this.invalidFormAlert}}
{{/if}}
<main class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.returnedFields as |field|}}
{{#let (find-by "name" field @model.allFields) as |attr|}}
{{#if attr.options.detailLinkTo}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
>
<LinkTo @route={{attr.options.detailLinkTo}} @model={{get @model attr.name}}>{{get @model attr.name}}</LinkTo>
</InfoTableRow>
{{else if attr.options.masked}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
>
<MaskedInput @value={{get @model attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
/>
{{/if}}
{{/let}}
{{/each}}
<InfoTableRow @label="Private key">
{{#if @model.privateKey}}
<MaskedInput @value={{@model.privateKey}} @displayOnly={{true}} @allowCopy={{true}} />
{{else}}
<span class="tag">internal</span>
{{/if}}
</InfoTableRow>
<InfoTableRow @label="Private key type" @value={{@model.privateKeyType}}>
<span class="{{unless @model.privateKeyType 'tag'}}">{{or @model.privateKeyType "internal"}}</span>
</InfoTableRow>
</main>
<footer>
<div class="field is-grouped is-fullwidth has-top-margin-l">
<div class="control">
<AlertInline
@type="danger"
@paddingTop={{true}}
@message={{this.invalidFormAlert}}
@mimicRefresh={{true}}
data-test-pki-generate-root-validation-error
/>
<button type="button" class="button is-primary" {{on "click" @onComplete}} data-test-done>
Done
</button>
</div>
</div>
</footer>
{{else}}
<form {{on "submit" (perform this.save)}} data-test-pki-config-generate-root-form>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<h2 class="title is-size-5 has-border-bottom-light page-header" data-test-generate-root-title="Root parameters">
Root parameters
</h2>
{{#each this.defaultFields as |field|}}
{{#let (find-by "name" field @model.allFields) as |attr|}}
<FormField @attr={{attr}} @model={{@model}} @modelValidations={{this.modelValidations}} data-test-field>
{{#if (eq field "customTtl")}}
{{! customTtl attr has editType yield, which will render this }}
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
{{/if}}
</FormField>
{{/let}}
{{/each}}
<PkiGenerateToggleGroups @model={{@model}} />
{{#if @urls}}
<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-urls-section>
<h2
class="title is-size-5 page-header {{if @urls.canCreate 'has-border-bottom-light' 'is-borderless'}}"
data-test-generate-root-title="Issuer URLs"
>
Issuer URLs
</h2>
{{#if @urls.canSet}}
{{#each @urls.allFields as |attr|}}
{{#if (not (eq attr.name "mountPath"))}}
<FormField
@attr={{attr}}
@mode="create"
@model={{@urls}}
@showHelpText={{attr.options.showHelpText}}
data-test-urls-field
/>
{{/if}}
{{/each}}
{{else}}
<EmptyState
@title="You do not have permissions to set URLs."
@message="These are not required but will need to be configured later. You can do this via the CLI or by changing your permissions and returning to this form."
/>
{{/if}}
</fieldset>
{{/if}}
</div>
</form>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="submit" class="button is-primary" data-test-pki-generate-root-save>
Done
</button>
<button {{on "click" @onCancel}} type="button" class="button has-left-margin-s" data-test-pki-generate-root-cancel>
Cancel
</button>
</div>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline
@type="danger"
@paddingTop={{true}}
@message={{this.invalidFormAlert}}
@mimicRefresh={{true}}
data-test-pki-generate-root-validation-error
/>
</div>
{{/if}}
</div>
</form>
{{/if}}

View File

@ -4,34 +4,53 @@
*/
import { action } from '@ember/object';
import RouterService from '@ember/routing/router-service';
import { service } from '@ember/service';
import { waitFor } from '@ember/test-waiters';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
import PkiActionModel from 'vault/models/pki/action';
import PkiUrlsModel from 'vault/models/pki/urls';
import FlashMessageService from 'vault/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
interface AdapterOptions {
actionType: string;
useIssuer: boolean | undefined;
}
interface Args {
model: PkiActionModel;
urls: PkiUrlsModel;
onCancel: CallableFunction;
onComplete: CallableFunction;
onSave?: CallableFunction;
adapterOptions: AdapterOptions;
}
/**
* @module PkiGenerateRoot
* PkiGenerateRoot shows only the fields valid for the generate root endpoint.
* This form handles the model save and rollback actions, and will call the passed
* onSave and onCancel args for transition (passed from parent).
* NOTE: this component is not TS because decorator-added parameters (eg validator and
* formFields) aren't recognized on the model.
* This component renders the form, handles the model save and rollback actions,
* and shows the resulting data on success. onCancel is required for the cancel
* transition, and if onSave is provided it will call that after save for any
* side effects in the parent.
*
* @example
* ```js
* <PkiGenerateRoot @model={{this.model}} @onCancel={{transition-to "vault.cluster"}} @onSave={{transition-to "vault.cluster.secrets"}} @adapterOptions={{hash actionType="import" useIssuer=false}} />
* <PkiGenerateRoot @model={{this.model}} @onCancel={{transition-to "vault.cluster"}} @onSave={{fn (mut this.title) "Successful"}} @adapterOptions={{hash actionType="import" useIssuer=false}} />
* ```
*
* @param {Object} model - pki/action model.
* @callback onCancel - Callback triggered when cancel button is clicked, after model is unloaded
* @callback onSave - Optional - Callback triggered after model is saved, as a side effect. Results are shown on the same component
* @callback onComplete - Callback triggered when "Done" button clicked, on results view
* @param {Object} adapterOptions - object passed as adapterOptions on the model.save method
*/
export default class PkiGenerateRootComponent extends Component {
@service flashMessages;
@service router;
@tracked showGroup = null;
export default class PkiGenerateRootComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly router: RouterService;
@tracked modelValidations = null;
@tracked errorBanner = '';
@tracked invalidFormAlert = '';
@ -49,6 +68,19 @@ export default class PkiGenerateRootComponent extends Component {
];
}
get returnedFields() {
return [
'certificate',
'expiration',
'issuerId',
'issuerName',
'issuingCa',
'keyId',
'keyName',
'serialNumber',
];
}
@action cancel() {
// Generate root form will always have a new model
this.args.model.unloadRecord();
@ -68,19 +100,17 @@ export default class PkiGenerateRootComponent extends Component {
@task
@waitFor
*save(event) {
*save(event: Event) {
event.preventDefault();
const continueSave = this.checkFormValidity();
if (!continueSave) return;
try {
yield this.setUrls();
const result = yield this.args.model.save({ adapterOptions: this.args.adapterOptions });
yield this.args.model.save({ adapterOptions: this.args.adapterOptions });
this.flashMessages.success('Successfully generated root.');
this.router.transitionTo(
'vault.cluster.secrets.backend.pki.issuers.issuer.details',
result.backend,
result.issuerId
);
if (this.args.onSave) {
this.args.onSave();
}
} catch (e) {
this.errorBanner = errorMessage(e);
this.invalidFormAlert = 'There was a problem generating the root.';

View File

@ -1,36 +1,87 @@
<div class="field">
<div class="form-section">
<label class="title has-padding-top is-5">
Certificate parameters
</label>
<form {{on "submit" (perform this.submitForm)}} data-test-pki-import-pem-bundle-form>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-l">
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" />
<p class="has-top-margin-m has-bottom-margin-l">
Issuer URLs (Issuing certificates, CRL distribution points, OCSP servers, and delta CRL URLs) can be specified by
editing the individual issuer once it is uploaded to Vault.
</p>
</div>
<div class="has-top-padding-s">
<button
type="submit"
class="button is-primary {{if this.submitForm.isRunning 'is-loading'}}"
disabled={{this.submitForm.isRunning}}
data-test-pki-import-pem-bundle
>
Import issuer
</button>
<button
type="button"
class="button has-left-margin-s"
disabled={{this.submitForm.isRunning}}
{{on "click" this.cancel}}
data-test-pki-ca-cert-cancel
>
Cancel
</button>
</div>
</form>
{{#if this.importedResponse}}
<Toolbar />
<div class="is-flex-start has-top-margin-xs">
<div class="is-flex-1 basis-0 has-text-grey has-bottom-margin-xs">
<h2>
Imported Issuer
</h2>
</div>
<div class="is-flex-1 basis-0 has-text-grey has-bottom-margin-xs">
<h2>
Imported Key
</h2>
</div>
</div>
</div>
<div class="box is-fullwidth is-sideless is-marginless is-paddingless" data-test-imported-bundle-mapping>
{{#each-in this.importedResponse as |issuer key|}}
<div class="box is-marginless no-top-shadow has-slim-padding">
<div class="is-flex-start">
<div class="is-flex-1 basis-0 has-bottom-margin-xs" data-test-imported-issuer>
{{#if issuer}}
<LinkTo @route="issuers.issuer.details" @model={{issuer}}>
{{issuer}}
</LinkTo>
{{else}}
None
{{/if}}
</div>
<div class="is-flex-1 basis-0 has-bottom-margin-xs" data-test-imported-key>
{{#if key}}
<LinkTo @route="keys.key.details" @model={{key}}>
{{key}}
</LinkTo>
{{else}}
None
{{/if}}
</div>
</div>
</div>
{{/each-in}}
</div>
<footer>
<div class="field is-grouped is-fullwidth has-top-margin-l">
<div class="control">
<button type="button" class="button is-primary" {{on "click" @onComplete}} data-test-done>
Done
</button>
</div>
</div>
</footer>
{{else}}
<div class="field">
<div class="form-section">
<label class="title has-padding-top is-5" data-test-import-section-label>
Certificate parameters
</label>
<form {{on "submit" (perform this.submitForm)}} data-test-pki-import-pem-bundle-form>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<div class="box is-sideless is-fullwidth is-marginless has-top-padding-l">
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" />
<p class="has-top-margin-m has-bottom-margin-l">
Issuer URLs (Issuing certificates, CRL distribution points, OCSP servers, and delta CRL URLs) can be specified by
editing the individual issuer once it is uploaded to Vault.
</p>
</div>
<div class="has-top-padding-s">
<button
type="submit"
class="button is-primary {{if this.submitForm.isRunning 'is-loading'}}"
disabled={{this.submitForm.isRunning}}
data-test-pki-import-pem-bundle
>
Import issuer
</button>
<button
type="button"
class="button has-left-margin-s"
disabled={{this.submitForm.isRunning}}
{{on "click" this.cancel}}
data-test-pki-ca-cert-cancel
>
Cancel
</button>
</div>
</form>
</div>
</div>
{{/if}}

View File

@ -33,8 +33,9 @@ interface AdapterOptions {
useIssuer: boolean | undefined;
}
interface Args {
onSave: CallableFunction;
onSave?: CallableFunction;
onCancel: CallableFunction;
onComplete: CallableFunction;
model: PkiActionModel;
adapterOptions: AdapterOptions;
}
@ -44,14 +45,28 @@ export default class PkiImportPemBundle extends Component<Args> {
@tracked errorBanner = '';
get importedResponse() {
// mapping only exists after success
// TODO VAULT-14791: handle issuer already exists, but key doesn't -- empty object returned here
return this.args.model.mapping;
}
@task
@waitFor
*submitForm(event: Event) {
event.preventDefault();
this.errorBanner = '';
if (!this.args.model.pemBundle) {
this.errorBanner = 'please upload your PEM bundle';
return;
}
try {
yield this.args.model.save({ adapterOptions: this.args.adapterOptions });
this.flashMessages.success('Successfully imported data.');
this.args.onSave();
// This component shows the results, but call `onSave` for any side effects on parent
if (this.args.onSave) {
this.args.onSave();
}
} catch (error) {
this.errorBanner = errorMessage(error);
}

View File

@ -1,15 +1,5 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-configuration-page-title>
Configure PKI
</h1>
</p.levelLeft>
</PageHeader>
<PkiConfigureForm
<Page::PkiConfigureCreate
@breadcrumbs={{this.breadcrumbs}}
@config={{this.model.config}}
@urls={{this.model.urls}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.overview"}}

View File

@ -1,16 +1 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3">
Generate intermediate CSR
</h1>
</p.levelLeft>
</PageHeader>
<PkiGenerateCsr
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onComplete={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
/>
<Page::PkiIssuerGenerateIntermediate @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />

View File

@ -1,16 +1 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-issuers-page-title>
Generate root
</h1>
</p.levelLeft>
</PageHeader>
<PkiGenerateRoot
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@adapterOptions={{hash actionType="generate-root" useIssuer=this.model.canGenerateIssuerRoot}}
/>
<Page::PkiIssuerGenerateRoot @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />

View File

@ -1,16 +1 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-issuer-page-title>
Import a CA
</h1>
</p.levelLeft>
</PageHeader>
<PkiImportPemBundle
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
@adapterOptions={{hash actionType="import" useIssuer=true}}
/>
<Page::PkiIssuerImport @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />

View File

@ -0,0 +1,274 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, skip, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, currentURL, fillIn, typeIn, visit } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { SELECTORS as S } from 'vault/tests/helpers/pki/workflow';
import { issuerPemBundle } from 'vault/tests/helpers/pki/values';
module('Acceptance | pki action forms test', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
await authPage.login();
// Setup PKI engine
const mountPath = `pki-workflow-${uuidv4()}`;
await enablePage.enable('pki', mountPath);
this.mountPath = mountPath;
await logout.visit();
});
hooks.afterEach(async function () {
await logout.visit();
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('import', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
this.pemBundle = issuerPemBundle;
});
test('happy path', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
await click(S.emptyStateLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`);
assert.dom(S.configuration.title).hasText('Configure PKI');
assert.dom(S.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default');
await click(S.configuration.optionByKey('import'));
assert.dom(S.configuration.emptyState).doesNotExist();
// Submit before filling out form shows an error
await click('[data-test-pki-import-pem-bundle]');
assert.dom('[data-test-alert-banner="alert"]').hasText('Error please upload your PEM bundle');
// Fill in form data
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
await click('[data-test-pki-import-pem-bundle]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/configuration/create`,
'stays on page on success'
);
assert.dom(S.configuration.title).hasText('View imported items');
assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save');
assert.dom(S.configuration.importMapping).exists('import mapping is shown after save');
await click('[data-test-done]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/overview`,
'redirects to overview when done'
);
});
skip('with many imports', async function (assert) {
// TODO VAULT-14791
this.server.post(`${this.mountPath}/config/ca`, () => {
return {
request_id: 'some-config-id',
data: {
imported_issuers: ['my-imported-issuer', 'imported2'],
imported_keys: ['my-imported-key', 'imported3'],
mapping: {
'my-imported-issuer': 'my-imported-key',
imported2: '',
},
},
};
});
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`);
await click(S.configuration.optionByKey('import'));
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
await click('[data-test-pki-import-pem-bundle]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/configuration/create`,
'stays on page on success'
);
assert.dom(S.configuration.title).hasText('View imported items');
assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save');
assert.dom(S.configuration.importMapping).exists('import mapping is shown after save');
assert.dom(S.configuration.importedIssuer).hasText('my-imported-issuer', 'Issuer value is displayed');
assert.dom(S.configuration.importedKey).hasText('my-imported-key', 'Key value is displayed');
await click('[data-test-done]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/overview`,
'redirects to overview when done'
);
});
skip('shows imported items when keys is empty', async function (assert) {
// TODO VAULT-14791
this.server.post(`${this.mountPath}/config/ca`, () => {
return {
request_id: 'some-config-id',
data: {
imported_issuers: ['my-imported-issuer', 'my-imported-issuer2'],
imported_keys: null,
mapping: {
'my-imported-issuer': '',
'my-imported-issuer2': '',
},
},
};
});
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`);
await click(S.configuration.optionByKey('import'));
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
await click('[data-test-pki-import-pem-bundle]');
assert.dom(S.configuration.importForm).doesNotExist('import form is hidden after save');
assert.dom(S.configuration.importMapping).exists('import mapping is shown after save');
assert.dom(S.configuration.importedIssuer).hasText('my-imported-issuer', 'Issuer value is displayed');
assert.dom(S.configuration.importedKey).hasText('my-imported-key', 'Key value is displayed');
});
});
module('generate root', function () {
test('happy path', async function (assert) {
const commonName = 'my-common-name';
const issuerName = 'my-first-issuer';
const keyName = 'my-first-key';
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
await click(S.emptyStateLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`);
assert.dom(S.configuration.title).hasText('Configure PKI');
assert.dom(S.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default');
await click(S.configuration.optionByKey('generate-root'));
assert.dom(S.configuration.emptyState).doesNotExist();
// The URLs section is populated based on params returned from OpenAPI. This test will break when
// the backend adds fields. We should update the count accordingly.
assert.dom(S.configuration.urlField).exists({ count: 4 });
// Fill in form
await fillIn(S.configuration.typeField, 'internal');
await typeIn(S.configuration.inputByName('commonName'), commonName);
await typeIn(S.configuration.inputByName('issuerName'), issuerName);
await click(S.configuration.keyParamsGroupToggle);
await typeIn(S.configuration.inputByName('keyName'), keyName);
await click(S.configuration.generateRootSave);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/configuration/create`,
'stays on page on success'
);
assert.dom(S.configuration.title).hasText('View root certificate');
assert.dom('[data-test-alert-banner="alert"]').doesNotExist('no private key warning');
assert.dom(S.configuration.title).hasText('View root certificate', 'Updates title on page');
assert.dom(S.configuration.saved.certificate).hasClass('allow-copy', 'copyable certificate is masked');
assert.dom(S.configuration.saved.issuerName).hasText(issuerName);
assert.dom(S.configuration.saved.issuerLink).exists('Issuer link exists');
assert.dom(S.configuration.saved.keyLink).exists('Key link exists');
assert.dom(S.configuration.saved.keyName).hasText(keyName);
assert.dom('[data-test-done]').exists('Done button exists');
// Check that linked issuer has correct common name
await click(S.configuration.saved.issuerLink);
assert.dom(S.issuerDetails.valueByName('Common name')).hasText(commonName);
});
test('type=exported', async function (assert) {
const commonName = 'my-exported-name';
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/configuration/create`);
await click(S.configuration.optionByKey('generate-root'));
// Fill in form
await fillIn(S.configuration.typeField, 'exported');
await typeIn(S.configuration.inputByName('commonName'), commonName);
await click(S.configuration.generateRootSave);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/configuration/create`,
'stays on page on success'
);
assert.dom(S.configuration.title).hasText('View root certificate');
assert
.dom('[data-test-alert-banner="alert"]')
.hasText('Next steps The private_key is only available once. Make sure you copy and save it now.');
assert.dom(S.configuration.title).hasText('View root certificate', 'Updates title on page');
assert
.dom(S.configuration.saved.certificate)
.hasClass('allow-copy', 'copyable masked certificate exists');
assert
.dom(S.configuration.saved.issuerName)
.doesNotExist('Issuer name not shown because it was not named');
assert.dom(S.configuration.saved.issuerLink).exists('Issuer link exists');
assert.dom(S.configuration.saved.keyLink).exists('Key link exists');
assert
.dom(S.configuration.saved.privateKey)
.hasClass('allow-copy', 'copyable masked private key exists');
assert.dom(S.configuration.saved.keyName).doesNotExist('Key name not shown because it was not named');
assert.dom('[data-test-done]').exists('Done button exists');
// Check that linked issuer has correct common name
await click(S.configuration.saved.issuerLink);
assert.dom(S.issuerDetails.valueByName('Common name')).hasText(commonName);
});
});
module('generate CSR', function () {
test('happy path', async function (assert) {
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(S.emptyStateLink);
assert.dom(S.configuration.title).hasText('Configure PKI');
await click(S.configuration.optionByKey('generate-csr'));
await fillIn(S.configuration.typeField, 'internal');
await fillIn(S.configuration.inputByName('commonName'), 'my-common-name');
await click('[data-test-save]');
assert.dom(S.configuration.title).hasText('View generated CSR');
await assert.dom(S.configuration.csrDetails).exists('renders CSR details after save');
await click('[data-test-done]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/overview`,
'Transitions to overview after viewing csr details'
);
});
test('type = exported', async function (assert) {
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(S.emptyStateLink);
await click(S.configuration.optionByKey('generate-csr'));
await fillIn(S.configuration.typeField, 'exported');
await fillIn(S.configuration.inputByName('commonName'), 'my-common-name');
await click('[data-test-save]');
await assert.dom(S.configuration.csrDetails).exists('renders CSR details after save');
assert.dom(S.configuration.title).hasText('View generated CSR');
assert
.dom('[data-test-alert-banner="alert"]')
.hasText(
'Next steps Copy the CSR below for a parent issuer to sign and then import the signed certificate back into this mount. The private_key is only available once. Make sure you copy and save it now.'
);
assert
.dom(S.configuration.saved.privateKey)
.hasClass('allow-copy', 'copyable masked private key exists');
await click('[data-test-done]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/overview`,
'Transitions to overview after viewing csr details'
);
});
});
});

View File

@ -12,7 +12,7 @@ import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { click, currentURL, fillIn, visit } from '@ember/test-helpers';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { SELECTORS } from '../helpers/pki/workflow';
import { SELECTORS } from 'vault/tests/helpers/pki/workflow';
/**
* This test module should test that dirty route models are cleaned up when the user leaves the page
@ -295,14 +295,12 @@ module('Acceptance | pki engine route cleanup test', function (hooks) {
await fillIn(SELECTORS.configuration.typeField, 'internal');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert');
await click(SELECTORS.configuration.generateRootSave);
// Go to list view so we fetch all the issuers
await visit(`/vault/secrets/${this.mountPath}/pki/issuers`);
issuers = this.store.peekAll('pki/issuer');
const issuerId = issuers.objectAt(0).id;
assert.strictEqual(issuers.length, 1, 'Issuer exists on model');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`,
'url is correct'
);
await visit(`/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`);
await click(SELECTORS.issuerDetails.configure);
issuer = this.store.peekRecord('pki/issuer', issuerId);
assert.false(issuer.hasDirtyAttributes, 'Model not dirty');

View File

@ -14,7 +14,6 @@ import { click, currentURL, fillIn, find, isSettled, visit } from '@ember/test-h
import { SELECTORS } from 'vault/tests/helpers/pki/workflow';
import { adminPolicy, readerPolicy, updatePolicy } from 'vault/tests/helpers/policy-generator/pki';
import { tokenWithPolicy, runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { rootPem } from 'vault/tests/helpers/pki/values';
/**
* This test module should test the PKI workflow, including:
@ -76,74 +75,6 @@ module('Acceptance | pki workflow', function (hooks) {
assertEmptyState(assert, 'keys');
});
module('configuration', function (hooks) {
hooks.beforeEach(function () {
this.pemBundle = rootPem;
});
test('import happy path', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`);
assert.dom(SELECTORS.configuration.title).hasText('Configure PKI');
assert.dom(SELECTORS.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default');
await click(SELECTORS.configuration.optionByKey('import'));
assert.dom(SELECTORS.configuration.emptyState).doesNotExist();
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', this.pemBundle);
await click('[data-test-pki-import-pem-bundle]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/issuers`,
'redirects to issuers list on success'
);
});
test('generate-root happy path', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/create`);
assert.dom(SELECTORS.configuration.title).hasText('Configure PKI');
assert.dom(SELECTORS.configuration.emptyState).exists({ count: 1 }, 'Shows empty state by default');
await click(SELECTORS.configuration.optionByKey('generate-root'));
assert.dom(SELECTORS.configuration.emptyState).doesNotExist();
// The URLs section is populated based on params returned from OpenAPI. This test will break when
// the backend adds fields. We should update the count accordingly.
assert.dom(SELECTORS.configuration.urlField).exists({ count: 4 });
// Fill in form
await fillIn(SELECTORS.configuration.typeField, 'exported');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name');
await fillIn(SELECTORS.configuration.inputByName('issuerName'), 'my-first-issuer');
await click(SELECTORS.configuration.generateRootSave);
assert
.dom(SELECTORS.issuerDetails.title)
.hasText('View issuer certificate', 'Redirects to view issuer page');
assert.dom(SELECTORS.issuerDetails.valueByName('Common name')).hasText('my-common-name');
assert.dom(SELECTORS.issuerDetails.valueByName('Issuer name')).hasText('my-first-issuer');
});
test('it should generate intermediate csr', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
await click(SELECTORS.configuration.optionByKey('generate-csr'));
await fillIn(SELECTORS.configuration.typeField, 'exported');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-common-name');
await click('[data-test-save]');
await assert.dom(SELECTORS.configuration.csrDetails).exists('renders CSR details after save');
await click('[data-test-done]');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/issuers`,
'Transitions to issuers after viewing csr details'
);
});
});
module('roles', function (hooks) {
hooks.beforeEach(async function () {
await authPage.login();
@ -334,7 +265,6 @@ module('Acceptance | pki workflow', function (hooks) {
`/vault/secrets/${this.mountPath}/pki/keys/${keyId}/details`,
'navigates to details after save'
);
await this.pauseTest;
assert.dom(SELECTORS.keyPages.keyNameValue).hasText('test-key', 'updates key name');
// key generate and delete navigation

View File

@ -6,7 +6,9 @@
import { SELECTORS as GENERATE_ROOT } from './pki-generate-root';
export const SELECTORS = {
// pki-configure-form
// page::pki-configure-create
breadcrumbContainer: '[data-test-breadcrumbs]',
title: '[data-test-pki-engine-page-title]',
option: '[data-test-pki-config-option]',
optionByKey: (key) => `[data-test-pki-config-option="${key}"]`,
cancelButton: '[data-test-pki-config-cancel]',
@ -15,6 +17,10 @@ export const SELECTORS = {
...GENERATE_ROOT,
// pki-ca-cert-import
importForm: '[data-test-pki-import-pem-bundle-form]',
importSectionLabel: '[data-test-import-section-label]',
importMapping: '[data-test-imported-bundle-mapping]',
importedIssuer: '[data-test-imported-issuer]',
importedKey: '[data-test-imported-key]',
// generate-intermediate
csrDetails: '[data-test-generate-csr-result]',
};

View File

@ -19,4 +19,15 @@ export const SELECTORS = {
formInvalidError: '[data-test-pki-generate-root-validation-error]',
urlsSection: '[data-test-urls-section]',
urlField: '[data-test-urls-section] [data-test-input]',
// Shown values after save
saved: {
certificate: '[data-test-value-div="Certificate"] [data-test-masked-input]',
commonName: '[data-test-row-value="Common name"]',
issuerName: '[data-test-row-value="Issuer name"]',
issuerLink: '[data-test-value-div="Issuer ID"] a',
keyName: '[data-test-row-value="Key name"]',
keyLink: '[data-test-value-div="Key ID"] a',
privateKey: '[data-test-value-div="Private key"] [data-test-masked-input]',
serialNumber: '[data-test-row-value="Serial number"]',
},
};

View File

@ -8,7 +8,7 @@ import { SELECTORS as GENERATECERT } from './pki-role-generate';
import { SELECTORS as KEYFORM } from './pki-key-form';
import { SELECTORS as KEYPAGES } from './page/pki-keys';
import { SELECTORS as ISSUERDETAILS } from './pki-issuer-details';
import { SELECTORS as CONFIGURATION } from './pki-configure-form';
import { SELECTORS as CONFIGURATION } from './pki-configure-create';
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',

View File

@ -8,21 +8,40 @@ import { setupRenderingTest } from 'vault/tests/helpers';
import { click, render } from '@ember/test-helpers';
import { setupEngine } from 'ember-engines/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-configure-form';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-configure-create';
import sinon from 'sinon';
module('Integration | Component | pki-configure-form', function (hooks) {
module('Integration | Component | page/pki-configure-create', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
hooks.beforeEach(function () {
this.context = { owner: this.engine }; // this.engine set by setupEngine
this.store = this.owner.lookup('service:store');
this.cancelSpy = sinon.spy();
this.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: 'pki', route: 'overview' },
{ label: 'configure' },
];
this.config = this.store.createRecord('pki/action');
this.urls = this.store.createRecord('pki/urls');
});
test('it renders', async function (assert) {
await render(hbs`<PkiConfigureForm @onCancel={{this.cancelSpy}} @config={{this.config}} />`, {
owner: this.engine,
});
await render(
hbs`
<Page::PkiConfigureCreate
@breadcrumbs={{this.breadcrumbs}}
@config={{this.config}}
@urls={{this.urls}}
@onCancel={{this.cancelSpy}}
/>
`,
this.context
);
assert.dom(SELECTORS.breadcrumbContainer).exists('breadcrumbs exist');
assert.dom(SELECTORS.title).hasText('Configure PKI');
assert.dom(SELECTORS.option).exists({ count: 3 }, 'Three configuration options are shown');
assert.dom(SELECTORS.cancelButton).exists('Cancel link is shown');
assert.dom(SELECTORS.saveButton).isDisabled('Done button is disabled');
@ -32,5 +51,11 @@ module('Integration | Component | pki-configure-form', function (hooks) {
await click(SELECTORS.optionByKey('generate-csr'));
assert.dom(SELECTORS.optionByKey('generate-csr')).isChecked('Selected item is checked');
await click(SELECTORS.optionByKey('generate-root'));
assert.dom(SELECTORS.optionByKey('generate-root')).isChecked('Selected item is checked');
await click(SELECTORS.generateRootCancel);
assert.ok(this.cancelSpy.calledOnce);
});
});

View File

@ -0,0 +1,80 @@
import { module, test } from 'qunit';
import { click, fillIn, render } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { Response } from 'miragejs';
import { v4 as uuidv4 } from 'uuid';
import { setupRenderingTest } from 'vault/tests/helpers';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-configure-create';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
/**
* this test is for the page component only. A separate test is written for the form rendered
*/
module('Integration | Component | page/pki-issuer-generate-intermediate', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.breadcrumbs = [{ label: 'something' }];
this.model = this.store.createRecord('pki/action', {
actionType: 'generate-csr',
});
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = 'pki-component';
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
});
test('it renders correct title before and after submit', async function (assert) {
assert.expect(3);
this.server.post(`/pki-component/issuers/generate/intermediate/internal`, () => {
assert.true(true, 'Issuers endpoint called');
return {
request_id: uuidv4(),
data: {
csr: '------BEGIN CERTIFICATE------',
key_id: 'some-key-id',
},
};
});
await render(
hbs`<Page::PkiIssuerGenerateIntermediate @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`,
{
owner: this.engine,
}
);
assert.dom('[data-test-pki-page-title]').hasText('Generate intermediate CSR');
await fillIn(SELECTORS.typeField, 'internal');
await fillIn(SELECTORS.inputByName('commonName'), 'foobar');
await click('[data-test-save]');
assert.dom('[data-test-pki-page-title]').hasText('View generated CSR');
});
test('it does not update title if API response is an error', async function (assert) {
assert.expect(2);
this.server.post(
'/pki-component/issuers/generate/intermediate/internal',
() => new Response(403, {}, { errors: ['API returns this error'] })
);
await render(
hbs`<Page::PkiIssuerGenerateIntermediate @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`,
{
owner: this.engine,
}
);
assert.dom('[data-test-pki-page-title]').hasText('Generate intermediate CSR');
// Fill in
await fillIn(SELECTORS.typeField, 'internal');
await fillIn(SELECTORS.inputByName('commonName'), 'foobar');
await click('[data-test-save]');
assert
.dom('[data-test-pki-page-title]')
.hasText('Generate intermediate CSR', 'title does not change if response is unsuccessful');
});
});

View File

@ -0,0 +1,77 @@
import { module, test } from 'qunit';
import { click, fillIn, render } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { Response } from 'miragejs';
import { v4 as uuidv4 } from 'uuid';
import { setupRenderingTest } from 'vault/tests/helpers';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-configure-create';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
/**
* this test is for the page component only. A separate test is written for the form rendered
*/
module('Integration | Component | page/pki-issuer-generate-root', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.breadcrumbs = [{ label: 'something' }];
this.model = this.store.createRecord('pki/action', {
actionType: 'generate-csr',
});
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = 'pki-component';
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
});
test('it renders correct title before and after submit', async function (assert) {
assert.expect(3);
this.server.post(`/pki-component/root/generate/internal`, () => {
assert.true(true, 'Root endpoint called');
return {
request_id: uuidv4(),
data: {
certificate: '------BEGIN CERTIFICATE------',
key_id: 'some-key-id',
},
};
});
await render(
hbs`<Page::PkiIssuerGenerateRoot @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`,
{
owner: this.engine,
}
);
assert.dom('[data-test-pki-page-title]').hasText('Generate root');
await fillIn(SELECTORS.typeField, 'internal');
await fillIn(SELECTORS.inputByName('commonName'), 'foobar');
await click(SELECTORS.generateRootSave);
assert.dom('[data-test-pki-page-title]').hasText('View generated root');
});
test('it does not update title if API response is an error', async function (assert) {
assert.expect(2);
this.server.post(`/pki-component/root/generate/internal`, () => new Response(404, {}, { errors: [] }));
await render(
hbs`<Page::PkiIssuerGenerateRoot @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`,
{
owner: this.engine,
}
);
assert.dom('[data-test-pki-page-title]').hasText('Generate root');
// Fill in
await fillIn(SELECTORS.typeField, 'internal');
await fillIn(SELECTORS.inputByName('commonName'), 'foobar');
await click(SELECTORS.generateRootSave);
assert
.dom('[data-test-pki-page-title]')
.hasText('Generate root', 'title does not change if response is unsuccessful');
});
});

View File

@ -0,0 +1,67 @@
import { module, test } from 'qunit';
import { click, fillIn, render } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { Response } from 'miragejs';
import { v4 as uuidv4 } from 'uuid';
import { setupRenderingTest } from 'vault/tests/helpers';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
/**
* this test is for the page component only. A separate test is written for the form rendered
*/
module('Integration | Component | page/pki-issuer-import', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.breadcrumbs = [{ label: 'something' }];
this.model = this.store.createRecord('pki/action', {
actionType: 'generate-csr',
});
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = 'pki-component';
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
});
test('it renders correct title before and after submit', async function (assert) {
assert.expect(3);
this.server.post(`/pki-component/issuers/import/bundle`, () => {
assert.true(true, 'Import endpoint called');
return {
request_id: uuidv4(),
data: {},
};
});
await render(hbs`<Page::PkiIssuerImport @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
owner: this.engine,
});
assert.dom('[data-test-pki-page-title]').hasText('Import a CA');
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', 'dummy-pem-bundle');
await click('[data-test-pki-import-pem-bundle]');
assert.dom('[data-test-pki-page-title]').hasText('View imported items');
});
test('it does not update title if API response is an error', async function (assert) {
assert.expect(2);
// this.server.post('/pki-component/issuers/import/bundle', () => new Response(404, {}, { errors: ['Some error occurred'] }));
this.server.post(`/pki-component/issuers/import/bundle`, () => new Response(404, {}, { errors: [] }));
await render(hbs`<Page::PkiIssuerImport @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />`, {
owner: this.engine,
});
assert.dom('[data-test-pki-page-title]').hasText('Import a CA');
// Fill in
await click('[data-test-text-toggle]');
await fillIn('[data-test-text-file-textarea]', 'dummy-pem-bundle');
await click('[data-test-pki-import-pem-bundle]');
assert
.dom('[data-test-pki-page-title]')
.hasText('Import a CA', 'title does not change if response is unsuccessful');
});
});

View File

@ -28,3 +28,9 @@ export interface ModelValidation {
};
invalidFormMessage: string;
}
export interface Breadcrumb {
label: string;
route?: string;
linkExternal?: boolean;
}

View File

@ -9,9 +9,42 @@ import CapabilitiesModel from '../capabilities';
export default class PkiActionModel extends Model {
secretMountPath: unknown;
pemBundle: string;
type: string;
actionType: string | null;
pemBundle: string;
importedIssuers: unknown;
importedKeys: unknown;
mapping: unknown;
type: string;
issuerName: string;
keyName: string;
keyRef: string;
commonName: string;
altNames: string[];
ipSans: string[];
uriSans: string[];
otherSans: string[];
format: string;
privateKeyFormat: string;
keyType: string;
keyBits: string;
maxPathLength: number;
excludeCnFromSans: boolean;
permittedDnsDomains: string;
ou: string[];
serialNumber: string;
addBasicConstraints: boolean;
notBeforeDuration: string;
managedKeyName: string;
managedKeyId: string;
customTtl: string;
ttl: string;
notAfter: string;
issuerId: string;
csr: string;
caChain: string;
keyId: string;
privateKey: string;
privateKeyType: string;
get backend(): string;
// apiPaths for capabilities
importBundlePath: Promise<CapabilitiesModel>;