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.

- - - -
-
- - - -
-
- - - -
- - -
- -
- - - {{#if this.invalidFormAlert}} -
- -
- {{/if}} -
- \ 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}} + + {{/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}} + + + +{{/if}} + +{{! TIDY OPTIONS MODAL }} + + +
+ + + +
+
+ +{{! CANCEL TIDY CONFIRMATION MODAL }} +{{#if this.confirmCancelTidy}} + + +
+ + +
+
+{{/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 +

+ + + +
+ {{#if (and (eq @tidyType "auto") this.intervalDurationAttr)}} + {{#let this.intervalDurationAttr as |attr|}} + + {{/let}} + {{/if}} + {{#each @tidy.formFieldGroups as |fieldGroup|}} + {{#each-in fieldGroup as |group fields|}} + {{#if (or (eq @tidyType "manual") @tidy.enabled)}} + + {{#each fields as |attr|}} + {{#if (eq attr.name "acmeAccountSafetyBuffer")}} + + {{else}} + {{! tidyAcme is handled by the ttl above }} + {{#if (not-eq attr.name "tidyAcme")}} + + {{/if}} + {{/if}} + {{/each}} + {{/if}} + {{/each-in}} + {{/each}} + +
+ +
+ + + {{#if this.invalidFormAlert}} +
+ +
+ {{/if}} +
+ \ 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[]; +}