From: Abhishek Desai Date: Mon, 20 Jul 2026 13:50:20 +0000 (+0530) Subject: mgr/dashboard : Access denied issue for cephfs-mgr role X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=281e220dbb5a3603aeb56be56f3442e45872e745;p=ceph.git mgr/dashboard : Access denied issue for cephfs-mgr role fixes : https://tracker.ceph.com/issues/78414 Signed-off-by: Abhishek Desai Conflicts: src/pybind/mgr/dashboard/tests/test_access_control.py - conflict due to other access denied PRs --- diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index bbe30a6259e..5b216ebed30 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -1176,7 +1176,8 @@ The list of system roles are: - **cluster-manager**: allows full permissions for the *hosts*, *osd*, *monitor*, *manager*, and *config-opt* scopes. - **pool-manager**: allows full permissions for the *pool* scope. -- **cephfs-manager**: allows full permissions for the *cephfs* scope. +- **cephfs-manager**: allows full permissions for the *cephfs* scope, and + *read* permission for the *hosts* and *pool* scopes. The list of available roles can be retrieved with the following command: diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts index 7bb3d2a625d..fa8551bb5be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts @@ -9,7 +9,9 @@ import { SharedModule } from '~/app/shared/shared.module'; import { ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; +import { PoolService } from '~/app/shared/api/pool.service'; +import { CephfsService } from '~/app/shared/api/cephfs.service'; import { CheckboxModule, ComboBoxModule, @@ -23,6 +25,8 @@ describe('CephfsVolumeFormComponent', () => { let fixture: ComponentFixture; let formHelper: FormHelper; let orchService: OrchestratorService; + let poolService: PoolService; + let cephfsService: CephfsService; configureTestBed({ imports: [ @@ -44,6 +48,8 @@ describe('CephfsVolumeFormComponent', () => { component = fixture.componentInstance; formHelper = new FormHelper(component.form); orchService = TestBed.inject(OrchestratorService); + poolService = TestBed.inject(PoolService); + cephfsService = TestBed.inject(CephfsService); spyOn(orchService, 'status').and.returnValue(of({ available: true })); fixture.detectChanges(); }); @@ -135,4 +141,32 @@ describe('CephfsVolumeFormComponent', () => { expect(submitButton.nativeElement.disabled).toBeFalsy(); }); }); + + describe('pool list error handling when creating', () => { + beforeEach(() => { + component.editing = false; + spyOn(cephfsService, 'getUsedPools').and.returnValue(of([])); + }); + + it('should treat a 403 on pool list as an empty list', fakeAsync(() => { + spyOn(poolService, 'getList').and.returnValue(throwError({ status: 403 })); + component.ngOnInit(); + tick(); + expect(component.pools).toEqual([]); + })); + + it('should propagate non-403 errors from pool list', fakeAsync(() => { + const err = { status: 500 }; + spyOn(poolService, 'getList').and.returnValue(throwError(err)); + let errorHandled = false; + const origSetErrors = component.form.setErrors.bind(component.form); + spyOn(component.form, 'setErrors').and.callFake((errors: any) => { + if (errors?.cdSubmitButton) errorHandled = true; + origSetErrors(errors); + }); + component.ngOnInit(); + tick(); + expect(errorHandled).toBeTruthy(); + })); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts index 97b785f0efd..639865db4ba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts @@ -4,8 +4,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import _ from 'lodash'; import { NgbNav, NgbTooltip, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; -import { forkJoin, Observable, Subject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { forkJoin, Observable, of, Subject, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { CephfsService } from '~/app/shared/api/cephfs.service'; import { HostService } from '~/app/shared/api/host.service'; @@ -154,28 +154,39 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit { } else { forkJoin({ usedPools: this.cephfsService.getUsedPools(), - pools: this.poolService.getList() - }).subscribe(({ usedPools, pools }) => { - // filtering pools if - // * pool is labelled with cephfs - // * its not already used by cephfs - // * its not erasure coded - // * and only if its empty - const filteredPools = Object.values(pools).filter( - (pool: Pool) => - this.cephfsService.isCephFsPool(pool) && - !usedPools.includes(pool.pool) && - pool.type !== 'erasure' && - pool.stats.bytes_used.latest === 0 - ); - if (filteredPools.length < 2) this.form.get('customPools').disable(); - this.pools = filteredPools; - this.metadatPools = this.dataPools = this.pools; + pools: this.poolService + .getList() + .pipe(catchError((err) => (err.status === 403 ? of([]) : throwError(() => err)))) + }).subscribe({ + next: ({ usedPools, pools }) => { + // filtering pools if + // * pool is labelled with cephfs + // * its not already used by cephfs + // * its not erasure coded + // * and only if its empty + const filteredPools = Object.values(pools).filter( + (pool: Pool) => + this.cephfsService.isCephFsPool(pool) && + !usedPools.includes(pool.pool) && + pool.type !== 'erasure' && + pool.stats.bytes_used.latest === 0 + ); + if (filteredPools.length < 2) this.form.get('customPools').disable(); + this.pools = filteredPools; + this.metadatPools = this.dataPools = this.pools; + }, + error: () => { + this.form.setErrors({ cdSubmitButton: true }); + } }); this.hostsAndLabels$ = forkJoin({ - hosts: this.hostService.getAllHosts(), - labels: this.hostService.getLabels() + hosts: this.hostService + .getAllHosts() + .pipe(catchError((err) => (err.status === 403 ? of([]) : throwError(() => err)))), + labels: this.hostService + .getLabels() + .pipe(catchError((err) => (err.status === 403 ? of([]) : throwError(() => err)))) }).pipe( map(({ hosts, labels }) => ({ hosts: hosts.map((host: Host) => ({ content: host['hostname'] })), diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 77e1918ac5b..d8da59c9d48 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -277,6 +277,8 @@ POOL_MGR_ROLE = Role( CEPHFS_MGR_ROLE = Role( 'cephfs-manager', 'allows full permissions for the cephfs scope', { Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], + Scope.HOSTS: [_P.READ], + Scope.POOL: [_P.READ], Scope.GRAFANA: [_P.READ], Scope.PROMETHEUS: [_P.READ] }) diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index 402128d436c..2d25d2b4dec 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -12,8 +12,8 @@ from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE from .. import mgr from ..security import Permission, Scope -from ..services.access_control import SYSTEM_ROLES, AccessControlDB, \ - PasswordPolicy, load_access_control_db, password_hash +from ..services.access_control import CEPHFS_MGR_ROLE, SYSTEM_ROLES, \ + AccessControlDB, PasswordPolicy, load_access_control_db, password_hash from ..settings import Settings from ..tests import CLICommandTestMixin, CmdException @@ -180,6 +180,15 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): role = self.exec_cmd('ac-role-show', rolename='block-manager') self.assertEqual(role['scopes_permissions'][Scope.HOSTS], [Permission.READ]) + def test_cephfs_mgr_role_scopes(self): + role = self.exec_cmd('ac-role-show', rolename='cephfs-manager') + self.assertEqual(role['name'], CEPHFS_MGR_ROLE.name) + scopes_perms = role['scopes_permissions'] + self.assertIn(Scope.HOSTS, scopes_perms) + self.assertEqual(scopes_perms[Scope.HOSTS], [Permission.READ]) + self.assertIn(Scope.POOL, scopes_perms) + self.assertEqual(scopes_perms[Scope.POOL], [Permission.READ]) + def test_delete_system_role(self): with self.assertRaises(CmdException) as ctx: self.exec_cmd('ac-role-delete', rolename='administrator')