UI: VAULT-9409 Pki Tidy Form (#20043)

This commit is contained in:
Kianna 2023-04-10 23:07:26 -07:00 committed by GitHub
parent 45737ddd3c
commit 6873c3c58e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 368 additions and 4 deletions

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { assert } from '@ember/debug';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import ApplicationAdapter from '../application';
export default class PkiTidyAdapter extends ApplicationAdapter {
namespace = 'v1';
urlForCreateRecord(snapshot) {
const { backend } = snapshot.record;
const { tidyType } = snapshot.adapterOptions;
if (!backend) {
throw new Error('Backend missing');
}
const baseUrl = `${this.buildURL()}/${encodePath(backend)}`;
switch (tidyType) {
case 'manual-tidy':
return `${baseUrl}/tidy`;
case 'auto-tidy':
return `${baseUrl}/config/auto-tidy`;
default:
assert('type must be one of manual-tidy, auto-tidy');
}
}
createRecord(store, type, snapshot) {
const url = this.urlForCreateRecord(snapshot);
return this.ajax(url, 'POST', { data: this.serialize(snapshot) });
}
}

12
ui/app/models/pki/tidy.js Normal file
View File

@ -0,0 +1,12 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Model, { attr } from '@ember-data/model';
export default class PkiTidyModel extends Model {
@attr('boolean', { defaultValue: false }) tidyCertStore;
@attr('boolean', { defaultValue: false }) tidyRevocationQueue;
@attr('string', { defaultValue: '72h' }) safetyBuffer;
}

View File

@ -13,7 +13,7 @@
</button>
<div class="toolbar-separator"></div>
{{/if}}
<ToolbarLink @route="configuration.tidy">
<ToolbarLink @route="configuration.tidy" data-test-tidy-toolbar>
Tidy
</ToolbarLink>
<ToolbarLink @route="configuration.edit">

View File

@ -0,0 +1,83 @@
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3">
<Icon @name="pki" @size="24" class="has-text-grey-light" />
Tidy
</h1>
</p.levelLeft>
</PageHeader>
<hr class="is-marginless has-background-gray-200" />
<p class="has-top-margin-m has-bottom-margin-l">Tidying cleans up the storage backend and/or CRL by removing certificates
that have expired and are past a certain buffer period beyond their expiration time.</p>
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
<form class="has-bottom-margin-s" {{on "submit" (perform this.save)}}>
<div class="has-bottom-margin-s">
<Input
@type="checkbox"
@checked={{@tidy.tidyCertStore}}
id="tidy-certificate-store"
{{on "input" (fn (mut @tidy.tidyCertStore) (not @tidy.tidyCertStore))}}
data-test-tidy-cert-store-checkbox
/>
<label for="tidy-certificate-store" class="is-label" data-test-tidy-cert-store-label>
Tidy the certificate store
</label>
</div>
<div class="has-bottom-margin-s">
<Input
@type="checkbox"
@checked={{@tidy.tidyRevocationQueue}}
id="tidy-revocation-queue"
{{on "input" (fn (mut @tidy.tidyRevocationQueue) (not @tidy.tidyRevocationQueue))}}
data-test-tidy-revocation-queue-checkbox
/>
<label for="tidy-revocation-queue" class="is-label" data-test-tidy-revocation-queue-label>
Tidy the revocation list (CRL)
</label>
</div>
<TtlPicker
class="has-top-margin-l has-bottom-margin-l"
@initialValue={{@tidy.safetyBuffer}}
@onChange={{this.updateSafetyBuffer}}
@hideToggle={{true}}
@label="Safety buffer"
@helperTextEnabled="For a certificate to be expunged, the time must be after the expiration time of the certificate (according to the local
clock) plus the safety buffer. The default is 72 hours."
/>
<hr class="is-marginless has-background-gray-200" />
<div class="has-top-margin-m">
<button
type="submit"
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
disabled={{this.save.isRunning}}
data-test-pki-tidy-button
>
Tidy
</button>
<button
type="button"
class="button is-secondary"
disabled={{this.save.isRunning}}
{{on "click" this.cancel}}
data-test-pki-tidy-cancel
>
Cancel
</button>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
</div>
{{/if}}
</div>
</form>

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import errorMessage from 'vault/utils/error-message';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import { tracked } from '@glimmer/tracking';
import PkiTidyModel from 'vault/models/pki/tidy';
import RouterService from '@ember/routing/router-service';
interface Args {
tidy: PkiTidyModel;
adapterOptions: object;
}
export default class PkiTidyForm extends Component<Args> {
@service declare readonly router: RouterService;
@tracked errorBanner = '';
@tracked invalidFormAlert = '';
returnToConfiguration() {
this.router.transitionTo('vault.cluster.secrets.backend.pki.configuration.index');
}
@action
updateSafetyBuffer({ goSafeTimeString }: { goSafeTimeString: string }) {
this.args.tidy.safetyBuffer = goSafeTimeString;
}
@task
@waitFor
*save(event: Event) {
event.preventDefault();
try {
yield this.args.tidy.save({ adapterOptions: this.args.adapterOptions });
this.returnToConfiguration();
} catch (e) {
this.errorBanner = errorMessage(e);
this.invalidFormAlert = 'There was an error submitting this form.';
}
}
@action
cancel() {
this.returnToConfiguration();
}
}

View File

@ -4,5 +4,25 @@
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
export default class PkiConfigurationTidyRoute extends Route {}
@withConfirmLeave('model.tidy')
export default class PkiConfigurationTidyRoute extends Route {
@service store;
@service secretMountPath;
model() {
return this.store.createRecord('pki/tidy', { backend: this.secretMountPath.currentPath });
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: this.secretMountPath.currentPath, route: 'overview' },
{ label: 'configuration', route: 'configuration.index' },
{ label: 'tidy' },
];
}
}

View File

@ -1 +1 @@
configuration.tidy
<Page::PkiTidyForm @breadcrumbs={{this.breadcrumbs}} @tidy={{this.model}} @adapterOptions={{hash tidyType="manual-tidy"}} />

View File

@ -16,7 +16,7 @@ import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { SELECTORS } from 'vault/tests/helpers/pki/workflow';
import { issuerPemBundle } from 'vault/tests/helpers/pki/values';
module('Acceptance | pki configuration', function (hooks) {
module('Acceptance | pki configuration test', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {

View File

@ -399,6 +399,27 @@ module('Acceptance | pki workflow', function (hooks) {
.dom('[data-test-input="commonName"]')
.hasValue('Hashicorp Test', 'form prefilled with parent issuer cn');
});
test('it navigates to the tidy page from configuration toolbar', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/configuration`);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`);
await click(SELECTORS.configuration.tidyToolbar);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/tidy`);
});
test('it returns to the configuration page after submit', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/configuration`);
await click(SELECTORS.configuration.tidyToolbar);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration/tidy`);
await click(SELECTORS.configuration.tidyCertStoreCheckbox);
await click(SELECTORS.configuration.tidyRevocationCheckbox);
await fillIn(SELECTORS.configuration.safetyBufferInput, '100');
await fillIn(SELECTORS.configuration.safetyBufferInputDropdown, 'd');
await click(SELECTORS.configuration.tidySave);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/configuration`);
});
});
module('rotate', function (hooks) {

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export const SELECTORS = {
tidyCertStoreLabel: '[data-test-tidy-cert-store-label]',
tidyRevocationList: '[data-test-tidy-revocation-queue-label]',
safetyBufferTTL: '[data-test-ttl-inputs]',
tidyCertStoreCheckbox: '[data-test-tidy-cert-store-checkbox]',
tidyRevocationCheckbox: '[data-test-tidy-revocation-queue-checkbox]',
safetyBufferInput: '[data-test-ttl-value="Safety buffer"]',
safetyBufferInputDropdown: '[data-test-select="ttl-unit"]',
tidyToolbar: '[data-test-tidy-toolbar]',
tidySave: '[data-test-pki-tidy-button]',
tidyCancel: '[data-test-pki-tidy-cancel]',
};

View File

@ -10,6 +10,7 @@ import { SELECTORS as KEYPAGES } from './page/pki-keys';
import { SELECTORS as ISSUERDETAILS } from './pki-issuer-details';
import { SELECTORS as CONFIGURATION } from './pki-configure-create';
import { SELECTORS as DELETE } from './pki-delete-all-issuers';
import { SELECTORS as TIDY } from './page/pki-tidy-form';
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
@ -66,5 +67,6 @@ export const SELECTORS = {
pkiBetaBannerLink: '[data-test-pki-configuration-banner] a',
...CONFIGURATION,
...DELETE,
...TIDY,
},
};

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render, fillIn } 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/page/pki-tidy-form';
module('Integration | Component | pki | Page::PkiTidyForm', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = 'pki-test';
this.tidy = this.store.createRecord('pki/tidy', { backend: 'pki-test' });
this.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: 'pki-test', route: 'overview' },
{ label: 'configuration', route: 'configuration.index' },
{ label: 'tidy' },
];
});
test('it should render tidy fields', async function (assert) {
await render(hbs`<Page::PkiTidyForm @tidy={{this.tidy}} @breadcrumbs={{this.breadcrumbs}} />`, {
owner: this.engine,
});
assert.dom(SELECTORS.tidyCertStoreLabel).hasText('Tidy the certificate store');
assert.dom(SELECTORS.tidyRevocationList).hasText('Tidy the revocation list (CRL)');
assert.dom(SELECTORS.safetyBufferTTL).exists();
assert.dom(SELECTORS.safetyBufferInput).hasValue('3');
assert.dom('[data-test-select="ttl-unit"]').hasValue('d');
});
test('it should change the attributes on the model', async function (assert) {
await render(hbs`<Page::PkiTidyForm @tidy={{this.tidy}} @breadcrumbs={{this.breadcrumbs}} />`, {
owner: this.engine,
});
await click(SELECTORS.tidyCertStoreCheckbox);
await click(SELECTORS.tidyRevocationCheckbox);
await fillIn(SELECTORS.safetyBufferInput, '5');
assert.true(this.tidy.tidyCertStore);
assert.true(this.tidy.tidyRevocationQueue);
assert.dom(SELECTORS.safetyBufferInput).hasValue('5');
assert.dom('[data-test-select="ttl-unit"]').hasValue('d');
assert.strictEqual(this.tidy.safetyBuffer, '120h');
});
});

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
module('Unit | Adapter | pki/tidy', 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.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
});
test('it exists', function (assert) {
const adapter = this.owner.lookup('adapter:pki/tidy');
assert.ok(adapter);
});
test('it calls the correct endpoint when tidyType = manual-tidy', async function (assert) {
assert.expect(1);
this.server.post(`${this.backend}/tidy`, () => {
assert.ok(true, 'request made to correct endpoint on create');
return {};
});
this.payload = {
tidy_cert_store: true,
tidy_revocation_queue: false,
safetyBuffer: '120h',
backend: this.backend,
};
await this.store
.createRecord('pki/tidy', this.payload)
.save({ adapterOptions: { tidyType: 'manual-tidy' } });
});
test('it calls the correct endpoint when tidyType = auto-tidy', async function (assert) {
assert.expect(1);
this.server.post(`${this.backend}/config/auto-tidy`, () => {
assert.ok(true, 'request made to correct endpoint on create');
return {};
});
this.payload = {
enabled: true,
interval_duration: '72h',
backend: this.backend,
};
await this.store
.createRecord('pki/tidy', this.payload)
.save({ adapterOptions: { tidyType: 'auto-tidy' } });
});
});