ui: delete pki key functionality (#18146)

* add deletekey

* fix types

* move page components into folder

* finish tests

* make linting changes

* declare flashmessages ts service

* restructure pki test files

* add delete test

* add more folders
This commit is contained in:
claire bontempo 2022-11-30 17:24:40 -08:00 committed by GitHub
parent e75633eddc
commit 5f79edc49c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 234 additions and 49 deletions

View file

@ -1,9 +1,7 @@
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default class PkiKeyAdapter extends ApplicationAdapter {
namespace = 'v1';
getUrl(backend, id) {
const url = `${this.buildURL()}/${encodePath(backend)}`;
if (id) {
@ -21,4 +19,9 @@ export default class PkiKeyAdapter extends ApplicationAdapter {
const { backend, id } = query;
return this.ajax(this.getUrl(backend, id), 'GET');
}
deleteRecord(store, type, snapshot) {
const { id, record } = snapshot;
return this.ajax(this.getUrl(record.backend, id), 'DELETE');
}
}

View file

@ -1,18 +1,19 @@
import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
import { inject as service } from '@ember/service';
import { withFormFields } from 'vault/decorators/model-form-fields';
@withFormFields(['keyId', 'keyName', 'keyType', 'keyBits'])
export default class PkiKeyModel extends Model {
@attr('string', { readOnly: true }) backend;
@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', { detailsLabel: 'Key bit length' }) keyBits; // TODO confirm with crypto team to remove this field from details page
@attr('string') keyBits;
// TODO refactor when field-to-attrs util is refactored as decorator
constructor() {
super(...arguments);
this.formFields = expandAttributeMeta(this, ['keyId', 'keyName', 'keyType', 'keyBits']);
get backend() {
return this.secretMountPath.currentPath;
}
}

View file

@ -1,10 +1,10 @@
import FlashMessages from 'ember-cli-flash/services/flash-messages';
export default FlashMessages.extend({
stickyInfo(message) {
export default class FlashMessageService extends FlashMessages {
stickyInfo(message: string) {
return this.info(message, {
sticky: true,
priority: 300,
});
},
});
}
}

View file

@ -1,6 +1,6 @@
// accepts an error and returns error.errors joined with a comma, error.message or a fallback message
export default function (error, fallbackMessage = 'An error occurred, please try again') {
if (error?.errors) {
if (error instanceof Error && error?.errors) {
return error.errors.join(', ');
}
return error?.message || fallbackMessage;

View file

@ -0,0 +1,41 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import RouterService from '@ember/routing/router-service';
import FlashMessageService from 'vault/services/flash-messages';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
interface Args {
key: {
rollbackAttributes: () => void;
destroyRecord: () => void;
backend: string;
keyName: string;
keyId: string;
};
}
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 {
await this.args.key.destroyRecord();
this.flashMessages.success('Key deleted successfully');
this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.index');
} catch (error) {
this.args.key.rollbackAttributes();
this.flashMessages.danger(errorMessage(error));
}
}
}

View file

@ -1,27 +0,0 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
interface Args {
key: {
backend: string;
keyName: string;
keyId: string;
};
}
export default class PkiKeyDetails extends Component<Args> {
@service declare secretMountPath: { currentPath: string };
get breadcrumbs() {
return [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: this.secretMountPath.currentPath || 'pki', route: 'overview' },
{ label: 'keys', route: 'keys.index' },
{ label: this.args.key.keyId },
];
}
@action deleteKey() {
// TODO handle delete
}
}

View file

@ -1 +1 @@
<PkiKeyDetails @key={{this.model}} />
<Page::PkiKeyDetails @key={{this.model}} />

View file

@ -1 +1 @@
<PkiRoleDetailsPage @role={{this.model}} />
<Page::PkiRoleDetails @role={{this.model}} />

View file

@ -0,0 +1,11 @@
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
breadcrumbs: '[data-test-breadcrumbs] li',
title: '[data-test-key-details-title]',
keyIdValue: '[data-test-value-div="Key ID"]',
keyNameValue: '[data-test-value-div="Key name"]',
keyTypeValue: '[data-test-value-div="Key type"]',
keyBitsValue: '[data-test-value-div="Key bits"]',
keyDeleteButton: '[data-test-pki-key-delete] button',
confirmDelete: '[data-test-confirm-button]',
};

View file

@ -1,7 +1,6 @@
export const PKI_BASE_URL = `/vault/cluster/secrets/backend/pki/roles`;
export const SELECTORS = {
// Pki role
roleName: '[data-test-input="name"]',
issuerRef: '[data-test-input="issuerRef"]',
customTtl: '[data-test-field="customTtl"]',

View file

@ -0,0 +1,53 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, 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';
import { SELECTORS } from 'vault/tests/helpers/pki/keys/page-details';
module('Integration | Component | pki key details page', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(function () {
this.owner.lookup('service:flash-messages').registerTypes(['success', 'danger']);
this.store = this.owner.lookup('service:store');
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.backend = 'pki-test';
this.secretMountPath.currentPath = this.backend;
this.store.pushPayload('pki/key', {
modelName: 'pki/key',
key_id: '724862ff-6438-bad0-b598-77a6c7f4e934',
key_type: 'ec',
key_name: 'test-key',
});
this.model = this.store.peekRecord('pki/key', '724862ff-6438-bad0-b598-77a6c7f4e934');
});
test('it renders the page component and deletes a key', async function (assert) {
assert.expect(9);
this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => {
assert.ok(true, 'confirming delete fires off destroyRecord()');
});
await render(
hbs`
<Page::PkiKeyDetails @key={{this.model}} />
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumb containers exist');
assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs');
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');
assert.dom(SELECTORS.keyBitsValue).doesNotExist('does not render empty value');
assert.dom(SELECTORS.keyDeleteButton).exists('renders delete button');
await click(SELECTORS.keyDeleteButton);
await click(SELECTORS.confirmDelete);
});
});

View file

@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki-engine';
import { SELECTORS } from 'vault/tests/helpers/pki/roles/form';
module('Integration | Component | pki-key-parameters', function (hooks) {
setupRenderingTest(hooks);

View file

@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render, click, 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-engine';
import { SELECTORS } from 'vault/tests/helpers/pki/roles/form';
module('Integration | Component | pki-key-usage', function (hooks) {
setupRenderingTest(hooks);

View file

@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki-engine';
import { SELECTORS } from 'vault/tests/helpers/pki/roles/form';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Integration | Component | pki-role-form', function (hooks) {

View file

@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki/page-role-details';
import { SELECTORS } from 'vault/tests/helpers/pki/roles/page-details';
module('Integration | Component | pki role details page', function (hooks) {
setupRenderingTest(hooks);
@ -24,7 +24,7 @@ module('Integration | Component | pki role details page', function (hooks) {
assert.expect(7);
await render(
hbs`
<PkiRoleDetailsPage @role={{this.model}} />
<Page::PkiRoleDetails @role={{this.model}} />
`,
{ owner: this.engine }
);

View file

@ -0,0 +1,60 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Unit | Adapter | pki/key', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.backend = 'pki-test';
this.secretMountPath.currentPath = this.backend;
this.data = {
key_id: '724862ff-6438-bad0-b598-77a6c7f4e934',
key_type: 'ec',
key_name: 'test-key',
key_bits: '256',
};
});
hooks.afterEach(function () {
this.server.shutdown();
});
test('it should make request to correct endpoint on query', async function (assert) {
assert.expect(1);
const { key_id, ...otherAttrs } = this.data; // excludes key_id from key_info data
const key_info = { [key_id]: { ...otherAttrs } };
this.server.get(`${this.backend}/keys`, (schema, req) => {
assert.strictEqual(req.queryParams.list, 'true', 'request is made to correct endpoint on query');
return { data: { keys: [key_id], key_info } };
});
this.store.query('pki/key', { backend: this.backend });
});
test('it should make request to correct endpoint on queryRecord', async function (assert) {
assert.expect(1);
this.server.get(`${this.backend}/key/${this.data.key_id}`, () => {
assert.ok(true, 'request is made to correct endpoint on query record');
return { data: this.data };
});
this.store.queryRecord('pki/key', { backend: this.backend, id: this.data.key_id });
});
test('it should make request to correct endpoint on delete', async function (assert) {
assert.expect(1);
this.store.pushPayload('pki/key', { modelName: 'pki/key', ...this.data });
this.server.get(`${this.backend}/key/${this.data.key_id}`, () => ({ data: this.data }));
this.server.delete(`${this.backend}/key/${this.data.key_id}`, () => {
assert.ok(true, 'request made to correct endpoint on delete');
});
const model = await this.store.queryRecord('pki/key', { backend: this.backend, id: this.data.key_id });
await model.destroyRecord();
});
});

View file

@ -0,0 +1,44 @@
declare module 'ember-cli-flash/services/flash-messages' {
import Service from '@ember/service';
import FlashObject from 'ember-cli-flash/flash/object';
import { A } from '@ember/array';
type Partial<T> = { [K in keyof T]?: T[K] };
interface MessageOptions {
type: string;
priority: number;
timeout: number;
sticky: boolean;
showProgress: boolean;
extendedTimeout: number;
destroyOnClick: boolean;
onDestroy: () => void;
[key: string]: unknown;
}
interface CustomMessageInfo extends Partial<MessageOptions> {
message: string;
}
interface FlashFunction {
(message: string, options?: Partial<MessageOptions>): FlashMessageService;
}
class FlashMessageService extends Service {
queue: A<FlashObject>;
success: FlashFunction;
warning: FlashFunction;
info: FlashFunction;
error: FlashFunction;
danger: FlashFunction;
alert: FlashFunction;
secondary: FlashFunction;
add(messageInfo: CustomMessageInfo): FlashMessageService;
clearMessages(): FlashMessageService;
registerTypes(types: string[]): FlashMessageService;
getFlashObject(): FlashObject;
}
export default FlashMessageService;
}