From: Tatjana Dehler Date: Thu, 27 Sep 2018 11:22:05 +0000 (+0200) Subject: mgr/dashboard: add pattern validators X-Git-Tag: v14.0.1~99^2~3 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a4dacee4b604ac8357632afb00a931b5db1a2204;p=ceph.git mgr/dashboard: add pattern validators Add some pattern validators (ip, uuid, number, decimalNumber) that are needed to validate the config option input values. Fixes: http://tracker.ceph.com/issues/24455 Signed-off-by: Tatjana Dehler --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts index 9cad1c0e6918..fbf15dc87f68 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts @@ -23,6 +23,420 @@ describe('CdValidators', () => { }); }); + describe('ip validator', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + }); + + it('should not error on empty IPv4 addresses', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue(''); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept valid IPv4 address', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue('19.117.23.141'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on IPv4 address containing whitespace', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue('155.144.133.122 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('155. 144.133 .122'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue(' 155.144.133.122'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on IPv4 address containing invalid char', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue('155.144.eee.122 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('155.1?.133 .1&2'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on IPv4 address containing blocks higher than 255', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue('155.270.133.122 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('155.144.133.290 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should not error on empty IPv6 addresses', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(4)); + x.setValue(''); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept valid IPv6 address', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(6)); + x.setValue('c4dc:1475:cb0b:24ed:3c80:468b:70cd:1a95'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on IPv6 address containing too many blocks', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(6)); + x.setValue('c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95:a3f3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on IPv6 address containing more than 4 digits per block', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(6)); + x.setValue('c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on IPv6 address containing whitespace', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(6)); + x.setValue('c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('c4dc:14753 :cb0b:24ed:3c80 :468b:70cd :1a95'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue(' c4dc:14753:cb0b:24ed:3c80:468b:70cd:1a95'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on IPv6 address containing invalid char', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip(6)); + x.setValue('c4dx:14753:cb0b:24ed:3c80:468b:70cd:1a95 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('c4dx:14753:cb0b:24ed:3$80:468b:70cd:1a95 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should accept valid IPv4/6 addresses if not protocol version is given', () => { + const x = form.get('x'); + x.setValidators(CdValidators.ip()); + x.setValue('19.117.23.141'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('c4dc:1475:cb0b:24ed:3c80:468b:70cd:1a95'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + }); + + describe('uuid validator', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + form.controls['x'].setValidators(CdValidators.uuid()); + }); + + it('should accept empty value', () => { + const x = form.get('x'); + x.setValue(''); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept valid version 1 uuid', () => { + const x = form.get('x'); + x.setValue('171af0b2-c305-11e8-a355-529269fb1459'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept valid version 4 uuid', () => { + const x = form.get('x'); + x.setValue('e33bbcb6-fcc3-40b1-ae81-3f81706a35d5'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on uuid containing too many blocks', () => { + const x = form.get('x'); + x.setValue('e33bbcb6-fcc3-40b1-ae81-3f81706a35d5-23d3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on uuid containing too many chars in block', () => { + const x = form.get('x'); + x.setValue('aae33bbcb6-fcc3-40b1-ae81-3f81706a35d5'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on uuid containing invalid char', () => { + const x = form.get('x'); + x.setValue('x33bbcb6-fcc3-40b1-ae81-3f81706a35d5'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('$33bbcb6-fcc3-40b1-ae81-3f81706a35d5'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + }); + + describe('number validator', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + form.controls['x'].setValidators(CdValidators.number()); + }); + + it('should accept empty value', () => { + const x = form.get('x'); + x.setValue(''); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept numbers', () => { + const x = form.get('x'); + x.setValue(42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue(-42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on decimal numbers', () => { + const x = form.get('x'); + x.setValue(42.3); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue(-42.3); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('42.3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on chars', () => { + const x = form.get('x'); + x.setValue('char'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('42char'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on whitespaces', () => { + const x = form.get('x'); + x.setValue('42 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('4 2'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + }); + + describe('number validator (without negative values)', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + form.controls['x'].setValidators(CdValidators.number(false)); + }); + + it('should accept positive numbers', () => { + const x = form.get('x'); + x.setValue(42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on negative numbers', () => { + const x = form.get('x'); + x.setValue(-42); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('-42'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + }); + + describe('decimal number validator', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + form.controls['x'].setValidators(CdValidators.decimalNumber()); + }); + + it('should accept empty value', () => { + const x = form.get('x'); + x.setValue(''); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should accept numbers and decimal numbers', () => { + const x = form.get('x'); + x.setValue(42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue(-42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue(42.3); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue(-42.3); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42.3'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on chars', () => { + const x = form.get('x'); + x.setValue('42e'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('e42.3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + + it('should error on whitespaces', () => { + const x = form.get('x'); + x.setValue('42.3 '); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('42 .3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + }); + + describe('decimal number validator (without negative values)', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl() + }); + form.controls['x'].setValidators(CdValidators.decimalNumber(false)); + }); + + it('should accept positive numbers and decimals', () => { + const x = form.get('x'); + x.setValue(42); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue(42.3); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + + x.setValue('42.3'); + expect(x.valid).toBeTruthy(); + expect(x.hasError('pattern')).toBeFalsy(); + }); + + it('should error on negative numbers and decimals', () => { + const x = form.get('x'); + x.setValue(-42); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('-42'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue(-42.3); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + + x.setValue('-42.3'); + expect(x.valid).toBeFalsy(); + expect(x.hasError('pattern')).toBeTruthy(); + }); + }); + describe('requiredIf', () => { let form: FormGroup; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts index ac1fe187f769..0d4824309305 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts @@ -29,6 +29,65 @@ export class CdValidators { return Validators.email(control); } + /** + * Validator function in order to validate IP addresses. + * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and + * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a + * function to validate the input string against IPv4 OR IPv6. + * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` + * if the validation failed, otherwise `null`. + */ + static ip(version: number = 0): ValidatorFn { + // prettier-ignore + const ipv4Rgx = + /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i; + const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; + + if (version === 4) { + return Validators.pattern(ipv4Rgx); + } else if (version === 6) { + return Validators.pattern(ipv6Rgx); + } else { + return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source)); + } + } + + /** + * Validator function in order to validate uuids. + * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` + * if the validation failed, otherwise `null`. + */ + static uuid(): ValidatorFn { + const uuidRgx = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return Validators.pattern(uuidRgx); + } + + /** + * Validator function in order to validate numbers. + * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` + * if the validation failed, otherwise `null`. + */ + static number(allowsNegative: boolean = true): ValidatorFn { + if (allowsNegative) { + return Validators.pattern(/^-?[0-9]+$/i); + } else { + return Validators.pattern(/^[0-9]+$/i); + } + } + + /** + * Validator function in order to validate decimal numbers. + * @returns {ValidatorFn} A validator function that returns an error map containing `pattern` + * if the validation failed, otherwise `null`. + */ + static decimalNumber(allowsNegative: boolean = true): ValidatorFn { + if (allowsNegative) { + return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i); + } else { + return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i); + } + } + /** * Validator that requires controls to fulfill the specified condition if * the specified prerequisites matches. If the prerequisites are fulfilled,