From 42be6df308c263e0733b8424769bf33554c2e1be Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Fri, 27 Sep 2019 13:04:48 +0200 Subject: [PATCH] mgr/dashboard: Bucket names cannot be formatted as IP address 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. Fixes: https://tracker.ceph.com/issues/42069 Signed-off-by: Volker Theile --- .../rgw-bucket-form.component.spec.ts | 88 +++++++++++++------ .../rgw-bucket-form.component.ts | 51 ++++++++++- 2 files changed, 109 insertions(+), 30 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts index 8d3fec0cdfd62..7f28e4b53d0a2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts @@ -46,45 +46,81 @@ describe('RgwBucketFormComponent', () => { }); describe('bucketNameValidator', () => { - it('should validate name (1/4)', () => { + const testValidator = (name, valid) => { const validatorFn = component.bucketNameValidator(); - const ctrl = new FormControl(''); + const ctrl = new FormControl(name); + ctrl.markAsDirty(); const validatorPromise = validatorFn(ctrl); expect(validatorPromise instanceof Promise).toBeTruthy(); if (validatorPromise instanceof Promise) { validatorPromise.then((resp) => { - expect(resp).toBe(null); + if (valid) { + expect(resp).toBe(null); + } else { + expect(resp instanceof Object).toBeTruthy(); + expect(resp.bucketNameInvalid).toBeTruthy(); + } }); } + }; + + it('should validate empty name', () => { + testValidator('', true); }); - it('should validate name (2/4)', () => { - const validatorFn = component.bucketNameValidator(); - const ctrl = new FormControl('ab'); - ctrl.markAsDirty(); - const validatorPromise = validatorFn(ctrl); - expect(validatorPromise instanceof Promise).toBeTruthy(); - if (validatorPromise instanceof Promise) { - validatorPromise.then((resp) => { - expect(resp.bucketNameInvalid).toBeTruthy(); - }); - } + it('bucket names cannot be formatted as IP address', () => { + testValidator('172.10.4.51', false); }); - it('should validate name (3/4)', () => { - const validatorFn = component.bucketNameValidator(); - const ctrl = new FormControl('abc'); - ctrl.markAsDirty(); - const validatorPromise = validatorFn(ctrl); - expect(validatorPromise instanceof Promise).toBeTruthy(); - if (validatorPromise instanceof Promise) { - validatorPromise.then((resp) => { - expect(resp).toBe(null); - }); - } + it('bucket name must be >= 3 characters long (1/2)', () => { + testValidator('ab', false); + }); + + it('bucket name must be >= 3 characters long (2/2)', () => { + testValidator('abc', true); + }); + + it('bucket name must be <= than 63 characters long (1/2)', () => { + testValidator(_.repeat('a', 64), false); + }); + + it('bucket name must be <= than 63 characters long (2/2)', () => { + testValidator(_.repeat('a', 63), true); + }); + + it('bucket names must not contain uppercase characters or underscores (1/2)', () => { + testValidator('iAmInvalid', false); + }); + + it('bucket names must not contain uppercase characters or underscores (2/2)', () => { + testValidator('i_am_invalid', false); + }); + + it('bucket names with invalid labels (1/3)', () => { + testValidator('abc.1def.Ghi2', false); + }); + + it('bucket names with invalid labels (2/3)', () => { + testValidator('abc.1-xy', false); + }); + + it('bucket names with invalid labels (3/3)', () => { + testValidator('abc.*def', false); + }); + + it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (1/3)', () => { + testValidator('xyz.abc', true); + }); + + it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (2/3)', () => { + testValidator('abc.1-def', true); + }); + + it('bucket names must be a series of one or more labels and can contain lowercase letters, numbers, and hyphens (3/3)', () => { + testValidator('abc.ghi2', true); }); - it('should validate name (4/4)', () => { + it('bucket names must be unique', () => { spyOn(rgwBucketService, 'enumerate').and.returnValue(observableOf(['abcd'])); const validatorFn = component.bucketNameValidator(); const ctrl = new FormControl('abcd'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts index c9e0b47fe3182..5c9be7ae2e324 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -12,6 +12,7 @@ import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.consta import { NotificationType } from '../../../shared/enum/notification-type.enum'; import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { CdValidators } from '../../../shared/forms/cd-validators'; import { NotificationService } from '../../../shared/services/notification.service'; @Component({ @@ -161,6 +162,19 @@ export class RgwBucketFormComponent implements OnInit { } } + /** + * 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. + */ bucketNameValidator(): AsyncValidatorFn { const rgwBucketService = this.rgwBucketService; return (control: AbstractControl): Promise => { @@ -171,13 +185,42 @@ export class RgwBucketFormComponent implements OnInit { resolve(null); return; } - // Validate the bucket name. - const nameRe = /^[0-9A-Za-z][\w-\.]{2,254}$/; - if (!nameRe.test(control.value)) { + const constraints = []; + // - Bucket names cannot be formatted as IP address. + constraints.push((name) => { + const validatorFn = CdValidators.ip(); + return !validatorFn(name); + }); + // - Bucket names can be between 3 and 63 characters long. + constraints.push((name) => _.inRange(name.length, 3, 64)); + // - 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) => { + const labels = _.split(name, '.'); + return _.every(labels, (label) => { + // Bucket names must not contain uppercase characters or underscores. + if (label !== _.toLower(label) || label.includes('_')) { + return false; + } + // Bucket names can contain lowercase letters, numbers, and hyphens. + if (!/[0-9a-z-]/.test(label)) { + return false; + } + // Each label must start and end with a lowercase letter or a number. + return _.every([0, label.length], (index) => { + return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index])); + }); + }); + }); + if (!_.every(constraints, (func) => func(control.value))) { resolve({ bucketNameInvalid: true }); return; } - // Does any bucket with the given name already exist? + // - Bucket names must be unique. rgwBucketService.exists(control.value).subscribe((resp: boolean) => { if (!resp) { resolve(null); -- 2.39.5