From 2fba7b733ac7f0b7b9316f96bf5af871a23b0751 Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:56:12 +0530 Subject: [PATCH 1/6] mgr/dashboard: allow read-only users to view Silences page Remove SilenceFormComponent from the silences list page, which incorrectly required create permission and caused a blank page for users with prometheus read access only. Fixes: https://tracker.ceph.com/issues/77952 Signed-off-by: Abhishek Desai Conflicts: src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html - Added permissions --- .../prometheus-tabs.component.html | 21 +++++----- .../prometheus-tabs.component.spec.ts | 18 ++++++++- .../prometheus-tabs.component.ts | 11 ++++- .../silence-list.component.spec.ts | 7 ++++ .../silence-list/silence-list.component.ts | 40 +++++++++++++------ .../app/shared/models/alertmanager-silence.ts | 4 +- 6 files changed, 72 insertions(+), 29 deletions(-) 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 40f70bcadd4..803ffad1f62 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,17 +24,16 @@ > - - Silences - + @if (canViewSilences) { + + Silences + + } { configureTestBed({ imports: [RouterTestingModule, NgbNavModule], declarations: [PrometheusTabsComponent], - providers: [{ provide: PrometheusAlertService, useValue: { alerts: [] } }] + providers: [ + { provide: PrometheusAlertService, useValue: { alerts: [] } }, + { + provide: AuthStorageService, + useValue: { + getPermissions: () => ({ + prometheus: new Permission(['read']) + }) + } + } + ] }); beforeEach(() => { @@ -26,4 +38,8 @@ describe('PrometheusTabsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should show silences to users with read access', () => { + expect(component.canViewSilences).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 423cd386fbc..f3824a299b6 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 @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; @Component({ selector: 'cd-prometheus-tabs', @@ -9,5 +10,13 @@ import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.s standalone: false }) export class PrometheusTabsComponent { - constructor(public prometheusAlertService: PrometheusAlertService) {} + canViewSilences: boolean; + + constructor( + public prometheusAlertService: PrometheusAlertService, + private authStorageService: AuthStorageService + ) { + const prometheusPermission = this.authStorageService.getPermissions().prometheus; + this.canViewSilences = prometheusPermission.read; + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts index be93bb5f42e..e116a311d7a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.spec.ts @@ -54,6 +54,13 @@ describe('SilenceListComponent', () => { expect(component).toBeTruthy(); }); + it('should create for read-only prometheus users', () => { + (authStorageService.getPermissions as jasmine.Spy).and.callFake(() => ({ + prometheus: new Permission(['read']) + })); + expect(() => TestBed.createComponent(SilenceListComponent)).not.toThrow(); + }); + it('should test all TableActions combinations', () => { const permissionHelper: PermissionHelper = new PermissionHelper(component.permission); const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts index 75ce093af3f..6b7e3be8185 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts @@ -4,7 +4,6 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { Observable, Subscriber } from 'rxjs'; import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper'; -import { SilenceFormComponent } from '~/app/ceph/cluster/prometheus/silence-form/silence-form.component'; import { PrometheusService } from '~/app/shared/api/prometheus.service'; import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants'; @@ -29,10 +28,7 @@ import { CdSortPropDir } from '~/app/shared/models/cd-sort-prop-dir'; const BASE_URL = 'monitoring/silences'; @Component({ - providers: [ - { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }, - SilenceFormComponent - ], + providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }], selector: 'cd-silences-list', templateUrl: './silence-list.component.html', styleUrls: ['./silence-list.component.scss'], @@ -62,7 +58,6 @@ export class SilenceListComponent extends PrometheusListHelper { private urlBuilder: URLBuilderService, private actionLabels: ActionLabelsI18n, private succeededLabels: SucceededActionLabelsI18n, - private silenceFormComponent: SilenceFormComponent, private silenceMatcher: PrometheusSilenceMatcherService, @Inject(PrometheusService) prometheusService: PrometheusService ) { @@ -172,7 +167,7 @@ export class SilenceListComponent extends PrometheusListHelper { const activeSilences = silences.filter( (silence: AlertmanagerSilence) => silence.status.state !== 'expired' ); - this.getAlerts(activeSilences); + this.loadRulesAndMatchAlerts(activeSilences); }, () => { this.prometheusService.disableAlertmanagerConfig(); @@ -185,13 +180,32 @@ export class SilenceListComponent extends PrometheusListHelper { this.selection = selection; } - getAlerts(silences: any) { - const rules = this.silenceFormComponent.getRules(); - silences.forEach((silence: any) => { - silence.matchers.forEach((matcher: any) => { - this.rules = this.silenceMatcher.getMatchedRules(matcher, rules); + private loadRulesAndMatchAlerts(silences: AlertmanagerSilence[]) { + this.prometheusService.ifPrometheusConfigured( + () => + this.prometheusService.getRules().subscribe( + (groups) => { + this.rules = groups.groups.flatMap((group) => group.rules); + this.getAlerts(silences); + }, + () => { + this.rules = []; + this.getAlerts(silences); + } + ), + () => { + this.rules = []; + this.getAlerts(silences); + } + ); + } + + getAlerts(silences: AlertmanagerSilence[]) { + silences.forEach((silence) => { + silence.matchers.forEach((matcher) => { + const matchedRules = this.silenceMatcher.getMatchedRules(matcher, this.rules); const alertNames: string[] = []; - for (const rule of this.rules) { + for (const rule of matchedRules) { alertNames.push(rule.name); } silence.silencedAlerts = alertNames; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts index 5f69f1e1e81..7a09abae617 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts @@ -1,5 +1,3 @@ -import { PrometheusRule } from './prometheus-alerts'; - export class AlertmanagerSilenceMatcher { name: string; value: any; @@ -22,5 +20,5 @@ export class AlertmanagerSilence { status?: { state: 'expired' | 'active' | 'pending'; }; - silencedAlerts?: PrometheusRule[]; + silencedAlerts?: string[]; } From 21c6df7d22b7f7de7a2bf9e97fae981827318207 Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:10:41 +0530 Subject: [PATCH 2/6] mgr/dashboard: hide Report an Issue for users without config-opt read The Help menu option redirected read-only users to an Access Denied page. Hide it when the user lacks config-opt read permission. Fixes: https://tracker.ceph.com/issues/77952 Signed-off-by: Abhishek Desai --- .../dashboard-help.component.html | 16 +++++--------- .../dashboard-help.component.spec.ts | 22 ++++++++++++++++++- .../dashboard-help.component.ts | 10 +++++++-- 3 files changed, 35 insertions(+), 13 deletions(-) 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 7c43de237e6..63fdec3d880 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,16 +31,12 @@ > - About - Report an issue... + About + @if (configOptPermission.read) { + 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 1c9e0a5f7e6..7b836feb0b2 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 @@ -1,21 +1,29 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '~/app/shared/shared.module'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { Permission, Permissions } from '~/app/shared/models/permissions'; import { configureTestBed } from '~/testing/unit-test-helper'; import { DashboardHelpComponent } from './dashboard-help.component'; describe('DashboardHelpComponent', () => { let component: DashboardHelpComponent; let fixture: ComponentFixture; + let permissions: Permissions; configureTestBed({ imports: [HttpClientTestingModule, SharedModule, RouterTestingModule], - declarations: [DashboardHelpComponent] + declarations: [DashboardHelpComponent], + providers: [AuthStorageService] }); beforeEach(() => { + permissions = new Permissions({}); + permissions.configOpt = new Permission(['read']); + spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.returnValue(permissions); fixture = TestBed.createComponent(DashboardHelpComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -24,4 +32,16 @@ describe('DashboardHelpComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + 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); + + fixture = TestBed.createComponent(DashboardHelpComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('cds-overflow-menu-option')); + expect(options.length).toBe(3); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts index 6a85bb4564e..f2fae397142 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Icons } from '~/app/shared/enum/icons.enum'; +import { Permission } from '~/app/shared/models/permissions'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { DocService } from '~/app/shared/services/doc.service'; import { AboutComponent } from '../about/about.component'; @@ -16,11 +18,15 @@ import { FeedbackComponent } from '~/app/ceph/shared/feedback/feedback.component export class DashboardHelpComponent implements OnInit { docsUrl: string; icons = Icons; + configOptPermission: Permission; constructor( private docService: DocService, - private modalCdsService: ModalCdsService - ) {} + private modalCdsService: ModalCdsService, + private authStorageService: AuthStorageService + ) { + this.configOptPermission = this.authStorageService.getPermissions().configOpt; + } ngOnInit() { this.docService.subscribeOnce('dashboard', (url: string) => { From 65034ed4e8a4950eb18988d898022b6c33a40c87 Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:12:47 +0530 Subject: [PATCH 3/6] mgr/dashboard: exclude user scope from read-only system role Read-only users could see the Settings icon but were denied access to user management pages. Remove user read permission from the read-only role so the administration menu stays hidden. Fixes: https://tracker.ceph.com/issues/77952 Signed-off-by: Abhishek Desai --- src/pybind/mgr/dashboard/services/access_control.py | 4 ++-- src/pybind/mgr/dashboard/tests/test_access_control.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 392b3ab7999..81b0995a1e9 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -218,9 +218,9 @@ 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 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.DASHBOARD_SETTINGS, Scope.CONFIG_OPT) + if scope_name not in (Scope.USER, Scope.DASHBOARD_SETTINGS, Scope.CONFIG_OPT) }) diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index a5f425e6db3..bd68733c1e3 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -156,7 +156,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): self.assertEqual(role['name'], 'read-only') self.assertEqual( role['description'], - 'allows read permission for all security scope except dashboard settings and config-opt' + 'allows read permission for all security scope except user, dashboard settings and config-opt' ) def test_delete_system_role(self): From 4bdd697d0286f2af32ef36aeae42299bdf5d745d Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:12:58 +0530 Subject: [PATCH 4/6] mgr/dashboard: grant pool read to cluster-manager role Cluster-manager users were denied access to the CRUSH map page because the underlying API requires pool read permission. Fixes: https://tracker.ceph.com/issues/77952 Signed-off-by: Abhishek Desai --- src/pybind/mgr/dashboard/services/access_control.py | 1 + src/pybind/mgr/dashboard/tests/test_access_control.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 81b0995a1e9..a3dfb78fc2e 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -253,6 +253,7 @@ CLUSTER_MGR_ROLE = Role( and config-opt scopes""", { Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], + Scope.POOL: [_P.READ], Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index bd68733c1e3..c14f1ea72f5 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -159,6 +159,10 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): 'allows read permission for all security scope except user, dashboard settings and config-opt' ) + def test_cluster_manager_role_has_pool_read(self): + role = self.exec_cmd('ac-role-show', rolename='cluster-manager') + self.assertEqual(role['scopes_permissions'][Scope.POOL], [Permission.READ]) + def test_delete_system_role(self): with self.assertRaises(CmdException) as ctx: self.exec_cmd('ac-role-delete', rolename='administrator') From 622483c52efc14cb64dd8c0e0a30d8394ba421a7 Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:13:08 +0530 Subject: [PATCH 5/6] mgr/dashboard: grant hosts read to smb-manager role SMB cluster creation needs to list cluster hosts but the smb-manager role lacked hosts read permission, causing Access Denied errors. Fixes: https://tracker.ceph.com/issues/77952 Signed-off-by: Abhishek Desai --- src/pybind/mgr/dashboard/services/access_control.py | 1 + src/pybind/mgr/dashboard/tests/test_access_control.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index a3dfb78fc2e..6dac2b8f1cd 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -292,6 +292,7 @@ GANESHA_MGR_ROLE = Role( 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.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 c14f1ea72f5..b67acd4d234 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -163,6 +163,10 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin): role = self.exec_cmd('ac-role-show', rolename='cluster-manager') self.assertEqual(role['scopes_permissions'][Scope.POOL], [Permission.READ]) + def test_smb_manager_role_has_hosts_read(self): + role = self.exec_cmd('ac-role-show', rolename='smb-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') From 0d0a241ba733ca804df2f42b59f2e0a31e36e180 Mon Sep 17 00:00:00 2001 From: Abhishek Desai Date: Mon, 6 Jul 2026 16:13:23 +0530 Subject: [PATCH 6/6] 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 --- qa/tasks/mgr/dashboard/test_host.py | 2 +- src/pybind/mgr/dashboard/controllers/health.py | 3 ++- .../prometheus-tabs.component.html | 17 ++++++++++------- .../prometheus-tabs.component.spec.ts | 2 +- .../prometheus-tabs.component.ts | 6 +++--- .../src/app/ceph/overview/overview.component.ts | 7 ++++++- .../dashboard-help.component.html | 14 ++++++++++---- .../dashboard-help.component.spec.ts | 14 +++++++++++++- .../mgr/dashboard/services/access_control.py | 5 ++++- .../mgr/dashboard/tests/test_access_control.py | 15 ++++++++++++++- 10 files changed, 64 insertions(+), 21 deletions(-) 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')