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 <cshaw@hashicorp.com> Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
parent
527f4fe2ba
commit
4da72c45ce
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -38,6 +38,10 @@ const PKI_ENGINE_BACKEND = {
|
|||
label: 'Certificates',
|
||||
link: 'certificates',
|
||||
},
|
||||
{
|
||||
label: 'Tidy',
|
||||
link: 'tidy',
|
||||
},
|
||||
{
|
||||
label: 'Configuration',
|
||||
link: 'configuration',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -14,9 +14,6 @@
|
|||
</button>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{/if}}
|
||||
<ToolbarLink @route="configuration.tidy" data-test-tidy-toolbar>
|
||||
Tidy
|
||||
</ToolbarLink>
|
||||
<ToolbarLink @route="configuration.edit">
|
||||
Edit configuration
|
||||
</ToolbarLink>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
<Icon @name="pki" @size="24" class="has-text-grey-light" />
|
||||
Configure automatic tidy
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiTidyForm
|
||||
@tidy={{@model}}
|
||||
@tidyType="auto"
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.tidy.auto"}}
|
||||
@onCancel={{transition-to (concat "vault.cluster.secrets.backend.pki.tidy" (if @model.enabled ".auto" ""))}}
|
||||
/>
|
|
@ -0,0 +1,40 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-header-title>
|
||||
Automatic tidy configuration
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<LinkTo @route="tidy.auto.configure" class="toolbar-link" data-test-pki-edit-tidy-auto-link>
|
||||
Edit auto-tidy
|
||||
<Icon @name="chevron-right" />
|
||||
</LinkTo>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<main>
|
||||
{{#each @model.allGroups as |group|}}
|
||||
{{#each-in group as |label fields|}}
|
||||
{{#if (not-eq label "autoTidy")}}
|
||||
<h2 class="title is-5 has-top-margin-l has-bottom-margin-xs" data-test-group-title={{label}}>
|
||||
{{label}}
|
||||
</h2>
|
||||
{{/if}}
|
||||
|
||||
{{#each fields as |attr|}}
|
||||
<InfoTableRow
|
||||
@label={{or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name))}}
|
||||
@value={{get @model attr.name}}
|
||||
@formatTtl={{attr.options.formatTtl}}
|
||||
data-test-row={{attr.name}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
</main>
|
|
@ -1,83 +0,0 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
<Icon @name="pki" @size="24" class="has-text-grey-light" />
|
||||
Tidy
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<p class="has-top-margin-m has-bottom-margin-l">Tidying cleans up the storage backend and/or CRL by removing certificates
|
||||
that have expired and are past a certain buffer period beyond their expiration time.</p>
|
||||
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
|
||||
<form class="has-bottom-margin-s" {{on "submit" (perform this.save)}}>
|
||||
<div class="has-bottom-margin-s">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@tidy.tidyCertStore}}
|
||||
id="tidy-certificate-store"
|
||||
{{on "input" (fn (mut @tidy.tidyCertStore) (not @tidy.tidyCertStore))}}
|
||||
data-test-tidy-cert-store-checkbox
|
||||
/>
|
||||
|
||||
<label for="tidy-certificate-store" class="is-label" data-test-tidy-cert-store-label>
|
||||
Tidy the certificate store
|
||||
</label>
|
||||
</div>
|
||||
<div class="has-bottom-margin-s">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@tidy.tidyRevocationQueue}}
|
||||
id="tidy-revocation-queue"
|
||||
{{on "input" (fn (mut @tidy.tidyRevocationQueue) (not @tidy.tidyRevocationQueue))}}
|
||||
data-test-tidy-revocation-queue-checkbox
|
||||
/>
|
||||
|
||||
<label for="tidy-revocation-queue" class="is-label" data-test-tidy-revocation-queue-label>
|
||||
Tidy the revocation list (CRL)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<TtlPicker
|
||||
class="has-top-margin-l has-bottom-margin-l"
|
||||
@initialValue={{@tidy.safetyBuffer}}
|
||||
@onChange={{this.updateSafetyBuffer}}
|
||||
@hideToggle={{true}}
|
||||
@label="Safety buffer"
|
||||
@helperTextEnabled="For a certificate to be expunged, the time must be after the expiration time of the certificate (according to the local
|
||||
clock) plus the safety buffer. The default is 72 hours."
|
||||
/>
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<div class="has-top-margin-m">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-pki-tidy-button
|
||||
>
|
||||
Tidy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" this.cancel}}
|
||||
data-test-pki-tidy-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,18 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
<Page::Breadcrumbs @breadcrumbs={{@breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
<Icon @name="pki" @size="24" class="has-text-grey-light" />
|
||||
Manual tidy
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PkiTidyForm
|
||||
@tidy={{@model}}
|
||||
@tidyType="manual"
|
||||
@onSave={{transition-to "vault.cluster.secrets.backend.pki.tidy"}}
|
||||
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.tidy"}}
|
||||
/>
|
|
@ -0,0 +1,193 @@
|
|||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
<div class="toolbar-separator"></div>
|
||||
{{#if @autoTidyConfig.enabled}}
|
||||
<ToolbarLink @route="tidy.auto" data-test-pki-auto-tidy-config>
|
||||
Auto-tidy configuration
|
||||
</ToolbarLink>
|
||||
<ToolbarLink @route="tidy.manual" data-test-pki-manual-tidy-config>
|
||||
Perform manual tidy
|
||||
</ToolbarLink>
|
||||
{{else}}
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-link"
|
||||
{{on "click" (fn (mut this.tidyOptionsModal) true)}}
|
||||
data-test-pki-tidy-options-modal
|
||||
>
|
||||
Tidy
|
||||
<Icon @name="chevron-right" />
|
||||
</button>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#if this.hasTidyConfig}}
|
||||
<Hds::Alert
|
||||
@type="inline"
|
||||
@color={{this.tidyStateAlertBanner.color}}
|
||||
@icon={{this.tidyStateAlertBanner.icon}}
|
||||
class="has-top-margin-m"
|
||||
data-test-hds-alert
|
||||
as |A|
|
||||
>
|
||||
<A.Title data-test-hds-alert-title>{{this.tidyStateAlertBanner.title}}</A.Title>
|
||||
<A.Description data-test-hds-alert-description>{{this.tidyStateAlertBanner.message}}</A.Description>
|
||||
{{#if this.tidyStateAlertBanner.shouldShowCancelTidy}}
|
||||
<A.Button
|
||||
@text="Cancel tidy"
|
||||
@color="critical"
|
||||
@icon={{if this.cancelTidy.isRunning "loading"}}
|
||||
{{on "click" (fn (mut this.confirmCancelTidy) true)}}
|
||||
data-test-cancel-tidy-action
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @tidyStatus.responseTimestamp}}
|
||||
<A.Description class="has-top-margin-xs" data-test-hds-alert-updated-at>
|
||||
Updated
|
||||
{{date-format @tidyStatus.responseTimestamp "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
|
||||
</A.Description>
|
||||
{{/if}}
|
||||
</Hds::Alert>
|
||||
|
||||
{{#each this.tidyStatusGeneralFields as |attr|}}
|
||||
<InfoTableRow
|
||||
@label={{humanize (dasherize attr)}}
|
||||
@formatDate={{if
|
||||
(includes attr (array "time_started" "time_finished" "last_auto_tidy_finished"))
|
||||
"MMM dd, yyyy hh:mm:ss a"
|
||||
}}
|
||||
@value={{get @tidyStatus attr}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-l has-border-bottom-light has-bottom-padding-s">
|
||||
{{if (eq this.tidyState "Running") "Current" "Last"}}
|
||||
tidy settings
|
||||
</h2>
|
||||
{{#each this.tidyStatusConfigFields as |attr|}}
|
||||
<InfoTableRow
|
||||
@label={{humanize (dasherize attr)}}
|
||||
@value={{get @tidyStatus attr}}
|
||||
@formatTtl={{includes attr (array "safety_buffer" "issuer_safety_buffer")}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#if this.isEnterprise}}
|
||||
{{#each this.crossClusterOperation as |attr|}}
|
||||
<InfoTableRow
|
||||
@label={{humanize (dasherize attr)}}
|
||||
@value={{get @tidyStatus attr}}
|
||||
@formatTtl={{eq attr "revocation_queue_safety_buffer"}}
|
||||
@alwaysRender={{true}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Tidy status unavailable"
|
||||
@message="After the next tidy operation has been performed, information about the current or most recent tidy operation will display here."
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="link"
|
||||
{{on "click" (fn (mut this.tidyOptionsModal) true)}}
|
||||
data-test-tidy-empty-state-configure
|
||||
>
|
||||
Tidy
|
||||
</button>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
|
||||
{{! TIDY OPTIONS MODAL }}
|
||||
<Modal
|
||||
@title="Tidy this mount"
|
||||
@onClose={{fn (mut this.tidyOptionsModal) false}}
|
||||
@isActive={{this.tidyOptionsModal}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section aria-label="tidy-options-modal-content" class="modal-card-body">
|
||||
<h3 class="title is-5">How tidying will work</h3>
|
||||
<p class="has-text-grey has-bottom-padding-s">
|
||||
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.
|
||||
<DocLink @path="/vault/docs/secrets/pki/considerations#automate-crl-building-and-tidying">
|
||||
Documentation.
|
||||
</DocLink>
|
||||
</p>
|
||||
<p class="has-text-grey">
|
||||
<ol class="has-left-margin-m has-bottom-margin-s">
|
||||
<li>Select a tidy operation:</li>
|
||||
<ul class="bullet has-bottom-margin-xs">
|
||||
<li><strong>Automatic tidy</strong>
|
||||
periodically runs a tidy operation with saved configuration settings after waiting the specified interval
|
||||
duration between tidies
|
||||
</li>
|
||||
<li><strong>Manual tidy</strong> runs a tidy operation once</li>
|
||||
</ul>
|
||||
<li>Configure the parameters that determine how to tidy and run the operation.</li>
|
||||
</ol>
|
||||
</p>
|
||||
<div class="has-top-margin-l has-padding">
|
||||
<img src={{img-path "~/pki-tidy.png"}} alt="tidy operation diagram" />
|
||||
</div>
|
||||
</section>
|
||||
<footer aria-label="tidy-option-buttons" class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
{{on "click" (transition-to "vault.cluster.secrets.backend.pki.tidy.auto.configure")}}
|
||||
data-test-tidy-modal-auto-button
|
||||
>
|
||||
Automatic tidy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
{{on "click" (transition-to "vault.cluster.secrets.backend.pki.tidy.manual")}}
|
||||
data-test-tidy-modal-manual-button
|
||||
>
|
||||
Manual tidy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
{{on "click" (fn (mut this.tidyOptionsModal) false)}}
|
||||
data-test-tidy-modal-cancel-button
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
|
||||
{{! CANCEL TIDY CONFIRMATION MODAL }}
|
||||
{{#if this.confirmCancelTidy}}
|
||||
<Modal
|
||||
@type="warning"
|
||||
@title="Cancel tidy?"
|
||||
@onClose={{fn (mut this.confirmCancelTidy) false}}
|
||||
@isActive={{this.confirmCancelTidy}}
|
||||
@showCloseButton={{true}}
|
||||
>
|
||||
<section aria-label="confirm-cancel-modal-content" class="modal-card-body">
|
||||
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.
|
||||
<p class="has-top-margin-s">Click “Confirm” to cancel the running tidy operation.</p>
|
||||
</section>
|
||||
<footer aria-label="confirm-cancel-buttons" class="modal-card-foot modal-card-foot-outlined">
|
||||
<button
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
{{on "click" (perform this.cancelTidy)}}
|
||||
data-test-tidy-modal-cancel-button
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.confirmCancelTidy) false)}}>
|
||||
Cancel
|
||||
</button>
|
||||
</footer>
|
||||
</Modal>
|
||||
{{/if}}
|
|
@ -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<Args> {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<p class="has-top-margin-m has-bottom-margin-l">Tidying cleans up the storage backend and/or CRL by removing certificates
|
||||
that have expired and are past a certain buffer period beyond their expiration time.
|
||||
<DocLink @path="/vault/api-docs/secret/pki#{{if (eq @tidyType 'manual') 'tidy' 'configure-automatic-tidy'}}">Learn more</DocLink>
|
||||
</p>
|
||||
|
||||
<MessageError @errorMessage={{this.errorBanner}} class="has-top-margin-s" />
|
||||
|
||||
<form class="has-bottom-margin-s" {{on "submit" (perform this.save)}} data-test-tidy-form={{@tidyType}}>
|
||||
{{#if (and (eq @tidyType "auto") this.intervalDurationAttr)}}
|
||||
{{#let this.intervalDurationAttr as |attr|}}
|
||||
<TtlPicker
|
||||
data-test-input={{attr.name}}
|
||||
@onChange={{fn this.handleTtl attr}}
|
||||
@label={{attr.options.label}}
|
||||
@labelDisabled={{attr.options.labelDisabled}}
|
||||
@helperTextDisabled={{attr.options.helperTextDisabled}}
|
||||
@helperTextEnabled={{attr.options.helperTextEnabled}}
|
||||
@initialEnabled={{get @tidy attr.options.mapToBoolean}}
|
||||
@initialValue={{get @tidy attr.name}}
|
||||
/>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
{{#each @tidy.formFieldGroups as |fieldGroup|}}
|
||||
{{#each-in fieldGroup as |group fields|}}
|
||||
{{#if (or (eq @tidyType "manual") @tidy.enabled)}}
|
||||
<h2 class="title is-size-5 has-border-bottom-light page-header" data-test-tidy-header={{group}}>
|
||||
{{group}}
|
||||
</h2>
|
||||
{{#each fields as |attr|}}
|
||||
{{#if (eq attr.name "acmeAccountSafetyBuffer")}}
|
||||
<TtlPicker
|
||||
data-test-input={{attr.name}}
|
||||
@onChange={{fn this.handleTtl attr}}
|
||||
@label={{attr.options.label}}
|
||||
@labelDisabled={{attr.options.labelDisabled}}
|
||||
@helperTextDisabled={{attr.options.helperTextDisabled}}
|
||||
@helperTextEnabled={{attr.options.helperTextEnabled}}
|
||||
@initialEnabled={{get @tidy attr.options.mapToBoolean}}
|
||||
@initialValue={{get @tidy attr.name}}
|
||||
/>
|
||||
{{else}}
|
||||
{{! tidyAcme is handled by the ttl above }}
|
||||
{{#if (not-eq attr.name "tidyAcme")}}
|
||||
<FormField @attr={{attr}} @model={{@tidy}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each-in}}
|
||||
{{/each}}
|
||||
|
||||
<hr class="is-marginless has-background-gray-200" />
|
||||
|
||||
<div class="has-top-margin-m">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
|
||||
disabled={{this.save.isRunning}}
|
||||
data-test-pki-tidy-button
|
||||
>
|
||||
{{if (eq @tidyType "manual") "Perform tidy" "Save"}}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
disabled={{this.save.isRunning}}
|
||||
{{on "click" @onCancel}}
|
||||
data-test-pki-tidy-cancel
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if this.invalidFormAlert}}
|
||||
<div class="control">
|
||||
<AlertInline @type="danger" @paddingTop={{true}} @message={{this.invalidFormAlert}} @mimicRefresh={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
|
@ -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<Args> {
|
||||
|
@ -24,13 +37,8 @@ export default class PkiTidyForm extends Component<Args> {
|
|||
@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<Args> {
|
|||
*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<Args> {
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PkiTidyAutoRoute extends Route {
|
||||
model() {
|
||||
const { autoTidyConfig } = this.modelFor('tidy');
|
||||
return autoTidyConfig;
|
||||
}
|
||||
}
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<Page::PkiTidyForm @breadcrumbs={{this.breadcrumbs}} @tidy={{this.model}} @adapterOptions={{hash tidyType="manual-tidy"}} />
|
|
@ -0,0 +1 @@
|
|||
{{outlet}}
|
|
@ -0,0 +1 @@
|
|||
{{outlet}}
|
|
@ -0,0 +1 @@
|
|||
<Page::PkiTidyAutoConfigure @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -0,0 +1 @@
|
|||
<Page::PkiTidyAutoSettings @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -0,0 +1,20 @@
|
|||
<SecretListHeader
|
||||
@model={{this.model.engine}}
|
||||
@backendCrumb={{hash
|
||||
label=this.model.engine.id
|
||||
text=this.model.engine.id
|
||||
path="vault.cluster.secrets.backend.list-root"
|
||||
model=this.model.engine.id
|
||||
}}
|
||||
@isEngine={{true}}
|
||||
/>
|
||||
{{#if this.model.hasConfig}}
|
||||
<Page::PkiTidyStatus @autoTidyConfig={{this.model.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} />
|
||||
{{else}}
|
||||
<Toolbar />
|
||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||
<LinkTo @route="configuration.create">
|
||||
Configure PKI
|
||||
</LinkTo>
|
||||
</EmptyState>
|
||||
{{/if}}
|
|
@ -0,0 +1 @@
|
|||
<Page::PkiTidyManual @model={{this.model}} @breadcrumbs={{this.breadcrumbs}} />
|
|
@ -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",
|
||||
|
|
Binary file not shown.
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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]',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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]',
|
||||
|
|
|
@ -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`<Page::PkiTidyAutoSettings @breadcrumbs={{this.breadcrumbs}} @model={{this.model}} />`,
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -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`<Page::PkiTidyForm @tidy={{this.tidy}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert.dom(SELECTORS.tidyCertStoreLabel).hasText('Tidy the certificate store');
|
||||
assert.dom(SELECTORS.tidyRevocationList).hasText('Tidy the revocation list (CRL)');
|
||||
assert.dom(SELECTORS.safetyBufferTTL).exists();
|
||||
assert.dom(SELECTORS.safetyBufferInput).hasValue('3');
|
||||
assert.dom('[data-test-select="ttl-unit"]').hasValue('d');
|
||||
});
|
||||
|
||||
test('it should change the attributes on the model', async function (assert) {
|
||||
await render(hbs`<Page::PkiTidyForm @tidy={{this.tidy}} @breadcrumbs={{this.breadcrumbs}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
await click(SELECTORS.tidyCertStoreCheckbox);
|
||||
await click(SELECTORS.tidyRevocationCheckbox);
|
||||
await fillIn(SELECTORS.safetyBufferInput, '5');
|
||||
assert.true(this.tidy.tidyCertStore);
|
||||
assert.true(this.tidy.tidyRevocationQueue);
|
||||
assert.dom(SELECTORS.safetyBufferInput).hasValue('5');
|
||||
assert.dom('[data-test-select="ttl-unit"]').hasValue('d');
|
||||
assert.strictEqual(this.tidy.safetyBuffer, '120h');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,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`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ 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`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy is inactive');
|
||||
// finished state
|
||||
this.tidyStatus.state = 'Finished';
|
||||
await render(
|
||||
hbs`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation finished');
|
||||
// error state
|
||||
this.tidyStatus.state = 'Error';
|
||||
await render(
|
||||
hbs`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation failed');
|
||||
// cancelling state
|
||||
this.tidyStatus.state = 'Cancelling';
|
||||
await render(
|
||||
hbs`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.hdsAlertTitle).hasText('Tidy operation cancelling');
|
||||
// cancelled state
|
||||
this.tidyStatus.state = 'Cancelled';
|
||||
await render(
|
||||
hbs`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ 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`<Page::PkiTidyStatus @autoTidyConfig={{this.autoTidyConfig}} @tidyStatus={{this.tidyStatus}} /> <div id="modal-wormhole"></div>,`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.timeStartedRow).exists();
|
||||
assert.dom(SELECTORS.timeFinishedRow).exists();
|
||||
});
|
||||
});
|
|
@ -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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.autoTidy}}
|
||||
@tidyType="auto"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.autoTidy}}
|
||||
@tidyType="auto"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.manualTidy}}
|
||||
@tidyType="manual"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.autoTidy}}
|
||||
@tidyType="auto"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.manualTidy}}
|
||||
@tidyType="manual"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.autoTidy}}
|
||||
@tidyType="auto"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.autoTidy}}
|
||||
@tidyType="auto"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ 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`
|
||||
<PkiTidyForm
|
||||
@tidy={{this.manualTidy}}
|
||||
@tidyType="manual"
|
||||
@onSave={{this.onSave}}
|
||||
@onCancel={{this.onCancel}}
|
||||
/>
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
|
||||
await click(SELECTORS.tidySave);
|
||||
await click(SELECTORS.tidyCancel);
|
||||
});
|
||||
});
|
|
@ -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 {};
|
||||
});
|
||||
this.payload = {
|
||||
enabled: true,
|
||||
interval_duration: '72h',
|
||||
backend: this.backend,
|
||||
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,
|
||||
};
|
||||
await this.store
|
||||
.createRecord('pki/tidy', this.payload)
|
||||
.save({ adapterOptions: { tidyType: 'auto-tidy' } });
|
||||
});
|
||||
|
||||
this.store.findRecord('pki/tidy', this.backend);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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[];
|
||||
}
|
Loading…
Reference in New Issue