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 <aasharma@redhat.com>
"@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": "8.14.0",
"@typescript-eslint/parser": "8.14.0",
"table": "6.8.0",
"transifex-i18ntool": "1.1.0",
"ts-node": "9.0.0",
- "typescript": "5.4.5"
+ "typescript": "5.4.5",
+ "validator": "13.12.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
"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/wrap-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
"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",
"@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": "8.14.0",
"@typescript-eslint/parser": "8.14.0",
"table": "6.8.0",
"transifex-i18ntool": "1.1.0",
"ts-node": "9.0.0",
- "typescript": "5.4.5"
+ "typescript": "5.4.5",
+ "validator": "13.12.0"
},
"cypress-cucumber-preprocessor": {
"stepDefinitions": "cypress/e2e/common"
*ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
- *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
- i18n>Please enter a valid IP address.</span>
+ *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
+ i18n>Please enter a valid URL.</span>
</div>
</div>
<div class="form-group row">
*ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
- *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'endpoint')"
- i18n>Please enter a valid IP address.</span>
+ *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'invalidURL')"
+ i18n>Please enter a valid URL.</span>
</div>
</div>
<div class="form-group row">
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();
})
]
}),
- 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
- ]
- ),
username: new UntypedFormControl(null, {
validators: [Validators.required]
})
*ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
- *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'endpoint')"
- i18n>Please enter a valid IP address.</span>
+ *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'invalidURL')"
+ i18n>Please enter a valid URL.</span>
</div>
</div>
<div class="form-group row">
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;
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('', {}),
*ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')"
i18n>This field is required.</span>
<span class="invalid-feedback"
- *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
- i18n>Please enter a valid IP address.</span>
+ *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
+ i18n>Please enter a valid URL.</span>
</div>
</div>
<div class="form-group row"
styleUrls: ['./rgw-multisite-zonegroup-form.component.scss']
})
export class RgwMultisiteZonegroupFormComponent 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;
icons = Icons;
multisiteZonegroupForm: CdFormGroup;
}),
master_zonegroup: new UntypedFormControl(false),
selectedRealm: new UntypedFormControl(null),
- zonegroup_endpoints: new UntypedFormControl(null, [
- 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
- ]),
+ zonegroup_endpoints: new UntypedFormControl(null, {
+ validators: [CdValidators.url, Validators.required]
+ }),
placementTargets: this.formBuilder.array([])
});
}
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();
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';
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();
+ });
+ });
});
});
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;
return { invalidAddress: !(addressTest && portTest) };
};
}
+
+ /**
+ * 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;
+ }
}