]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add validation for username 70046/head
authorSagar Gopale <sagar.gopale@ibm.com>
Wed, 8 Jul 2026 14:48:13 +0000 (20:18 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Wed, 15 Jul 2026 08:52:00 +0000 (14:22 +0530)
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/user.service.ts

index 409b84d1f9aaa99167aaedfa46af8bc5f7dc653e..e0e28e9b23408273fc3563c743ccf653419fed54 100644 (file)
@@ -20,6 +20,8 @@
         <cds-text-label
           labelInputID="username"
           cdRequiredField="Username"
+          helperText="Username can only contain letters, numbers, '.', '_', '@', '+' or '-'."
+          i18n-helperText
           [invalid]="!userForm.controls.username.valid && userForm.controls.username.dirty"
           [invalidText]="usernameError"
           i18n
           <span *ngIf="userForm.showError('username', formDir, 'notUnique')">
             <ng-container i18n> The username already exists. </ng-container>
           </span>
+          <span
+            *ngIf="userForm.showError('username', formDir, 'pattern')"
+            i18n
+          >
+            Username can only contain letters, numbers, '.', '_', '&#64;', '+' or '-'.
+          </span>
         </ng-template>
       </div>
       <!-- Password -->
index 850dff1c5a7d7a8b88c3cc6065719f44587cf28d..55bb829a4839981d9900e3719dbecf2ee633e88d 100644 (file)
@@ -103,6 +103,12 @@ describe('UserFormComponent', () => {
       formHelper.expectValidChange('username', 'user1');
     });
 
+    it('should reject invalid usernames', () => {
+      formHelper.expectErrorChange('username', '..', 'pattern');
+      formHelper.expectErrorChange('username', '??', 'pattern');
+      formHelper.expectErrorChange('username', 'user/name', 'pattern');
+    });
+
     it('should validate password match', () => {
       formHelper.setValue('password', 'aaa');
       formHelper.expectErrorChange('confirmpassword', 'bbb', 'match');
index 2f33440f2e98a9654662e7fde8c9fdcefe00ef20..12fb843692d7e659e07b1dc9486cdc49d9037174 100644 (file)
@@ -29,6 +29,8 @@ import { UserFormMode } from './user-form-mode.enum';
 import { UserFormRoleModel } from './user-form-role.model';
 import { UserFormModel } from './user-form.model';
 
+const DASHBOARD_USERNAME_PATTERN = /^(?!\.+$)[a-zA-Z0-9._@+-]+$/;
+
 @Component({
   selector: 'cd-user-form',
   templateUrl: './user-form.component.html',
@@ -88,7 +90,7 @@ export class UserFormComponent extends CdForm implements OnInit {
       {
         username: [
           '',
-          [Validators.required],
+          [Validators.required, Validators.pattern(DASHBOARD_USERNAME_PATTERN)],
           [CdValidators.unique(this.userService.validateUserName, this.userService)]
         ],
         name: [''],
index 44a8ba8258377480a56aa5e271cc7be9bfb96003..44b2e6a7a380f2bfa8df06ce17da29ea871bfee9 100755 (executable)
@@ -69,7 +69,8 @@ export class UserListComponent implements OnInit {
       permission: 'update',
       icon: Icons.edit,
       routerLink: () =>
-        this.selection.first() && this.urlBuilder.getEdit(this.selection.first().username),
+        this.selection.first() &&
+        this.urlBuilder.getEdit(encodeURIComponent(this.selection.first().username)),
       name: this.actionLabels.EDIT
     };
     const deleteAction: CdTableAction = {
index ba038a72553b864e9289677304a25e3a8ca7f007..8a634cbeb677ad6ef2c3a3683771cfdd82d52890 100644 (file)
@@ -46,6 +46,12 @@ describe('UserService', () => {
     expect(req.request.method).toBe('DELETE');
   });
 
+  it('should URL-encode username on delete', () => {
+    service.delete('??').subscribe();
+    const req = httpTesting.expectOne('api/user/%3F%3F');
+    expect(req.request.method).toBe('DELETE');
+  });
+
   it('should call update', () => {
     const user = new UserFormModel();
     user.username = 'user0';
@@ -65,6 +71,12 @@ describe('UserService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should URL-encode username on get', () => {
+    service.get('??').subscribe();
+    const req = httpTesting.expectOne('api/user/%3F%3F');
+    expect(req.request.method).toBe('GET');
+  });
+
   it('should call list', () => {
     service.list().subscribe();
     const req = httpTesting.expectOne('api/user');
index d432e318471c5c472451dd24dad79fb4a6d0316c..dc2792a32811e06c51642b2c4ca7831a33ec999c 100644 (file)
@@ -17,7 +17,7 @@ export class UserService {
   }
 
   delete(username: string) {
-    return this.http.delete(`api/user/${username}`);
+    return this.http.delete(`api/user/${encodeURIComponent(username)}`);
   }
 
   get(username: string) {
@@ -29,14 +29,14 @@ export class UserService {
   }
 
   update(user: UserFormModel) {
-    return this.http.put(`api/user/${user.username}`, user);
+    return this.http.put(`api/user/${encodeURIComponent(user.username)}`, user);
   }
 
   changePassword(username: string, oldPassword: string, newPassword: string) {
     // Note, the specified user MUST be logged in to be able to change
     // the password. The backend ensures that the password of another
     // user can not be changed, otherwise an error will be thrown.
-    return this.http.post(`api/user/${username}/change_password`, {
+    return this.http.post(`api/user/${encodeURIComponent(username)}/change_password`, {
       old_password: oldPassword,
       new_password: newPassword
     });