From: Naman Munet Date: Tue, 30 Jun 2026 13:03:25 +0000 (+0530) Subject: mgr/dashboard: add account name duplicate check X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=8dafeeb22e5c7d2ea6cc4e52c91ac780ae9194fe;p=ceph.git mgr/dashboard: add account name duplicate check fixes: https://tracker.ceph.com/issues/77828 Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py index 7bc9ff56b8c..a7b5429be99 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw_iam.py +++ b/src/pybind/mgr/dashboard/controllers/rgw_iam.py @@ -1,6 +1,8 @@ from typing import Optional from ..controllers.rgw import RgwRESTController +from ..exceptions import DashboardException +from ..rest_client import RequestException from ..security import Scope from ..services.rgw_iam import RgwAccounts from ..tools import str_to_bool @@ -77,6 +79,23 @@ class RgwUserAccountsController(RgwRESTController): """ return self.get_account(account_id, daemon_name) + @EndpointDoc("Check if account name exists", + parameters={'account_name': (str, 'Account name'), + 'daemon_name': (str, 'Name of the daemon')}) + @RESTController.Collection(method='GET', path='/exists') + def exists(self, account_name: str, daemon_name=None): + """ + Check if an account with the given name exists + Returns True if account exists, False otherwise + """ + try: + self.proxy(daemon_name, 'GET', 'account', {'name': account_name}) + # If we get a result without error, the account exists + return True + except (DashboardException, RequestException): + # If we get an error (e.g., account not found), it doesn't exist + return False + def get_account(self, account_id, daemon_name=None) -> dict: return self.proxy(daemon_name, 'GET', 'account', {'id': account_id}) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html index 7233732e63f..f0687548848 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html @@ -23,6 +23,9 @@ placeholder="Enter account name" id="acc_name" name="acc_name" + cdValidate + #accName="cdValidate" + [invalid]="accName.isInvalid" formControlName="name"/> @@ -30,6 +33,11 @@ class="invalid-feedback"> This field is required. + @if(accountForm.showError('name', formDir, 'notUnique')) { +
+ An account with this name already exists. +
+ }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts index 0db3d09f6b1..c138789b473 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick, flush } from '@angular/core/testing'; import { RgwUserAccountsFormComponent } from './rgw-user-accounts-form.component'; import { ComponentsModule } from '~/app/shared/components/components.module'; @@ -10,11 +10,13 @@ import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.servi import { ModalModule } from 'carbon-components-angular'; import { ReactiveFormsModule } from '@angular/forms'; import { RgwUserAccountsComponent } from '../rgw-user-accounts/rgw-user-accounts.component'; +import { DUE_TIMER } from '~/app/shared/forms/cd-validators'; class MockRgwUserAccountsService { create = jest.fn().mockReturnValue(of(null)); modify = jest.fn().mockReturnValue(of(null)); setQuota = jest.fn().mockReturnValue(of(null)); + exists = jest.fn().mockReturnValue(of(false)); } describe('RgwUserAccountsFormComponent', () => { @@ -50,9 +52,10 @@ describe('RgwUserAccountsFormComponent', () => { expect(component).toBeTruthy(); }); - it('should call create method of MockRgwUserAccountsService and show success notification', () => { + it('should call create method of MockRgwUserAccountsService and show success notification', fakeAsync(() => { component.editing = false; - component.accountForm.get('name').setValue('test'); + component.accountForm.get('name').setValue('test', { emitEvent: true }); + tick(DUE_TIMER); const payload = { account_name: 'test', email: '', @@ -63,18 +66,24 @@ describe('RgwUserAccountsFormComponent', () => { max_group: 1000, max_access_keys: 4 }; + const mockAccount = { id: 'RGW12312312312312312', name: 'test' }; const spy = jest.spyOn(component, 'submit'); - const createDataSpy = jest.spyOn(rgwUserAccountsService, 'create').mockReturnValue(of(null)); + const createDataSpy = jest + .spyOn(rgwUserAccountsService, 'create') + .mockReturnValue(of(mockAccount)); component.submit(); expect(component.accountForm.valid).toBe(true); expect(spy).toHaveBeenCalled(); expect(createDataSpy).toHaveBeenCalled(); expect(createDataSpy).toHaveBeenCalledWith(payload); - }); + flush(); + })); - it('should call modify method of MockRgwUserAccountsService and show success notification', () => { + it('should call modify method of MockRgwUserAccountsService and show success notification', fakeAsync(() => { component.editing = true; - component.accountForm.get('name').setValue('test'); + component.originalAccountName = 'test'; + component.accountForm.get('name').setValue('test', { emitEvent: true }); + tick(DUE_TIMER); component.accountForm.get('id').setValue('RGW12312312312312312'); component.accountForm.get('email').setValue('test@test.com'); const payload = { @@ -88,14 +97,18 @@ describe('RgwUserAccountsFormComponent', () => { max_group: 1000, max_access_keys: 4 }; + const mockAccount = { id: 'RGW12312312312312312', name: 'test' }; const spy = jest.spyOn(component, 'submit'); - const modifyDataSpy = jest.spyOn(rgwUserAccountsService, 'modify').mockReturnValue(of(null)); + const modifyDataSpy = jest + .spyOn(rgwUserAccountsService, 'modify') + .mockReturnValue(of(mockAccount)); component.submit(); expect(component.accountForm.valid).toBe(true); expect(spy).toHaveBeenCalled(); expect(modifyDataSpy).toHaveBeenCalled(); expect(modifyDataSpy).toHaveBeenCalledWith(payload); - }); + flush(); + })); it('should call setQuota for "account" if account quota is dirty', () => { component.accountForm.get('id').setValue('123'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts index 1c1506d7afe..da78beb5d69 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts @@ -26,6 +26,7 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { resource: string; editing: boolean = false; submitObservables: Observable[] = []; + originalAccountName: string = ''; constructor( private router: Router, @@ -48,6 +49,7 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { this.route.paramMap.subscribe((params: any) => { const account_id = params.get('id'); this.rgwUserAccountsService.get(account_id).subscribe((accountData: Account) => { + this.originalAccountName = accountData.name; // Get the default values. const defaults = _.clone(this.accountForm.value); // Extract the values displayed in the form. @@ -112,7 +114,11 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { this.accountForm = this.formBuilder.group({ id: [''], tenant: [''], - name: ['', Validators.required], + name: [ + '', + [Validators.required], + [CdValidators.unique(this.rgwUserAccountsService.exists, this.rgwUserAccountsService)] + ], email: ['', CdValidators.email], max_users_mode: ['1'], max_users: [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts index b5035fd344a..4c53a46c054 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user-accounts.service.ts @@ -54,4 +54,11 @@ export class RgwUserAccountsService { ) { return this.http.put(`${this.url}/${account_id}/quota`, payload); } + + exists(account_name: string): Observable { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.append('account_name', account_name); + return this.http.get(`${this.url}/exists`, { params }); + }); + } } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 83f0ca14911..3c8f1804188 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -18053,6 +18053,47 @@ paths: summary: Update RGW account info tags: - RgwUserAccounts + /api/rgw/accounts/exists: + get: + description: "\n Check if an account with the given name exists\n \ + \ Returns True if account exists, False otherwise\n " + parameters: + - description: Account name + in: query + name: account_name + required: true + schema: + type: string + - allowEmptyValue: true + description: Name of the daemon + in: query + name: daemon_name + schema: + type: string + responses: + '200': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Check if account name exists + tags: + - RgwUserAccounts /api/rgw/accounts/{account_id}: delete: description: "\n Removes an account\n\n :param account_id: account\ diff --git a/src/pybind/mgr/dashboard/services/rgw_iam.py b/src/pybind/mgr/dashboard/services/rgw_iam.py index ed1bfe09581..928560f154d 100644 --- a/src/pybind/mgr/dashboard/services/rgw_iam.py +++ b/src/pybind/mgr/dashboard/services/rgw_iam.py @@ -25,6 +25,15 @@ class RgwAccounts: get_accounts_cmd = ['account', 'list'] return cls.send_rgw_cmd(get_accounts_cmd) + @classmethod + def get_account_by_name(cls, account_name: str): + """ + Get account info by account name + Returns account info if found, raises exception otherwise + """ + get_account_cmd = ['account', 'info', '--account-name', account_name] + return cls.send_rgw_cmd(get_account_cmd) + @classmethod def set_quota(cls, quota_type: str, account_id: str, max_size: str, max_objects: str, enabled: bool):