From eafc85928feaf91cd5791879d8cf38a07b4aff9b Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Tue, 26 Nov 2024 15:26:38 +0530 Subject: [PATCH] mgr/dashboard: When configuring the RGW Multisite endpoints from the UI allow FQDN(Not only IP) When configuring the RGW Multisite endpoints from the UI allow FQDN, at the moment when using a FQDN it's not allowed Fixes: https://tracker.ceph.com/issues/69055 Signed-off-by: Aashish Sharma (cherry picked from commit 9f3619af9ae911955916195084d225928d4b2f43) Conflicts: src/pybind/mgr/dashboard/frontend/package-lock.json (conflicts with typescript package version, kept the existing one) src/pybind/mgr/dashboard/frontend/package.json (conflicts with typescript package version, kept the existing one) src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts (conflicts with automated system user creation in main) src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts (conflicts with oauthAddressTest validator) --- .../mgr/dashboard/frontend/package-lock.json | 19 +++++- .../mgr/dashboard/frontend/package.json | 4 +- .../rgw-multisite-migrate.component.html | 8 +-- .../rgw-multisite-migrate.component.ts | 59 ++----------------- .../rgw-multisite-zone-form.component.html | 4 +- .../rgw-multisite-zone-form.component.ts | 27 +-------- ...gw-multisite-zonegroup-form.component.html | 4 +- .../rgw-multisite-zonegroup-form.component.ts | 33 ++--------- .../app/shared/forms/cd-validators.spec.ts | 49 ++++++++++++++- .../src/app/shared/forms/cd-validators.ts | 26 ++++++++ 10 files changed, 115 insertions(+), 118 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index 8e02f5de5e731..40053f238a196 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -68,6 +68,7 @@ "@types/lodash": "4.14.161", "@types/node": "18.17.12", "@types/swagger-ui": "3.52.0", + "@types/validator": "13.12.2", "@types/xml2js": "0.4.14", "@typescript-eslint/eslint-plugin": "5.27.1", "@typescript-eslint/parser": "5.27.1", @@ -100,7 +101,8 @@ "table": "6.8.0", "transifex-i18ntool": "1.1.0", "ts-node": "9.0.0", - "typescript": "4.9.5" + "typescript": "4.9.5", + "validator": "13.12.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -7189,6 +7191,12 @@ "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -29840,6 +29848,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index fd4f7a0bff6b7..6251e43fc3fe0 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -102,6 +102,7 @@ "@types/lodash": "4.14.161", "@types/node": "18.17.12", "@types/swagger-ui": "3.52.0", + "@types/validator": "13.12.2", "@types/xml2js": "0.4.14", "@typescript-eslint/eslint-plugin": "5.27.1", "@typescript-eslint/parser": "5.27.1", @@ -134,7 +135,8 @@ "table": "6.8.0", "transifex-i18ntool": "1.1.0", "ts-node": "9.0.0", - "typescript": "4.9.5" + "typescript": "4.9.5", + "validator": "13.12.0" }, "cypress-cucumber-preprocessor": { "stepDefinitions": "cypress/e2e/common" diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html index b18c5a0b9d76b..0f611e5114b19 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html @@ -66,8 +66,8 @@ *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')" i18n>This field is required. Please enter a valid IP address. + *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'invalidURL')" + i18n>Please enter a valid URL.
@@ -105,8 +105,8 @@ *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')" i18n>This field is required. Please enter a valid IP address. + *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'invalidURL')" + i18n>Please enter a valid URL.
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts index 4c2f53b6af1fa..277aea97c85e5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts @@ -21,10 +21,6 @@ import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; styleUrls: ['./rgw-multisite-migrate.component.scss'] }) export class RgwMultisiteMigrateComponent implements OnInit { - readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/; - readonly 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; - readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; - @Output() submitAction = new EventEmitter(); @@ -84,57 +80,12 @@ export class RgwMultisiteMigrateComponent implements OnInit { }) ] }), - zone_endpoints: new UntypedFormControl([], { - validators: [ - CdValidators.custom('endpoint', (value: string) => { - if (_.isEmpty(value)) { - return false; - } else { - if (value.includes(',')) { - value.split(',').forEach((url: string) => { - return ( - !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) - ); - }); - } else { - return ( - !this.endpoints.test(value) && - !this.ipv4Rgx.test(value) && - !this.ipv6Rgx.test(value) - ); - } - return false; - } - }), - Validators.required - ] + zone_endpoints: new UntypedFormControl(null, { + validators: [CdValidators.url, Validators.required] + }), + zonegroup_endpoints: new UntypedFormControl(null, { + validators: [CdValidators.url, Validators.required] }), - zonegroup_endpoints: new UntypedFormControl( - [], - [ - CdValidators.custom('endpoint', (value: string) => { - if (_.isEmpty(value)) { - return false; - } else { - if (value.includes(',')) { - value.split(',').forEach((url: string) => { - return ( - !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) - ); - }); - } else { - return ( - !this.endpoints.test(value) && - !this.ipv4Rgx.test(value) && - !this.ipv6Rgx.test(value) - ); - } - return false; - } - }), - Validators.required - ] - ), access_key: new UntypedFormControl(null), secret_key: new UntypedFormControl(null) }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html index 6a9814651612c..76ce27546d834 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html @@ -105,8 +105,8 @@ *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')" i18n>This field is required. Please enter a valid IP address. + *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'invalidURL')" + i18n>Please enter a valid URL.
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts index bd7dde62c3681..32f8508eeff8e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts @@ -20,9 +20,6 @@ import { ModalService } from '~/app/shared/services/modal.service'; styleUrls: ['./rgw-multisite-zone-form.component.scss'] }) export class RgwMultisiteZoneFormComponent implements OnInit { - readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/; - readonly 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; - readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; action: string; info: any; multisiteZoneForm: CdFormGroup; @@ -88,29 +85,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit { master_zone: new UntypedFormControl(false), selectedZonegroup: new UntypedFormControl(null), zone_endpoints: new UntypedFormControl(null, { - validators: [ - CdValidators.custom('endpoint', (value: string) => { - if (_.isEmpty(value)) { - return false; - } else { - if (value.includes(',')) { - value.split(',').forEach((url: string) => { - return ( - !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) - ); - }); - } else { - return ( - !this.endpoints.test(value) && - !this.ipv4Rgx.test(value) && - !this.ipv6Rgx.test(value) - ); - } - return false; - } - }), - Validators.required - ] + validators: [CdValidators.url, Validators.required] }), access_key: new UntypedFormControl('', {}), secret_key: new UntypedFormControl('', {}), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html index fe32f082cbc45..196426bc5b604 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html @@ -99,8 +99,8 @@ *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')" i18n>This field is required. Please enter a valid IP address. + *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'invalidURL')" + i18n>Please enter a valid URL.
{ - if (_.isEmpty(value)) { - return false; - } else { - if (value.includes(',')) { - value.split(',').forEach((url: string) => { - return ( - !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url) - ); - }); - } else { - return ( - !this.endpoints.test(value) && - !this.ipv4Rgx.test(value) && - !this.ipv6Rgx.test(value) - ); - } - return false; - } - }), - Validators.required - ]), + zonegroup_endpoints: new UntypedFormControl(null, { + validators: [CdValidators.url, Validators.required] + }), placementTargets: this.formBuilder.array([]) }); } @@ -170,7 +147,9 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit { this.multisiteZonegroupForm.get('selectedRealm').setValue(this.info.data.parent); this.multisiteZonegroupForm.get('default_zonegroup').setValue(this.info.data.is_default); this.multisiteZonegroupForm.get('master_zonegroup').setValue(this.info.data.is_master); - this.multisiteZonegroupForm.get('zonegroup_endpoints').setValue(this.info.data.endpoints); + this.multisiteZonegroupForm + .get('zonegroup_endpoints') + .setValue(this.info.data.endpoints.toString()); if (this.info.data.is_default) { this.multisiteZonegroupForm.get('default_zonegroup').disable(); 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 2924c9c641408..0c03357f25feb 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 @@ -1,5 +1,5 @@ import { fakeAsync, tick } from '@angular/core/testing'; -import { FormControl, Validators } from '@angular/forms'; +import { FormControl, UntypedFormControl, Validators } from '@angular/forms'; import _ from 'lodash'; import { of as observableOf } from 'rxjs'; @@ -902,5 +902,52 @@ describe('CdValidators', () => { testValidator('testName', true); })); }); + + describe('url', () => { + it('should return null for a valid URL with port', () => { + const control = new UntypedFormControl('https://example.com:8080'); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return null for multiple valid URLs with ports', () => { + const control = new UntypedFormControl('https://example.com:8080,http://localhost:3000'); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return null for a URL without a port', () => { + const control = new UntypedFormControl('https://example.com'); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return an error object for multiple invalid URLs', () => { + const control = new UntypedFormControl('https://example.com,http://192.1666.33.00:099999'); + expect(CdValidators.url(control)).toEqual({ invalidURL: true }); + }); + + it('should return an error object for a non-URL string', () => { + const control = new UntypedFormControl('randomstring'); + expect(CdValidators.url(control)).toEqual({ invalidURL: true }); + }); + + it('should return null for a valid IP address with port', () => { + const control = new UntypedFormControl('https://192.168.1.1:9090'); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return null for an IP address without a port', () => { + const control = new UntypedFormControl('https://192.168.1.1'); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return null for an empty value', () => { + const control = new UntypedFormControl(null); + expect(CdValidators.url(control)).toBeNull(); + }); + + it('should return null for an empty string', () => { + const control = new UntypedFormControl(''); + expect(CdValidators.url(control)).toBeNull(); + }); + }); }); }); 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 e2bd674184286..27cc5015d5f81 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 @@ -13,6 +13,7 @@ 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'; +import validator from 'validator'; export function isEmptyInputValue(value: any): boolean { return value == null || value.length === 0; @@ -626,4 +627,29 @@ export class CdValidators { } }; } + + /** + * Validator function to validate endpoints, allowing FQDN, IPv4, and IPv6 addresses with ports. + * Accepts multiple endpoints separated by commas. + */ + static url(control: AbstractControl): ValidationErrors | null { + const value = control.value; + + if (_.isEmpty(value)) { + return null; + } + + const urls = value.includes(',') ? value.split(',') : [value]; + + const invalidUrls = urls.filter( + (url: string) => + !validator.isURL(url, { + require_protocol: true, + allow_underscores: true, + require_tld: false + }) && !validator.isIP(url) + ); + + return invalidUrls.length > 0 ? { invalidURL: true } : null; + } } -- 2.39.5