Pki Generate Intermediate CSR (#18807)
* adds pki generate csr component * adds keyParamsByType helper to pki-generate-toggle-groups component * removes unused router service from pki-generate-csr component * updates common pki generate form fields * addresses feedback and adds tests
This commit is contained in:
parent
bf3e266929
commit
e873d27e83
|
@ -1,5 +1,6 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
@ -10,7 +11,7 @@ const validations = {
|
|||
issuerName: [
|
||||
{
|
||||
validator(model) {
|
||||
if (model.issuerName === 'default') return false;
|
||||
if (model.actionType === 'generate-root' && model.issuerName === 'default') return false;
|
||||
return true;
|
||||
},
|
||||
message: 'Issuer name must be unique across all issuers and not be the reserved value default.',
|
||||
|
@ -23,6 +24,8 @@ const validations = {
|
|||
export default class PkiActionModel extends Model {
|
||||
@service secretMountPath;
|
||||
|
||||
@tracked actionType; // used to toggle between different form fields when creating configuration
|
||||
|
||||
/* actionType import */
|
||||
@attr('string') pemBundle;
|
||||
|
||||
|
@ -33,7 +36,7 @@ export default class PkiActionModel extends Model {
|
|||
})
|
||||
type;
|
||||
|
||||
@attr('string') issuerName; // REQUIRED, cannot be "default"
|
||||
@attr('string') issuerName; // REQUIRED for generate-root actionType, cannot be "default"
|
||||
|
||||
@attr('string') keyName; // cannot be "default"
|
||||
|
||||
|
@ -123,6 +126,11 @@ export default class PkiActionModel extends Model {
|
|||
})
|
||||
serialNumber;
|
||||
|
||||
@attr('boolean', {
|
||||
subText: 'Whether to add a Basic Constraints extension with CA: true.',
|
||||
})
|
||||
addBasicConstraints;
|
||||
|
||||
@attr({
|
||||
label: 'Backdate validity',
|
||||
detailsLabel: 'Issued certificate backdating',
|
||||
|
|
|
@ -25,35 +25,41 @@ export default class PkiActionSerializer extends ApplicationSerializer {
|
|||
|
||||
_allowedParamsByType(actionType, type) {
|
||||
const keyFields = keyParamsByType(type).map((attrName) => underscore(attrName).toLowerCase());
|
||||
switch (actionType) {
|
||||
case 'import':
|
||||
return ['pem_bundle'];
|
||||
case 'generate-root':
|
||||
return [
|
||||
const commonProps = [
|
||||
'alt_names',
|
||||
'common_name',
|
||||
'country',
|
||||
'exclude_cn_from_sans',
|
||||
'format',
|
||||
'ip_sans',
|
||||
'issuer_name',
|
||||
'locality',
|
||||
'max_path_length',
|
||||
'not_after',
|
||||
'not_before_duration',
|
||||
'organization',
|
||||
'other_sans',
|
||||
'ou',
|
||||
'permitted_dns_domains',
|
||||
'postal_code',
|
||||
'private_key_format',
|
||||
'province',
|
||||
'serial_number',
|
||||
'street_address',
|
||||
'ttl',
|
||||
'type',
|
||||
'uri_sans',
|
||||
...keyFields,
|
||||
];
|
||||
switch (actionType) {
|
||||
case 'import':
|
||||
return ['pem_bundle'];
|
||||
case 'generate-root':
|
||||
return [
|
||||
...commonProps,
|
||||
'issuer_name',
|
||||
'max_path_length',
|
||||
'not_after',
|
||||
'not_before_duration',
|
||||
'permitted_dns_domains',
|
||||
'private_key_format',
|
||||
'ttl',
|
||||
];
|
||||
case 'generate-csr':
|
||||
return [...commonProps, 'add_basic_constraints'];
|
||||
default:
|
||||
// if type doesn't match, serialize all
|
||||
return null;
|
||||
|
|
|
@ -304,7 +304,7 @@
|
|||
{{on "input" this.onChangeWithEvent}}
|
||||
{{on "change" this.onChangeWithEvent}}
|
||||
{{on "keyup" this.handleKeyUp}}
|
||||
class="input {{if this.validationError 'has-error-border'}} has-bottom-margin-m"
|
||||
class="input {{if this.validationError 'has-error-border' 'has-bottom-margin-m'}}"
|
||||
maxLength={{@attr.options.characterLimit}}
|
||||
/>
|
||||
{{#if @attr.options.validationAttr}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="columns">
|
||||
{{#each this.configTypes as |option|}}
|
||||
<div class="column is-flex">
|
||||
<label for={{option.key}} class="box-label is-column {{if (eq this.actionType option.key) 'is-selected'}}">
|
||||
<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}} />
|
||||
|
@ -17,8 +17,8 @@
|
|||
id={{option.key}}
|
||||
name="pki-config-type"
|
||||
@value={{option.key}}
|
||||
@groupValue={{this.actionType}}
|
||||
@onChange={{fn (mut this.actionType) option.key}}
|
||||
@groupValue={{@config.actionType}}
|
||||
@onChange={{fn (mut @config.actionType) option.key}}
|
||||
data-test-pki-config-option={{option.key}}
|
||||
/>
|
||||
<label for={{option.key}}></label>
|
||||
|
@ -27,22 +27,26 @@
|
|||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if (eq this.actionType "import")}}
|
||||
{{#if (eq @config.actionType "import")}}
|
||||
<PkiCaCertificateImport
|
||||
@model={{@config}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
||||
@adapterOptions={{hash actionType=this.actionType useIssuer=@config.canImportBundle}}
|
||||
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canImportBundle}}
|
||||
/>
|
||||
{{else if (eq this.actionType "generate-root")}}
|
||||
{{else if (eq @config.actionType "generate-root")}}
|
||||
<PkiGenerateRoot
|
||||
@model={{@config}}
|
||||
@urls={{@urls}}
|
||||
@onCancel={{this.cancel}}
|
||||
@adapterOptions={{hash actionType="generate-root" useIssuer=@config.canGenerateIssuerRoot}}
|
||||
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canGenerateIssuerRoot}}
|
||||
/>
|
||||
{{else if (eq @config.actionType "generate-csr")}}
|
||||
<PkiGenerateCsr
|
||||
@model={{@config}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
|
||||
@onCancel={{this.cancel}}
|
||||
/>
|
||||
{{else if (eq this.actionType "generate-csr")}}
|
||||
<code>POST /intermediate/generate/:type ~or~ /issuers/generate/intermediate/:type</code>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Choose an option"
|
||||
|
|
|
@ -4,7 +4,6 @@ import { inject as service } from '@ember/service';
|
|||
import Store from '@ember-data/store';
|
||||
import Router from '@ember/routing/router';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import PkiActionModel from 'vault/models/pki/action';
|
||||
|
||||
|
@ -23,7 +22,6 @@ export default class PkiConfigureForm extends Component<Args> {
|
|||
@service declare readonly store: Store;
|
||||
@service declare readonly router: Router;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@tracked actionType = '';
|
||||
|
||||
get configTypes() {
|
||||
return [
|
||||
|
@ -51,24 +49,6 @@ export default class PkiConfigureForm extends Component<Args> {
|
|||
];
|
||||
}
|
||||
|
||||
shouldUseIssuerEndpoint() {
|
||||
const { config } = this.args;
|
||||
// To determine which endpoint the config adapter should use,
|
||||
// we want to check capabilities on the newer endpoints (those
|
||||
// prefixed with "issuers") and use the old path as fallback
|
||||
// if user does not have permissions.
|
||||
switch (this.actionType) {
|
||||
case 'import':
|
||||
return config.canImportBundle;
|
||||
case 'generate-root':
|
||||
return config.canGenerateIssuerRoot;
|
||||
case 'generate-csr':
|
||||
return config.canGenerateIssuerIntermediate;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@action cancel() {
|
||||
this.args.config.rollbackAttributes();
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.pki.overview');
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<form {{on "submit" (perform this.save)}}>
|
||||
<MessageError @errorMessage={{this.error}} class="has-top-margin-s" />
|
||||
<h2 class="title is-size-5 has-border-bottom-light page-header">
|
||||
CSR parameters
|
||||
</h2>
|
||||
|
||||
{{#each this.formFields as |field|}}
|
||||
<FormField @attr={{field}} @model={{@model}} @modelValidations={{this.modelValidations}} />
|
||||
{{/each}}
|
||||
|
||||
<PkiGenerateToggleGroups @model={{@model}} />
|
||||
|
||||
<div class="field is-grouped box is-fullwidth is-bottomless has-top-margin-l">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary" data-test-save>
|
||||
Done
|
||||
</button>
|
||||
<button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-cancel>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{{#if this.alert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.alert}} @mimicRefresh={{true}} data-test-alert />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,76 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import FlashMessageService from 'vault/services/flash-messages';
|
||||
import PkiActionModel from 'vault/models/pki/action';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
interface Args {
|
||||
model: PkiActionModel;
|
||||
useIssuer: boolean;
|
||||
onSave: CallableFunction;
|
||||
onCancel: CallableFunction;
|
||||
}
|
||||
|
||||
export default class PkiGenerateIntermediateComponent extends Component<Args> {
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
@tracked modelValidations = null;
|
||||
@tracked error: string | null = null;
|
||||
@tracked alert: string | null = null;
|
||||
|
||||
formFields;
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
this.formFields = expandAttributeMeta(this.args.model, [
|
||||
'type',
|
||||
'commonName',
|
||||
'excludeCnFromSans',
|
||||
'format',
|
||||
'serialNumber',
|
||||
'addBasicConstraints',
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.args.model.unloadRecord();
|
||||
this.args.onCancel();
|
||||
}
|
||||
|
||||
async getCapability(): Promise<boolean> {
|
||||
try {
|
||||
const issuerCapabilities = await this.args.model.generateIssuerCsrPath;
|
||||
return issuerCapabilities.get('canCreate') === true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save(event: Event): Generator<Promise<boolean | PkiActionModel>> {
|
||||
event.preventDefault();
|
||||
try {
|
||||
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.');
|
||||
onSave();
|
||||
} else {
|
||||
this.modelValidations = state;
|
||||
this.alert = invalidFormMessage;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error = errorMessage(e);
|
||||
this.alert = 'There was a problem generating the CSR.';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,58 +20,7 @@
|
|||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
{{! togglable groups }}
|
||||
{{#each-in this.groups as |group fields|}}
|
||||
<ToggleButton
|
||||
@isOpen={{eq this.showGroup group}}
|
||||
@openLabel={{concat "Hide " group}}
|
||||
@closedLabel={{group}}
|
||||
@onClick={{fn this.toggleGroup group}}
|
||||
class="is-block"
|
||||
data-test-toggle-group={{group}}
|
||||
/>
|
||||
{{#if (eq this.showGroup group)}}
|
||||
<div class="box is-marginless" data-test-group={{group}}>
|
||||
{{#if (eq group "Key parameters")}}
|
||||
<p class="has-bottom-margin-m" data-test-toggle-group-description>
|
||||
{{#if (eq @model.type "internal")}}
|
||||
This certificate type is internal. This means that the private key will not be returned and cannot be retrieved
|
||||
later. Below, you will name the key and define its type and key bits.
|
||||
{{else if (eq @model.type "kms")}}
|
||||
This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell Vault
|
||||
where to find it in your KMS or HSM.
|
||||
<DocLink @path="/vault/docs/enterprise/managed-keys">Learn more about managed keys.</DocLink>
|
||||
{{else if (eq @model.type "exported")}}
|
||||
This certificate type is exported. This means the private key will be returned in the response. Below, you will
|
||||
name the key and define its type and key bits.
|
||||
{{else if (eq @model.type "existing")}}
|
||||
You chose to use an existing key. This means that we’ll use the key reference to create the CSR or root. Please
|
||||
provide the reference to the key.
|
||||
{{else}}
|
||||
Please choose a type to see key parameter options.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#if this.keyParamFields}}
|
||||
<PkiKeyParameters @model={{@model}} @fields={{this.keyParamFields}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p class="has-bottom-margin-m" data-test-toggle-group-description>
|
||||
{{#if (eq group "Subject Alternative Name (SAN) Options")}}
|
||||
SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names,
|
||||
etc.) to be protected by a single certificate.
|
||||
{{else if (eq group "Additional subject fields")}}
|
||||
These fields provide more information about the client to which the certificate belongs.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#each fields as |fieldName|}}
|
||||
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||
<FormField data-test-field @attr={{attr}} @mode="create" @model={{@model}} />
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
<PkiGenerateToggleGroups @model={{@model}} />
|
||||
|
||||
{{#if @urls}}
|
||||
<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-urls-section>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { waitFor } from '@ember/test-waiters';
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { keyParamsByType } from 'pki/utils/action-params';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
|
@ -32,11 +31,6 @@ export default class PkiGenerateRootComponent extends Component {
|
|||
@tracked errorBanner = '';
|
||||
@tracked invalidFormAlert = '';
|
||||
|
||||
@action
|
||||
toggleGroup(group, isOpen) {
|
||||
this.showGroup = isOpen ? group : null;
|
||||
}
|
||||
|
||||
get defaultFields() {
|
||||
return [
|
||||
'type',
|
||||
|
@ -49,14 +43,6 @@ export default class PkiGenerateRootComponent extends Component {
|
|||
'maxPathLength',
|
||||
];
|
||||
}
|
||||
get keyParamFields() {
|
||||
const { type } = this.args.model;
|
||||
if (!type) return null;
|
||||
const fields = keyParamsByType(type);
|
||||
return fields.map((fieldName) => {
|
||||
return this.args.model.allFields.find((attr) => attr.name === fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
@action cancel() {
|
||||
// Generate root form will always have a new model
|
||||
|
@ -64,29 +50,6 @@ export default class PkiGenerateRootComponent extends Component {
|
|||
this.args.onCancel();
|
||||
}
|
||||
|
||||
get groups() {
|
||||
return {
|
||||
'Key parameters': this.keyParamFields,
|
||||
'Subject Alternative Name (SAN) Options': [
|
||||
'excludeCnFromSans',
|
||||
'serialNumber',
|
||||
'altNames',
|
||||
'ipSans',
|
||||
'uriSans',
|
||||
'otherSans',
|
||||
],
|
||||
'Additional subject fields': [
|
||||
'ou',
|
||||
'organization',
|
||||
'country',
|
||||
'locality',
|
||||
'province',
|
||||
'streetAddress',
|
||||
'postalCode',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
checkFormValidity() {
|
||||
if (this.args.model.validate) {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{{#each-in this.groups as |group fields|}}
|
||||
<ToggleButton
|
||||
@isOpen={{eq this.showGroup group}}
|
||||
@openLabel={{concat "Hide " group}}
|
||||
@closedLabel={{group}}
|
||||
@onClick={{fn this.toggleGroup group}}
|
||||
class="is-block"
|
||||
data-test-toggle-group={{group}}
|
||||
/>
|
||||
{{#if (eq this.showGroup group)}}
|
||||
<div class="box is-marginless" data-test-group={{group}}>
|
||||
{{#if (eq group "Key parameters")}}
|
||||
<p class="has-bottom-margin-m" data-test-toggle-group-description>
|
||||
{{#if (eq @model.type "internal")}}
|
||||
This certificate type is internal. This means that the private key will not be returned and cannot be retrieved
|
||||
later. Below, you will name the key and define its type and key bits.
|
||||
{{else if (eq @model.type "kms")}}
|
||||
This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell Vault
|
||||
where to find it in your KMS or HSM.
|
||||
<DocLink @path="/vault/docs/enterprise/managed-keys">Learn more about managed keys.</DocLink>
|
||||
{{else if (eq @model.type "exported")}}
|
||||
This certificate type is exported. This means the private key will be returned in the response. Below, you will
|
||||
name the key and define its type and key bits.
|
||||
{{else if (eq @model.type "existing")}}
|
||||
You chose to use an existing key. This means that we’ll use the key reference to create the CSR or root. Please
|
||||
provide the reference to the key.
|
||||
{{else}}
|
||||
Please choose a type to see key parameter options.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#if this.keyParamFields}}
|
||||
<PkiKeyParameters @model={{@model}} @fields={{this.keyParamFields}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p class="has-bottom-margin-m" data-test-toggle-group-description>
|
||||
{{#if (eq group "Subject Alternative Name (SAN) Options")}}
|
||||
SAN fields are an extension that allow you specify additional host names (sites, IP addresses, common names,
|
||||
etc.) to be protected by a single certificate.
|
||||
{{else if (eq group "Additional subject fields")}}
|
||||
These fields provide more information about the client to which the certificate belongs.
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#each fields as |fieldName|}}
|
||||
{{#let (find-by "name" fieldName @model.allFields) as |attr|}}
|
||||
<FormField data-test-field @attr={{attr}} @mode="create" @model={{@model}} />
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each-in}}
|
|
@ -0,0 +1,48 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { keyParamsByType } from 'pki/utils/action-params';
|
||||
import PkiActionModel from 'vault/models/pki/action';
|
||||
|
||||
interface Args {
|
||||
model: PkiActionModel;
|
||||
}
|
||||
|
||||
export default class PkiGenerateToggleGroupsComponent extends Component<Args> {
|
||||
@tracked showGroup: string | null = null;
|
||||
|
||||
get keyParamFields() {
|
||||
const { type } = this.args.model;
|
||||
if (!type) return null;
|
||||
const fields = keyParamsByType(type);
|
||||
return fields.map((fieldName) => {
|
||||
return this.args.model.allFields.find((attr) => attr.name === fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
get groups() {
|
||||
const groups = {
|
||||
'Key parameters': this.keyParamFields,
|
||||
'Subject Alternative Name (SAN) Options': ['altNames', 'ipSans', 'uriSans', 'otherSans'],
|
||||
'Additional subject fields': [
|
||||
'ou',
|
||||
'organization',
|
||||
'country',
|
||||
'locality',
|
||||
'province',
|
||||
'streetAddress',
|
||||
'postalCode',
|
||||
],
|
||||
};
|
||||
// excludeCnFromSans and serialNumber are present in default fields for generate-csr -- only include for other types
|
||||
if (this.args.model.actionType !== 'generate-csr') {
|
||||
groups['Subject Alternative Name (SAN) Options'].unshift('excludeCnFromSans', 'serialNumber');
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleGroup(group: string, isOpen: boolean) {
|
||||
this.showGroup = isOpen ? group : null;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,12 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import PkiIssuersIndexRoute from '.';
|
||||
|
||||
export default class PkiIssuersGenerateIntermediateRoute extends Route {}
|
||||
export default class PkiIssuersGenerateIntermediateRoute extends PkiIssuersIndexRoute {
|
||||
model() {
|
||||
return this.store.createRecord('pki/action', { actionType: 'generate-csr' });
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs.push({ label: 'generate CSR' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,16 @@
|
|||
route: issuers.generate-intermediate
|
||||
<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"}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers.index"}}
|
||||
/>
|
|
@ -127,6 +127,21 @@ module('Acceptance | pki workflow', function (hooks) {
|
|||
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]');
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
`/vault/secrets/${this.mountPath}/pki/issuers`,
|
||||
'Transitions to issuers on save success'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('roles', function (hooks) {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { click, fillIn, 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';
|
||||
|
||||
module('Integration | Component | PkiGenerateCsr', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.owner.lookup('service:secretMountPath').update('pki-test');
|
||||
this.model = this.owner
|
||||
.lookup('service:store')
|
||||
.createRecord('pki/action', { actionType: 'generate-csr' });
|
||||
|
||||
this.server.post('/sys/capabilities-self', () => ({
|
||||
data: {
|
||||
capabilities: ['root'],
|
||||
'pki-test/issuers/generate/intermediate/exported': ['root'],
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
test('it should render fields and save', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
this.server.post('/pki-test/issuers/generate/intermediate/exported', (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
assert.strictEqual(payload.common_name, 'foo', 'Request made to correct endpoint on save');
|
||||
});
|
||||
|
||||
this.onSave = () => assert.ok(true, 'onSave action fires');
|
||||
|
||||
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onSave={{this.onSave}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
|
||||
const fields = [
|
||||
'type',
|
||||
'commonName',
|
||||
'excludeCnFromSans',
|
||||
'format',
|
||||
'serialNumber',
|
||||
'addBasicConstraints',
|
||||
];
|
||||
fields.forEach((key) => {
|
||||
assert.dom(`[data-test-input="${key}"]`).exists(`${key} form field renders`);
|
||||
});
|
||||
|
||||
assert.dom('[data-test-toggle-group]').exists({ count: 3 }, 'Toggle groups render');
|
||||
|
||||
await fillIn('[data-test-input="type"]', 'exported');
|
||||
await fillIn('[data-test-input="commonName"]', 'foo');
|
||||
await click('[data-test-save]');
|
||||
});
|
||||
|
||||
test('it should display validation errors', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.onCancel = () => assert.ok(true, 'onCancel action fires');
|
||||
|
||||
await render(hbs`<PkiGenerateCsr @model={{this.model}} @onCancel={{this.onCancel}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
|
||||
await click('[data-test-save]');
|
||||
|
||||
assert
|
||||
.dom('[data-test-field-validation="type"]')
|
||||
.hasText('Type is required.', 'Type validation error renders');
|
||||
assert
|
||||
.dom('[data-test-field="commonName"] [data-test-inline-alert]')
|
||||
.hasText('Common name is required.', 'Common name validation error renders');
|
||||
assert.dom('[data-test-alert]').hasText('There are 2 errors with this form.', 'Alert renders');
|
||||
|
||||
await click('[data-test-cancel]');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { click, render, settled } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
|
||||
const selectors = {
|
||||
keys: '[data-test-toggle-group="Key parameters"]',
|
||||
sanOptions: '[data-test-toggle-group="Subject Alternative Name (SAN) Options"]',
|
||||
subjectFields: '[data-test-toggle-group="Additional subject fields"]',
|
||||
};
|
||||
|
||||
module('Integration | Component | PkiGenerateToggleGroups', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.model = this.owner
|
||||
.lookup('service:store')
|
||||
.createRecord('pki/action', { actionType: 'generate-root' });
|
||||
});
|
||||
|
||||
test('it should render key parameters', async function (assert) {
|
||||
await render(hbs`<PkiGenerateToggleGroups @model={{this.model}} />`, { owner: this.engine });
|
||||
|
||||
assert.dom(selectors.keys).hasText('Key parameters', 'Key parameters group renders');
|
||||
|
||||
await click(selectors.keys);
|
||||
|
||||
assert
|
||||
.dom('[data-test-toggle-group-description]')
|
||||
.hasText(
|
||||
'Please choose a type to see key parameter options.',
|
||||
'Placeholder renders for key params when type is not selected'
|
||||
);
|
||||
const fields = {
|
||||
exported: ['keyName', 'keyType', 'keyBits'],
|
||||
internal: ['keyName', 'keyType', 'keyBits'],
|
||||
existing: ['keyRef'],
|
||||
kms: ['keyName', 'managedKeyName', 'managedKeyId'],
|
||||
};
|
||||
for (const type in fields) {
|
||||
this.model.type = type;
|
||||
await settled();
|
||||
assert
|
||||
.dom('[data-test-field]')
|
||||
.exists({ count: fields[type].length }, `Correct number of fields render for ${type} type`);
|
||||
fields[type].forEach((key) => {
|
||||
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders for ${type} type`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('it should render SAN options', async function (assert) {
|
||||
await render(hbs`<PkiGenerateToggleGroups @model={{this.model}} />`, { owner: this.engine });
|
||||
|
||||
assert
|
||||
.dom(selectors.sanOptions)
|
||||
.hasText('Subject Alternative Name (SAN) Options', 'SAN options group renders');
|
||||
|
||||
await click(selectors.sanOptions);
|
||||
|
||||
const fields = ['excludeCnFromSans', 'serialNumber', 'altNames', 'ipSans', 'uriSans', 'otherSans'];
|
||||
assert.dom('[data-test-field]').exists({ count: 6 }, `Correct number of fields render`);
|
||||
fields.forEach((key) => {
|
||||
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders for generate-root actionType`);
|
||||
});
|
||||
|
||||
this.model.actionType = 'generate-csr';
|
||||
await settled();
|
||||
|
||||
assert
|
||||
.dom('[data-test-field]')
|
||||
.exists({ count: 4 }, 'Correct number of fields render for generate-csr actionType');
|
||||
|
||||
assert
|
||||
.dom('[data-test-input="excludeCnFromSans"]')
|
||||
.doesNotExist('excludeCnFromSans field hidden for generate-csr actionType');
|
||||
assert
|
||||
.dom('[data-test-input="serialNumber"]')
|
||||
.doesNotExist('serialNumber field hidden for generate-csr actionType');
|
||||
});
|
||||
|
||||
test('it should render additional subject fields', async function (assert) {
|
||||
await render(hbs`<PkiGenerateToggleGroups @model={{this.model}} />`, { owner: this.engine });
|
||||
|
||||
assert.dom(selectors.subjectFields).hasText('Additional subject fields', 'SAN options group renders');
|
||||
|
||||
await click(selectors.subjectFields);
|
||||
|
||||
const fields = ['ou', 'organization', 'country', 'locality', 'province', 'streetAddress', 'postalCode'];
|
||||
assert.dom('[data-test-field]').exists({ count: fields.length }, 'Correct number of fields render');
|
||||
fields.forEach((key) => {
|
||||
assert.dom(`[data-test-input="${key}"]`).exists(`${key} input renders`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -20,6 +20,8 @@
|
|||
"vault/tests/*": ["tests/*"],
|
||||
"vault/mirage/*": ["mirage/*"],
|
||||
"vault/*": [
|
||||
"types/*",
|
||||
"types/vault/*",
|
||||
"app/*",
|
||||
"lib/core/app/*",
|
||||
"lib/css/app/*",
|
||||
|
|
|
@ -8,3 +8,18 @@ export interface FormField {
|
|||
export interface FormFieldGroups {
|
||||
[key: string]: Array<FormField>;
|
||||
}
|
||||
|
||||
export interface FormFieldGroupOptions {
|
||||
[key: string]: Array<string>;
|
||||
}
|
||||
|
||||
export interface ModelValidation {
|
||||
isValid: boolean;
|
||||
state: {
|
||||
[key: string]: {
|
||||
isValid: boolean;
|
||||
errors: Array<string>;
|
||||
};
|
||||
};
|
||||
invalidFormMessage: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import ComputedProperty from '@ember/object/computed';
|
||||
import Model from '@ember-data/model';
|
||||
|
||||
interface CapabilitiesModel extends Model {
|
||||
path: string;
|
||||
capabilities: Array<string>;
|
||||
canSudo: ComputedProperty<boolean | undefined>;
|
||||
canRead: ComputedProperty<boolean | undefined>;
|
||||
canCreate: ComputedProperty<boolean | undefined>;
|
||||
canUpdate: ComputedProperty<boolean | undefined>;
|
||||
canDelete: ComputedProperty<boolean | undefined>;
|
||||
canList: ComputedProperty<boolean | undefined>;
|
||||
// these don't seem to be used anywhere
|
||||
// inferring type from key name
|
||||
allowedParameters: Array<string>;
|
||||
deniedParameters: Array<string>;
|
||||
}
|
||||
|
||||
export default CapabilitiesModel;
|
||||
export const SUDO_PATHS: string[];
|
||||
export const SUDO_PATH_PREFIXES: string[];
|
|
@ -1,15 +1,20 @@
|
|||
import Model from '@ember-data/model';
|
||||
import { FormField, ModelValidations } from 'vault/app-types';
|
||||
import CapabilitiesModel from '../capabilities';
|
||||
|
||||
export default class PkiActionModel extends Model {
|
||||
secretMountPath: unknown;
|
||||
pemBundle: string;
|
||||
type: string;
|
||||
actionType: string | null;
|
||||
get backend(): string;
|
||||
// apiPaths for capabilities
|
||||
importBundlePath: string;
|
||||
generateIssuerRootPath: string;
|
||||
generateIssuerCsrPath: string;
|
||||
importBundlePath: Promise<CapabilitiesModel>;
|
||||
generateIssuerRootPath: Promise<CapabilitiesModel>;
|
||||
generateIssuerCsrPath: Promise<CapabilitiesModel>;
|
||||
crossSignPath: string;
|
||||
allFields: Array<FormField>;
|
||||
validate(): ModelValidations;
|
||||
// Capabilities
|
||||
get canImportBundle(): boolean;
|
||||
get canGenerateIssuerRoot(): boolean;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import Model from '@ember-data/model';
|
||||
import { FormField, FormFieldGroups, FormFieldGroupOptions } from 'vault/app-types';
|
||||
|
||||
export default function _default(modelClass: Model, fieldGroups: FormFieldGroupOptions): FormFieldGroups;
|
||||
|
||||
export function expandAttributeMeta(modelClass: Model, attributeNames: Array<string>): Array<FormField>;
|
Loading…
Reference in New Issue