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:
claire bontempo 2022-12-20 21:46:25 -07:00 committed by GitHub
parent 424e7439dc
commit a76bbcfe84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 166 additions and 21 deletions

View File

@ -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');
} }
} }

View File

@ -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;
} }

View File

@ -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) {

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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' });
}
}

View File

@ -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}}
/>