mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
ui: generate pki key (#18268)
* create generate key form * disable key bits unless key type selected * add create method to adapter, update serializer to remove type * refactor key parameters component * convert to typescript * refactor routes to add controller breadcrumbs * remove unnecessary attr * revert typescript changes * add validations to key type * fix tests * cleanup breadcrumbs * update tests, change all bit types to strings * add form test
This commit is contained in:
@@ -2,6 +2,15 @@ import ApplicationAdapter from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
export default class PkiKeyAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
createRecord(store, type, snapshot) {
|
||||
const { record } = snapshot;
|
||||
const url = this.getUrl(record.backend) + '/generate/' + record.type;
|
||||
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((resp) => {
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
getUrl(backend, id) {
|
||||
const url = `${this.buildURL()}/${encodePath(backend)}`;
|
||||
if (id) {
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { withFormFields } from 'vault/decorators/model-form-fields';
|
||||
import { withModelValidations } from 'vault/decorators/model-validations';
|
||||
|
||||
@withFormFields(['keyId', 'keyName', 'keyType', 'keyBits'])
|
||||
const validations = {
|
||||
type: [{ type: 'presence', message: 'Type is required.' }],
|
||||
keyType: [{ type: 'presence', message: 'Please select a key type.' }],
|
||||
};
|
||||
const displayFields = ['keyId', 'keyName', 'keyType', 'keyBits'];
|
||||
const formFieldGroups = [{ default: ['keyName', 'type'] }, { 'Key parameters': ['keyType', 'keyBits'] }];
|
||||
@withModelValidations(validations)
|
||||
@withFormFields(displayFields, formFieldGroups)
|
||||
export default class PkiKeyModel extends Model {
|
||||
@service secretMountPath;
|
||||
|
||||
@attr('boolean') isDefault;
|
||||
@attr('string', { possibleValues: ['internal', 'external'] }) type;
|
||||
@attr('string', { detailsLabel: 'Key ID' }) keyId;
|
||||
@attr('string') keyName;
|
||||
@attr('string') keyType;
|
||||
@attr('string') keyBits;
|
||||
@attr('string', { subText: 'Optional, human-readable name for this key.' }) keyName;
|
||||
@attr('string') privateKey;
|
||||
@attr('string', {
|
||||
noDefault: true,
|
||||
possibleValues: ['internal', 'exported'],
|
||||
subText:
|
||||
'The type of operation. If exported, the private key will be returned in the response; if internal the private key will not be returned and cannot be retrieved later.',
|
||||
})
|
||||
type;
|
||||
@attr('string', {
|
||||
noDefault: true,
|
||||
possibleValues: ['rsa', 'ec', 'ed25519'],
|
||||
subText: 'The type of key that will be generated. Must be rsa, ed25519, or ec. ',
|
||||
})
|
||||
keyType;
|
||||
@attr('string', {
|
||||
label: 'Key bits',
|
||||
noDefault: true,
|
||||
subText: 'Bit length of the key to generate.',
|
||||
})
|
||||
keyBits; // no possibleValues because dependent on selected key type
|
||||
|
||||
get backend() {
|
||||
return this.secretMountPath.currentPath;
|
||||
|
||||
@@ -175,15 +175,15 @@ export default class PkiRoleModel extends Model {
|
||||
|
||||
@attr('string', {
|
||||
label: 'Key bits',
|
||||
defaultValue: 2048,
|
||||
defaultValue: '2048',
|
||||
})
|
||||
keyBits; // keyBits is a conditional value based on keyType. The model param is handled in the pkiKeyParameters component.
|
||||
keyBits; // no possibleValues because options are dependent on selected key type
|
||||
|
||||
@attr('number', {
|
||||
label: 'Signature bits',
|
||||
subText: `Only applicable for key_type 'RSA'. Ignore for other key types.`,
|
||||
defaultValue: 0,
|
||||
possibleValues: [0, 256, 384, 512],
|
||||
defaultValue: '0',
|
||||
possibleValues: ['0', '256', '384', '512'],
|
||||
})
|
||||
signatureBits;
|
||||
/* End of overriding Key parameters options */
|
||||
|
||||
@@ -2,6 +2,10 @@ import ApplicationSerializer from '../application';
|
||||
|
||||
export default class PkiKeySerializer extends ApplicationSerializer {
|
||||
primaryKey = 'key_id';
|
||||
attrs = {
|
||||
type: { serialize: false },
|
||||
};
|
||||
|
||||
// rehydrate each keys model so all model attributes are accessible from the LIST response
|
||||
normalizeItems(payload) {
|
||||
if (payload.data) {
|
||||
|
||||
@@ -109,11 +109,16 @@ label {
|
||||
.input.variable {
|
||||
font-family: $family-monospace;
|
||||
}
|
||||
|
||||
.select select[disabled],
|
||||
.input[disabled],
|
||||
.textarea[disabled] {
|
||||
border-color: $grey-light;
|
||||
background-color: $white-ter;
|
||||
color: $grey-light;
|
||||
background-color: $ui-gray-100;
|
||||
color: $ui-gray-500;
|
||||
&:hover {
|
||||
border-color: $grey-light;
|
||||
}
|
||||
}
|
||||
|
||||
.control {
|
||||
@@ -335,7 +340,6 @@ select.has-error-border {
|
||||
border: 1px solid $red-500;
|
||||
}
|
||||
|
||||
|
||||
.autocomplete-input {
|
||||
background: $white !important;
|
||||
border: 1px solid $grey-light;
|
||||
|
||||
@@ -33,7 +33,13 @@
|
||||
{{else}}
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select name={{@attr.name}} id={{@attr.name}} onchange={{this.onChangeWithEvent}} data-test-input={{@attr.name}}>
|
||||
<select
|
||||
class="{{if this.validationError 'has-error-border'}}"
|
||||
name={{@attr.name}}
|
||||
id={{@attr.name}}
|
||||
onchange={{this.onChangeWithEvent}}
|
||||
data-test-input={{@attr.name}}
|
||||
>
|
||||
{{#if @attr.options.noDefault}}
|
||||
<option value="">
|
||||
Select one
|
||||
@@ -46,6 +52,9 @@
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
{{#if this.validationError}}
|
||||
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else if (and (eq @attr.type "string") (eq @attr.options.editType "boolean"))}}
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-key-details-title>
|
||||
<Icon @name="certificate" @size="24" class="has-text-grey-light" />
|
||||
View key
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<ConfirmAction
|
||||
@@ -21,6 +9,7 @@
|
||||
>
|
||||
Delete
|
||||
</ConfirmAction>
|
||||
<div class="toolbar-separator"></div>
|
||||
<ToolbarLink @route="keys.key.edit" @model={{@key.model.name}}>
|
||||
Edit key
|
||||
</ToolbarLink>
|
||||
@@ -28,10 +17,24 @@
|
||||
</Toolbar>
|
||||
|
||||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#if @key.privateKey}}
|
||||
<div class="has-top-margin-m">
|
||||
<AlertBanner
|
||||
@title="Next steps"
|
||||
@type="warning"
|
||||
@message="This private key material will only be available once. Copy or download it now."
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#each @key.formFields as |attr|}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{get @key attr.name}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if @key.privateKey}}
|
||||
<InfoTableRow @label="Private key">
|
||||
<MaskedInput @value={{@key.privateKey}} @name="Private key" @displayOnly={{true}} @allowCopy={{true}} />
|
||||
</InfoTableRow>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -18,15 +18,6 @@ export default class PkiKeyDetails extends Component<Args> {
|
||||
@service declare readonly router: RouterService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
get breadcrumbs() {
|
||||
return [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.args.key.backend || 'pki', route: 'overview' },
|
||||
{ label: 'keys', route: 'keys.index' },
|
||||
{ label: this.args.key.keyId },
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteKey() {
|
||||
try {
|
||||
|
||||
47
ui/lib/pki/addon/components/pki-key-form.hbs
Normal file
47
ui/lib/pki/addon/components/pki-key-form.hbs
Normal file
@@ -0,0 +1,47 @@
|
||||
<form {{on "submit" (perform this.save)}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
<NamespaceReminder @mode={{if @model.isNew "create" "update"}} @noun="PKI key" />
|
||||
{{#each @model.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
{{#if (eq group "Key parameters")}}
|
||||
<PkiKeyParameters @model={{@model}} @fields={{fields}} @modelValidations={{this.modelValidations}} />
|
||||
{{else}}
|
||||
{{#each fields as |attr|}}
|
||||
<FormField
|
||||
data-test-field={{attr}}
|
||||
@attr={{attr}}
|
||||
@model={{@model}}
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="has-top-padding-s">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-pki-key-save
|
||||
>
|
||||
{{if @model.isNew "Create" "Update"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button has-left-margin-s"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-pki-key-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
55
ui/lib/pki/addon/components/pki-key-form.js
Normal file
55
ui/lib/pki/addon/components/pki-key-form.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module PkiKeyForm
|
||||
* PkiKeyForm components are used to create and update PKI keys.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiKeyForm @model={{this.model}}/>
|
||||
* ```
|
||||
*
|
||||
* @param {Object} model - pki/key model.
|
||||
* @callback onCancel - Callback triggered when cancel button is clicked.
|
||||
* @callback onSave - Callback triggered on save success.
|
||||
*/
|
||||
|
||||
export default class PkiKeyForm extends Component {
|
||||
@service store;
|
||||
@service flashMessages;
|
||||
|
||||
@tracked errorBanner;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked modelValidations;
|
||||
|
||||
@task
|
||||
*save(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const { isValid, state, invalidFormMessage } = this.args.model.validate();
|
||||
this.modelValidations = isValid ? null : state;
|
||||
this.invalidFormAlert = invalidFormMessage;
|
||||
if (isValid) {
|
||||
const { isNew, keyName } = this.args.model;
|
||||
yield this.args.model.save();
|
||||
this.flashMessages.success(`Successfully ${isNew ? 'generated' : 'updated'} the key ${keyName}.`);
|
||||
this.args.onSave();
|
||||
}
|
||||
} catch (error) {
|
||||
this.errorBanner = errorMessage(error);
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
|
||||
this.args.model[method]();
|
||||
this.args.onCancel();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,39 @@
|
||||
{{#each @fields as |attr|}}
|
||||
{{#if (eq attr.name "keyBits")}}
|
||||
<div class="field">
|
||||
<FormFieldLabel for={{attr.name}} @label={{attr.options.label}} />
|
||||
<FormFieldLabel for={{attr.name}} @label={{attr.options.label}} @subText={{attr.options.subText}} />
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select name={{attr.name}} id={{attr.name}} onchange={{this.onKeyBitsChange}} data-test-input={{attr.name}}>
|
||||
{{#each this.keyBitOptions as |val|}}
|
||||
<option selected={{eq (get @model this.valuePath) (or val.value val)}} value={{val}}>
|
||||
{{val}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<ToolTip @verticalPosition="above" @horizontalPosition="center" as |T|>
|
||||
<T.Trigger tabindex="-1">
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
id={{attr.name}}
|
||||
name={{attr.name}}
|
||||
data-test-input={{attr.name}}
|
||||
disabled={{unless @model.keyType true}}
|
||||
{{on "change" this.onKeyBitsChange}}
|
||||
>
|
||||
{{#if (and attr.options.noDefault (not @model.keyType))}}
|
||||
<option value="">
|
||||
Select one
|
||||
</option>
|
||||
{{/if}}
|
||||
{{#each this.keyBitOptions as |val|}}
|
||||
<option selected={{eq (get @model "keyBits") val}} value={{val}}>
|
||||
{{val}}
|
||||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</T.Trigger>
|
||||
{{#unless @model.keyType}}
|
||||
<T.Content @defaultClass="tool-tip smaller-font">
|
||||
<div class="box">
|
||||
Choose a key type before specifying bit length.
|
||||
</div>
|
||||
</T.Content>
|
||||
{{/unless}}
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
@@ -21,7 +43,7 @@
|
||||
@model={{@model}}
|
||||
@modelValidations={{@modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
@onChange={{this.onSignatureBitsOrKeyTypeChange}}
|
||||
@onChange={{this.handleSelection}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
@@ -3,43 +3,40 @@ import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* @module PkiKeyParameters
|
||||
* PkiKeyParameters components are used to display a list of key bit options depending on the selected key type.
|
||||
* PkiKeyParameters components are used to display a list of key bit options depending on the selected key type. The key bits field is disabled until a key type is selected.
|
||||
* If the component renders in a group, other attrs may be passed in and will be rendered using the <FormField> component
|
||||
* @example
|
||||
* ```js
|
||||
* <PkiKeyParameters @model={{@model}} @fields={{fields}}/>
|
||||
* ```
|
||||
* @param {class} model - The pki/role model.
|
||||
* @param {string} fields - The name of the fields created in the model. In this case, it's the "Key parameters" fields.
|
||||
* @param {string} fields - Array of attributes from a formFieldGroup generated by the @withFormFields decorator ex: [{ name: 'attrName', type: 'string', options: {...} }]
|
||||
*/
|
||||
|
||||
// first value in array is the default bits for that key type
|
||||
const KEY_BITS_OPTIONS = {
|
||||
rsa: [2048, 3072, 4096],
|
||||
ec: [256, 224, 384, 521],
|
||||
ed25519: [0],
|
||||
any: [0],
|
||||
rsa: ['2048', '3072', '4096'],
|
||||
ec: ['256', '224', '384', '521'],
|
||||
ed25519: ['0'],
|
||||
any: ['0'],
|
||||
};
|
||||
|
||||
export default class PkiKeyParameters extends Component {
|
||||
// TODO clarify types here
|
||||
get keyBitOptions() {
|
||||
return KEY_BITS_OPTIONS[this.args.model.keyType];
|
||||
}
|
||||
|
||||
get keyBitsDefault() {
|
||||
return Number(KEY_BITS_OPTIONS[this.args.model.keyType][0]);
|
||||
}
|
||||
@action handleSelection(name, selection) {
|
||||
this.args.model[name] = selection;
|
||||
|
||||
@action onKeyBitsChange(selection) {
|
||||
this.args.model.set('keyBits', Number(selection.target.value));
|
||||
}
|
||||
|
||||
@action onSignatureBitsOrKeyTypeChange(name, selection) {
|
||||
if (name === 'signatureBits') {
|
||||
this.args.model.set(name, Number(selection));
|
||||
}
|
||||
if (name === 'keyType') {
|
||||
this.args.model.set('keyBits', this.keyBitsDefault);
|
||||
this.args.model.keyBits = Object.keys(KEY_BITS_OPTIONS).includes(selection)
|
||||
? KEY_BITS_OPTIONS[selection][0]
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@action onKeyBitsChange({ target }) {
|
||||
this.handleSelection(target.name, target.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import PkiKeysIndexRoute from '.';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||
|
||||
export default class PkiKeysCreateRoute extends Route {}
|
||||
@withConfirmLeave()
|
||||
export default class PkiKeysCreateRoute extends PkiKeysIndexRoute {
|
||||
@service store;
|
||||
@service secretMountPath;
|
||||
|
||||
model() {
|
||||
return this.store.createRecord('pki/key');
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs.push({ label: 'generate' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,14 @@ export default class PkiKeysIndexRoute extends Route {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
const backend = this.secretMountPath.currentPath || 'pki';
|
||||
controller.breadcrumbs = [
|
||||
{ label: 'secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: backend, route: 'overview' },
|
||||
{ label: 'keys', route: 'keys.index' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import PkiKeysIndexRoute from '.';
|
||||
|
||||
export default class PkiKeyDetailsRoute extends Route {}
|
||||
export default class PkiKeyDetailsRoute extends PkiKeysIndexRoute {
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs.push({ label: resolvedModel.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import PkiKeysIndexRoute from '.';
|
||||
|
||||
export default class PkiKeyEditRoute extends Route {}
|
||||
export default class PkiKeyEditRoute extends PkiKeysIndexRoute {
|
||||
setupController(controller, resolvedModel) {
|
||||
super.setupController(controller, resolvedModel);
|
||||
controller.breadcrumbs.push({ label: resolvedModel.id, route: 'keys.key.details' }, { label: 'edit' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
keys.generate
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-pki-key-page-title>
|
||||
Generate key
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.keys.index"}}
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.keys.key.details" this.model.id}}
|
||||
/>
|
||||
@@ -1 +1,13 @@
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-key-details-title>
|
||||
<Icon @name="certificate" @size="24" class="has-text-grey-light" />
|
||||
View key
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Page::PkiKeyDetails @key={{this.model}} />
|
||||
@@ -1 +1,11 @@
|
||||
keys.key.:id.edit
|
||||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-key-details-title>
|
||||
<Icon @name="certificate" @size="24" class="has-text-grey-light" />
|
||||
Edit key
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
9
ui/tests/helpers/pki/keys/form.js
Normal file
9
ui/tests/helpers/pki/keys/form.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const SELECTORS = {
|
||||
keyCreateButton: '[data-test-pki-key-save]',
|
||||
keyCancelButton: '[data-test-pki-key-cancel]',
|
||||
keyNameInput: '[data-test-input="keyName"]',
|
||||
typeInput: '[data-test-input="type"]',
|
||||
keyTypeInput: '[data-test-input="keyType"]',
|
||||
keyBitsInput: '[data-test-input="keyBits"]',
|
||||
inlineAlert: '[data-test-inline-error-message]',
|
||||
};
|
||||
175
ui/tests/integration/components/pki/keys/form-test.js
Normal file
175
ui/tests/integration/components/pki/keys/form-test.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, fillIn, findAll } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { SELECTORS } from 'vault/tests/helpers/pki/keys/form';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Integration | Component | pki key 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.model = this.store.createRecord('pki/key');
|
||||
this.backend = 'pki-test';
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.secretMountPath.currentPath = this.backend;
|
||||
});
|
||||
|
||||
test('it should render fields and show validation messages', async function (assert) {
|
||||
assert.expect(7);
|
||||
await render(
|
||||
hbs`
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.keyNameInput).exists('renders name input');
|
||||
assert.dom(SELECTORS.typeInput).exists('renders type input');
|
||||
assert.dom(SELECTORS.keyTypeInput).exists('renders key type input');
|
||||
assert.dom(SELECTORS.keyBitsInput).exists('renders key bits input');
|
||||
|
||||
await click(SELECTORS.keyCreateButton);
|
||||
const [type, keyType, formErrorCount] = findAll('[data-test-inline-error-message]');
|
||||
assert.strictEqual(type.innerText, 'Type is required.', 'renders presence validation for type of key');
|
||||
assert.strictEqual(
|
||||
keyType.innerText,
|
||||
'Please select a key type.',
|
||||
'renders selection prompt for key type'
|
||||
);
|
||||
assert.strictEqual(
|
||||
formErrorCount.innerText,
|
||||
'There are 2 errors with this form.',
|
||||
'renders correct form error count'
|
||||
);
|
||||
});
|
||||
|
||||
test('it generates a key type=exported', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.post(`/${this.backend}/keys/generate/exported`, (schema, req) => {
|
||||
assert.ok(true, 'Request made to the correct endpoint to generate exported key');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
assert.propEqual(
|
||||
request,
|
||||
{
|
||||
key_name: 'test-key',
|
||||
key_type: 'rsa',
|
||||
key_bits: '2048',
|
||||
},
|
||||
'sends params in correct type'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.keyNameInput, 'test-key');
|
||||
await fillIn(SELECTORS.typeInput, 'exported');
|
||||
assert.dom(SELECTORS.keyBitsInput).isDisabled('key bits disabled when no key type selected');
|
||||
await fillIn(SELECTORS.keyTypeInput, 'rsa');
|
||||
await click(SELECTORS.keyCreateButton);
|
||||
});
|
||||
|
||||
test('it generates a key type=internal', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.server.post(`/${this.backend}/keys/generate/internal`, (schema, req) => {
|
||||
assert.ok(true, 'Request made to the correct endpoint to generate internal key');
|
||||
const request = JSON.parse(req.requestBody);
|
||||
assert.propEqual(
|
||||
request,
|
||||
{
|
||||
key_name: 'test-key',
|
||||
key_type: 'rsa',
|
||||
key_bits: '2048',
|
||||
},
|
||||
'sends params in correct type'
|
||||
);
|
||||
return {};
|
||||
});
|
||||
this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.keyNameInput, 'test-key');
|
||||
await fillIn(SELECTORS.typeInput, 'internal');
|
||||
assert.dom(SELECTORS.keyBitsInput).isDisabled('key bits disabled when no key type selected');
|
||||
await fillIn(SELECTORS.keyTypeInput, 'rsa');
|
||||
await click(SELECTORS.keyCreateButton);
|
||||
});
|
||||
|
||||
test('it should rollback attributes or unload record on cancel', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
|
||||
await render(
|
||||
hbs`
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await click(SELECTORS.keyCancelButton);
|
||||
assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
|
||||
|
||||
/* COMMENT IN WHEN EDIT IS COMPLETE
|
||||
this.store.pushPayload('pki/key', {
|
||||
modelName: this.modelName,
|
||||
keyName: 'test-key',
|
||||
type: 'exported',
|
||||
keyId: 'some-key-id',
|
||||
keyType: 'rsa',
|
||||
keyBits: '2048',
|
||||
};);
|
||||
this.model = this.store.peekRecord('pki/key', this.data.keyId;
|
||||
|
||||
await render(
|
||||
hbs`
|
||||
<PkiKeyForm
|
||||
@model={{this.model}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSave={{this.onSave}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.keyNameInput, 'new-name');
|
||||
await click(SELECTORS.keyCancelButton);
|
||||
assert.strictEqual(this.model.keyName, undefined, 'Model attributes rolled back on cancel');
|
||||
*/
|
||||
});
|
||||
|
||||
/* FUTURE TEST TODO:
|
||||
* it should update key
|
||||
*/
|
||||
});
|
||||
@@ -27,7 +27,7 @@ module('Integration | Component | pki key details page', function (hooks) {
|
||||
});
|
||||
|
||||
test('it renders the page component and deletes a key', async function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(6);
|
||||
this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => {
|
||||
assert.ok(true, 'confirming delete fires off destroyRecord()');
|
||||
});
|
||||
@@ -39,7 +39,6 @@ module('Integration | Component | pki key details page', function (hooks) {
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
assert.dom(SELECTORS.title).containsText('View key', 'title renders');
|
||||
assert.dom(SELECTORS.keyIdValue).hasText(' 724862ff-6438-bad0-b598-77a6c7f4e934', 'key id renders');
|
||||
assert.dom(SELECTORS.keyNameValue).hasText('test-key', 'key name renders');
|
||||
assert.dom(SELECTORS.keyTypeValue).hasText('ec', 'key type renders');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { SELECTORS } from 'vault/tests/helpers/pki/roles/form';
|
||||
|
||||
module('Integration | Component | pki-key-parameters', function (hooks) {
|
||||
module('Integration | Component | pki key parameters', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
|
||||
@@ -47,17 +47,17 @@ module('Integration | Component | pki-key-parameters', function (hooks) {
|
||||
{ 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.');
|
||||
assert.strictEqual(this.model.keyBits, '2048', 'sets the default value for key_bits on the model.');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
0,
|
||||
'0',
|
||||
'sets the default value for signature_bits on the model.'
|
||||
);
|
||||
await fillIn(SELECTORS.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,
|
||||
'256',
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
@@ -69,30 +69,30 @@ module('Integration | Component | pki-key-parameters', function (hooks) {
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.model.keyBits,
|
||||
0,
|
||||
'0',
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.keyType, 'ec');
|
||||
await fillIn(SELECTORS.keyBits, 384);
|
||||
await fillIn(SELECTORS.keyBits, '384');
|
||||
assert.strictEqual(this.model.keyType, 'ec', 'sets the new selected value for key_type on the model.');
|
||||
assert.strictEqual(
|
||||
this.model.keyBits,
|
||||
384,
|
||||
'384',
|
||||
'sets the new selected value for key_bits on the model based on the selection of key_type.'
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.signatureBits, '384');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
384,
|
||||
'384',
|
||||
'sets the new selected value for signature_bits on the model.'
|
||||
);
|
||||
|
||||
await fillIn(SELECTORS.signatureBits, '0');
|
||||
assert.strictEqual(
|
||||
this.model.signatureBits,
|
||||
0,
|
||||
'0',
|
||||
'sets the default value for signature_bits on the model.'
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user