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}}
|
{{yield}}
|
||||||
{{#each this.inputList as |row index|}}
|
{{#each this.inputList as |row index|}}
|
||||||
<div class="field is-grouped">
|
{{#let (get @validationErrors index) as |rowValidations|}}
|
||||||
{{#each-in row as |inputKey value|}}
|
<div class="field is-grouped">
|
||||||
{{#let (find-by "key" inputKey @objectKeys) as |field|}}
|
{{#each-in row as |inputKey value|}}
|
||||||
<div class="control is-expanded">
|
{{#let (find-by "key" inputKey @objectKeys) as |field|}}
|
||||||
{{#if (eq index 0)}}
|
<div class="control is-expanded">
|
||||||
<h2 data-test-object-list-label={{inputKey}}>{{field.label}}</h2>
|
{{#if (eq index 0)}}
|
||||||
{{/if}}
|
<h2 data-test-object-list-label={{inputKey}}>{{field.label}}</h2>
|
||||||
<Input
|
{{/if}}
|
||||||
data-test-object-list-input="{{field.key}}-{{index}}"
|
{{#let (get rowValidations field.key) as |inputError|}}
|
||||||
aria-label="{{or field.placeholder field.label}} input for row index {{index}}"
|
<Input
|
||||||
class="input"
|
data-test-object-list-input="{{field.key}}-{{index}}"
|
||||||
placeholder={{or field.placeholder ""}}
|
aria-label="{{or field.placeholder field.label}} input for row index {{index}}"
|
||||||
@type="text"
|
class="input {{if (and (not inputError.isValid) inputError.errors) 'has-error-border'}}"
|
||||||
@value={{value}}
|
placeholder={{or field.placeholder ""}}
|
||||||
name={{field.key}}
|
@type="text"
|
||||||
id="{{field.key}}-{{index}}"
|
@value={{value}}
|
||||||
{{on "input" (fn this.handleInput index)}}
|
name={{field.key}}
|
||||||
/>
|
id="{{field.key}}-{{index}}"
|
||||||
</div>
|
{{on "input" (fn this.handleInput index)}}
|
||||||
{{/let}}
|
/>
|
||||||
{{/each-in}}
|
{{#if (and (not inputError.isValid) inputError.errors)}}
|
||||||
<div class="control is-align-end">
|
<AlertInline @type="danger" @message={{inputError.errors}} @paddingTop={{true}} />
|
||||||
{{#if (eq (inc index) this.inputList.length)}}
|
{{/if}}
|
||||||
<button
|
{{/let}}
|
||||||
data-test-object-list-add-button
|
</div>
|
||||||
type="button"
|
{{/let}}
|
||||||
class="button is-outlined is-primary"
|
{{/each-in}}
|
||||||
disabled={{this.disableAdd}}
|
<div class="control {{if (includes false (map-by 'isValid' rowValidations)) '' 'is-align-end'}}">
|
||||||
aria-label="add"
|
{{#if (eq index 0)}}
|
||||||
{{on "click" this.addRow}}
|
<br />
|
||||||
>
|
{{/if}}
|
||||||
Add
|
{{#if (eq (inc index) this.inputList.length)}}
|
||||||
</button>
|
<button
|
||||||
{{else}}
|
data-test-object-list-add-button
|
||||||
<button
|
type="button"
|
||||||
data-test-object-list-delete-button={{index}}
|
class="button is-outlined is-primary"
|
||||||
type="button"
|
disabled={{this.disableAdd}}
|
||||||
class="button is-icon"
|
aria-label="add"
|
||||||
aria-label="remove"
|
{{on "click" this.addRow}}
|
||||||
{{on "click" (fn this.removeRow index)}}
|
>
|
||||||
>
|
Add
|
||||||
<Icon @name="trash" />
|
</button>
|
||||||
</button>
|
{{else}}
|
||||||
{{/if}}
|
<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>
|
||||||
</div>
|
{{/let}}
|
||||||
{{/each}}
|
{{/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
|
* @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' } ]
|
* @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 {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 {
|
export default class ObjectListInput extends Component {
|
||||||
|
|
|
@ -81,7 +81,11 @@
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form {{on "submit" (perform this.submit)}} data-test-cross-sign-form>
|
<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">
|
<div class="control box is-fullwidth has-only-top-shadow">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -40,6 +40,8 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
@service store;
|
@service store;
|
||||||
@tracked formData = [];
|
@tracked formData = [];
|
||||||
@tracked signedIssuers = [];
|
@tracked signedIssuers = [];
|
||||||
|
@tracked intermediateIssuers = {};
|
||||||
|
@tracked validationErrors = [];
|
||||||
|
|
||||||
inputFields = [
|
inputFields = [
|
||||||
{
|
{
|
||||||
|
@ -73,6 +75,23 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
*submit(e) {
|
*submit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.signedIssuers = [];
|
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
|
// iterate through submitted data and cross-sign each certificate
|
||||||
for (let row = 0; row < this.formData.length; row++) {
|
for (let row = 0; row < this.formData.length; row++) {
|
||||||
|
@ -99,7 +118,6 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
@action
|
@action
|
||||||
async crossSignIntermediate(intMount, intName, newCrossSignedIssuer) {
|
async crossSignIntermediate(intMount, intName, newCrossSignedIssuer) {
|
||||||
// 1. Fetch issuer we want to sign
|
// 1. Fetch issuer we want to sign
|
||||||
//
|
|
||||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||||
const existingIssuer = await this.store.queryRecord('pki/issuer', {
|
const existingIssuer = await this.store.queryRecord('pki/issuer', {
|
||||||
backend: intMount,
|
backend: intMount,
|
||||||
|
@ -117,7 +135,6 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Create the new CSR
|
// 2. Create the new CSR
|
||||||
//
|
|
||||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||||
const newCsr = await this.store
|
const newCsr = await this.store
|
||||||
.createRecord('pki/action', {
|
.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"
|
// 3. Sign newCSR with correct parent to create cross-signed cert, "issuing"
|
||||||
// an intermediate certificate.
|
// an intermediate certificate.
|
||||||
//
|
|
||||||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||||||
const signedCaChain = await this.store
|
const signedCaChain = await this.store
|
||||||
.createRecord('pki/action', {
|
.createRecord('pki/action', {
|
||||||
|
@ -151,9 +167,7 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
.then(({ caChain }) => caChain.join('\n'));
|
.then(({ caChain }) => caChain.join('\n'));
|
||||||
|
|
||||||
// 4. Import the newly cross-signed cert to become an issuer
|
// 4. Import the newly cross-signed cert to become an issuer
|
||||||
//
|
|
||||||
// What/Recovery:
|
// What/Recovery:
|
||||||
//
|
|
||||||
// 1. Permission issue -> give the cert (`signedCaChain`) to the user,
|
// 1. Permission issue -> give the cert (`signedCaChain`) to the user,
|
||||||
// let them import & name. (Issue you have is that you already issued
|
// let them import & name. (Issue you have is that you already issued
|
||||||
// it (step 3) and so "undo" would mean revoking the cert, which
|
// 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
|
// matching key is the issuer_id
|
||||||
(key) => importedIssuer.mapping[key] === existingIssuer.keyId
|
(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
|
// 5. Fetch issuer imported above by issuer_id, name and save
|
||||||
|
@ -210,6 +228,16 @@ export default class PkiIssuerCrossSign extends Component {
|
||||||
@action
|
@action
|
||||||
reset() {
|
reset() {
|
||||||
this.signedIssuers = [];
|
this.signedIssuers = [];
|
||||||
|
this.validationErrors = [];
|
||||||
this.formData = [];
|
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 a new issue