From a73b7fdf604994ecf7dc60ec6290ea85c874a446 Mon Sep 17 00:00:00 2001 From: Afreen Misbah Date: Thu, 21 Aug 2025 15:11:43 +0530 Subject: [PATCH] mgr/dashboard: Dashboard nfs export editor rejects ipv6 addresses Fixes https://tracker.ceph.com/issues/72660 Signed-off-by: Afreen Misbah (cherry picked from commit 6c9a5536f3459cb3e676e240730eb5f7c45352ff) --- .../nfs-form-client.component.html | 6 +-- .../nfs-form-client.component.spec.ts | 42 +++++++++++++++++++ .../nfs-form-client.component.ts | 30 +++++++++++-- .../src/app/shared/forms/cd-validators.ts | 4 +- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html index 6f52a00af0c..5d2cef35e8a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html @@ -41,7 +41,7 @@ name="addresses" id="addresses" formControlName="addresses" - placeholder="192.168.0.10, 192.168.1.0/8" + placeholder="e.g. 192.168.0.10, 192.168.1.0/8" [invalid]="!item.controls['addresses'].valid && (item.controls['addresses'].dirty)"> @@ -49,8 +49,8 @@ This field is required. - - Must contain one or more comma-separated values + + Must contain one or more comma-separated valid addresses.
For example: 192.168.0.10, 192.168.1.0/8
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts index 70d885d848e..4f1d95fd2ab 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts @@ -68,4 +68,46 @@ describe('NfsFormClientComponent', () => { component.removeClient(0); expect(component.form.getValue('clients')).toEqual([]); }); + + describe(`test 'isValidClientAddress'`, () => { + it('should return false for empty value', () => { + expect(component.isValidClientAddress('')).toBeFalsy(); + expect(component.isValidClientAddress(null)).toBeFalsy(); + }); + + it('should return false for valid single IPv4 address', () => { + expect(component.isValidClientAddress('192.168.1.1')).toBeFalsy(); + }); + + it('should return false for valid IPv6 address', () => { + expect(component.isValidClientAddress('2001:db8::1')).toBeFalsy(); + }); + + it('should return false for valid FQDN', () => { + expect(component.isValidClientAddress('nfs.example.com')).toBeFalsy(); + }); + + it('should return false for valid IP CIDR range', () => { + expect(component.isValidClientAddress('192.168.0.0/24')).toBeFalsy(); + expect(component.isValidClientAddress('2001:db8::/64')).toBeFalsy(); + }); + + it('should return false for multiple valid addresses separated by comma', () => { + const input = '192.168.1.1, 2001:db8::1, nfs.example.com, 10.0.0.0/8'; + expect(component.isValidClientAddress(input)).toBeFalsy(); + }); + + it('should return true for invalid single address', () => { + expect(component.isValidClientAddress('invalid-address')).toBeTruthy(); + }); + + it('should return true for mixed valid and invalid addresses', () => { + const input = '192.168.1.1, invalid-address'; + expect(component.isValidClientAddress(input)).toBeTruthy(); + }); + + it('should return true for URLs with protocols', () => { + expect(component.isValidClientAddress('http://nfs.example.com')).toBeTruthy(); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts index f7b4cc0fdf8..834448bf723 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts @@ -2,10 +2,12 @@ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/co import { UntypedFormArray, UntypedFormControl, NgForm, Validators } from '@angular/forms'; import _ from 'lodash'; +import validator from 'validator'; import { NfsService } from '~/app/shared/api/nfs.service'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; @Component({ selector: 'cd-nfs-form-client', @@ -59,14 +61,36 @@ export class NfsFormClientComponent implements OnInit { return $localize`-- Select what kind of user id squashing is performed --`; } + isValidClientAddress(value: string): boolean { + if (_.isEmpty(value)) { + return false; + } + + const addressList = value.includes(',') + ? value.split(',').map((addr) => addr.trim()) + : [value.trim()]; + const invalidAddresses = addressList.filter( + (address: string) => + !validator.isFQDN(address, { + allow_underscores: true, + require_tld: true + }) && + !validator.isIP(address) && + !validator.isIPRange(address) + ); + + return invalidAddresses.length > 0; + } + addClient() { this.clientsFormArray = this.form.get('clients') as UntypedFormArray; - const REGEX_IP = `(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\.([0-9]{1,3})([/](\\d|[1-2]\\d|3[0-2]))?)`; - const REGEX_LIST_IP = `${REGEX_IP}([ ,]{1,2}${REGEX_IP})*`; const fg = new CdFormGroup({ addresses: new UntypedFormControl('', { - validators: [Validators.required, Validators.pattern(REGEX_LIST_IP)] + validators: [ + Validators.required, + CdValidators.custom('invalidAddress', this.isValidClientAddress) + ] }), access_type: new UntypedFormControl(''), squash: new UntypedFormControl('') 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 531a2790cdf..47011423459 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 @@ -709,7 +709,9 @@ export class CdValidators { return null; } - const urls = value.includes(',') ? value.split(',') : [value]; + const urls = value.includes(',') + ? value.split(',').map((v: string) => v.trim()) + : [value.trim()]; const invalidUrls = urls.filter( (url: string) => -- 2.39.5