]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / shared / forms / cd-validators.ts
1 import {
2 AbstractControl,
3 AsyncValidatorFn,
4 ValidationErrors,
5 ValidatorFn,
6 Validators
7 } from '@angular/forms';
8
9 import _ from 'lodash';
10 import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
11 import { map, switchMapTo, take } from 'rxjs/operators';
12
13 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
14 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
15 import { FormatterService } from '~/app/shared/services/formatter.service';
16
17 export function isEmptyInputValue(value: any): boolean {
18 return value == null || value.length === 0;
19 }
20
21 export type existsServiceFn = (value: any, args?: any) => Observable<boolean>;
22
23 export class CdValidators {
24 /**
25 * Validator that performs email validation. In contrast to the Angular
26 * email validator an empty email will not be handled as invalid.
27 */
28 static email(control: AbstractControl): ValidationErrors | null {
29 // Exit immediately if value is empty.
30 if (isEmptyInputValue(control.value)) {
31 return null;
32 }
33 return Validators.email(control);
34 }
35
36 /**
37 * Validator function in order to validate IP addresses.
38 * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and
39 * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a
40 * function to validate the input string against IPv4 OR IPv6.
41 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
42 * if the validation check fails, otherwise `null`.
43 */
44 static ip(version: number = 0): ValidatorFn {
45 // prettier-ignore
46 const ipv4Rgx =
47 /^((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;
48 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
49
50 if (version === 4) {
51 return Validators.pattern(ipv4Rgx);
52 } else if (version === 6) {
53 return Validators.pattern(ipv6Rgx);
54 } else {
55 return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source));
56 }
57 }
58
59 /**
60 * Validator function in order to validate numbers.
61 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
62 * if the validation check fails, otherwise `null`.
63 */
64 static number(allowsNegative: boolean = true): ValidatorFn {
65 if (allowsNegative) {
66 return Validators.pattern(/^-?[0-9]+$/i);
67 } else {
68 return Validators.pattern(/^[0-9]+$/i);
69 }
70 }
71
72 /**
73 * Validator function in order to validate decimal numbers.
74 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
75 * if the validation check fails, otherwise `null`.
76 */
77 static decimalNumber(allowsNegative: boolean = true): ValidatorFn {
78 if (allowsNegative) {
79 return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i);
80 } else {
81 return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i);
82 }
83 }
84
85 /**
86 * Validator that performs SSL certificate validation.
87 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
88 * if the validation check fails, otherwise `null`.
89 */
90 static sslCert(): ValidatorFn {
91 return Validators.pattern(
92 /^-----BEGIN CERTIFICATE-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END CERTIFICATE-----[\n\r\f]*$/
93 );
94 }
95
96 /**
97 * Validator that performs SSL private key validation.
98 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
99 * if the validation check fails, otherwise `null`.
100 */
101 static sslPrivKey(): ValidatorFn {
102 return Validators.pattern(
103 /^-----BEGIN RSA PRIVATE KEY-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END RSA PRIVATE KEY-----[\n\r\f]*$/
104 );
105 }
106
107 /**
108 * Validator that performs SSL certificate validation of pem format.
109 * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
110 * if the validation check fails, otherwise `null`.
111 */
112 static pemCert(): ValidatorFn {
113 return Validators.pattern(/^-----BEGIN .+-----$.+^-----END .+-----$/ms);
114 }
115
116 /**
117 * Validator that requires controls to fulfill the specified condition if
118 * the specified prerequisites matches. If the prerequisites are fulfilled,
119 * then the given function is executed and if it succeeds, the 'required'
120 * validation error will be returned, otherwise null.
121 * @param {Object} prerequisites An object containing the prerequisites.
122 * To do additional checks rather than checking for equality you can
123 * use the extended prerequisite syntax:
124 * 'field_name': { 'op': '<OPERATOR>', arg1: '<OPERATOR_ARGUMENT>' }
125 * The following operators are supported:
126 * * empty
127 * * !empty
128 * * equal
129 * * !equal
130 * * minLength
131 * ### Example
132 * ```typescript
133 * {
134 * 'generate_key': true,
135 * 'username': 'Max Mustermann'
136 * }
137 * ```
138 * ### Example - Extended prerequisites
139 * ```typescript
140 * {
141 * 'generate_key': { 'op': 'equal', 'arg1': true },
142 * 'username': { 'op': 'minLength', 'arg1': 5 }
143 * }
144 * ```
145 * Only if all prerequisites are fulfilled, then the validation of the
146 * control will be triggered.
147 * @param {Function | undefined} condition The function to be executed when all
148 * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
149 * function will be used by default. The control's value is used as function
150 * argument. The function must return true to set the validation error.
151 * @return {ValidatorFn} Returns the validator function.
152 */
153 static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn {
154 let isWatched = false;
155
156 return (control: AbstractControl): ValidationErrors | null => {
157 if (!isWatched && control.parent) {
158 Object.keys(prerequisites).forEach((key) => {
159 control.parent.get(key).valueChanges.subscribe(() => {
160 control.updateValueAndValidity({ emitEvent: false });
161 });
162 });
163
164 isWatched = true;
165 }
166
167 // Check if all prerequisites met.
168 if (
169 !Object.keys(prerequisites).every((key) => {
170 if (!control.parent) {
171 return false;
172 }
173 const value = control.parent.get(key).value;
174 const prerequisite = prerequisites[key];
175 if (_.isObjectLike(prerequisite)) {
176 let result = false;
177 switch (prerequisite['op']) {
178 case 'empty':
179 result = _.isEmpty(value);
180 break;
181 case '!empty':
182 result = !_.isEmpty(value);
183 break;
184 case 'equal':
185 result = value === prerequisite['arg1'];
186 break;
187 case '!equal':
188 result = value !== prerequisite['arg1'];
189 break;
190 case 'minLength':
191 if (_.isString(value)) {
192 result = value.length >= prerequisite['arg1'];
193 }
194 break;
195 }
196 return result;
197 }
198 return value === prerequisite;
199 })
200 ) {
201 return null;
202 }
203 const success = _.isFunction(condition)
204 ? condition.call(condition, control.value)
205 : isEmptyInputValue(control.value);
206 return success ? { required: true } : null;
207 };
208 }
209
210 /**
211 * Compose multiple validators into a single function that returns the union of
212 * the individual error maps for the provided control when the given prerequisites
213 * are fulfilled.
214 *
215 * @param {Object} prerequisites An object containing the prerequisites as
216 * key/value pairs.
217 * ### Example
218 * ```typescript
219 * {
220 * 'generate_key': true,
221 * 'username': 'Max Mustermann'
222 * }
223 * ```
224 * @param {ValidatorFn[]} validators List of validators that should be taken
225 * into action when the prerequisites are met.
226 * @return {ValidatorFn} Returns the validator function.
227 */
228 static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn {
229 let isWatched = false;
230 return (control: AbstractControl): ValidationErrors | null => {
231 if (!isWatched && control.parent) {
232 Object.keys(prerequisites).forEach((key) => {
233 control.parent.get(key).valueChanges.subscribe(() => {
234 control.updateValueAndValidity({ emitEvent: false });
235 });
236 });
237 isWatched = true;
238 }
239 // Check if all prerequisites are met.
240 if (
241 !Object.keys(prerequisites).every((key) => {
242 return control.parent && control.parent.get(key).value === prerequisites[key];
243 })
244 ) {
245 return null;
246 }
247 return Validators.compose(validators)(control);
248 };
249 }
250
251 /**
252 * Custom validation by passing a name for the error and a function as error condition.
253 *
254 * @param {string} error
255 * @param {Function} condition - a truthy return value will trigger the error
256 * @returns {ValidatorFn}
257 */
258 static custom(error: string, condition: Function): ValidatorFn {
259 return (control: AbstractControl): { [key: string]: any } => {
260 const value = condition.call(this, control.value);
261 if (value) {
262 return { [error]: value };
263 }
264 return null;
265 };
266 }
267
268 /**
269 * Validate form control if condition is true with validators.
270 *
271 * @param {AbstractControl} formControl
272 * @param {Function} condition
273 * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
274 * when the condition is met
275 * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
276 * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
277 * Every time one of this controls value is updated, the validation will be triggered
278 */
279 static validateIf(
280 formControl: AbstractControl,
281 condition: Function,
282 conditionalValidators: ValidatorFn[],
283 permanentValidators: ValidatorFn[] = [],
284 watchControls: AbstractControl[] = []
285 ) {
286 conditionalValidators = conditionalValidators.concat(permanentValidators);
287
288 formControl.setValidators((control: AbstractControl): {
289 [key: string]: any;
290 } => {
291 const value = condition.call(this);
292 if (value) {
293 return Validators.compose(conditionalValidators)(control);
294 }
295 if (permanentValidators.length > 0) {
296 return Validators.compose(permanentValidators)(control);
297 }
298 return null;
299 });
300
301 watchControls.forEach((control: AbstractControl) => {
302 control.valueChanges.subscribe(() => {
303 formControl.updateValueAndValidity({ emitEvent: false });
304 });
305 });
306 }
307
308 /**
309 * Validator that requires that both specified controls have the same value.
310 * Error will be added to the `path2` control.
311 * @param {string} path1 A dot-delimited string that define the path to the control.
312 * @param {string} path2 A dot-delimited string that define the path to the control.
313 * @return {ValidatorFn} Returns a validator function that always returns `null`.
314 * If the validation fails an error map with the `match` property will be set
315 * on the `path2` control.
316 */
317 static match(path1: string, path2: string): ValidatorFn {
318 return (control: AbstractControl): { [key: string]: any } => {
319 const ctrl1 = control.get(path1);
320 const ctrl2 = control.get(path2);
321 if (!ctrl1 || !ctrl2) {
322 return null;
323 }
324 if (ctrl1.value !== ctrl2.value) {
325 ctrl2.setErrors({ match: true });
326 } else {
327 const hasError = ctrl2.hasError('match');
328 if (hasError) {
329 // Remove the 'match' error. If no more errors exists, then set
330 // the error value to 'null', otherwise the field is still marked
331 // as invalid.
332 const errors = ctrl2.errors;
333 _.unset(errors, 'match');
334 ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
335 }
336 }
337 return null;
338 };
339 }
340
341 /**
342 * Asynchronous validator that requires the control's value to be unique.
343 * The validation is only executed after the specified delay. Every
344 * keystroke during this delay will restart the timer.
345 * @param serviceFn {existsServiceFn} The service function that is
346 * called to check whether the given value exists. It must return
347 * boolean 'true' if the given value exists, otherwise 'false'.
348 * @param serviceFnThis {any} The object to be used as the 'this' object
349 * when calling the serviceFn function. Defaults to null.
350 * @param {number|Date} dueTime The delay time to wait before the
351 * serviceFn call is executed. This is useful to prevent calls on
352 * every keystroke. Defaults to 500.
353 * @return {AsyncValidatorFn} Returns an asynchronous validator function
354 * that returns an error map with the `notUnique` property if the
355 * validation check succeeds, otherwise `null`.
356 */
357 static unique(
358 serviceFn: existsServiceFn,
359 serviceFnThis: any = null,
360 usernameFn?: Function,
361 uidField = false,
362 extraArgs = ''
363 ): AsyncValidatorFn {
364 let uName: string;
365 return (control: AbstractControl): Observable<ValidationErrors | null> => {
366 // Exit immediately if user has not interacted with the control yet
367 // or the control value is empty.
368 if (control.pristine || isEmptyInputValue(control.value)) {
369 return observableOf(null);
370 }
371 uName = control.value;
372 if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') {
373 if (uidField) {
374 uName = `${control.value}$${usernameFn()}`;
375 } else {
376 uName = `${usernameFn()}$${control.value}`;
377 }
378 }
379
380 return observableTimer().pipe(
381 switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)),
382 map((resp: boolean) => {
383 if (!resp) {
384 return null;
385 } else {
386 return { notUnique: true };
387 }
388 }),
389 take(1)
390 );
391 };
392 }
393
394 /**
395 * Validator function for UUIDs.
396 * @param required - Defines if it is mandatory to fill in the UUID
397 * @return Validator function that returns an error object containing `invalidUuid` if the
398 * validation failed, `null` otherwise.
399 */
400 static uuid(required = false): ValidatorFn {
401 const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
402 return (control: AbstractControl): { [key: string]: any } | null => {
403 if (control.pristine && control.untouched) {
404 return null;
405 } else if (!required && !control.value) {
406 return null;
407 } else if (uuidRe.test(control.value)) {
408 return null;
409 }
410 return { invalidUuid: 'This is not a valid UUID' };
411 };
412 }
413
414 /**
415 * A simple minimum validator vor cd-binary inputs.
416 *
417 * To use the validation message pass I18n into the function as it cannot
418 * be called in a static one.
419 */
420 static binaryMin(bytes: number): ValidatorFn {
421 return (control: AbstractControl): { [key: string]: () => string } | null => {
422 const formatterService = new FormatterService();
423 const currentBytes = new FormatterService().toBytes(control.value);
424 if (bytes <= currentBytes) {
425 return null;
426 }
427 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
428 return {
429 binaryMin: () => $localize`Size has to be at least ${value} or more`
430 };
431 };
432 }
433
434 /**
435 * A simple maximum validator vor cd-binary inputs.
436 *
437 * To use the validation message pass I18n into the function as it cannot
438 * be called in a static one.
439 */
440 static binaryMax(bytes: number): ValidatorFn {
441 return (control: AbstractControl): { [key: string]: () => string } | null => {
442 const formatterService = new FormatterService();
443 const currentBytes = formatterService.toBytes(control.value);
444 if (bytes >= currentBytes) {
445 return null;
446 }
447 const value = new DimlessBinaryPipe(formatterService).transform(bytes);
448 return {
449 binaryMax: () => $localize`Size has to be at most ${value} or less`
450 };
451 };
452 }
453
454 /**
455 * Asynchronous validator that checks if the password meets the password
456 * policy.
457 * @param userServiceThis The object to be used as the 'this' object
458 * when calling the 'validatePassword' method of the 'UserService'.
459 * @param usernameFn Function to get the username that should be
460 * taken into account.
461 * @param callback Callback function that is called after the validation
462 * has been done.
463 * @return {AsyncValidatorFn} Returns an asynchronous validator function
464 * that returns an error map with the `passwordPolicy` property if the
465 * validation check fails, otherwise `null`.
466 */
467 static passwordPolicy(
468 userServiceThis: any,
469 usernameFn?: Function,
470 callback?: (valid: boolean, credits?: number, valuation?: string) => void
471 ): AsyncValidatorFn {
472 return (control: AbstractControl): Observable<ValidationErrors | null> => {
473 if (control.pristine || control.value === '') {
474 if (_.isFunction(callback)) {
475 callback(true, 0);
476 }
477 return observableOf(null);
478 }
479 let username;
480 if (_.isFunction(usernameFn)) {
481 username = usernameFn();
482 }
483 return observableTimer(500).pipe(
484 switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
485 map((resp: { valid: boolean; credits: number; valuation: string }) => {
486 if (_.isFunction(callback)) {
487 callback(resp.valid, resp.credits, resp.valuation);
488 }
489 if (resp.valid) {
490 return null;
491 } else {
492 return { passwordPolicy: true };
493 }
494 }),
495 take(1)
496 );
497 };
498 }
499
500 /**
501 * Validate the bucket name. In general, bucket names should follow domain
502 * name constraints:
503 * - Bucket names must be unique.
504 * - Bucket names cannot be formatted as IP address.
505 * - Bucket names can be between 3 and 63 characters long.
506 * - Bucket names must not contain uppercase characters or underscores.
507 * - Bucket names must start with a lowercase letter or number.
508 * - Bucket names must be a series of one or more labels. Adjacent
509 * labels are separated by a single period (.). Bucket names can
510 * contain lowercase letters, numbers, and hyphens. Each label must
511 * start and end with a lowercase letter or a number.
512 */
513 static bucketName(): AsyncValidatorFn {
514 return (control: AbstractControl): Observable<ValidationErrors | null> => {
515 if (control.pristine || !control.value) {
516 return observableOf({ required: true });
517 }
518 const constraints = [];
519 let errorName: string;
520 // - Bucket names cannot be formatted as IP address.
521 constraints.push(() => {
522 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;
523 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
524 const name = control.value;
525 let notIP = true;
526 if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
527 errorName = 'ipAddress';
528 notIP = false;
529 }
530 return notIP;
531 });
532 // - Bucket names can be between 3 and 63 characters long.
533 constraints.push((name: string) => {
534 if (!_.inRange(name.length, 3, 64)) {
535 errorName = 'shouldBeInRange';
536 return false;
537 }
538 // Bucket names can only contain lowercase letters, numbers, periods and hyphens.
539 if (!/^[0-9a-z.-]+$/.test(control.value)) {
540 errorName = 'bucketNameInvalid';
541 return false;
542 }
543 return true;
544 });
545 // - Bucket names must not contain uppercase characters or underscores.
546 // - Bucket names must start with a lowercase letter or number.
547 // - Bucket names must be a series of one or more labels. Adjacent
548 // labels are separated by a single period (.). Bucket names can
549 // contain lowercase letters, numbers, and hyphens. Each label must
550 // start and end with a lowercase letter or a number.
551 constraints.push((name: string) => {
552 const labels = _.split(name, '.');
553 return _.every(labels, (label) => {
554 // Bucket names must not contain uppercase characters or underscores.
555 if (label !== _.toLower(label) || label.includes('_')) {
556 errorName = 'containsUpperCase';
557 return false;
558 }
559 // Bucket labels can contain lowercase letters, numbers, and hyphens.
560 if (!/^[0-9a-z-]+$/.test(label)) {
561 errorName = 'onlyLowerCaseAndNumbers';
562 return false;
563 }
564 // Each label must start and end with a lowercase letter or a number.
565 return _.every([0, label.length - 1], (index) => {
566 errorName = 'lowerCaseOrNumber';
567 return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
568 });
569 });
570 });
571 if (!_.every(constraints, (func: Function) => func(control.value))) {
572 return observableOf(
573 (() => {
574 switch (errorName) {
575 case 'onlyLowerCaseAndNumbers':
576 return { onlyLowerCaseAndNumbers: true };
577 case 'shouldBeInRange':
578 return { shouldBeInRange: true };
579 case 'ipAddress':
580 return { ipAddress: true };
581 case 'containsUpperCase':
582 return { containsUpperCase: true };
583 case 'lowerCaseOrNumber':
584 return { lowerCaseOrNumber: true };
585 default:
586 return { bucketNameInvalid: true };
587 }
588 })()
589 );
590 }
591
592 return observableOf(null);
593 };
594 }
595
596 static bucketExistence(
597 requiredExistenceResult: boolean,
598 rgwBucketService: RgwBucketService
599 ): AsyncValidatorFn {
600 return (control: AbstractControl): Observable<ValidationErrors | null> => {
601 if (control.pristine || !control.value) {
602 return observableOf({ required: true });
603 }
604 return rgwBucketService
605 .exists(control.value)
606 .pipe(
607 map((existenceResult: boolean) =>
608 existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true }
609 )
610 );
611 };
612 }
613 }