summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts612
1 files changed, 612 insertions, 0 deletions
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
new file mode 100644
index 000000000..22371a50f
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
@@ -0,0 +1,612 @@
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ ValidationErrors,
+ ValidatorFn,
+ Validators
+} from '@angular/forms';
+
+import _ from 'lodash';
+import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
+import { map, switchMapTo, take } from 'rxjs/operators';
+
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+
+export function isEmptyInputValue(value: any): boolean {
+ return value == null || value.length === 0;
+}
+
+export type existsServiceFn = (value: any) => Observable<boolean>;
+
+export class CdValidators {
+ /**
+ * Validator that performs email validation. In contrast to the Angular
+ * email validator an empty email will not be handled as invalid.
+ */
+ static email(control: AbstractControl): ValidationErrors | null {
+ // Exit immediately if value is empty.
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ 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 check fails, 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 numbers.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, 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 check fails, 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 performs SSL certificate validation.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static sslCert(): ValidatorFn {
+ return Validators.pattern(
+ /^-----BEGIN CERTIFICATE-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END CERTIFICATE-----[\n\r\f]*$/
+ );
+ }
+
+ /**
+ * Validator that performs SSL private key validation.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static sslPrivKey(): ValidatorFn {
+ return Validators.pattern(
+ /^-----BEGIN RSA PRIVATE KEY-----(\n|\r|\f)((.+)?((\n|\r|\f).+)*)(\n|\r|\f)-----END RSA PRIVATE KEY-----[\n\r\f]*$/
+ );
+ }
+
+ /**
+ * Validator that performs SSL certificate validation of pem format.
+ * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
+ * if the validation check fails, otherwise `null`.
+ */
+ static pemCert(): ValidatorFn {
+ return Validators.pattern(/^-----BEGIN .+-----$.+^-----END .+-----$/ms);
+ }
+
+ /**
+ * Validator that requires controls to fulfill the specified condition if
+ * the specified prerequisites matches. If the prerequisites are fulfilled,
+ * then the given function is executed and if it succeeds, the 'required'
+ * validation error will be returned, otherwise null.
+ * @param {Object} prerequisites An object containing the prerequisites.
+ * To do additional checks rather than checking for equality you can
+ * use the extended prerequisite syntax:
+ * 'field_name': { 'op': '<OPERATOR>', arg1: '<OPERATOR_ARGUMENT>' }
+ * The following operators are supported:
+ * * empty
+ * * !empty
+ * * equal
+ * * !equal
+ * * minLength
+ * ### Example
+ * ```typescript
+ * {
+ * 'generate_key': true,
+ * 'username': 'Max Mustermann'
+ * }
+ * ```
+ * ### Example - Extended prerequisites
+ * ```typescript
+ * {
+ * 'generate_key': { 'op': 'equal', 'arg1': true },
+ * 'username': { 'op': 'minLength', 'arg1': 5 }
+ * }
+ * ```
+ * Only if all prerequisites are fulfilled, then the validation of the
+ * control will be triggered.
+ * @param {Function | undefined} condition The function to be executed when all
+ * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
+ * function will be used by default. The control's value is used as function
+ * argument. The function must return true to set the validation error.
+ * @return {ValidatorFn} Returns the validator function.
+ */
+ static requiredIf(prerequisites: object, condition?: Function | undefined): ValidatorFn {
+ let isWatched = false;
+
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!isWatched && control.parent) {
+ Object.keys(prerequisites).forEach((key) => {
+ control.parent.get(key).valueChanges.subscribe(() => {
+ control.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+
+ isWatched = true;
+ }
+
+ // Check if all prerequisites met.
+ if (
+ !Object.keys(prerequisites).every((key) => {
+ if (!control.parent) {
+ return false;
+ }
+ const value = control.parent.get(key).value;
+ const prerequisite = prerequisites[key];
+ if (_.isObjectLike(prerequisite)) {
+ let result = false;
+ switch (prerequisite['op']) {
+ case 'empty':
+ result = _.isEmpty(value);
+ break;
+ case '!empty':
+ result = !_.isEmpty(value);
+ break;
+ case 'equal':
+ result = value === prerequisite['arg1'];
+ break;
+ case '!equal':
+ result = value !== prerequisite['arg1'];
+ break;
+ case 'minLength':
+ if (_.isString(value)) {
+ result = value.length >= prerequisite['arg1'];
+ }
+ break;
+ }
+ return result;
+ }
+ return value === prerequisite;
+ })
+ ) {
+ return null;
+ }
+ const success = _.isFunction(condition)
+ ? condition.call(condition, control.value)
+ : isEmptyInputValue(control.value);
+ return success ? { required: true } : null;
+ };
+ }
+
+ /**
+ * Compose multiple validators into a single function that returns the union of
+ * the individual error maps for the provided control when the given prerequisites
+ * are fulfilled.
+ *
+ * @param {Object} prerequisites An object containing the prerequisites as
+ * key/value pairs.
+ * ### Example
+ * ```typescript
+ * {
+ * 'generate_key': true,
+ * 'username': 'Max Mustermann'
+ * }
+ * ```
+ * @param {ValidatorFn[]} validators List of validators that should be taken
+ * into action when the prerequisites are met.
+ * @return {ValidatorFn} Returns the validator function.
+ */
+ static composeIf(prerequisites: object, validators: ValidatorFn[]): ValidatorFn {
+ let isWatched = false;
+ return (control: AbstractControl): ValidationErrors | null => {
+ if (!isWatched && control.parent) {
+ Object.keys(prerequisites).forEach((key) => {
+ control.parent.get(key).valueChanges.subscribe(() => {
+ control.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ isWatched = true;
+ }
+ // Check if all prerequisites are met.
+ if (
+ !Object.keys(prerequisites).every((key) => {
+ return control.parent && control.parent.get(key).value === prerequisites[key];
+ })
+ ) {
+ return null;
+ }
+ return Validators.compose(validators)(control);
+ };
+ }
+
+ /**
+ * Custom validation by passing a name for the error and a function as error condition.
+ *
+ * @param {string} error
+ * @param {Function} condition - a truthy return value will trigger the error
+ * @returns {ValidatorFn}
+ */
+ static custom(error: string, condition: Function): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: any } => {
+ const value = condition.call(this, control.value);
+ if (value) {
+ return { [error]: value };
+ }
+ return null;
+ };
+ }
+
+ /**
+ * Validate form control if condition is true with validators.
+ *
+ * @param {AbstractControl} formControl
+ * @param {Function} condition
+ * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
+ * when the condition is met
+ * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
+ * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
+ * Every time one of this controls value is updated, the validation will be triggered
+ */
+ static validateIf(
+ formControl: AbstractControl,
+ condition: Function,
+ conditionalValidators: ValidatorFn[],
+ permanentValidators: ValidatorFn[] = [],
+ watchControls: AbstractControl[] = []
+ ) {
+ conditionalValidators = conditionalValidators.concat(permanentValidators);
+
+ formControl.setValidators((control: AbstractControl): {
+ [key: string]: any;
+ } => {
+ const value = condition.call(this);
+ if (value) {
+ return Validators.compose(conditionalValidators)(control);
+ }
+ if (permanentValidators.length > 0) {
+ return Validators.compose(permanentValidators)(control);
+ }
+ return null;
+ });
+
+ watchControls.forEach((control: AbstractControl) => {
+ control.valueChanges.subscribe(() => {
+ formControl.updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ }
+
+ /**
+ * Validator that requires that both specified controls have the same value.
+ * Error will be added to the `path2` control.
+ * @param {string} path1 A dot-delimited string that define the path to the control.
+ * @param {string} path2 A dot-delimited string that define the path to the control.
+ * @return {ValidatorFn} Returns a validator function that always returns `null`.
+ * If the validation fails an error map with the `match` property will be set
+ * on the `path2` control.
+ */
+ static match(path1: string, path2: string): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: any } => {
+ const ctrl1 = control.get(path1);
+ const ctrl2 = control.get(path2);
+ if (!ctrl1 || !ctrl2) {
+ return null;
+ }
+ if (ctrl1.value !== ctrl2.value) {
+ ctrl2.setErrors({ match: true });
+ } else {
+ const hasError = ctrl2.hasError('match');
+ if (hasError) {
+ // Remove the 'match' error. If no more errors exists, then set
+ // the error value to 'null', otherwise the field is still marked
+ // as invalid.
+ const errors = ctrl2.errors;
+ _.unset(errors, 'match');
+ ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
+ }
+ }
+ return null;
+ };
+ }
+
+ /**
+ * Asynchronous validator that requires the control's value to be unique.
+ * The validation is only executed after the specified delay. Every
+ * keystroke during this delay will restart the timer.
+ * @param serviceFn {existsServiceFn} The service function that is
+ * called to check whether the given value exists. It must return
+ * boolean 'true' if the given value exists, otherwise 'false'.
+ * @param serviceFnThis {any} The object to be used as the 'this' object
+ * when calling the serviceFn function. Defaults to null.
+ * @param {number|Date} dueTime The delay time to wait before the
+ * serviceFn call is executed. This is useful to prevent calls on
+ * every keystroke. Defaults to 500.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `notUnique` property if the
+ * validation check succeeds, otherwise `null`.
+ */
+ static unique(
+ serviceFn: existsServiceFn,
+ serviceFnThis: any = null,
+ usernameFn?: Function,
+ uidField = false
+ ): AsyncValidatorFn {
+ let uName: string;
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ // Exit immediately if user has not interacted with the control yet
+ // or the control value is empty.
+ if (control.pristine || isEmptyInputValue(control.value)) {
+ return observableOf(null);
+ }
+ uName = control.value;
+ if (_.isFunction(usernameFn) && usernameFn() !== null && usernameFn() !== '') {
+ if (uidField) {
+ uName = `${control.value}$${usernameFn()}`;
+ } else {
+ uName = `${usernameFn()}$${control.value}`;
+ }
+ }
+
+ return observableTimer().pipe(
+ switchMapTo(serviceFn.call(serviceFnThis, uName)),
+ map((resp: boolean) => {
+ if (!resp) {
+ return null;
+ } else {
+ return { notUnique: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
+
+ /**
+ * Validator function for UUIDs.
+ * @param required - Defines if it is mandatory to fill in the UUID
+ * @return Validator function that returns an error object containing `invalidUuid` if the
+ * validation failed, `null` otherwise.
+ */
+ static uuid(required = false): ValidatorFn {
+ 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;
+ return (control: AbstractControl): { [key: string]: any } | null => {
+ if (control.pristine && control.untouched) {
+ return null;
+ } else if (!required && !control.value) {
+ return null;
+ } else if (uuidRe.test(control.value)) {
+ return null;
+ }
+ return { invalidUuid: 'This is not a valid UUID' };
+ };
+ }
+
+ /**
+ * A simple minimum validator vor cd-binary inputs.
+ *
+ * To use the validation message pass I18n into the function as it cannot
+ * be called in a static one.
+ */
+ static binaryMin(bytes: number): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: () => string } | null => {
+ const formatterService = new FormatterService();
+ const currentBytes = new FormatterService().toBytes(control.value);
+ if (bytes <= currentBytes) {
+ return null;
+ }
+ const value = new DimlessBinaryPipe(formatterService).transform(bytes);
+ return {
+ binaryMin: () => $localize`Size has to be at least ${value} or more`
+ };
+ };
+ }
+
+ /**
+ * A simple maximum validator vor cd-binary inputs.
+ *
+ * To use the validation message pass I18n into the function as it cannot
+ * be called in a static one.
+ */
+ static binaryMax(bytes: number): ValidatorFn {
+ return (control: AbstractControl): { [key: string]: () => string } | null => {
+ const formatterService = new FormatterService();
+ const currentBytes = formatterService.toBytes(control.value);
+ if (bytes >= currentBytes) {
+ return null;
+ }
+ const value = new DimlessBinaryPipe(formatterService).transform(bytes);
+ return {
+ binaryMax: () => $localize`Size has to be at most ${value} or less`
+ };
+ };
+ }
+
+ /**
+ * Asynchronous validator that checks if the password meets the password
+ * policy.
+ * @param userServiceThis The object to be used as the 'this' object
+ * when calling the 'validatePassword' method of the 'UserService'.
+ * @param usernameFn Function to get the username that should be
+ * taken into account.
+ * @param callback Callback function that is called after the validation
+ * has been done.
+ * @return {AsyncValidatorFn} Returns an asynchronous validator function
+ * that returns an error map with the `passwordPolicy` property if the
+ * validation check fails, otherwise `null`.
+ */
+ static passwordPolicy(
+ userServiceThis: any,
+ usernameFn?: Function,
+ callback?: (valid: boolean, credits?: number, valuation?: string) => void
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || control.value === '') {
+ if (_.isFunction(callback)) {
+ callback(true, 0);
+ }
+ return observableOf(null);
+ }
+ let username;
+ if (_.isFunction(usernameFn)) {
+ username = usernameFn();
+ }
+ return observableTimer(500).pipe(
+ switchMapTo(_.invoke(userServiceThis, 'validatePassword', control.value, username)),
+ map((resp: { valid: boolean; credits: number; valuation: string }) => {
+ if (_.isFunction(callback)) {
+ callback(resp.valid, resp.credits, resp.valuation);
+ }
+ if (resp.valid) {
+ return null;
+ } else {
+ return { passwordPolicy: true };
+ }
+ }),
+ take(1)
+ );
+ };
+ }
+
+ /**
+ * Validate the bucket name. In general, bucket names should follow domain
+ * name constraints:
+ * - Bucket names must be unique.
+ * - Bucket names cannot be formatted as IP address.
+ * - Bucket names can be between 3 and 63 characters long.
+ * - Bucket names must not contain uppercase characters or underscores.
+ * - Bucket names must start with a lowercase letter or number.
+ * - Bucket names must be a series of one or more labels. Adjacent
+ * labels are separated by a single period (.). Bucket names can
+ * contain lowercase letters, numbers, and hyphens. Each label must
+ * start and end with a lowercase letter or a number.
+ */
+ static bucketName(): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || !control.value) {
+ return observableOf({ required: true });
+ }
+ const constraints = [];
+ let errorName: string;
+ // - Bucket names cannot be formatted as IP address.
+ constraints.push(() => {
+ 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;
+ const name = control.value;
+ let notIP = true;
+ if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
+ errorName = 'ipAddress';
+ notIP = false;
+ }
+ return notIP;
+ });
+ // - Bucket names can be between 3 and 63 characters long.
+ constraints.push((name: string) => {
+ if (!_.inRange(name.length, 3, 64)) {
+ errorName = 'shouldBeInRange';
+ return false;
+ }
+ // Bucket names can only contain lowercase letters, numbers, periods and hyphens.
+ if (!/^[0-9a-z.-]+$/.test(control.value)) {
+ errorName = 'bucketNameInvalid';
+ return false;
+ }
+ return true;
+ });
+ // - Bucket names must not contain uppercase characters or underscores.
+ // - Bucket names must start with a lowercase letter or number.
+ // - Bucket names must be a series of one or more labels. Adjacent
+ // labels are separated by a single period (.). Bucket names can
+ // contain lowercase letters, numbers, and hyphens. Each label must
+ // start and end with a lowercase letter or a number.
+ constraints.push((name: string) => {
+ const labels = _.split(name, '.');
+ return _.every(labels, (label) => {
+ // Bucket names must not contain uppercase characters or underscores.
+ if (label !== _.toLower(label) || label.includes('_')) {
+ errorName = 'containsUpperCase';
+ return false;
+ }
+ // Bucket labels can contain lowercase letters, numbers, and hyphens.
+ if (!/^[0-9a-z-]+$/.test(label)) {
+ errorName = 'onlyLowerCaseAndNumbers';
+ return false;
+ }
+ // Each label must start and end with a lowercase letter or a number.
+ return _.every([0, label.length - 1], (index) => {
+ errorName = 'lowerCaseOrNumber';
+ return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
+ });
+ });
+ });
+ if (!_.every(constraints, (func: Function) => func(control.value))) {
+ return observableOf(
+ (() => {
+ switch (errorName) {
+ case 'onlyLowerCaseAndNumbers':
+ return { onlyLowerCaseAndNumbers: true };
+ case 'shouldBeInRange':
+ return { shouldBeInRange: true };
+ case 'ipAddress':
+ return { ipAddress: true };
+ case 'containsUpperCase':
+ return { containsUpperCase: true };
+ case 'lowerCaseOrNumber':
+ return { lowerCaseOrNumber: true };
+ default:
+ return { bucketNameInvalid: true };
+ }
+ })()
+ );
+ }
+
+ return observableOf(null);
+ };
+ }
+
+ static bucketExistence(
+ requiredExistenceResult: boolean,
+ rgwBucketService: RgwBucketService
+ ): AsyncValidatorFn {
+ return (control: AbstractControl): Observable<ValidationErrors | null> => {
+ if (control.pristine || !control.value) {
+ return observableOf({ required: true });
+ }
+ return rgwBucketService
+ .exists(control.value)
+ .pipe(
+ map((existenceResult: boolean) =>
+ existenceResult === requiredExistenceResult ? null : { bucketNameNotAllowed: true }
+ )
+ );
+ };
+ }
+}