]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Access denied issue for cephfs-mgr role 70338/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 20 Jul 2026 13:50:20 +0000 (19:20 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Wed, 22 Jul 2026 14:23:04 +0000 (19:53 +0530)
fixes : https://tracker.ceph.com/issues/78414
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
 Conflicts:
src/pybind/mgr/dashboard/tests/test_access_control.py
        - conflict due to other access denied PRs

doc/mgr/dashboard.rst
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/tests/test_access_control.py

index bbe30a6259ef2bc729c219293efdeb3cd0180598..5b216ebed3093164de3d1a44f4a851b138fbf6d7 100644 (file)
@@ -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:
 
index 7bb3d2a625d80cbd1e2563518af69a5215b33478..fa8551bb5bef81446203f1d1bb634f4321df39bd 100644 (file)
@@ -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<CephfsVolumeFormComponent>;
   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();
+    }));
+  });
 });
index 97b785f0efdcf3f629b8c069c5930ad7204c43eb..639865db4ba76db4ddc3ef8d7aeeb74091aede8c 100644 (file)
@@ -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'] })),
index 77e1918ac5b1bb5f9377351831e74b033d8f8ffc..d8da59c9d48a2b51a66a35eadb51aa08a030d5a6 100644 (file)
@@ -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]
     })
index 402128d436cc510bc891ad565c6e3653ce54a792..2d25d2b4decd56c5b3aa599125878d35771bc001 100644 (file)
@@ -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')