]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: grant hosts read to block-manager role 69958/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 6 Jul 2026 10:43:23 +0000 (16:13 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Tue, 21 Jul 2026 11:38:56 +0000 (17:08 +0530)
Block-manager users were denied access to the NVMe/TCP page because
host listing APIs require hosts read permission.

Fixes: https://tracker.ceph.com/issues/77952
Signed-off-by: Abhishek Desai <adesai@redhat.com>
qa/tasks/mgr/dashboard/test_host.py
src/pybind/mgr/dashboard/controllers/health.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/tests/test_access_control.py

index 78d784473f3caa79ac8f5d3d023a4544e0a64b83..93e7f8f07915dc812fe68d574fa8ef542310086e 100644 (file)
@@ -30,7 +30,7 @@ class HostControllerTest(DashboardTestCase):
     def test_data_daemons(self):
         return self.ORCHESTRATOR_TEST_DATA['daemons']
 
-    @DashboardTestCase.RunAs('test', 'test', ['block-manager'])
+    @DashboardTestCase.RunAs('test', 'test', ['rgw-manager'])
     def test_access_permissions(self):
         self._get(self.URL_HOST, version='1.1')
         self.assertStatus(403)
index d0d4e24e05f83341173bdeac4b91e8f43ede923c..b72e4f8ed390d728486ef1e0a4ea211da045310b 100644 (file)
@@ -395,7 +395,8 @@ class Health(BaseController):
                 'quorum': data.get('quorum', {})
             }
 
-        if self._has_permissions(Permission.READ, Scope.OSD):
+        if (self._has_permissions(Permission.READ, Scope.OSD)
+                or self._has_permissions(Permission.READ, Scope.POOL)):
             summary['osdmap'] = {
                 'in': data.get('osdmap', {}).get('num_in_osds'),
                 'up': data.get('osdmap', {}).get('num_up_osds'),
index 803ffad1f6283e01caf4310a05084608d3ae7859..09e221536b46faa098ae45378a11df8550f850a8 100644 (file)
       ></a
     >
   </ng-container>
-  @if (canViewSilences) {
+  @if (prometheusPermissions.read) {
     <ng-container ngbNavItem>
-      <a class="nav-link"
-         routerLink="/monitoring/silences"
-         routerLinkActive="active"
-         ariaCurrentWhenActive="page"
-         [routerLinkActiveOptions]="{exact: true}"
-         i18n>Silences</a>
+      <a
+        class="nav-link"
+        routerLink="/monitoring/silences"
+        routerLinkActive="active"
+        ariaCurrentWhenActive="page"
+        [routerLinkActiveOptions]="{ exact: true }"
+        i18n
+        >Silences</a
+      >
     </ng-container>
   }
   <ng-container ngbNavItem>
index 29d6001e50f4701dc90b680c87d9ee08ce12eb82..ef42e38e4c3341405f07e474e8d39bebc57fd75d 100644 (file)
@@ -40,6 +40,6 @@ describe('PrometheusTabsComponent', () => {
   });
 
   it('should show silences to users with read access', () => {
-    expect(component.canViewSilences).toBe(true);
+    expect(component.prometheusPermissions.read).toBe(true);
   });
 });
index f3824a299b6f3ef4c7973514bf4b85b1ebbfd0e4..2a2527cbaa83ac880fa251f9c16166f35df52447 100644 (file)
@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 
 import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permission } from '~/app/shared/models/permissions';
 
 @Component({
   selector: 'cd-prometheus-tabs',
@@ -10,13 +11,12 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
   standalone: false
 })
 export class PrometheusTabsComponent {
-  canViewSilences: boolean;
+  prometheusPermissions: Permission;
 
   constructor(
     public prometheusAlertService: PrometheusAlertService,
     private authStorageService: AuthStorageService
   ) {
-    const prometheusPermission = this.authStorageService.getPermissions().prometheus;
-    this.canViewSilences = prometheusPermission.read;
+    this.prometheusPermissions = this.authStorageService.getPermissions().prometheus;
   }
 }
index 4f8b582a6855a369c6ae8c489ad7130dbbd427e0..da31bb8145f8c3334ec32f5a3bbae7feda30355e 100644 (file)
@@ -99,7 +99,12 @@ export class OverviewComponent {
   );
 
   readonly hasNoOSDs$ = this.healthData$.pipe(
-    map((data: HealthSnapshotMap) => (data?.osdmap?.num_osds ?? 0) === 0),
+    map((data: HealthSnapshotMap) => {
+      if (data?.osdmap == null) {
+        return false;
+      }
+      return data.osdmap.num_osds === 0;
+    }),
     shareReplay({ bufferSize: 1, refCount: true })
   );
 
index 63fdec3d88095989cf7c6277f19c336d27fc09e5..3ef15cf605b1280ab8da6081f14af255fdb01c5b 100644 (file)
       ></svg>
     </a>
   </li>
-  <cds-overflow-menu-option (click)="openAboutModal()"
-                            i18n>About</cds-overflow-menu-option>
+  <cds-overflow-menu-option
+    (click)="openAboutModal()"
+    i18n
+    >About</cds-overflow-menu-option
+  >
   @if (configOptPermission.read) {
-    <cds-overflow-menu-option (click)="openFeedbackModal()"
-                              i18n>Report an issue...</cds-overflow-menu-option>
+    <cds-overflow-menu-option
+      (click)="openFeedbackModal()"
+      i18n
+      >Report an issue...</cds-overflow-menu-option
+    >
   }
 </cds-overflow-menu>
 
index 7b836feb0b28a01fdefa11b4f55e509f4b4201ef..9a2452ca0bcf139fa155c51a2e5d7d4f594af18b 100644 (file)
@@ -33,6 +33,16 @@ describe('DashboardHelpComponent', () => {
     expect(component).toBeTruthy();
   });
 
+  it('should show report issue when config-opt is readable', () => {
+    const options = fixture.debugElement.queryAll(By.css('cds-overflow-menu-option'));
+    // About + Report an issue (Documentation and API are plain <li> links)
+    expect(options.length).toBe(2);
+    expect(options.map((o) => o.nativeElement.textContent.trim())).toEqual([
+      'About',
+      'Report an issue...'
+    ]);
+  });
+
   it('should hide report issue when config-opt is not readable', () => {
     permissions.configOpt = new Permission([]);
     (TestBed.inject(AuthStorageService).getPermissions as jasmine.Spy).and.returnValue(permissions);
@@ -42,6 +52,8 @@ describe('DashboardHelpComponent', () => {
     fixture.detectChanges();
 
     const options = fixture.debugElement.queryAll(By.css('cds-overflow-menu-option'));
-    expect(options.length).toBe(3);
+    // Only About remains; Documentation and API are plain <li> links
+    expect(options.length).toBe(1);
+    expect(options[0].nativeElement.textContent.trim()).toBe('About');
   });
 });
index 6dac2b8f1cdfe833b2fec696e3d95d60fe81349a..77e1918ac5b1bb5f9377351831e74b033d8f8ffc 100644 (file)
@@ -218,7 +218,8 @@ ADMIN_ROLE = Role(
 # read-only role provides read-only permission for all scopes
 READ_ONLY_ROLE = Role(
     'read-only',
-    'allows read permission for all security scope except user, dashboard settings and config-opt', {
+    'allows read permission for all security scope except user, '
+    'dashboard settings and config-opt', {
         scope_name: [_P.READ] for scope_name in Scope.all_scopes()
         if scope_name not in (Scope.USER, Scope.DASHBOARD_SETTINGS, Scope.CONFIG_OPT)
     })
@@ -231,6 +232,7 @@ BLOCK_MGR_ROLE = Role(
         Scope.POOL: [_P.READ],
         Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
         Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
+        Scope.HOSTS: [_P.READ],
         Scope.GRAFANA: [_P.READ],
         Scope.NVME_OF: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
         Scope.PROMETHEUS: [_P.READ]
@@ -293,6 +295,7 @@ SMB_MGR_ROLE = Role(
     'smb-manager', 'allows full permissions for the smb scope', {
         Scope.SMB: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
         Scope.HOSTS: [_P.READ],
+        Scope.POOL: [_P.READ],
         Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
         Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
         Scope.GRAFANA: [_P.READ],
index b67acd4d2341cd798de566e5a0b6e38deac36d38..402128d436cc510bc891ad565c6e3653ce54a792 100644 (file)
@@ -156,7 +156,8 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.assertEqual(role['name'], 'read-only')
         self.assertEqual(
             role['description'],
-            'allows read permission for all security scope except user, dashboard settings and config-opt'
+            'allows read permission for all security scope except user, '
+            'dashboard settings and config-opt'
         )
 
     def test_cluster_manager_role_has_pool_read(self):
@@ -167,6 +168,18 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         role = self.exec_cmd('ac-role-show', rolename='smb-manager')
         self.assertEqual(role['scopes_permissions'][Scope.HOSTS], [Permission.READ])
 
+    def test_smb_manager_role_has_pool_read(self):
+        role = self.exec_cmd('ac-role-show', rolename='smb-manager')
+        self.assertEqual(role['scopes_permissions'][Scope.POOL], [Permission.READ])
+
+    def test_block_manager_role_has_pool_read(self):
+        role = self.exec_cmd('ac-role-show', rolename='block-manager')
+        self.assertEqual(role['scopes_permissions'][Scope.POOL], [Permission.READ])
+
+    def test_block_manager_role_has_hosts_read(self):
+        role = self.exec_cmd('ac-role-show', rolename='block-manager')
+        self.assertEqual(role['scopes_permissions'][Scope.HOSTS], [Permission.READ])
+
     def test_delete_system_role(self):
         with self.assertRaises(CmdException) as ctx:
             self.exec_cmd('ac-role-delete', rolename='administrator')