From: Devika Babrekar Date: Tue, 28 Apr 2026 13:13:42 +0000 (+0530) Subject: mgr/dashboard: "Access Denied" being shown on overview page for read-only user X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=4ebd2276bf4550f6c2afb32385a0b68b2aa17ea5;p=ceph.git mgr/dashboard: "Access Denied" being shown on overview page for read-only user Fix: https://tracker.ceph.com/issues/76293 Signed-off-by: Devika Babrekar --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts index 7f32ab092274..644353a63a32 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts @@ -112,16 +112,18 @@ export class OverviewHealthCardComponent { this.viewPGStates.emit(); } + private readonly permissions = this.authStorageService.getPermissions(); + readonly data$: Observable = combineLatest([ this.summaryService.summaryData$.pipe(filter((summary): summary is Summary => !!summary)), - this.upgradeService.listCached().pipe( - startWith(null as UpgradeInfoInterface | null), - catchError(() => of(null)) - ) + this.permissions?.configOpt?.read + ? this.upgradeService.listCached().pipe( + startWith(null as UpgradeInfoInterface | null), + catchError(() => of(null)) + ) + : of(null) ]).pipe(map(([summary, upgrade]) => ({ summary, upgrade }))); - private readonly permissions = this.authStorageService.getPermissions(); - readonly enabled$: Observable = this.permissions?.configOpt?.read ? this.mgrModuleService.getConfig('cephadm').pipe( map((resp: any) => !!resp?.hw_monitoring), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html index 64fd7c5bcc06..3e004b1fccc1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html @@ -11,9 +11,10 @@
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts index 352c1048ed4e..11841216d7bf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts @@ -8,11 +8,12 @@ import { MgrModuleService } from '../../api/mgr-module.service'; import { PerformanceData } from '../../models/performance-data'; import { DatePipe } from '@angular/common'; import { NumberFormatterService } from '../../services/number-formatter.service'; +import { AuthStorageService } from '../../services/auth-storage.service'; +import { Permissions } from '../../models/permissions'; describe('PerformanceCardComponent', () => { let component: PerformanceCardComponent; let fixture: ComponentFixture; - let prometheusService: PrometheusService; let performanceCardService: PerformanceCardService; let mgrModuleService: MgrModuleService; @@ -64,6 +65,10 @@ describe('PerformanceCardComponent', () => { transform: jest.fn().mockReturnValue('01 Jan, 00:00:00') }; + const authStorageServiceMock = { + getPermissions: jest.fn().mockReturnValue(new Permissions({ 'config-opt': ['read'] })) + }; + await TestBed.configureTestingModule({ imports: [HttpClientTestingModule, PerformanceCardComponent], providers: [ @@ -71,13 +76,13 @@ describe('PerformanceCardComponent', () => { { provide: PerformanceCardService, useValue: performanceCardServiceMock }, { provide: MgrModuleService, useValue: mgrModuleServiceMock }, { provide: NumberFormatterService, useValue: numberFormatterMock }, - { provide: DatePipe, useValue: datePipeMock } + { provide: DatePipe, useValue: datePipeMock }, + { provide: AuthStorageService, useValue: authStorageServiceMock } ] }).compileComponents(); fixture = TestBed.createComponent(PerformanceCardComponent); component = fixture.componentInstance; - prometheusService = TestBed.inject(PrometheusService); performanceCardService = TestBed.inject(PerformanceCardService); mgrModuleService = TestBed.inject(MgrModuleService); }); @@ -156,18 +161,18 @@ describe('PerformanceCardComponent', () => { flush(); })); - it('should set emptyStateKey when prometheus is not configured', fakeAsync(() => { - (prometheusService.ifPrometheusConfigured as jest.Mock).mockImplementation((_fn, elseFn) => { - if (elseFn) { - elseFn(); - } - }); + it('should set emptyStateKey to empty string when user lacks configOpt read', fakeAsync(() => { + const auth = TestBed.inject(AuthStorageService); + (auth.getPermissions as jest.Mock).mockReturnValue(new Permissions({})); + + fixture = TestBed.createComponent(PerformanceCardComponent); + component = fixture.componentInstance; const time = { start: 1000, end: 2000, step: 14 }; component.loadCharts(time); tick(); - expect(component.emptyStateKey()).toBe('prometheusNotAvailable'); + expect(component.emptyStateKey()).toBe(''); })); it('should cleanup subscriptions on ngOnDestroy', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts index 2479ee514188..65baa2473799 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts @@ -17,7 +17,7 @@ import { } from '~/app/shared/models/performance-data'; import { PerformanceCardService } from '../../api/performance-card.service'; import { DropdownModule, GridModule, LayoutModule, ListItem } from 'carbon-components-angular'; -import { Subject, Subscription } from 'rxjs'; +import { of, Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { ProductiveCardComponent } from '../productive-card/productive-card.component'; import { CommonModule } from '@angular/common'; @@ -25,6 +25,7 @@ import { TimePickerComponent } from '../time-picker/time-picker.component'; import { AreaChartComponent } from '../area-chart/area-chart.component'; import { MgrModuleService } from '../../api/mgr-module.service'; import { toSignal } from '@angular/core/rxjs-interop'; +import { AuthStorageService } from '../../services/auth-storage.service'; @Component({ selector: 'cd-performance-card', @@ -82,14 +83,21 @@ export class PerformanceCardComponent implements OnInit, OnDestroy { } ]; + role: string = ''; + private prometheusService = inject(PrometheusService); private performanceCardService = inject(PerformanceCardService); private mgrModuleService = inject(MgrModuleService); + private readonly authStorageService = inject(AuthStorageService); time = { ...this.prometheusService.lastHourDateObject }; private chartSub?: Subscription; - readonly list = toSignal(this.mgrModuleService.list(), { initialValue: [] }); + private readonly permissions = this.authStorageService.getPermissions(); + + readonly list = this.permissions?.configOpt?.read + ? toSignal(this.mgrModuleService.list(), { initialValue: [] }) + : toSignal(of([]), { initialValue: [] }); ngOnInit() { this.loadCharts(this.time); @@ -104,26 +112,31 @@ export class PerformanceCardComponent implements OnInit, OnDestroy { .getChartData(time) .pipe(takeUntil(this.destroy$)) .subscribe((data) => { - this.chartDataSignal.set(data); - this.prometheusService.ifPrometheusConfigured( - () => { - let enabled$ = this.list().filter((a) => a.name === 'prometheus')[0].enabled; - if (enabled$) { - this.chartDataSignal.set(data); - this.emptyStateKey.set(''); - } else if (!enabled$) { - this.emptyStateKey.set('prometheusDisabled'); - } else { - this.emptyStateKey.set('storageNotAvailable'); - } - }, - () => { - this.emptyStateKey.set('prometheusNotAvailable'); - } - ); + if (this.permissions?.configOpt?.read) { + this.followEmptyStateMsgCheck(data); + } else { + this.skipEmptyStateMsgCheck(data); + } }); } + followEmptyStateMsgCheck(data: PerformanceData) { + let enabled$ = this.list().filter((a) => a.name === 'prometheus')[0].enabled; + this.chartDataSignal.set(data); + if (enabled$) { + this.emptyStateKey.set(''); + } else if (!enabled$) { + this.emptyStateKey.set('prometheusDisabled'); + } else { + this.emptyStateKey.set('storageNotAvailable'); + } + } + + skipEmptyStateMsgCheck(data: PerformanceData) { + this.chartDataSignal.set(data); + this.emptyStateKey.set(''); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete();