ui: pki import key (#18454)
* Move text-file to addon * create key import component * build out import component * add perform helper * small text-file changes * add file to import component * revert text-filechanges * Revert "small text-file changes" This reverts commit dc4c4864a3165b48daa9d3dfc0c03d6bf073fd46. * small text-file changes * remove index from policy set file onchange arg * Revert "remove index from policy set file onchange arg" This reverts commit e80198e063f4886d242359da25bfb2a63a811171. * Revert "small text-file changes" This reverts commit bc3ebccc4cc658431729ea4d6ffff2c17d2fd4ba. * finish key import * update key adapter * address comments * remove validations from import and unnecessary store service * add waitfor to key form * fix prettier * import changes from edit pki key pr * add waitFor to concurrency task * add adapter options to form save method Co-authored-by: Chelsea Shaw <cshaw@hashicorp.com>
This commit is contained in:
parent
424e7439dc
commit
a76bbcfe84
|
@ -3,9 +3,22 @@ import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||||
export default class PkiKeyAdapter extends ApplicationAdapter {
|
export default class PkiKeyAdapter extends ApplicationAdapter {
|
||||||
namespace = 'v1';
|
namespace = 'v1';
|
||||||
|
|
||||||
|
_baseUrl(backend, id) {
|
||||||
|
const url = `${this.buildURL()}/${encodePath(backend)}`;
|
||||||
|
if (id) {
|
||||||
|
return url + '/key/' + encodePath(id);
|
||||||
|
}
|
||||||
|
return url + '/keys';
|
||||||
|
}
|
||||||
|
|
||||||
createRecord(store, type, snapshot) {
|
createRecord(store, type, snapshot) {
|
||||||
const { record } = snapshot;
|
const { record, adapterOptions } = snapshot;
|
||||||
const url = this.getUrl(record.backend) + '/generate/' + record.type;
|
let url = this._baseUrl(record.backend);
|
||||||
|
if (adapterOptions.import) {
|
||||||
|
url = `${url}/import`;
|
||||||
|
} else {
|
||||||
|
url = `${url}/generate/${record.type}`;
|
||||||
|
}
|
||||||
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((resp) => {
|
return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then((resp) => {
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
|
@ -14,30 +27,22 @@ export default class PkiKeyAdapter extends ApplicationAdapter {
|
||||||
updateRecord(store, type, snapshot) {
|
updateRecord(store, type, snapshot) {
|
||||||
const { record } = snapshot;
|
const { record } = snapshot;
|
||||||
const { key_name } = this.serialize(snapshot);
|
const { key_name } = this.serialize(snapshot);
|
||||||
const url = this.getUrl(record.backend, record.id);
|
const url = this._baseUrl(record.backend, record.id);
|
||||||
return this.ajax(url, 'POST', { data: { key_name } });
|
return this.ajax(url, 'POST', { data: { key_name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl(backend, id) {
|
|
||||||
const url = `${this.buildURL()}/${encodePath(backend)}`;
|
|
||||||
if (id) {
|
|
||||||
return url + '/key/' + encodePath(id);
|
|
||||||
}
|
|
||||||
return url + '/keys';
|
|
||||||
}
|
|
||||||
|
|
||||||
query(store, type, query) {
|
query(store, type, query) {
|
||||||
const { backend } = query;
|
const { backend } = query;
|
||||||
return this.ajax(this.getUrl(backend), 'GET', { data: { list: true } });
|
return this.ajax(this._baseUrl(backend), 'GET', { data: { list: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
queryRecord(store, type, query) {
|
queryRecord(store, type, query) {
|
||||||
const { backend, id } = query;
|
const { backend, id } = query;
|
||||||
return this.ajax(this.getUrl(backend, id), 'GET');
|
return this.ajax(this._baseUrl(backend, id), 'GET');
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRecord(store, type, snapshot) {
|
deleteRecord(store, type, snapshot) {
|
||||||
const { id, record } = snapshot;
|
const { id, record } = snapshot;
|
||||||
return this.ajax(this.getUrl(record.backend, id), 'DELETE');
|
return this.ajax(this._baseUrl(record.backend, id), 'DELETE');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@ export default class PkiKeyModel extends Model {
|
||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
|
|
||||||
@attr('string', { detailsLabel: 'Key ID' }) keyId;
|
@attr('string', { detailsLabel: 'Key ID' }) keyId;
|
||||||
@attr('string', { subText: 'Optional, human-readable name for this key.' }) keyName;
|
@attr('string', {
|
||||||
@attr('string') privateKey;
|
subText: `Optional, human-readable name for this key. The name must be unique across all keys and cannot be 'default'.`,
|
||||||
|
})
|
||||||
|
keyName;
|
||||||
@attr('string', {
|
@attr('string', {
|
||||||
noDefault: true,
|
noDefault: true,
|
||||||
possibleValues: ['internal', 'exported'],
|
possibleValues: ['internal', 'exported'],
|
||||||
|
@ -38,6 +40,9 @@ export default class PkiKeyModel extends Model {
|
||||||
})
|
})
|
||||||
keyBits; // no possibleValues because dependent on selected key type
|
keyBits; // no possibleValues because dependent on selected key type
|
||||||
|
|
||||||
|
@attr('string') pemBundle;
|
||||||
|
@attr('string') privateKey;
|
||||||
|
|
||||||
get backend() {
|
get backend() {
|
||||||
return this.secretMountPath.currentPath;
|
return this.secretMountPath.currentPath;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import errorMessage from 'vault/utils/error-message';
|
import errorMessage from 'vault/utils/error-message';
|
||||||
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module PkiKeyForm
|
* @module PkiKeyForm
|
||||||
|
@ -20,7 +21,6 @@ import errorMessage from 'vault/utils/error-message';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class PkiKeyForm extends Component {
|
export default class PkiKeyForm extends Component {
|
||||||
@service store;
|
|
||||||
@service flashMessages;
|
@service flashMessages;
|
||||||
|
|
||||||
@tracked errorBanner;
|
@tracked errorBanner;
|
||||||
|
@ -28,6 +28,7 @@ export default class PkiKeyForm extends Component {
|
||||||
@tracked modelValidations;
|
@tracked modelValidations;
|
||||||
|
|
||||||
@task
|
@task
|
||||||
|
@waitFor
|
||||||
*save(event) {
|
*save(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
|
@ -38,7 +39,7 @@ export default class PkiKeyForm extends Component {
|
||||||
this.invalidFormAlert = invalidFormMessage;
|
this.invalidFormAlert = invalidFormMessage;
|
||||||
}
|
}
|
||||||
if (!isValid && isNew) return;
|
if (!isValid && isNew) return;
|
||||||
yield this.args.model.save();
|
yield this.args.model.save({ adapterOptions: { import: false } });
|
||||||
this.flashMessages.success(`Successfully ${isNew ? 'generated' : 'updated'} the key ${keyName}.`);
|
this.flashMessages.success(`Successfully ${isNew ? 'generated' : 'updated'} the key ${keyName}.`);
|
||||||
this.args.onSave();
|
this.args.onSave();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<form {{on "submit" (perform this.submitForm)}} data-test-pki-key-import-form>
|
||||||
|
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||||
|
<div class="box is-sideless is-fullwidth is-marginless">
|
||||||
|
<p class="has-bottom-margin-l">
|
||||||
|
Use this form to import a single pem encoded rsa, ec, or ed25519 key.
|
||||||
|
<DocLink @path="/vault/api-docs/secret/pki#import-key">
|
||||||
|
Learn more here.
|
||||||
|
</DocLink>
|
||||||
|
</p>
|
||||||
|
{{#let (find-by "name" "keyName" @model.formFields) as |attr|}}
|
||||||
|
<FormField data-test-field={{attr}} @attr={{attr}} @model={{@model}} @showHelpText={{false}} />
|
||||||
|
{{/let}}
|
||||||
|
<TextFile @onChange={{this.onFileUploaded}} @label="PEM Bundle" data-test-pki-key-file />
|
||||||
|
</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-key-import
|
||||||
|
>
|
||||||
|
Import key
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button has-left-margin-s"
|
||||||
|
disabled={{this.submitForm.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}}
|
||||||
|
data-test-pki-key-validation-error
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import errorMessage from 'vault/utils/error-message';
|
||||||
|
import trimRight from 'vault/utils/trim-right';
|
||||||
|
import { waitFor } from '@ember/test-waiters';
|
||||||
|
|
||||||
|
// TODO: convert to typescript after https://github.com/hashicorp/vault/pull/18387 is merged
|
||||||
|
/**
|
||||||
|
* @module PkiKeyImport
|
||||||
|
* PkiKeyImport components are used to import PKI keys.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* <PkiKeyImport @model={{this.model}} />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} model - pki/key model.
|
||||||
|
* @callback onCancel - Callback triggered when cancel button is clicked.
|
||||||
|
* @callback onSubmit - Callback triggered on submit success.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class PkiKeyImport extends Component {
|
||||||
|
@service flashMessages;
|
||||||
|
|
||||||
|
@tracked errorBanner;
|
||||||
|
@tracked invalidFormAlert;
|
||||||
|
|
||||||
|
@task
|
||||||
|
@waitFor
|
||||||
|
*submitForm(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
try {
|
||||||
|
const { keyName } = this.args.model;
|
||||||
|
yield this.args.model.save({ adapterOptions: { import: true } });
|
||||||
|
this.flashMessages.success(`Successfully imported key ${keyName}`);
|
||||||
|
this.args.onSave();
|
||||||
|
} catch (error) {
|
||||||
|
this.errorBanner = errorMessage(error);
|
||||||
|
this.invalidFormAlert = 'There was a problem importing key.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onFileUploaded({ value, filename }) {
|
||||||
|
this.args.model.pemBundle = value;
|
||||||
|
if (!this.args.model.keyName) {
|
||||||
|
const trimmedFileName = trimRight(filename, ['.json', '.pem']);
|
||||||
|
this.args.model.keyName = trimmedFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancel() {
|
||||||
|
this.args.model.unloadRecord();
|
||||||
|
this.args.onCancel();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,17 @@
|
||||||
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 PkiKeysImportRoute extends Route {}
|
@withConfirmLeave()
|
||||||
|
export default class PkiKeysImportRoute extends PkiKeysIndexRoute {
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.store.createRecord('pki/key');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller, resolvedModel) {
|
||||||
|
super.setupController(controller, resolvedModel);
|
||||||
|
controller.breadcrumbs.push({ label: 'import' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,16 @@
|
||||||
keys.import
|
<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>
|
||||||
|
Import key
|
||||||
|
</h1>
|
||||||
|
</p.levelLeft>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<PkiKeyImport
|
||||||
|
@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}}
|
||||||
|
/>
|
Loading…
Reference in New Issue