]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: "Access Denied" being shown on overview page for read-only user 68653/head
authorDevika Babrekar <devika.babrekar@ibm.com>
Tue, 28 Apr 2026 13:13:42 +0000 (18:43 +0530)
committerDevika Babrekar <devika.babrekar@ibm.com>
Wed, 6 May 2026 10:18:15 +0000 (15:48 +0530)
Fix: https://tracker.ceph.com/issues/76293
Signed-off-by: Devika Babrekar <devika.babrekar@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts

index 7f32ab09227486237a41467d656d0c4122223196..644353a63a3203403541f9d2c98c27db7a99be7e 100644 (file)
@@ -112,16 +112,18 @@ export class OverviewHealthCardComponent {
     this.viewPGStates.emit();
   }
 
+  private readonly permissions = this.authStorageService.getPermissions();
+
   readonly data$: Observable<OverviewHealthData> = 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<boolean> = this.permissions?.configOpt?.read
     ? this.mgrModuleService.getConfig('cephadm').pipe(
         map((resp: any) => !!resp?.hw_monitoring),
index 64fd7c5bcc0619ce015bf7fd365d1ccbc8fc6c61..3e004b1fccc111875113e1d88d7c92dd3afad47f 100644 (file)
       <div cdsStack="horizontal"
            gap="2">
         <cd-time-picker
-          [dropdwonSize]="'sm'"
-          [label]="'Time Span'"
+          dropdownSize="sm"
+          label="Time Span"
           (selectedTime)="loadCharts($event)"
+          i18n-label
         ></cd-time-picker>
       </div>
     </ng-template>
index 352c1048ed4e2c45756535246c8a224a141d02d3..11841216d7bf7e41722b0e6ae9e403b693a79c8d 100644 (file)
@@ -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<PerformanceCardComponent>;
-  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', () => {
index 2479ee514188aca09d1da37bdc3e1989878d2803..65baa2473799291d8883d263125cbd537739bb5a 100644 (file)
@@ -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();