UI: VAULT-9409 Pki Tidy Form (#20043)
This commit is contained in:
parent
45737ddd3c
commit
6873c3c58e
|
@ -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) });
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
configuration.tidy
|
||||
<Page::PkiTidyForm @breadcrumbs={{this.breadcrumbs}} @tidy={{this.model}} @adapterOptions={{hash tidyType="manual-tidy"}} />
|
|
@ -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 () {
|
|
@ -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) {
|
||||
|
|
|
@ -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]',
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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' } });
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue