From 4da72c45ce1a6abedbdb642cf7396025fe1d2f1d Mon Sep 17 00:00:00 2001
From: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Date: Tue, 23 May 2023 16:05:15 -0700
Subject: [PATCH] UI: pki auto-tidy views (#20685)
* UI: plumbing for pki tidy work (#20611)
* update tidy model
* Dynamic group on tidy based on version
* UI: VAULT-16261 PKI autotidy config view (#20641)
* UI: VAULT-16203 tidy status page (#20635)
* ui: pki tidy form (#20630)
* order routes to match tabs
* add tidy routes
* add tidy-status page component
* update routes rename edit to configure, remove manage
* add page component to route template
* add comment
* finish routing
* change to queryRecord, delete old tidy file
* remove findRecord
* fix serializer name
* tidy.index only needs controller empty state logic
* build form and page components
* update tidy model
* alphabetize!
* revert model changes
* finish adapter
* move form out of page folder in tests
* refactor to accommodate model changes from chelseas pr
* WIP tests
* reuse shared fields in model
* finish tests
* update model hook and breadcrumbs
* remove subtext for checkbox
* fix tests add ACME fields
* Update ui/app/adapters/pki/tidy.js
* Update ui/app/adapters/pki/tidy.js
* refactor intervalDuration using feedback suggested
* move errors to second line, inside conditional brackets
* add ternary operator to allByKey attr
* surface error message
* make polling request longer
* UI: VAULT-16368 pki tidy custom method (#20696)
* ui: adds empty state and updates modal (#20695)
* add empty state to status page
* update tidy modal
* conditionally change cancel transition route for auto tidy form
* teeny copy update
* organize tidy-status conditoionals
* a couple more template cleanups
* fix conditional, change to settings
* UI: VAULT-16367 VAULT-16378 Tidy acceptance tests + tidy toolbar cleanup (#20698)
* update copy
* move tidyRevokedCertIssuerAssociations up to applicable section
* add tidy info to readme
* update copy
* UI: Add tidy as a tab to the error route (#20723)
* small cleanup items
* fix prettier
* cancel polling when we leave tidy.index (status view)
* revert changes to declaration file
* remove space
---------
Co-authored-by: Chelsea Shaw
Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
---
ui/app/adapters/pki/tidy.js | 52 +--
ui/app/models/pki/tidy.js | 165 ++++++++-
ui/app/serializers/pki/tidy.js | 17 +
.../core/addon/helpers/options-for-backend.js | 4 +
ui/lib/pki/README.md | 12 +
.../page/pki-configuration-details.hbs | 3 -
.../page/pki-tidy-auto-configure.hbs | 18 +
.../page/pki-tidy-auto-settings.hbs | 40 +++
.../addon/components/page/pki-tidy-form.hbs | 83 -----
.../addon/components/page/pki-tidy-manual.hbs | 18 +
.../addon/components/page/pki-tidy-status.hbs | 193 +++++++++++
.../addon/components/page/pki-tidy-status.ts | 157 +++++++++
ui/lib/pki/addon/components/pki-tidy-form.hbs | 80 +++++
.../components/{page => }/pki-tidy-form.ts | 39 ++-
ui/lib/pki/addon/controllers/tidy/index.js | 37 ++
ui/lib/pki/addon/routes.js | 32 +-
ui/lib/pki/addon/routes/application.js | 1 +
ui/lib/pki/addon/routes/error.js | 1 +
ui/lib/pki/addon/routes/tidy.js | 18 +
ui/lib/pki/addon/routes/tidy/auto.js | 8 +
.../pki/addon/routes/tidy/auto/configure.js | 23 ++
ui/lib/pki/addon/routes/tidy/auto/index.js | 20 ++
ui/lib/pki/addon/routes/tidy/index.js | 47 +++
.../{configuration/tidy.js => tidy/manual.js} | 7 +-
.../addon/templates/configuration/tidy.hbs | 1 -
ui/lib/pki/addon/templates/tidy.hbs | 1 +
ui/lib/pki/addon/templates/tidy/auto.hbs | 1 +
.../addon/templates/tidy/auto/configure.hbs | 1 +
.../pki/addon/templates/tidy/auto/index.hbs | 1 +
ui/lib/pki/addon/templates/tidy/index.hbs | 20 ++
ui/lib/pki/addon/templates/tidy/manual.hbs | 1 +
ui/lib/pki/package.json | 1 +
ui/public/images/pki-tidy.png | 3 +
.../pki/pki-engine-workflow-test.js | 26 +-
ui/tests/acceptance/pki/pki-tidy-test.js | 181 ++++++++++
ui/tests/helpers/pki/page/pki-tidy-form.js | 17 +-
ui/tests/helpers/pki/page/pki-tidy.js | 30 ++
ui/tests/helpers/pki/workflow.js | 1 +
.../pki/page/pki-tidy-auto-settings-test.js | 67 ++++
.../components/pki/page/pki-tidy-form-test.js | 58 ----
.../pki/page/pki-tidy-status-test.js | 114 +++++++
.../components/pki/pki-tidy-form-test.js | 315 ++++++++++++++++++
ui/tests/unit/adapters/pki/tidy-test.js | 49 ++-
.../ember-data/types/registries/adapter.d.ts | 2 +
ui/types/vault/adapters/pki/tidy.d.ts | 12 +
ui/types/vault/models/pki/tidy.d.ts | 29 ++
46 files changed, 1763 insertions(+), 243 deletions(-)
create mode 100644 ui/app/serializers/pki/tidy.js
create mode 100644 ui/lib/pki/addon/components/page/pki-tidy-auto-configure.hbs
create mode 100644 ui/lib/pki/addon/components/page/pki-tidy-auto-settings.hbs
delete mode 100644 ui/lib/pki/addon/components/page/pki-tidy-form.hbs
create mode 100644 ui/lib/pki/addon/components/page/pki-tidy-manual.hbs
create mode 100644 ui/lib/pki/addon/components/page/pki-tidy-status.hbs
create mode 100644 ui/lib/pki/addon/components/page/pki-tidy-status.ts
create mode 100644 ui/lib/pki/addon/components/pki-tidy-form.hbs
rename ui/lib/pki/addon/components/{page => }/pki-tidy-form.ts (56%)
create mode 100644 ui/lib/pki/addon/controllers/tidy/index.js
create mode 100644 ui/lib/pki/addon/routes/tidy.js
create mode 100644 ui/lib/pki/addon/routes/tidy/auto.js
create mode 100644 ui/lib/pki/addon/routes/tidy/auto/configure.js
create mode 100644 ui/lib/pki/addon/routes/tidy/auto/index.js
create mode 100644 ui/lib/pki/addon/routes/tidy/index.js
rename ui/lib/pki/addon/routes/{configuration/tidy.js => tidy/manual.js} (83%)
delete mode 100644 ui/lib/pki/addon/templates/configuration/tidy.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy/auto.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy/auto/configure.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy/auto/index.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy/index.hbs
create mode 100644 ui/lib/pki/addon/templates/tidy/manual.hbs
create mode 100644 ui/public/images/pki-tidy.png
create mode 100644 ui/tests/acceptance/pki/pki-tidy-test.js
create mode 100644 ui/tests/helpers/pki/page/pki-tidy.js
create mode 100644 ui/tests/integration/components/pki/page/pki-tidy-auto-settings-test.js
delete mode 100644 ui/tests/integration/components/pki/page/pki-tidy-form-test.js
create mode 100644 ui/tests/integration/components/pki/page/pki-tidy-status-test.js
create mode 100644 ui/tests/integration/components/pki/pki-tidy-form-test.js
create mode 100644 ui/types/vault/adapters/pki/tidy.d.ts
create mode 100644 ui/types/vault/models/pki/tidy.d.ts
diff --git a/ui/app/adapters/pki/tidy.js b/ui/app/adapters/pki/tidy.js
index 6657f8368..41ea1be66 100644
--- a/ui/app/adapters/pki/tidy.js
+++ b/ui/app/adapters/pki/tidy.js
@@ -2,35 +2,49 @@
* 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';
+import { encodePath } from 'vault/utils/path-encoding-helpers';
export default class PkiTidyAdapter extends ApplicationAdapter {
namespace = 'v1';
- urlForCreateRecord(snapshot) {
+ _baseUrl(backend) {
+ return `${this.buildURL()}/${encodePath(backend)}`;
+ }
+
+ // single tidy operations (manual) are always a new record
+ createRecord(store, type, snapshot) {
const { backend } = snapshot.record;
const { tidyType } = snapshot.adapterOptions;
-
- if (!backend) {
- throw new Error('Backend missing');
+ if (tidyType === 'auto') {
+ throw new Error('Auto tidy type models are never new, please use findRecord');
}
- 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');
- }
+ const url = `${this._baseUrl(backend)}/tidy`;
+ return this.ajax(url, 'POST', { data: this.serialize(snapshot, tidyType) });
}
- createRecord(store, type, snapshot) {
- const url = this.urlForCreateRecord(snapshot);
- return this.ajax(url, 'POST', { data: this.serialize(snapshot) });
+ // saving auto-tidy config POST requests will always update
+ updateRecord(store, type, snapshot) {
+ const backend = snapshot.record.id;
+ const { tidyType } = snapshot.adapterOptions;
+ if (tidyType === 'manual') {
+ throw new Error('Manual tidy type models are always new, please use createRecord');
+ }
+
+ const url = `${this._baseUrl(backend)}/config/auto-tidy`;
+ return this.ajax(url, 'POST', { data: this.serialize(snapshot, tidyType) });
+ }
+
+ findRecord(store, type, backend) {
+ // only auto-tidy will ever be read, no need to pass the type here
+ return this.ajax(`${this._baseUrl(backend)}/config/auto-tidy`, 'GET').then((resp) => {
+ return resp.data;
+ });
+ }
+
+ cancelTidy(backend) {
+ const url = `${this._baseUrl(backend)}`;
+ return this.ajax(`${url}/tidy-cancel`, 'POST');
}
}
diff --git a/ui/app/models/pki/tidy.js b/ui/app/models/pki/tidy.js
index e1c722a1a..4083a10e6 100644
--- a/ui/app/models/pki/tidy.js
+++ b/ui/app/models/pki/tidy.js
@@ -4,9 +4,168 @@
*/
import Model, { attr } from '@ember-data/model';
+import { service } from '@ember/service';
+import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes';
+@withExpandedAttributes()
export default class PkiTidyModel extends Model {
- @attr('boolean', { defaultValue: false }) tidyCertStore;
- @attr('boolean', { defaultValue: false }) tidyRevocationQueue;
- @attr('string', { defaultValue: '72h' }) safetyBuffer;
+ // the backend mount is the model id, only one pki/tidy model will ever persist (the auto-tidy config)
+ @service version;
+
+ @attr({
+ label: 'Tidy ACME enabled',
+ labelDisabled: 'Tidy ACME disabled',
+ mapToBoolean: 'tidyAcme',
+ helperTextDisabled: 'Tidying of ACME accounts, orders and authorizations is disabled',
+ helperTextEnabled:
+ 'The amount of time that must pass after creation that an account with no orders is marked revoked, and the amount of time after being marked revoked or deactivated.',
+ detailsLabel: 'ACME account safety buffer',
+ formatTtl: true,
+ })
+ acmeAccountSafetyBuffer;
+
+ @attr('boolean', {
+ label: 'Tidy ACME',
+ defaultValue: false,
+ })
+ tidyAcme;
+
+ @attr('boolean', {
+ label: 'Automatic tidy enabled',
+ defaultValue: false,
+ })
+ enabled; // auto-tidy only
+
+ @attr({
+ label: 'Automatic tidy enabled',
+ labelDisabled: 'Automatic tidy disabled',
+ mapToBoolean: 'enabled',
+ helperTextEnabled:
+ 'Sets the interval_duration between automatic tidy operations; note that this is from the end of one operation to the start of the next.',
+ helperTextDisabled: 'Automatic tidy operations will not run.',
+ detailsLabel: 'Automatic tidy duration',
+ formatTtl: true,
+ })
+ intervalDuration; // auto-tidy only
+
+ @attr('string', {
+ editType: 'ttl',
+ helperTextEnabled:
+ 'Specifies a duration that issuers should be kept for, past their NotAfter validity period. Defaults to 365 days (8760 hours).',
+ hideToggle: true,
+ formatTtl: true,
+ })
+ issuerSafetyBuffer;
+
+ @attr('string', {
+ editType: 'ttl',
+ helperTextEnabled:
+ 'Specifies the duration to pause between tidying individual certificates. This releases the revocation lock and allows other operations to continue while tidy is running.',
+ hideToggle: true,
+ formatTtl: true,
+ })
+ pauseDuration;
+
+ @attr('string', {
+ editType: 'ttl',
+ helperTextEnabled:
+ 'Specifies a duration after which cross-cluster revocation requests will be removed as expired.',
+ hideToggle: true,
+ formatTtl: true,
+ })
+ revocationQueueSafetyBuffer; // enterprise only
+
+ @attr('string', {
+ editType: 'ttl',
+ 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. Defaults to 72 hours.',
+ hideToggle: true,
+ formatTtl: true,
+ })
+ safetyBuffer;
+
+ @attr('boolean', { label: 'Tidy the certificate store' })
+ tidyCertStore;
+
+ @attr('boolean', {
+ label: 'Tidy cross-cluster revoked certificates',
+ subText: 'Remove expired, cross-cluster revocation entries.',
+ })
+ tidyCrossClusterRevokedCerts; // enterprise only
+
+ @attr('boolean', {
+ subText: 'Automatically remove expired issuers after the issuer safety buffer duration has elapsed.',
+ })
+ tidyExpiredIssuers;
+
+ @attr('boolean', {
+ label: 'Tidy legacy CA bundle',
+ subText:
+ 'Backup any legacy CA/issuers bundle (from Vault versions earlier than 1.11) to config/ca_bundle.bak. Migration will only occur after issuer safety buffer has passed.',
+ })
+ tidyMoveLegacyCaBundle;
+
+ @attr('boolean', {
+ label: 'Tidy cross-cluster revocation requests',
+ })
+ tidyRevocationQueue; // enterprise only
+
+ @attr('boolean', {
+ label: 'Tidy revoked certificate issuer associations',
+ })
+ tidyRevokedCertIssuerAssociations;
+
+ @attr('boolean', {
+ label: 'Tidy revoked certificates',
+ subText: 'Remove all invalid and expired certificates from storage.',
+ })
+ tidyRevokedCerts;
+
+ get useOpenAPI() {
+ return true;
+ }
+
+ getHelpUrl(backend) {
+ return `/v1/${backend}/config/auto-tidy?help=1`;
+ }
+
+ get allGroups() {
+ const groups = [{ autoTidy: ['enabled', 'intervalDuration'] }, ...this.sharedFields];
+ return this._expandGroups(groups);
+ }
+
+ // shared between auto and manual tidy operations
+ get sharedFields() {
+ const groups = [
+ {
+ 'Universal operations': [
+ 'tidyCertStore',
+ 'tidyRevokedCerts',
+ 'tidyRevokedCertIssuerAssociations',
+ 'safetyBuffer',
+ 'pauseDuration',
+ ],
+ },
+ {
+ 'ACME operations': ['tidyAcme', 'acmeAccountSafetyBuffer'],
+ },
+ {
+ 'Issuer operations': ['tidyExpiredIssuers', 'tidyMoveLegacyCaBundle', 'issuerSafetyBuffer'],
+ },
+ ];
+ if (this.version.isEnterprise) {
+ groups.push({
+ 'Cross-cluster operations': [
+ 'tidyRevocationQueue',
+ 'tidyCrossClusterRevokedCerts',
+ 'revocationQueueSafetyBuffer',
+ ],
+ });
+ }
+ return groups;
+ }
+
+ get formFieldGroups() {
+ return this._expandGroups(this.sharedFields);
+ }
}
diff --git a/ui/app/serializers/pki/tidy.js b/ui/app/serializers/pki/tidy.js
new file mode 100644
index 000000000..77f2a9871
--- /dev/null
+++ b/ui/app/serializers/pki/tidy.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import ApplicationSerializer from '../application';
+
+export default class PkiTidySerializer extends ApplicationSerializer {
+ serialize(snapshot, tidyType) {
+ const data = super.serialize(snapshot);
+ if (tidyType === 'manual') {
+ delete data?.enabled;
+ delete data?.intervalDuration;
+ }
+ return data;
+ }
+}
diff --git a/ui/lib/core/addon/helpers/options-for-backend.js b/ui/lib/core/addon/helpers/options-for-backend.js
index c1374ec2d..075936f0b 100644
--- a/ui/lib/core/addon/helpers/options-for-backend.js
+++ b/ui/lib/core/addon/helpers/options-for-backend.js
@@ -38,6 +38,10 @@ const PKI_ENGINE_BACKEND = {
label: 'Certificates',
link: 'certificates',
},
+ {
+ label: 'Tidy',
+ link: 'tidy',
+ },
{
label: 'Configuration',
link: 'configuration',
diff --git a/ui/lib/pki/README.md b/ui/lib/pki/README.md
index e019cfaae..0f1895cbd 100644
--- a/ui/lib/pki/README.md
+++ b/ui/lib/pki/README.md
@@ -32,6 +32,18 @@ If you couldn't tell from the documentation above, PKI is _complex_. As such, th
The `parsedCertificate` attribute is an object that houses all of the parsed certificate data returned by the [parse-pki-cert.js](../../app/utils/parse-pki-cert.js) util.
+- ### [pki/tidy](../../app/models/pki/tidy.js)
+
+ This model is used to manage [tidy](https://developer.hashicorp.com/vault/api-docs/secret/pki#tidy) operations in a few different contexts. All of the following endpoints share the same parameters _except_ `enabled` and `interval_duration` which are reserved for auto-tidy operations only.
+
+ > _`pki/tidy-status` does not use an Ember data model because it is read-only_
+
+ - `POST pki/tidy` - perform a single, manual tidy operation
+ - `POST pki/config/auto-tidy` - set configuration for automating the tidy process
+ - `GET pki/config/auto-tidy` - read auto-tidy configuration settings
+
+ The auto-tidy config is the only data that persists so `findRecord` and `updateRecord` in the `pki/tidy.js` [adapter](../../app/adapters/pki/tidy.js) only interact with the `/config/auto-tidy` endpoint. For each manual tidy operation, a new record is created so on `save()` the model uses the `createRecord` method which only ever uses the `/tidy` endpoint.
+
> _The following models more closely follow a CRUD pattern:_
- ### [pki/issuer](../../app/models/pki/issuer.js)
diff --git a/ui/lib/pki/addon/components/page/pki-configuration-details.hbs b/ui/lib/pki/addon/components/page/pki-configuration-details.hbs
index d6920bdfd..c10a86bec 100644
--- a/ui/lib/pki/addon/components/page/pki-configuration-details.hbs
+++ b/ui/lib/pki/addon/components/page/pki-configuration-details.hbs
@@ -14,9 +14,6 @@
{{/if}}
-
- Tidy
-
Edit configuration
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-auto-configure.hbs b/ui/lib/pki/addon/components/page/pki-tidy-auto-configure.hbs
new file mode 100644
index 000000000..a7c35a25e
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-tidy-auto-configure.hbs
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ Configure automatic tidy
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-auto-settings.hbs b/ui/lib/pki/addon/components/page/pki-tidy-auto-settings.hbs
new file mode 100644
index 000000000..de5d76d20
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-tidy-auto-settings.hbs
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ Automatic tidy configuration
+
+
+
+
+
+
+
+ Edit auto-tidy
+
+
+
+
+
+
+ {{#each @model.allGroups as |group|}}
+ {{#each-in group as |label fields|}}
+ {{#if (not-eq label "autoTidy")}}
+
+ {{label}}
+
+ {{/if}}
+
+ {{#each fields as |attr|}}
+
+ {{/each}}
+ {{/each-in}}
+ {{/each}}
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-form.hbs b/ui/lib/pki/addon/components/page/pki-tidy-form.hbs
deleted file mode 100644
index 6573aca76..000000000
--- a/ui/lib/pki/addon/components/page/pki-tidy-form.hbs
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
-
- Tidy
-
-
-
-
-
-
-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.
-
-
-
-
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-manual.hbs b/ui/lib/pki/addon/components/page/pki-tidy-manual.hbs
new file mode 100644
index 000000000..592919c94
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-tidy-manual.hbs
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ Manual tidy
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-status.hbs b/ui/lib/pki/addon/components/page/pki-tidy-status.hbs
new file mode 100644
index 000000000..651f00071
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-tidy-status.hbs
@@ -0,0 +1,193 @@
+
+
+
+ {{#if @autoTidyConfig.enabled}}
+
+ Auto-tidy configuration
+
+
+ Perform manual tidy
+
+ {{else}}
+
+ Tidy
+
+
+ {{/if}}
+
+
+
+{{#if this.hasTidyConfig}}
+
+ {{this.tidyStateAlertBanner.title}}
+ {{this.tidyStateAlertBanner.message}}
+ {{#if this.tidyStateAlertBanner.shouldShowCancelTidy}}
+
+ {{/if}}
+ {{#if @tidyStatus.responseTimestamp}}
+
+ Updated
+ {{date-format @tidyStatus.responseTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
+
+ {{/if}}
+
+
+ {{#each this.tidyStatusGeneralFields as |attr|}}
+
+ {{/each}}
+
+
+ {{if (eq this.tidyState "Running") "Current" "Last"}}
+ tidy settings
+
+ {{#each this.tidyStatusConfigFields as |attr|}}
+
+ {{/each}}
+
+ {{#if this.isEnterprise}}
+ {{#each this.crossClusterOperation as |attr|}}
+
+ {{/each}}
+ {{/if}}
+{{else}}
+
+
+ Tidy
+
+
+{{/if}}
+
+{{! TIDY OPTIONS MODAL }}
+
+
+ How tidying will work
+
+ 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.
+
+ Documentation.
+
+
+
+
+ Select a tidy operation:
+
+ Automatic tidy
+ periodically runs a tidy operation with saved configuration settings after waiting the specified interval
+ duration between tidies
+
+ Manual tidy runs a tidy operation once
+
+ Configure the parameters that determine how to tidy and run the operation.
+
+
+
+
+
+
+
+
+
+{{! CANCEL TIDY CONFIRMATION MODAL }}
+{{#if this.confirmCancelTidy}}
+
+
+ This will cancel the tidy at the next available checkpoint, which may process additional certificates between when the
+ operation was marked as cancelled and when the operation stopped.
+ Click “Confirm” to cancel the running tidy operation.
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-status.ts b/ui/lib/pki/addon/components/page/pki-tidy-status.ts
new file mode 100644
index 000000000..d15b8ad50
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-tidy-status.ts
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency';
+import { waitFor } from '@ember/test-waiters';
+import errorMessage from 'vault/utils/error-message';
+
+import type Store from '@ember-data/store';
+import type SecretMountPath from 'vault/services/secret-mount-path';
+import type FlashMessageService from 'vault/services/flash-messages';
+import type VersionService from 'vault/services/version';
+import type PkiTidyModel from 'vault/models/pki/tidy';
+import type RouterService from '@ember/routing/router-service';
+
+interface Args {
+ autoTidyConfig: PkiTidyModel;
+ tidyStatus: TidyStatusParams;
+}
+
+interface TidyStatusParams {
+ safety_buffer: number;
+ tidy_cert_store: boolean;
+ tidy_revoked_certs: boolean;
+ state: string;
+ error: string;
+ time_started: string | null;
+ time_finished: string | null;
+ message: string;
+ cert_store_deleted_count: number;
+ revoked_cert_deleted_count: number;
+ missing_issuer_cert_count: number;
+ tidy_expired_issuers: boolean;
+ issuer_safety_buffer: string;
+ tidy_move_legacy_ca_bundle: boolean;
+ tidy_revocation_queue: boolean;
+ revocation_queue_deleted_count: number;
+ tidy_cross_cluster_revoked_certs: boolean;
+ cross_revoked_cert_deleted_count: number;
+ revocation_queue_safety_buffer: string;
+}
+
+export default class PkiTidyStatusComponent extends Component {
+ @service declare readonly store: Store;
+ @service declare readonly secretMountPath: SecretMountPath;
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly version: VersionService;
+ @service declare readonly router: RouterService;
+
+ @tracked tidyOptionsModal = false;
+ @tracked confirmCancelTidy = false;
+
+ tidyStatusGeneralFields = [
+ 'time_started',
+ 'time_finished',
+ 'last_auto_tidy_finished',
+ 'cert_store_deleted_count',
+ 'missing_issuer_cert_count',
+ 'revocation_queue_deleted_count',
+ ];
+
+ tidyStatusConfigFields = [
+ 'tidy_cert_store',
+ 'tidy_revocation_queue',
+ 'tidy_cross_cluster_revoked_certs',
+ 'safety_buffer',
+ 'pause_duration',
+ 'tidy_expired_issuers',
+ 'tidy_move_legacy_ca_bundle',
+ 'issuer_safety_buffer',
+ ];
+
+ crossClusterOperation = ['tidy_revocation_queue', 'revocation_queue_safety_buffer'];
+
+ get isEnterprise() {
+ return this.version.isEnterprise;
+ }
+
+ get tidyState() {
+ return this.args.tidyStatus?.state;
+ }
+
+ get hasTidyConfig() {
+ return !this.tidyStatusConfigFields.every(
+ (attr) => this.args.tidyStatus[attr as keyof TidyStatusParams] === null
+ );
+ }
+
+ get tidyStateAlertBanner() {
+ const tidyState = this.tidyState;
+
+ switch (tidyState) {
+ case 'Inactive':
+ return {
+ color: 'highlight',
+ title: 'Tidy is inactive',
+ message: this.args.tidyStatus?.message,
+ };
+ case 'Running':
+ return {
+ color: 'highlight',
+ title: 'Tidy in progress',
+ message: this.args.tidyStatus?.message,
+ shouldShowCancelTidy: true,
+ };
+ case 'Finished':
+ return {
+ color: 'success',
+ title: 'Tidy operation finished',
+ message: this.args.tidyStatus?.message,
+ };
+ case 'Error':
+ return {
+ color: 'warning',
+ title: 'Tidy operation failed',
+ message: this.args.tidyStatus?.error,
+ };
+ case 'Cancelling':
+ return {
+ color: 'warning',
+ title: 'Tidy operation cancelling',
+ icon: 'loading',
+ };
+ case 'Cancelled':
+ return {
+ color: 'warning',
+ title: 'Tidy operation cancelled',
+ message:
+ 'Your tidy operation has been cancelled. If this was a mistake configure and run another tidy operation.',
+ };
+ default:
+ return {
+ color: 'warning',
+ title: 'Tidy status not found',
+ message: "The system reported no tidy status. It's recommended to perform a new tidy operation.",
+ };
+ }
+ }
+
+ @task
+ @waitFor
+ *cancelTidy() {
+ try {
+ const tidyAdapter = this.store.adapterFor('pki/tidy');
+ yield tidyAdapter.cancelTidy(this.secretMountPath.currentPath);
+ this.router.transitionTo('vault.cluster.secrets.backend.pki.tidy');
+ } catch (error) {
+ this.flashMessages.danger(errorMessage(error));
+ } finally {
+ this.confirmCancelTidy = false;
+ }
+ }
+}
diff --git a/ui/lib/pki/addon/components/pki-tidy-form.hbs b/ui/lib/pki/addon/components/pki-tidy-form.hbs
new file mode 100644
index 000000000..2a3d6e8bf
--- /dev/null
+++ b/ui/lib/pki/addon/components/pki-tidy-form.hbs
@@ -0,0 +1,80 @@
+
+
+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.
+ Learn more
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-tidy-form.ts b/ui/lib/pki/addon/components/pki-tidy-form.ts
similarity index 56%
rename from ui/lib/pki/addon/components/page/pki-tidy-form.ts
rename to ui/lib/pki/addon/components/pki-tidy-form.ts
index 0b8e62237..ca5ee1a65 100644
--- a/ui/lib/pki/addon/components/page/pki-tidy-form.ts
+++ b/ui/lib/pki/addon/components/pki-tidy-form.ts
@@ -4,18 +4,31 @@
*/
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 errorMessage from 'vault/utils/error-message';
-import type PkiTidyModel from 'vault/models/pki/tidy';
+
import type RouterService from '@ember/routing/router-service';
+import type PkiTidyModel from 'vault/models/pki/tidy';
+import type { FormField, TtlEvent } from 'vault/app-types';
interface Args {
tidy: PkiTidyModel;
- adapterOptions: object;
+ tidyType: string;
+ onSave: CallableFunction;
+ onCancel: CallableFunction;
+}
+
+interface PkiTidyTtls {
+ intervalDuration: string;
+ acmeAccountSafetyBuffer: string;
+}
+interface PkiTidyBooleans {
+ enabled: boolean;
+ tidyAcme: boolean;
}
export default class PkiTidyForm extends Component {
@@ -24,13 +37,8 @@ export default class PkiTidyForm extends Component {
@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;
+ get intervalDurationAttr() {
+ return this.args.tidy?.allByKey.intervalDuration;
}
@task
@@ -38,8 +46,8 @@ export default class PkiTidyForm extends Component {
*save(event: Event) {
event.preventDefault();
try {
- yield this.args.tidy.save({ adapterOptions: this.args.adapterOptions });
- this.returnToConfiguration();
+ yield this.args.tidy.save({ adapterOptions: { tidyType: this.args.tidyType } });
+ this.args.onSave();
} catch (e) {
this.errorBanner = errorMessage(e);
this.invalidFormAlert = 'There was an error submitting this form.';
@@ -47,7 +55,10 @@ export default class PkiTidyForm extends Component {
}
@action
- cancel() {
- this.returnToConfiguration();
+ handleTtl(attr: FormField, e: TtlEvent) {
+ const { enabled, goSafeTimeString } = e;
+ const ttlAttr = attr.name;
+ this.args.tidy[ttlAttr as keyof PkiTidyTtls] = goSafeTimeString;
+ this.args.tidy[attr.options.mapToBoolean as keyof PkiTidyBooleans] = enabled;
}
}
diff --git a/ui/lib/pki/addon/controllers/tidy/index.js b/ui/lib/pki/addon/controllers/tidy/index.js
new file mode 100644
index 000000000..ffa096286
--- /dev/null
+++ b/ui/lib/pki/addon/controllers/tidy/index.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import Ember from 'ember';
+import Controller from '@ember/controller';
+import { task, timeout } from 'ember-concurrency';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+
+const POLL_INTERVAL_MS = 5000;
+
+export default class PkiTidyIndexController extends Controller {
+ @service store;
+ @service secretMountPath;
+
+ @tracked tidyStatus = null;
+
+ // this task is cancelled by resetController() upon leaving the pki.tidy.index route
+ @task
+ *pollTidyStatus() {
+ while (true) {
+ // when testing, the polling loop causes promises to never settle so acceptance tests hang
+ // to get around that, we just disable the poll in tests
+ if (Ember.testing) {
+ return;
+ }
+ yield timeout(POLL_INTERVAL_MS);
+ try {
+ const tidyStatusResponse = yield this.fetchTidyStatus();
+ this.tidyStatus = tidyStatusResponse;
+ } catch (e) {
+ // we want to keep polling here
+ }
+ }
+ }
+}
diff --git a/ui/lib/pki/addon/routes.js b/ui/lib/pki/addon/routes.js
index 1768327c0..6429a155f 100644
--- a/ui/lib/pki/addon/routes.js
+++ b/ui/lib/pki/addon/routes.js
@@ -7,12 +7,6 @@ import buildRoutes from 'ember-engines/routes';
export default buildRoutes(function () {
this.route('overview');
- this.route('configuration', function () {
- this.route('index', { path: '/' });
- this.route('tidy');
- this.route('create');
- this.route('edit');
- });
this.route('roles', function () {
this.route('index', { path: '/' });
this.route('create');
@@ -36,13 +30,6 @@ export default buildRoutes(function () {
this.route('rotate-root');
});
});
- this.route('certificates', function () {
- this.route('index', { path: '/' });
- this.route('certificate', { path: '/:serial' }, function () {
- this.route('details');
- this.route('edit');
- });
- });
this.route('keys', function () {
this.route('index', { path: '/' });
this.route('create');
@@ -52,4 +39,23 @@ export default buildRoutes(function () {
this.route('edit');
});
});
+ this.route('certificates', function () {
+ this.route('index', { path: '/' });
+ this.route('certificate', { path: '/:serial' }, function () {
+ this.route('details');
+ this.route('edit');
+ });
+ });
+ this.route('tidy', function () {
+ this.route('index', { path: '/' });
+ this.route('auto', function () {
+ this.route('configure');
+ });
+ this.route('manual');
+ });
+ this.route('configuration', function () {
+ this.route('index', { path: '/' });
+ this.route('create');
+ this.route('edit');
+ });
});
diff --git a/ui/lib/pki/addon/routes/application.js b/ui/lib/pki/addon/routes/application.js
index ac5472eb0..05225c6a9 100644
--- a/ui/lib/pki/addon/routes/application.js
+++ b/ui/lib/pki/addon/routes/application.js
@@ -24,6 +24,7 @@ export default class PkiRoute extends Route {
signCsr: this.pathHelp.getNewModel('pki/sign-intermediate', mountPath),
certGenerate: this.pathHelp.getNewModel('pki/certificate/generate', mountPath),
certSign: this.pathHelp.getNewModel('pki/certificate/sign', mountPath),
+ tidy: this.pathHelp.getNewModel('pki/tidy', mountPath),
});
}
}
diff --git a/ui/lib/pki/addon/routes/error.js b/ui/lib/pki/addon/routes/error.js
index a9173e53c..071a7f726 100644
--- a/ui/lib/pki/addon/routes/error.js
+++ b/ui/lib/pki/addon/routes/error.js
@@ -21,6 +21,7 @@ export default class PkiRolesErrorRoute extends Route {
{ label: 'Issuers', route: 'issuers.index' },
{ label: 'Keys', route: 'keys.index' },
{ label: 'Certificates', route: 'certificates.index' },
+ { label: 'Tidy', route: 'tidy.index' },
{ label: 'Configuration', route: 'configuration.index' },
];
controller.title = this.secretMountPath.currentPath;
diff --git a/ui/lib/pki/addon/routes/tidy.js b/ui/lib/pki/addon/routes/tidy.js
new file mode 100644
index 000000000..d82b255c9
--- /dev/null
+++ b/ui/lib/pki/addon/routes/tidy.js
@@ -0,0 +1,18 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { withConfig } from 'pki/decorators/check-config';
+import { hash } from 'rsvp';
+
+@withConfig()
+export default class PkiTidyRoute extends Route {
+ @service store;
+
+ model() {
+ const engine = this.modelFor('application');
+ return hash({
+ hasConfig: this.shouldPromptConfig,
+ engine,
+ autoTidyConfig: this.store.findRecord('pki/tidy', engine.id),
+ });
+ }
+}
diff --git a/ui/lib/pki/addon/routes/tidy/auto.js b/ui/lib/pki/addon/routes/tidy/auto.js
new file mode 100644
index 000000000..d52962cb4
--- /dev/null
+++ b/ui/lib/pki/addon/routes/tidy/auto.js
@@ -0,0 +1,8 @@
+import Route from '@ember/routing/route';
+
+export default class PkiTidyAutoRoute extends Route {
+ model() {
+ const { autoTidyConfig } = this.modelFor('tidy');
+ return autoTidyConfig;
+ }
+}
diff --git a/ui/lib/pki/addon/routes/tidy/auto/configure.js b/ui/lib/pki/addon/routes/tidy/auto/configure.js
new file mode 100644
index 000000000..d119b5c28
--- /dev/null
+++ b/ui/lib/pki/addon/routes/tidy/auto/configure.js
@@ -0,0 +1,23 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { withConfirmLeave } from 'core/decorators/confirm-leave';
+
+@withConfirmLeave()
+export default class PkiTidyAutoConfigureRoute extends Route {
+ @service store;
+ @service secretMountPath;
+
+ // inherits model from tidy/auto
+
+ 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', route: 'tidy' },
+ { label: 'auto', route: 'tidy.auto' },
+ { label: 'configure' },
+ ];
+ }
+}
diff --git a/ui/lib/pki/addon/routes/tidy/auto/index.js b/ui/lib/pki/addon/routes/tidy/auto/index.js
new file mode 100644
index 000000000..bab854823
--- /dev/null
+++ b/ui/lib/pki/addon/routes/tidy/auto/index.js
@@ -0,0 +1,20 @@
+import Route from '@ember/routing/route';
+import { service } from '@ember/service';
+
+export default class TidyAutoIndexRoute extends Route {
+ @service secretMountPath;
+ @service store;
+
+ // inherits model from tidy/auto
+
+ setupController(controller) {
+ super.setupController(...arguments);
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: this.secretMountPath.currentPath, route: 'overview' },
+ { label: 'tidy', route: 'tidy.index' },
+ { label: 'auto' },
+ ];
+ controller.title = this.secretMountPath.currentPath;
+ }
+}
diff --git a/ui/lib/pki/addon/routes/tidy/index.js b/ui/lib/pki/addon/routes/tidy/index.js
new file mode 100644
index 000000000..335b713c8
--- /dev/null
+++ b/ui/lib/pki/addon/routes/tidy/index.js
@@ -0,0 +1,47 @@
+import Route from '@ember/routing/route';
+import { PKI_DEFAULT_EMPTY_STATE_MSG } from '../overview';
+import { hash } from 'rsvp';
+import { inject as service } from '@ember/service';
+import timestamp from 'core/utils/timestamp';
+
+export default class PkiTidyIndexRoute extends Route {
+ @service store;
+ @service secretMountPath;
+
+ async fetchTidyStatus() {
+ const adapter = this.store.adapterFor('application');
+ const tidyStatusResponse = await adapter.ajax(
+ `/v1/${this.secretMountPath.currentPath}/tidy-status`,
+ 'GET'
+ );
+ const responseTimestamp = timestamp.now();
+ tidyStatusResponse.data.responseTimestamp = responseTimestamp;
+ return tidyStatusResponse.data;
+ }
+
+ model() {
+ const { hasConfig, autoTidyConfig, engine } = this.modelFor('tidy');
+
+ return hash({
+ tidyStatus: this.fetchTidyStatus(),
+ hasConfig,
+ autoTidyConfig,
+ engine,
+ });
+ }
+
+ setupController(controller, resolvedModel) {
+ super.setupController(controller, resolvedModel);
+ controller.notConfiguredMessage = PKI_DEFAULT_EMPTY_STATE_MSG;
+
+ controller.tidyStatus = resolvedModel.tidyStatus;
+ controller.fetchTidyStatus = this.fetchTidyStatus;
+ controller.pollTidyStatus.perform();
+ }
+
+ resetController(controller, isExiting) {
+ if (isExiting) {
+ controller.pollTidyStatus.cancelAll();
+ }
+ }
+}
diff --git a/ui/lib/pki/addon/routes/configuration/tidy.js b/ui/lib/pki/addon/routes/tidy/manual.js
similarity index 83%
rename from ui/lib/pki/addon/routes/configuration/tidy.js
rename to ui/lib/pki/addon/routes/tidy/manual.js
index 914123708..48f2ecd05 100644
--- a/ui/lib/pki/addon/routes/configuration/tidy.js
+++ b/ui/lib/pki/addon/routes/tidy/manual.js
@@ -7,8 +7,8 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
-@withConfirmLeave('model.tidy')
-export default class PkiConfigurationTidyRoute extends Route {
+@withConfirmLeave()
+export default class PkiTidyManualRoute extends Route {
@service store;
@service secretMountPath;
@@ -22,7 +22,8 @@ export default class PkiConfigurationTidyRoute extends Route {
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: this.secretMountPath.currentPath, route: 'overview' },
{ label: 'configuration', route: 'configuration.index' },
- { label: 'tidy' },
+ { label: 'tidy', route: 'tidy' },
+ { label: 'manual' },
];
}
}
diff --git a/ui/lib/pki/addon/templates/configuration/tidy.hbs b/ui/lib/pki/addon/templates/configuration/tidy.hbs
deleted file mode 100644
index 24d7612bb..000000000
--- a/ui/lib/pki/addon/templates/configuration/tidy.hbs
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy.hbs b/ui/lib/pki/addon/templates/tidy.hbs
new file mode 100644
index 000000000..e2147cab0
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy.hbs
@@ -0,0 +1 @@
+{{outlet}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy/auto.hbs b/ui/lib/pki/addon/templates/tidy/auto.hbs
new file mode 100644
index 000000000..e2147cab0
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy/auto.hbs
@@ -0,0 +1 @@
+{{outlet}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy/auto/configure.hbs b/ui/lib/pki/addon/templates/tidy/auto/configure.hbs
new file mode 100644
index 000000000..cf2da60b4
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy/auto/configure.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy/auto/index.hbs b/ui/lib/pki/addon/templates/tidy/auto/index.hbs
new file mode 100644
index 000000000..82aec5f6f
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy/auto/index.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy/index.hbs b/ui/lib/pki/addon/templates/tidy/index.hbs
new file mode 100644
index 000000000..608d5b498
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy/index.hbs
@@ -0,0 +1,20 @@
+
+{{#if this.model.hasConfig}}
+
+{{else}}
+
+
+
+ Configure PKI
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/templates/tidy/manual.hbs b/ui/lib/pki/addon/templates/tidy/manual.hbs
new file mode 100644
index 000000000..b751914f5
--- /dev/null
+++ b/ui/lib/pki/addon/templates/tidy/manual.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/lib/pki/package.json b/ui/lib/pki/package.json
index 5f86b4715..873f81402 100644
--- a/ui/lib/pki/package.json
+++ b/ui/lib/pki/package.json
@@ -8,6 +8,7 @@
"ember-cli-babel": "*",
"ember-cli-htmlbars": "*",
"ember-cli-typescript": "*",
+ "@hashicorp/design-system-components": "*",
"@types/ember": "latest",
"@types/ember-data": "latest",
"@types/ember-data__adapter": "latest",
diff --git a/ui/public/images/pki-tidy.png b/ui/public/images/pki-tidy.png
new file mode 100644
index 000000000..ecd3b559a
--- /dev/null
+++ b/ui/public/images/pki-tidy.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c6ece7474ab26dd5f30d746ac9241464e7bfe8fe05fd6f0f11eed1d1080db003
+size 31153
diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js
index 5f1d21250..5bd1a97e6 100644
--- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js
+++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js
@@ -42,7 +42,7 @@ module('Acceptance | pki workflow', function (hooks) {
});
test('empty state messages are correct when PKI not configured', async function (assert) {
- assert.expect(17);
+ assert.expect(21);
const assertEmptyState = (assert, resource) => {
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/${resource}`);
assert
@@ -71,9 +71,10 @@ module('Acceptance | pki workflow', function (hooks) {
await click(SELECTORS.certsTab);
assertEmptyState(assert, 'certificates');
-
await click(SELECTORS.keysTab);
assertEmptyState(assert, 'keys');
+ await click(SELECTORS.tidyTab);
+ assertEmptyState(assert, 'tidy');
});
module('roles', function (hooks) {
@@ -443,27 +444,6 @@ 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) {
diff --git a/ui/tests/acceptance/pki/pki-tidy-test.js b/ui/tests/acceptance/pki/pki-tidy-test.js
new file mode 100644
index 000000000..4d6907c92
--- /dev/null
+++ b/ui/tests/acceptance/pki/pki-tidy-test.js
@@ -0,0 +1,181 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { click, currentRouteName, fillIn, visit } from '@ember/test-helpers';
+
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { v4 as uuidv4 } from 'uuid';
+
+import authPage from 'vault/tests/pages/auth';
+import logout from 'vault/tests/pages/logout';
+import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
+import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
+import { SELECTORS } from 'vault/tests/helpers/pki/page/pki-tidy';
+
+module('Acceptance | pki tidy', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(async function () {
+ await authPage.login();
+ // Setup PKI engine
+ const mountPath = `pki-workflow-${uuidv4()}`;
+ await enablePage.enable('pki', mountPath);
+ this.mountPath = mountPath;
+ await runCommands([
+ `write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test" name="Hashicorp Test"`,
+ ]);
+ await logout.visit();
+ });
+
+ hooks.afterEach(async function () {
+ await logout.visit();
+ await authPage.login();
+ // Cleanup engine
+ await runCommands([`delete sys/mounts/${this.mountPath}`]);
+ await logout.visit();
+ });
+
+ test('it configures a manual tidy operation and shows its details and tidy states', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ await click(SELECTORS.tidyEmptyStateConfigure);
+ assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
+ assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists');
+ assert
+ .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton)
+ .exists('Configure manual tidy button exists');
+ await click(SELECTORS.tidyConfigureModal.tidyModalManualButton);
+ assert.dom(SELECTORS.tidyForm.tidyFormName('manual')).exists('Manual tidy form exists');
+ await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore'));
+ await fillIn(SELECTORS.tidyForm.tidyPauseDuration, '10');
+ await click(SELECTORS.tidyForm.tidySave);
+ await click(SELECTORS.cancelTidyAction);
+ assert.dom(SELECTORS.cancelTidyModalBackground).exists('Confirm cancel tidy modal exits');
+ await click(SELECTORS.tidyConfigureModal.tidyModalCancelButton);
+ // we can't properly test the background refresh fetching of tidy status in testing
+ this.server.get(`${this.mountPath}/tidy-status`, () => {
+ return {
+ request_id: 'dba2d42d-1a6e-1551-80f8-4ddb364ede4b',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ acme_account_deleted_count: 0,
+ acme_account_revoked_count: 0,
+ acme_account_safety_buffer: 2592000,
+ acme_orders_deleted_count: 0,
+ cert_store_deleted_count: 0,
+ cross_revoked_cert_deleted_count: 0,
+ current_cert_store_count: null,
+ current_revoked_cert_count: null,
+ error: null,
+ internal_backend_uuid: '964a41f7-a159-53aa-d62e-fc1914e4a7e1',
+ issuer_safety_buffer: 31536000,
+ last_auto_tidy_finished: '2023-05-19T10:27:11.721825-07:00',
+ message: 'Tidying certificate store: checking entry 0 of 1',
+ missing_issuer_cert_count: 0,
+ pause_duration: '1m40s',
+ revocation_queue_deleted_count: 0,
+ revocation_queue_safety_buffer: 36000,
+ revoked_cert_deleted_count: 0,
+ safety_buffer: 2073600,
+ state: 'Cancelled',
+ tidy_acme: false,
+ tidy_cert_store: true,
+ tidy_cross_cluster_revoked_certs: false,
+ tidy_expired_issuers: false,
+ tidy_move_legacy_ca_bundle: false,
+ tidy_revocation_queue: false,
+ tidy_revoked_cert_issuer_associations: false,
+ tidy_revoked_certs: false,
+ time_finished: '2023-05-19T10:28:51.733092-07:00',
+ time_started: '2023-05-19T10:27:11.721846-07:00',
+ total_acme_account_count: 0,
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ };
+ });
+ await visit(`/vault/secrets/${this.mountPath}/pki/configuration`);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation cancelled');
+ assert
+ .dom(SELECTORS.hdsAlertDescription)
+ .hasText(
+ 'Your tidy operation has been cancelled. If this was a mistake configure and run another tidy operation.'
+ );
+ assert.dom(SELECTORS.alertUpdatedAt).exists();
+ });
+
+ test('it configures an auto tidy operation and shows its details', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ await click(SELECTORS.tidyEmptyStateConfigure);
+ assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
+ assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists');
+ assert
+ .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton)
+ .exists('Configure manual tidy button exists');
+ await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton);
+ assert.dom(SELECTORS.tidyForm.tidyFormName('auto')).exists('Auto tidy form exists');
+ await click(SELECTORS.tidyForm.tidyCancel);
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.index');
+ await click(SELECTORS.tidyEmptyStateConfigure);
+ await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton);
+ assert.dom(SELECTORS.tidyForm.tidyFormName('auto')).exists('Auto tidy form exists');
+ await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled'));
+ assert
+ .dom(SELECTORS.tidyForm.tidySectionHeader('ACME operations'))
+ .exists('Auto tidy form enabled shows ACME operations field');
+ await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore'));
+ await click(SELECTORS.tidyForm.tidySave);
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.index');
+ await click(SELECTORS.tidyForm.editAutoTidyButton);
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.configure');
+ await click(SELECTORS.tidyForm.inputByAttr('tidyRevokedCerts'));
+ await click(SELECTORS.tidyForm.tidySave);
+ assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.tidy.auto.index');
+ });
+
+ test('it opens a tidy modal when the user clicks on the tidy toolbar action', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ await click(SELECTORS.tidyConfigureModal.tidyOptionsModal);
+ assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
+ assert.dom(SELECTORS.tidyConfigureModal.tidyModalAutoButton).exists('Configure auto tidy button exists');
+ assert
+ .dom(SELECTORS.tidyConfigureModal.tidyModalManualButton)
+ .exists('Configure manual tidy button exists');
+ await click(SELECTORS.tidyConfigureModal.tidyModalCancelButton);
+ assert.dom(SELECTORS.tidyEmptyState).exists();
+ });
+
+ test('it should show correct toolbar action depending on whether auto tidy is enabled', async function (assert) {
+ await authPage.login(this.pkiAdminToken);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ assert
+ .dom(SELECTORS.tidyConfigureModal.tidyOptionsModal)
+ .exists('Configure tidy modal options button exists');
+ await click(SELECTORS.tidyConfigureModal.tidyOptionsModal);
+ assert.dom(SELECTORS.tidyConfigureModal.configureTidyModal).exists('Configure tidy modal exists');
+ await click(SELECTORS.tidyConfigureModal.tidyOptionsModal);
+ await click(SELECTORS.tidyConfigureModal.tidyModalAutoButton);
+ await click(SELECTORS.tidyForm.toggleLabel('Automatic tidy disabled'));
+ await click(SELECTORS.tidyForm.inputByAttr('tidyCertStore'));
+ await click(SELECTORS.tidyForm.inputByAttr('tidyRevokedCerts'));
+ await click(SELECTORS.tidyForm.tidySave);
+ await visit(`/vault/secrets/${this.mountPath}/pki/tidy`);
+ assert
+ .dom(SELECTORS.manualTidyToolbar)
+ .exists('Manual tidy toolbar action exists if auto tidy is configured');
+ assert
+ .dom(SELECTORS.autoTidyToolbar)
+ .exists('Auto tidy toolbar action exists if auto tidy is configured');
+ });
+});
diff --git a/ui/tests/helpers/pki/page/pki-tidy-form.js b/ui/tests/helpers/pki/page/pki-tidy-form.js
index 10ac46910..1a877f50a 100644
--- a/ui/tests/helpers/pki/page/pki-tidy-form.js
+++ b/ui/tests/helpers/pki/page/pki-tidy-form.js
@@ -4,14 +4,15 @@
*/
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]',
+ tidyFormName: (attr) => `[data-test-tidy-form="${attr}"]`,
+ inputByAttr: (attr) => `[data-test-input="${attr}"]`,
+ toggleInput: (attr) => `[data-test-input="${attr}"] input`,
+ intervalDuration: '[data-test-ttl-value="Automatic tidy enabled"]',
+ acmeAccountSafetyBuffer: '[data-test-ttl-value="Tidy ACME enabled"]',
+ toggleLabel: (label) => `[data-test-toggle-label="${label}"]`,
+ tidySectionHeader: (header) => `[data-test-tidy-header="${header}"]`,
tidySave: '[data-test-pki-tidy-button]',
tidyCancel: '[data-test-pki-tidy-cancel]',
+ tidyPauseDuration: '[data-test-ttl-value="Pause duration"]',
+ editAutoTidyButton: '[data-test-pki-edit-tidy-auto-link]',
};
diff --git a/ui/tests/helpers/pki/page/pki-tidy.js b/ui/tests/helpers/pki/page/pki-tidy.js
new file mode 100644
index 000000000..783efc8d6
--- /dev/null
+++ b/ui/tests/helpers/pki/page/pki-tidy.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import { SELECTORS as TIDY_FORM } from './pki-tidy-form';
+
+export const SELECTORS = {
+ hdsAlertTitle: '[data-test-hds-alert-title]',
+ hdsAlertDescription: '[data-test-hds-alert-description]',
+ alertUpdatedAt: '[data-test-hds-alert-updated-at]',
+ cancelTidyAction: '[data-test-cancel-tidy-action]',
+ hdsAlertButtonText: '[data-test-cancel-tidy-action] .hds-button__text',
+ timeStartedRow: '[data-test-value-div="Time started"]',
+ timeFinishedRow: '[data-test-value-div="Time finished"]',
+ cancelTidyModalBackground: '[data-test-modal-background="Cancel tidy?"]',
+ tidyEmptyStateConfigure: '[data-test-tidy-empty-state-configure]',
+ manualTidyToolbar: '[data-test-pki-manual-tidy-config]',
+ autoTidyToolbar: '[data-test-pki-auto-tidy-config]',
+ tidyConfigureModal: {
+ configureTidyModal: '[data-test-modal-background="Tidy this mount"]',
+ tidyModalAutoButton: '[data-test-tidy-modal-auto-button]',
+ tidyModalManualButton: '[data-test-tidy-modal-manual-button]',
+ tidyModalCancelButton: '[data-test-tidy-modal-cancel-button]',
+ tidyOptionsModal: '[data-test-pki-tidy-options-modal]',
+ },
+ tidyEmptyState: '[data-test-component="empty-state"]',
+ tidyForm: {
+ ...TIDY_FORM,
+ },
+};
diff --git a/ui/tests/helpers/pki/workflow.js b/ui/tests/helpers/pki/workflow.js
index 5b56734d8..b57612004 100644
--- a/ui/tests/helpers/pki/workflow.js
+++ b/ui/tests/helpers/pki/workflow.js
@@ -28,6 +28,7 @@ export const SELECTORS = {
issuersTab: '[data-test-secret-list-tab="Issuers"]',
certsTab: '[data-test-secret-list-tab="Certificates"]',
keysTab: '[data-test-secret-list-tab="Keys"]',
+ tidyTab: '[data-test-secret-list-tab="Tidy"]',
configTab: '[data-test-secret-list-tab="Configuration"]',
// ROLES
deleteRoleButton: '[data-test-pki-role-delete]',
diff --git a/ui/tests/integration/components/pki/page/pki-tidy-auto-settings-test.js b/ui/tests/integration/components/pki/page/pki-tidy-auto-settings-test.js
new file mode 100644
index 000000000..be518463e
--- /dev/null
+++ b/ui/tests/integration/components/pki/page/pki-tidy-auto-settings-test.js
@@ -0,0 +1,67 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupEngine } from 'ember-engines/test-support';
+
+module('Integration | Component | page/pki-tidy-auto-settings', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'pki');
+
+ hooks.beforeEach(function () {
+ const backend = 'pki-auto-tidy';
+ this.backend = backend;
+
+ this.context = { owner: this.engine };
+ this.store = this.owner.lookup('service:store');
+
+ this.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: backend, route: 'overview' },
+ { label: 'tidy', route: 'tidy.index' },
+ { label: 'auto' },
+ ];
+ });
+
+ test('it renders', async function (assert) {
+ const model = this.store.createRecord('pki/tidy', {
+ backend: this.backend,
+ tidyType: 'auto',
+ enabled: false,
+ intervalDuration: '2d',
+ tidyCertStore: false,
+ tidyExpiredIssuers: true,
+ });
+ this.set('model', model);
+
+ await render(
+ hbs` `,
+ this.context
+ );
+
+ assert.dom('[data-test-breadcrumbs] li').exists({ count: 4 }, 'an item exists for each breadcrumb');
+ assert.dom('[data-test-header-title]').hasText('Automatic tidy configuration', 'title is correct');
+ assert
+ .dom('[data-test-pki-edit-tidy-auto-link]')
+ .hasText('Edit auto-tidy', 'toolbar edit link has correct text');
+
+ assert.dom('[data-test-row="enabled"] [data-test-label-div]').hasText('Automatic tidy enabled');
+ assert.dom('[data-test-row="intervalDuration"] [data-test-label-div]').hasText('Automatic tidy duration');
+ // Universal operations
+ assert.dom('[data-test-group-title="Universal operations"]').hasText('Universal operations');
+ assert
+ .dom('[data-test-value-div="Tidy the certificate store"]')
+ .exists('Renders universal field when value exists');
+ assert
+ .dom('[data-test-value-div="Tidy revoked certificates"]')
+ .doesNotExist('Does not render universal field when value null');
+ // Issuer operations
+ assert.dom('[data-test-group-title="Issuer operations"]').hasText('Issuer operations');
+ assert
+ .dom('[data-test-value-div="Tidy expired issuers"]')
+ .exists('Renders issuer op field when value exists');
+ assert
+ .dom('[data-test-value-div="Tidy legacy CA bundle"]')
+ .doesNotExist('Does not render issuer op field when value null');
+ });
+});
diff --git a/ui/tests/integration/components/pki/page/pki-tidy-form-test.js b/ui/tests/integration/components/pki/page/pki-tidy-form-test.js
deleted file mode 100644
index 5fc776019..000000000
--- a/ui/tests/integration/components/pki/page/pki-tidy-form-test.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * 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` `, {
- 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` `, {
- 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');
- });
-});
diff --git a/ui/tests/integration/components/pki/page/pki-tidy-status-test.js b/ui/tests/integration/components/pki/page/pki-tidy-status-test.js
new file mode 100644
index 000000000..1a05407f1
--- /dev/null
+++ b/ui/tests/integration/components/pki/page/pki-tidy-status-test.js
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+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 { setupMirage } from 'ember-cli-mirage/test-support';
+import { SELECTORS } from 'vault/tests/helpers/pki/page/pki-tidy';
+
+module('Integration | Component | Page::PkiTidyStatus', 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.store.createRecord('pki/issuer', { issuerId: 'abcd-efgh' });
+ this.store.createRecord('pki/tidy', { backend: this.secretMountPath.currentPath, tidyType: 'auto' });
+
+ this.autoTidyConfig = this.store.peekAll('pki/tidy');
+ this.tidyStatus = {
+ acme_account_deleted_count: 0,
+ acme_account_revoked_count: 0,
+ acme_account_safety_buffer: 2592000,
+ acme_orders_deleted_count: 0,
+ cert_store_deleted_count: 0,
+ cross_revoked_cert_deleted_count: 0,
+ current_cert_store_count: null,
+ current_revoked_cert_count: null,
+ error: null,
+ internal_backend_uuid: '9d3bd186-0fdd-9ca4-f298-2e180536b743',
+ issuer_safety_buffer: 31536000,
+ last_auto_tidy_finished: '2023-05-18T13:27:36.390785-07:00',
+ message: 'Tidying certificate store: checking entry 0 of 1',
+ missing_issuer_cert_count: 0,
+ pause_duration: '15s',
+ revocation_queue_deleted_count: 0,
+ revocation_queue_safety_buffer: 36000,
+ revoked_cert_deleted_count: 0,
+ safety_buffer: 2073600,
+ state: 'Running',
+ tidy_acme: false,
+ tidy_cert_store: true,
+ tidy_cross_cluster_revoked_certs: false,
+ tidy_expired_issuers: false,
+ tidy_move_legacy_ca_bundle: false,
+ time_started: '2023-05-18T13:27:36.390959-07:00',
+ };
+ this.engineId = 'pki';
+ });
+
+ test('shows the correct titles for the alert banner based on states', async function (assert) {
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ // running state
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy in progress');
+ assert.dom(SELECTORS.cancelTidyAction).exists();
+ assert.dom(SELECTORS.hdsAlertButtonText).hasText('Cancel tidy');
+ // inactive state
+ this.tidyStatus.state = 'Inactive';
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy is inactive');
+ // finished state
+ this.tidyStatus.state = 'Finished';
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation finished');
+ // error state
+ this.tidyStatus.state = 'Error';
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation failed');
+ // cancelling state
+ this.tidyStatus.state = 'Cancelling';
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation cancelling');
+ // cancelled state
+ this.tidyStatus.state = 'Cancelled';
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation cancelled');
+ });
+ test('shows the fields even if the data returns null values', async function (assert) {
+ this.tidyStatus.time_started = null;
+ this.tidyStatus.time_finished = null;
+ await render(
+ hbs`
,`,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.timeStartedRow).exists();
+ assert.dom(SELECTORS.timeFinishedRow).exists();
+ });
+});
diff --git a/ui/tests/integration/components/pki/pki-tidy-form-test.js b/ui/tests/integration/components/pki/pki-tidy-form-test.js
new file mode 100644
index 000000000..3eadc2aeb
--- /dev/null
+++ b/ui/tests/integration/components/pki/pki-tidy-form-test.js
@@ -0,0 +1,315 @@
+/**
+ * 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 tidy form', function (hooks) {
+ setupRenderingTest(hooks);
+ setupEngine(hooks, 'pki');
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ this.version = this.owner.lookup('service:version');
+ this.version.version = '1.14.1+ent';
+ this.server.post('/sys/capabilities-self', () => {});
+ this.onSave = () => {};
+ this.onCancel = () => {};
+ this.manualTidy = this.store.createRecord('pki/tidy', { backend: 'pki-manual-tidy' });
+ this.store.pushPayload('pki/tidy', {
+ modelName: 'pki/tidy',
+ id: 'pki-auto-tidy',
+ });
+ this.autoTidy = this.store.peekRecord('pki/tidy', 'pki-auto-tidy');
+ });
+
+ test('it hides or shows fields depending on auto-tidy toggle', async function (assert) {
+ assert.expect(37);
+ this.version.version = '1.14.1+ent';
+ const sectionHeaders = [
+ 'Universal operations',
+ 'ACME operations',
+ 'Issuer operations',
+ 'Cross-cluster operations',
+ ];
+
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.toggleInput('intervalDuration')).isNotChecked('Automatic tidy is disabled');
+ assert.dom(`[data-test-ttl-form-label="Automatic tidy disabled"]`).exists('renders disabled label text');
+
+ this.autoTidy.eachAttribute((attr) => {
+ if (attr === 'enabled' || attr === 'intervalDuration') return;
+ assert.dom(SELECTORS.inputByAttr(attr)).doesNotExist(`does not render ${attr} when auto tidy disabled`);
+ });
+
+ sectionHeaders.forEach((group) => {
+ assert.dom(SELECTORS.tidySectionHeader(group)).doesNotExist(`does not render ${group} header`);
+ });
+
+ // ENABLE AUTO TIDY
+ await click(SELECTORS.toggleInput('intervalDuration'));
+ assert.dom(SELECTORS.toggleInput('intervalDuration')).isChecked('Automatic tidy is enabled');
+ assert.dom(`[data-test-ttl-form-label="Automatic tidy enabled"]`).exists('renders enabled text');
+
+ this.autoTidy.eachAttribute((attr) => {
+ const skipFields = ['enabled', 'tidyAcme', 'intervalDuration'];
+ if (skipFields.includes(attr)) return; // combined with duration ttl or asserted elsewhere
+ assert.dom(SELECTORS.inputByAttr(attr)).exists(`renders ${attr} when auto tidy enabled`);
+ });
+
+ sectionHeaders.forEach((group) => {
+ assert.dom(SELECTORS.tidySectionHeader(group)).exists(`renders ${group} header`);
+ });
+ });
+
+ test('it renders all attribute fields, including enterprise', async function (assert) {
+ assert.expect(25);
+ this.version.version = '1.14.1+ent';
+ this.autoTidy.enabled = true;
+ const skipFields = ['enabled', 'tidyAcme', 'intervalDuration']; // combined with duration ttl or asserted separately
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ this.autoTidy.eachAttribute((attr) => {
+ if (skipFields.includes(attr)) return;
+ assert.dom(SELECTORS.inputByAttr(attr)).exists(`renders ${attr} for auto tidyType`);
+ });
+
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+ assert.dom(SELECTORS.toggleInput('intervalDuration')).doesNotExist('hides automatic tidy toggle');
+
+ this.manualTidy.eachAttribute((attr) => {
+ if (skipFields.includes(attr)) return;
+ assert.dom(SELECTORS.inputByAttr(attr)).exists(`renders ${attr} for manual tidyType`);
+ });
+ });
+
+ test('it hides enterprise fields for OSS', async function (assert) {
+ assert.expect(7);
+ this.version.version = '1.14.1';
+ this.autoTidy.enabled = true;
+
+ const enterpriseFields = [
+ 'tidyRevocationQueue',
+ 'tidyCrossClusterRevokedCerts',
+ 'revocationQueueSafetyBuffer',
+ ];
+
+ // tidyType = auto
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ assert
+ .dom(SELECTORS.tidySectionHeader('Cross-cluster operations'))
+ .doesNotExist(`does not render ent header`);
+
+ enterpriseFields.forEach((entAttr) => {
+ assert.dom(SELECTORS.inputByAttr(entAttr)).doesNotExist(`does not render ${entAttr} for auto tidyType`);
+ });
+
+ // tidyType = manual
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ enterpriseFields.forEach((entAttr) => {
+ assert
+ .dom(SELECTORS.inputByAttr(entAttr))
+ .doesNotExist(`does not render ${entAttr} for manual tidyType`);
+ });
+ });
+
+ test('it should change the attributes on the model', async function (assert) {
+ assert.expect(12);
+ this.server.post('/pki-auto-tidy/config/auto-tidy', (schema, req) => {
+ assert.propEqual(
+ JSON.parse(req.requestBody),
+ {
+ acme_account_safety_buffer: '60s',
+ enabled: true,
+ interval_duration: '10s',
+ issuer_safety_buffer: '20s',
+ pause_duration: '30s',
+ revocation_queue_safety_buffer: '40s',
+ safety_buffer: '50s',
+ tidy_acme: true,
+ tidy_cert_store: true,
+ tidy_cross_cluster_revoked_certs: true,
+ tidy_expired_issuers: true,
+ tidy_move_legacy_ca_bundle: true,
+ tidy_revocation_queue: true,
+ tidy_revoked_cert_issuer_associations: true,
+ tidy_revoked_certs: true,
+ },
+ 'response contains updated model values'
+ );
+ });
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ assert.dom(SELECTORS.toggleInput('intervalDuration')).isNotChecked('Automatic tidy is disabled');
+ assert.dom(SELECTORS.toggleLabel('Automatic tidy disabled')).exists('auto tidy has disabled label');
+ assert.false(this.autoTidy.enabled, 'enabled is false on model');
+
+ // enable auto-tidy
+ await click(SELECTORS.toggleInput('intervalDuration'));
+ await fillIn(SELECTORS.intervalDuration, 10);
+
+ assert.dom(SELECTORS.toggleInput('intervalDuration')).isChecked('toggle enabled auto tidy');
+ assert.dom(SELECTORS.toggleLabel('Automatic tidy enabled')).exists('auto tidy has enabled label');
+
+ assert.dom(SELECTORS.toggleInput('acmeAccountSafetyBuffer')).isNotChecked('ACME tidy is disabled');
+ assert.dom(SELECTORS.toggleLabel('Tidy ACME disabled')).exists('ACME label has correct disabled text');
+ assert.false(this.autoTidy.tidyAcme, 'tidyAcme is false on model');
+
+ await click(SELECTORS.toggleInput('acmeAccountSafetyBuffer'));
+ await fillIn(SELECTORS.acmeAccountSafetyBuffer, 60);
+ assert.true(this.autoTidy.tidyAcme, 'tidyAcme toggles to true');
+
+ const fillInValues = {
+ issuerSafetyBuffer: 20,
+ pauseDuration: 30,
+ revocationQueueSafetyBuffer: 40,
+ safetyBuffer: 50,
+ };
+ this.autoTidy.eachAttribute(async (attr, { type }) => {
+ const skipFields = ['enabled', 'tidyAcme', 'intervalDuration', 'acmeAccountSafetyBuffer']; // combined with duration ttl or asserted separately
+ if (skipFields.includes(attr)) return;
+ if (type === 'boolean') {
+ await click(SELECTORS.inputByAttr(attr));
+ }
+ if (type === 'string') {
+ await fillIn(SELECTORS.toggleInput(attr), `${fillInValues[attr]}`);
+ }
+ });
+
+ assert.dom(SELECTORS.toggleInput('acmeAccountSafetyBuffer')).isChecked('ACME tidy is enabled');
+ assert.dom(SELECTORS.toggleLabel('Tidy ACME enabled')).exists('ACME label has correct enabled text');
+ await click(SELECTORS.tidySave);
+ });
+
+ test('it updates auto-tidy config', async function (assert) {
+ assert.expect(4);
+ this.server.post('/pki-auto-tidy/config/auto-tidy', (schema, req) => {
+ assert.ok(true, 'Request made to update auto-tidy');
+ assert.propEqual(
+ JSON.parse(req.requestBody),
+ {
+ enabled: false,
+ tidy_acme: false,
+ },
+ 'response contains auto-tidy params'
+ );
+ });
+ this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
+ this.onCancel = () => assert.ok(true, 'onCancel callback fires on save success');
+
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ await click(SELECTORS.tidySave);
+ await click(SELECTORS.tidyCancel);
+ });
+
+ test('it saves and performs manual tidy', async function (assert) {
+ assert.expect(4);
+
+ this.server.post('/pki-manual-tidy/tidy', (schema, req) => {
+ assert.ok(true, 'Request made to perform manual tidy');
+ assert.propEqual(
+ JSON.parse(req.requestBody),
+ { tidy_acme: false },
+ 'response contains manual tidy params'
+ );
+ });
+ this.onSave = () => assert.ok(true, 'onSave callback fires on save success');
+ this.onCancel = () => assert.ok(true, 'onCancel callback fires on save success');
+
+ await render(
+ hbs`
+
+ `,
+ { owner: this.engine }
+ );
+
+ await click(SELECTORS.tidySave);
+ await click(SELECTORS.tidyCancel);
+ });
+});
diff --git a/ui/tests/unit/adapters/pki/tidy-test.js b/ui/tests/unit/adapters/pki/tidy-test.js
index b82dda4d2..fd27c951d 100644
--- a/ui/tests/unit/adapters/pki/tidy-test.js
+++ b/ui/tests/unit/adapters/pki/tidy-test.js
@@ -25,7 +25,7 @@ module('Unit | Adapter | pki/tidy', function (hooks) {
assert.ok(adapter);
});
- test('it calls the correct endpoint when tidyType = manual-tidy', async function (assert) {
+ test('it calls the correct endpoint when tidyType = manual', async function (assert) {
assert.expect(1);
this.server.post(`${this.backend}/tidy`, () => {
@@ -38,24 +38,43 @@ module('Unit | Adapter | pki/tidy', function (hooks) {
safetyBuffer: '120h',
backend: this.backend,
};
- await this.store
- .createRecord('pki/tidy', this.payload)
- .save({ adapterOptions: { tidyType: 'manual-tidy' } });
+ await this.store.createRecord('pki/tidy', this.payload).save({ adapterOptions: { tidyType: 'manual' } });
});
- test('it calls the correct endpoint when tidyType = auto-tidy', async function (assert) {
+ test('it should make a request to correct endpoint for findRecord', async function (assert) {
assert.expect(1);
- this.server.post(`${this.backend}/config/auto-tidy`, () => {
+ this.server.get(`${this.backend}/config/auto-tidy`, () => {
assert.ok(true, 'request made to correct endpoint on create');
- return {};
+ return {
+ request_id: '2a4a1f36-20df-e71c-02d6-be15a09656f9',
+ lease_id: '',
+ renewable: false,
+ lease_duration: 0,
+ data: {
+ acme_account_safety_buffer: 2592000,
+ enabled: false,
+ interval_duration: 43200,
+ issuer_safety_buffer: 31536000,
+ maintain_stored_certificate_counts: false,
+ pause_duration: '0s',
+ publish_stored_certificate_count_metrics: false,
+ revocation_queue_safety_buffer: 172800,
+ safety_buffer: 259200,
+ tidy_acme: false,
+ tidy_cert_store: false,
+ tidy_cross_cluster_revoked_certs: false,
+ tidy_expired_issuers: false,
+ tidy_move_legacy_ca_bundle: false,
+ tidy_revocation_queue: false,
+ tidy_revoked_cert_issuer_associations: false,
+ tidy_revoked_certs: false,
+ },
+ wrap_info: null,
+ warnings: null,
+ auth: null,
+ };
});
- this.payload = {
- enabled: true,
- interval_duration: '72h',
- backend: this.backend,
- };
- await this.store
- .createRecord('pki/tidy', this.payload)
- .save({ adapterOptions: { tidyType: 'auto-tidy' } });
+
+ this.store.findRecord('pki/tidy', this.backend);
});
});
diff --git a/ui/types/ember-data/types/registries/adapter.d.ts b/ui/types/ember-data/types/registries/adapter.d.ts
index 57d6ccc59..abca4c358 100644
--- a/ui/types/ember-data/types/registries/adapter.d.ts
+++ b/ui/types/ember-data/types/registries/adapter.d.ts
@@ -7,12 +7,14 @@ import Application from 'vault/adapters/application';
import Adapter from 'ember-data/adapter';
import ModelRegistry from 'ember-data/types/registries/model';
import PkiIssuerAdapter from 'vault/adapters/pki/issuer';
+import PkiTidyAdapter from 'vault/adapters/pki/tidy';
/**
* Catch-all for ember-data.
*/
export default interface AdapterRegistry {
'pki/issuer': PkiIssuerAdapter;
+ 'pki/tidy': PkiTidyAdapter;
application: Application;
[key: keyof ModelRegistry]: Adapter;
}
diff --git a/ui/types/vault/adapters/pki/tidy.d.ts b/ui/types/vault/adapters/pki/tidy.d.ts
new file mode 100644
index 000000000..ac770aa63
--- /dev/null
+++ b/ui/types/vault/adapters/pki/tidy.d.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Store from '@ember-data/store';
+import { AdapterRegistry } from 'ember-data/adapter';
+
+export default interface PkiTidyAdapter extends AdapterRegistry {
+ namespace: string;
+ cancelTidy(backend: string);
+}
diff --git a/ui/types/vault/models/pki/tidy.d.ts b/ui/types/vault/models/pki/tidy.d.ts
new file mode 100644
index 000000000..b95d1ed6a
--- /dev/null
+++ b/ui/types/vault/models/pki/tidy.d.ts
@@ -0,0 +1,29 @@
+import Model from '@ember-data/model';
+import { FormField, FormFieldGroups } from 'vault/vault/app-types';
+
+export default class PkiTidyModel extends Model {
+ version: string;
+ acmeAccountSafetyBuffer: string;
+ tidyAcme: boolean;
+ enabled: boolean;
+ intervalDuration: string;
+ issuerSafetyBuffer: string;
+ pauseDuration: string;
+ revocationQueueSafetyBuffer: string;
+ safetyBuffer: string;
+ tidyCertStore: boolean;
+ tidyCrossClusterRevokedCerts: boolean;
+ tidyExpiredIssuers: boolean;
+ tidyMoveLegacyCaBundle: boolean;
+ tidyRevocationQueue: boolean;
+ tidyRevokedCertIssuerAssociations: boolean;
+ tidyRevokedCerts: boolean;
+ get useOpenAPI(): boolean;
+ getHelpUrl(backend: string): string;
+ allByKey: {
+ intervalDuration: FormField[];
+ };
+ get allGroups(): FormFieldGroups[];
+ get sharedFields(): FormFieldGroups[];
+ get formFieldGroups(): FormFieldGroups[];
+}