UI: add error-handling and validation to pki cross-signing (#19022)
* return signed ca_chain if request fails, check for existing issuer name * update docs * add error border class to input
This commit is contained in:
parent
8fd9c9df0d
commit
aef0296472
|
@ -1,49 +1,59 @@
|
|||
{{yield}}
|
||||
{{#each this.inputList as |row index|}}
|
||||
<div class="field is-grouped">
|
||||
{{#each-in row as |inputKey value|}}
|
||||
{{#let (find-by "key" inputKey @objectKeys) as |field|}}
|
||||
<div class="control is-expanded">
|
||||
{{#if (eq index 0)}}
|
||||
<h2 data-test-object-list-label={{inputKey}}>{{field.label}}</h2>
|
||||
{{/if}}
|
||||
<Input
|
||||
data-test-object-list-input="{{field.key}}-{{index}}"
|
||||
aria-label="{{or field.placeholder field.label}} input for row index {{index}}"
|
||||
class="input"
|
||||
placeholder={{or field.placeholder ""}}
|
||||
@type="text"
|
||||
@value={{value}}
|
||||
name={{field.key}}
|
||||
id="{{field.key}}-{{index}}"
|
||||
{{on "input" (fn this.handleInput index)}}
|
||||
/>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each-in}}
|
||||
<div class="control is-align-end">
|
||||
{{#if (eq (inc index) this.inputList.length)}}
|
||||
<button
|
||||
data-test-object-list-add-button
|
||||
type="button"
|
||||
class="button is-outlined is-primary"
|
||||
disabled={{this.disableAdd}}
|
||||
aria-label="add"
|
||||
{{on "click" this.addRow}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
data-test-object-list-delete-button={{index}}
|
||||
type="button"
|
||||
class="button is-icon"
|
||||
aria-label="remove"
|
||||
{{on "click" (fn this.removeRow index)}}
|
||||
>
|
||||
<Icon @name="trash" />
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#let (get @validationErrors index) as |rowValidations|}}
|
||||
<div class="field is-grouped">
|
||||
{{#each-in row as |inputKey value|}}
|
||||
{{#let (find-by "key" inputKey @objectKeys) as |field|}}
|
||||
<div class="control is-expanded">
|
||||
{{#if (eq index 0)}}
|
||||
<h2 data-test-object-list-label={{inputKey}}>{{field.label}}</h2>
|
||||
{{/if}}
|
||||
{{#let (get rowValidations field.key) as |inputError|}}
|
||||
<Input
|
||||
data-test-object-list-input="{{field.key}}-{{index}}"
|
||||
aria-label="{{or field.placeholder field.label}} input for row index {{index}}"
|
||||
class="input {{if (and (not inputError.isValid) inputError.errors) 'has-error-border'}}"
|
||||
placeholder={{or field.placeholder ""}}
|
||||
@type="text"
|
||||
@value={{value}}
|
||||
name={{field.key}}
|
||||
id="{{field.key}}-{{index}}"
|
||||
{{on "input" (fn this.handleInput index)}}
|
||||
/>
|
||||
{{#if (and (not inputError.isValid) inputError.errors)}}
|
||||
<AlertInline @type="danger" @message={{inputError.errors}} @paddingTop={{true}} />
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each-in}}
|
||||
<div class="control {{if (includes false (map-by 'isValid' rowValidations)) '' 'is-align-end'}}">
|
||||
{{#if (eq index 0)}}
|
||||
<br />
|
||||
{{/if}}
|
||||
{{#if (eq (inc index) this.inputList.length)}}
|
||||
<button
|
||||
data-test-object-list-add-button
|
||||
type="button"
|
||||
class="button is-outlined is-primary"
|
||||
disabled={{this.disableAdd}}
|
||||
aria-label="add"
|
||||
{{on "click" this.addRow}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
data-test-object-list-delete-button={{index}}
|
||||
type="button"
|
||||
class="button is-icon"
|
||||
aria-label="remove"
|
||||
{{on "click" (fn this.removeRow index)}}
|
||||
>
|
||||
<Icon @name="trash" />
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
|
@ -22,6 +22,9 @@ import { assert } from '@ember/debug';
|
|||
* @param {array} objectKeys - an array of objects (sample above), the length determines the number of columns the component renders
|
||||
* @callback onChange - callback triggered when any input changes or when a row is deleted, called with array of objects containing each input's key and value ex: [ { attrKey: 'some input value' } ]
|
||||
* @param {string} [inputValue] - an array of objects to pre-fill the component inputs, key name must match objectKey[key]
|
||||
* @param {array} [validationErrors] - an array of validation objects, the index of each object corresponds to the row with an invalid input. each object has a key that matches a key in objectKeys
|
||||
* ex: (the nested object with 'errors' and 'isValid' matches the structure returned by the model validations decorator)
|
||||
* { "attrKey": { "errors": ["Name is required."], "isValid": false } }
|
||||
*/
|
||||
|
||||
export default class ObjectListInput extends Component {
|
||||
|
|
|
@ -81,7 +81,11 @@
|
|||
</button>
|
||||
{{else}}
|
||||
<form {{on "submit" (perform this.submit)}} data-test-cross-sign-form>
|
||||
<ObjectListInput @objectKeys={{this.inputFields}} @onChange={{fn (mut this.formData)}} />
|
||||
<ObjectListInput
|
||||
@objectKeys={{this.inputFields}}
|
||||
@onChange={{fn (mut this.formData)}}
|
||||
@validationErrors={{this.validationErrors}}
|
||||
/>
|
||||
<div class="control box is-fullwidth has-only-top-shadow">
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
@ -40,6 +40,8 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
@service store;
|
||||
@tracked formData = [];
|
||||
@tracked signedIssuers = [];
|
||||
@tracked intermediateIssuers = {};
|
||||
@tracked validationErrors = [];
|
||||
|
||||
inputFields = [
|
||||
{
|
||||
|
@ -73,6 +75,23 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
*submit(e) {
|
||||
e.preventDefault();
|
||||
this.signedIssuers = [];
|
||||
this.validationErrors = [];
|
||||
|
||||
// Validate name input for new issuer does not already exist in mount
|
||||
for (let row = 0; row < this.formData.length; row++) {
|
||||
const { intermediateMount, newCrossSignedIssuer } = this.formData[row];
|
||||
const issuers = yield this.store
|
||||
.query('pki/issuer', { backend: intermediateMount })
|
||||
.then((resp) => resp.map(({ issuerName, issuerId }) => ({ issuerName, issuerId })))
|
||||
.catch(() => []);
|
||||
|
||||
// for cross-signing error handling we want to record the list of issuers before the process starts
|
||||
this.intermediateIssuers[intermediateMount] = issuers;
|
||||
this.validationErrors.addObject({
|
||||
newCrossSignedIssuer: this.nameValidation(newCrossSignedIssuer, issuers),
|
||||
});
|
||||
}
|
||||
if (this.validationErrors.any((row) => !row.newCrossSignedIssuer.isValid)) return;
|
||||
|
||||
// iterate through submitted data and cross-sign each certificate
|
||||
for (let row = 0; row < this.formData.length; row++) {
|
||||
|
@ -99,7 +118,6 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
@action
|
||||
async crossSignIntermediate(intMount, intName, newCrossSignedIssuer) {
|
||||
// 1. Fetch issuer we want to sign
|
||||
//
|
||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||
const existingIssuer = await this.store.queryRecord('pki/issuer', {
|
||||
backend: intMount,
|
||||
|
@ -117,7 +135,6 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
}
|
||||
|
||||
// 2. Create the new CSR
|
||||
//
|
||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||
const newCsr = await this.store
|
||||
.createRecord('pki/action', {
|
||||
|
@ -133,7 +150,6 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
|
||||
// 3. Sign newCSR with correct parent to create cross-signed cert, "issuing"
|
||||
// an intermediate certificate.
|
||||
//
|
||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||
const signedCaChain = await this.store
|
||||
.createRecord('pki/action', {
|
||||
|
@ -151,9 +167,7 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
.then(({ caChain }) => caChain.join('\n'));
|
||||
|
||||
// 4. Import the newly cross-signed cert to become an issuer
|
||||
//
|
||||
// What/Recovery:
|
||||
//
|
||||
// 1. Permission issue -> give the cert (`signedCaChain`) to the user,
|
||||
// let them import & name. (Issue you have is that you already issued
|
||||
// it (step 3) and so "undo" would mean revoking the cert, which
|
||||
|
@ -188,6 +202,10 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
// matching key is the issuer_id
|
||||
(key) => importedIssuer.mapping[key] === existingIssuer.keyId
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.debug('CA_CHAIN \n', signedCaChain); // eslint-disable-line
|
||||
throw new Error(`${errorMessage(e)} See console for signed ca_chain data.`);
|
||||
});
|
||||
|
||||
// 5. Fetch issuer imported above by issuer_id, name and save
|
||||
|
@ -210,6 +228,16 @@ export default class PkiIssuerCrossSign extends Component {
|
|||
@action
|
||||
reset() {
|
||||
this.signedIssuers = [];
|
||||
this.validationErrors = [];
|
||||
this.formData = [];
|
||||
}
|
||||
|
||||
nameValidation(nameInput, existing) {
|
||||
if (existing.any((i) => i.issuerName === nameInput || i.issuerId === nameInput))
|
||||
return {
|
||||
errors: [`Issuer reference '${nameInput}' already exists in this mount.`],
|
||||
isValid: false,
|
||||
};
|
||||
return { errors: [], isValid: true };
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue