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:
claire bontempo 2023-02-07 12:09:17 -08:00 committed by GitHub
parent 8fd9c9df0d
commit aef0296472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 51 deletions

View File

@ -1,5 +1,6 @@
{{yield}}
{{#each this.inputList as |row index|}}
{{#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|}}
@ -7,10 +8,11 @@
{{#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"
class="input {{if (and (not inputError.isValid) inputError.errors) 'has-error-border'}}"
placeholder={{or field.placeholder ""}}
@type="text"
@value={{value}}
@ -18,10 +20,17 @@
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 is-align-end">
<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
@ -46,4 +55,5 @@
{{/if}}
</div>
</div>
{{/let}}
{{/each}}

View File

@ -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 {

View File

@ -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"

View File

@ -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 };
}
}