Skip to content

Commit

Permalink
UI: pki configuration edit form (hashicorp#20245)
Browse files Browse the repository at this point in the history
* setup routing, move queries in ConfigurationIndex to parent resource route

* finish building out form, add model attrs build ttls

* add types

* update model attribute values, fix default ttl states

* remove defaults and use openApi, group with booleans

* add model to application route"

* add save functionality

* add error banner

* add transition after save

* use defaults from open api

* fix empty state language

* pass engine data

* change model attrs to ttl objects

* update types

* add invalid form alert to error block

* move data manipulation to serialize

* fix serializer, add comments

* add test for serializer

* edit configuration details view

* update details test

* change to updateRecord so POST request is made

* config/urls use POST instead of PUT

* add edit tests, update details

* add model hooks back to routes

* rearrange to remove dif

* remove createRecord for urls

* update comment

* wip sample ttl transform

* Revert "wip sample ttl transform"

This reverts commit 59fc179.

* revert changes, move model updates back to component

* simplify model fetches

* address comments;

* update pki/urls test

* update adapter test
  • Loading branch information
hellobontempo authored Apr 25, 2023
1 parent 445e2e9 commit ac1cd58
Show file tree
Hide file tree
Showing 23 changed files with 786 additions and 183 deletions.
5 changes: 5 additions & 0 deletions ui/app/adapters/pki/crl.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ export default class PkiCrlAdapter extends ApplicationAdapter {
return resp.data;
});
}

updateRecord(store, type, snapshot) {
const data = snapshot.serialize();
return this.ajax(this._url(snapshot.record.id), 'POST', { data });
}
}
9 changes: 4 additions & 5 deletions ui/app/adapters/pki/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ export default class PkiUrlsAdapter extends ApplicationAdapter {
return `${this.buildURL()}/${encodePath(backend)}/config/urls`;
}

urlForCreateRecord(modelName, snapshot) {
return this._url(snapshot.record.id);
updateRecord(store, type, snapshot) {
const data = snapshot.serialize();
return this.ajax(this._url(snapshot.record.id), 'POST', { data });
}

urlForFindRecord(id) {
return this._url(id);
}
urlForUpdateRecord(store, type, snapshot) {
return this._url(snapshot.record.id);
}
}
62 changes: 57 additions & 5 deletions ui/app/models/pki/crl.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,67 @@
*/

import Model, { attr } from '@ember-data/model';
import { withFormFields } from 'vault/decorators/model-form-fields';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';

@withFormFields(['expiry', 'autoRebuildGracePeriod', 'deltaRebuildInterval', 'ocspExpiry'])
export default class PkiCrlModel extends Model {
// This model uses the backend value as the model ID
get useOpenAPI() {
return true;
}

@attr('string') expiry;
@attr('boolean') autoRebuild;
@attr('string') ocspExpiry;
@attr('string', {
label: 'Auto-rebuild on',
labelDisabled: 'Auto-rebuild off',
mapToBoolean: 'autoRebuild',
isOppositeValue: false,
helperTextEnabled: 'Vault will rebuild the CRL in the below grace period before expiration',
helperTextDisabled: 'Vault will not automatically rebuild the CRL',
})
autoRebuildGracePeriod;

@attr('boolean') enableDelta;
@attr('string', {
label: 'Delta CRL building on',
labelDisabled: 'Delta CRL building off',
mapToBoolean: 'enableDelta',
isOppositeValue: false,
helperTextEnabled: 'Vault will rebuild the delta CRL at the interval below:',
helperTextDisabled: 'Vault will not rebuild the delta CRL at an interval',
})
deltaRebuildInterval;

@attr('boolean') disable;
@attr('string', {
label: 'Expiry',
labelDisabled: 'No expiry',
mapToBoolean: 'disable',
isOppositeValue: true,
helperTextDisabled: 'The CRL will not be built.',
helperTextEnabled: 'The CRL will expire after:',
})
expiry;

@attr('boolean') ocspDisable;
@attr('string', {
label: 'OCSP responder APIs enabled',
labelDisabled: 'OCSP responder APIs disabled',
mapToBoolean: 'ocspDisable',
isOppositeValue: true,
helperTextEnabled: "Requests about a certificate's status will be valid for:",
helperTextDisabled: 'Requests cannot be made to check if an individual certificate is valid.',
})
ocspExpiry;

// TODO follow-on ticket to add enterprise only attributes:
/*
@attr('boolean') crossClusterRevocation;
@attr('boolean') unifiedCrl;
@attr('boolean') unifiedCrlOnExistingPaths;
*/

@lazyCapabilities(apiPath`${'id'}/config/crl`, 'id') crlPath;

get canSet() {
return this.crlPath.get('canCreate') !== false;
}
}
4 changes: 4 additions & 0 deletions ui/lib/core/addon/components/ttl-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* @param onChange {Function} - This function will be passed a TTL object, which includes enabled{bool}, seconds{number}, timeString{string}, goSafeTimeString{string}.
* @param initialEnabled=false {Boolean} - Set this value if you want the toggle on when component is mounted
* @param label="Time to live (TTL)" {String} - Label is the main label that lives next to the toggle. Yielded values will replace the label
* @param labelDisabled=Label to display when TTL is toggled off
* @param helperTextEnabled="" {String} - This helper text is shown under the label when the toggle is switched on
* @param helperTextDisabled="" {String} - This helper text is shown under the label when the toggle is switched off
* @param initialValue=null {string} - InitialValue is the duration value which will be shown when the component is loaded. If it can't be parsed, will default to 0.
Expand Down Expand Up @@ -52,6 +53,9 @@ export default class TtlPickerComponent extends Component {
elementId = 'ttl-' + guidFor(this);

get label() {
if (this.args.label && this.args.labelDisabled) {
return this.enableTTL ? this.args.label : this.args.labelDisabled;
}
return this.args.label || 'Time to live (TTL)';
}
get helperText() {
Expand Down
31 changes: 27 additions & 4 deletions ui/lib/pki/addon/components/page/pki-configuration-details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,37 @@
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
Certificate Revocation List (CRL)
</h2>
<InfoTableRow @label="Expiry" @value={{@crl.expiry}} />
<InfoTableRow @label="Auto-rebuild" @value={{if @crl.autoRebuild "On" "Off"}} />

<InfoTableRow @label="CRL building" @value={{if @crl.disable "Disabled" "Enabled"}} />
{{#unless @crl.disable}}
<InfoTableRow @label="Expiry" @value={{@crl.expiry}} />
<InfoTableRow @label="Auto-rebuild">
<Icon
class={{if @crl.autoRebuild "icon-true" "icon-false"}}
@name={{if @crl.autoRebuild "check-circle" "x-square"}}
/>
{{if @crl.autoRebuild "On" "Off"}}
</InfoTableRow>
{{#if @crl.autoRebuild}}
<InfoTableRow @label="Auto-rebuild grace period" @value={{@crl.autoRebuildGracePeriod}} />
{{/if}}
<InfoTableRow @label="Delta CRL building">
<Icon
class={{if @crl.enableDelta "icon-true" "icon-false"}}
@name={{if @crl.enableDelta "check-circle" "x-square"}}
/>
{{if @crl.enableDelta "On" "Off"}}
</InfoTableRow>
{{#if @crl.enableDelta}}
<InfoTableRow @label="Delta rebuild interval" @value={{@crl.deltaRebuildInterval}} />
{{/if}}
{{/unless}}
<h2 class="title is-4 has-bottom-margin-xs has-top-margin-xl has-border-bottom-light has-bottom-padding-s">
Online Certificate Status Protocol (OCSP)
</h2>
<InfoTableRow @label="Responder APIs" @value={{if @crl.ocspDisable "Disabled" "Enabled"}} />
<InfoTableRow @label="Interval" @value={{@crl.ocspExpiry}} />
{{#unless @crl.ocspDisable}}
<InfoTableRow @label="Interval" @value={{@crl.ocspExpiry}} />
{{/unless}}
{{/if}}
{{else}}
<Toolbar>
Expand Down
99 changes: 99 additions & 0 deletions ui/lib/pki/addon/components/page/pki-configuration-edit.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<div class="box is-sideless is-fullwidth is-marginless">
{{#if this.errorBanner}}
<AlertBanner @type="danger" @message={{this.errorBanner}} data-test-error-banner />
{{/if}}
<form {{on "submit" (perform this.save)}}>
<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-urls-edit-section>
<h2 class="title is-size-5 has-border-bottom-light page-header">
Global URLs
</h2>
{{#if @urls.canSet}}
{{#each @urls.allFields as |attr|}}
<FormField @attr={{attr}} @model={{@urls}} @showHelpText={{false}} />
{{/each}}
{{else}}
<EmptyState
class="is-box-shadowless"
@title="You do not have permission to set URLs"
@message="Ask your administrator if you think you should have access to:"
>
<code>POST /{{@backend}}/config/urls</code>
</EmptyState>
{{/if}}
</fieldset>

<fieldset class="box is-shadowless is-marginless is-borderless is-fullwidth" data-test-crl-edit-section>
<h2 class="title is-size-5 has-border-bottom-light page-header">
Certificate Revocation List (CRL)
</h2>
{{#if @crl.canSet}}
{{#each @crl.formFields as |attr|}}
{{#if (eq attr.name "ocspExpiry")}}
<h2 class="title is-size-5 has-border-bottom-light page-header">
Online Certificate Status Protocol (OCSP)
</h2>
{{/if}}
{{#if (or (includes attr.name this.alwaysRender) (not @crl.disable))}}
{{#let (get @crl attr.options.mapToBoolean) as |booleanValue|}}
<div class="field">
<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={{if attr.options.isOppositeValue (not booleanValue) booleanValue}}
@initialValue={{get @crl attr.name}}
/>
</div>
{{/let}}
{{/if}}
{{/each}}
{{else}}
<EmptyState
class="is-box-shadowless"
@title="You do not have permission to set revocation configuration"
@message="Ask your administrator if you think you should have access to:"
>
<code>POST /{{@backend}}/config/crl</code>
</EmptyState>
{{/if}}
</fieldset>

<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
{{#if (or @urls.canSet @crl.canSet)}}
<button
type="submit"
class="button is-primary {{if this.save.isRunning 'is-loading'}}"
disabled={{this.save.isRunning}}
data-test-configuration-edit-save
>
Save
</button>
{{/if}}
<button
{{on "click" this.cancel}}
type="button"
class="button has-left-margin-s"
disabled={{this.save.isRunning}}
data-test-configuration-edit-cancel
>
Cancel
</button>
</div>
{{#if this.invalidFormAlert}}
<div class="control">
<AlertInline
@type="danger"
@paddingTop={{true}}
@message={{this.invalidFormAlert}}
@mimicRefresh={{true}}
data-test-configuration-edit-validation-alert
/>
</div>
{{/if}}
</div>
</form>
</div>
77 changes: 77 additions & 0 deletions ui/lib/pki/addon/components/page/pki-configuration-edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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 { action } from '@ember/object';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import RouterService from '@ember/routing/router-service';
import FlashMessageService from 'vault/services/flash-messages';
import { FormField, TtlEvent } from 'vault/app-types';
import PkiCrlModel from 'vault/models/pki/crl';
import PkiUrlsModel from 'vault/models/pki/urls';
import errorMessage from 'vault/utils/error-message';

interface Args {
crl: PkiCrlModel;
urls: PkiUrlsModel;
}
interface PkiCrlTtls {
autoRebuildGracePeriod: string;
expiry: string;
deltaRebuildInterval: string;
ocspExpiry: string;
}
interface PkiCrlBooleans {
autoRebuild: boolean;
enableDelta: boolean;
disable: boolean;
ocspDisable: boolean;
}
export default class PkiConfigurationEditComponent extends Component<Args> {
@service declare readonly router: RouterService;
@service declare readonly flashMessages: FlashMessageService;

@tracked invalidFormAlert = '';
@tracked errorBanner = '';

get alwaysRender() {
return ['expiry', 'ocspExpiry'];
}

@task
@waitFor
*save(event: Event) {
event.preventDefault();
try {
yield this.args.urls.save();
yield this.args.crl.save();
this.flashMessages.success('Successfully updated configuration');
this.router.transitionTo('vault.cluster.secrets.backend.pki.configuration.index');
} catch (error) {
this.invalidFormAlert = 'There was an error submitting this form.';
this.errorBanner = errorMessage(error);
}
}

@action
cancel() {
this.router.transitionTo('vault.cluster.secrets.backend.pki.configuration.index');
}

@action
handleTtl(attr: FormField, e: TtlEvent) {
const { enabled, goSafeTimeString } = e;
const ttlAttr = attr.name;
this.args.crl[ttlAttr as keyof PkiCrlTtls] = goSafeTimeString;
// expiry and ocspExpiry both correspond to 'disable' booleans
// so when ttl is enabled, the booleans are set to false
this.args.crl[attr.options.mapToBoolean as keyof PkiCrlBooleans] = attr.options.isOppositeValue
? !enabled
: enabled;
}
}
2 changes: 2 additions & 0 deletions ui/lib/pki/addon/decorators/check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

/**
* the overview, roles, issuers, certificates, and key routes all need to be aware of the whether there is a config for the engine
Expand All @@ -21,6 +22,7 @@ export function withConfig() {
return SuperClass;
}
return class CheckConfig extends SuperClass {
@service secretMountPath;
shouldPromptConfig = false;

async beforeModel() {
Expand Down
15 changes: 14 additions & 1 deletion ui/lib/pki/addon/routes/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,18 @@
*/

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';

export default class PkiConfigurationRoute extends Route {}
export default class PkiConfigurationRoute extends Route {
@service store;

model() {
const engine = this.modelFor('application');
return hash({
engine,
urls: this.store.findRecord('pki/urls', engine.id).catch((e) => e.httpStatus),
crl: this.store.findRecord('pki/crl', engine.id).catch((e) => e.httpStatus),
});
}
}
10 changes: 1 addition & 9 deletions ui/lib/pki/addon/routes/configuration/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class PkiConfigurationCreateRoute extends Route {
model() {
return hash({
config: this.store.createRecord('pki/action'),
urls: this.getOrCreateUrls(this.secretMountPath.currentPath),
urls: this.modelFor('configuration').urls,
});
}

Expand All @@ -28,12 +28,4 @@ export default class PkiConfigurationCreateRoute extends Route {
{ label: 'configure' },
];
}

async getOrCreateUrls(backend) {
try {
return this.store.findRecord('pki/urls', backend);
} catch (e) {
return this.store.createRecord('pki/urls', { id: backend });
}
}
}
Loading

0 comments on commit ac1cd58

Please sign in to comment.