From: Abhishek Desai Date: Mon, 6 Jul 2026 10:43:23 +0000 (+0530) Subject: mgr/dashboard: grant hosts read to block-manager role X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0d0a241ba733ca804df2f42b59f2e0a31e36e180;p=ceph.git mgr/dashboard: grant hosts read to block-manager role 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 --- diff --git a/qa/tasks/mgr/dashboard/test_host.py b/qa/tasks/mgr/dashboard/test_host.py index 78d784473f3..93e7f8f0791 100644 --- a/qa/tasks/mgr/dashboard/test_host.py +++ b/qa/tasks/mgr/dashboard/test_host.py @@ -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) diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index d0d4e24e05f..b72e4f8ed39 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -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'), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html index 803ffad1f62..09e221536b4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html @@ -24,14 +24,17 @@ > - @if (canViewSilences) { + @if (prometheusPermissions.read) { - Silences + Silences } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts index 29d6001e50f..ef42e38e4c3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.spec.ts @@ -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); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts index f3824a299b6..2a2527cbaa8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.ts @@ -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; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts index 4f8b582a685..da31bb8145f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts @@ -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 }) ); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html index 63fdec3d880..3ef15cf605b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html @@ -31,11 +31,17 @@ > - About + About @if (configOptPermission.read) { - Report an issue... + Report an issue... } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts index 7b836feb0b2..9a2452ca0bc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.spec.ts @@ -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
  • 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
  • links + expect(options.length).toBe(1); + expect(options[0].nativeElement.textContent.trim()).toBe('About'); }); }); diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 6dac2b8f1cd..77e1918ac5b 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -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], diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index b67acd4d234..402128d436c 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -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')