]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: allow read-only users to view Silences page
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 6 Jul 2026 11:26:12 +0000 (16:56 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 20 Jul 2026 08:36:13 +0000 (14:06 +0530)
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 <adesai@redhat.com>
 Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/prometheus-tabs/prometheus-tabs.component.html
        - Added permissions

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/cluster/prometheus/silence-list/silence-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-list/silence-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/alertmanager-silence.ts

index 40f70bcadd4a066b40b5eaa2fe922c44f90a30fd..803ffad1f6283e01caf4310a05084608d3ae7859 100644 (file)
       ></a
     >
   </ng-container>
-  <ng-container ngbNavItem>
-    <a
-      class="nav-link"
-      routerLink="/monitoring/silences"
-      routerLinkActive="active"
-      ariaCurrentWhenActive="page"
-      [routerLinkActiveOptions]="{ exact: true }"
-      i18n
-      >Silences</a
-    >
-  </ng-container>
+  @if (canViewSilences) {
+    <ng-container ngbNavItem>
+      <a class="nav-link"
+         routerLink="/monitoring/silences"
+         routerLinkActive="active"
+         ariaCurrentWhenActive="page"
+         [routerLinkActiveOptions]="{exact: true}"
+         i18n>Silences</a>
+    </ng-container>
+  }
   <ng-container ngbNavItem>
     <a
       class="nav-link"
index 3272ae32fd0be06791c74eb4c8f28c6ec4b319ff..29d6001e50f4701dc90b680c87d9ee08ce12eb82 100644 (file)
@@ -4,6 +4,8 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 
 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';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { PrometheusTabsComponent } from './prometheus-tabs.component';
 
@@ -14,7 +16,17 @@ describe('PrometheusTabsComponent', () => {
   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);
+  });
 });
index 423cd386fbc3979609d444aeb3ca3cb7a4f4add0..f3824a299b6f3ef4c7973514bf4b85b1ebbfd0e4 100644 (file)
@@ -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;
+  }
 }
index be93bb5f42e8808703dbfa7b6e2e6aadb98005ac..e116a311d7a7fd08ec22187f781c8bb67153385a 100644 (file)
@@ -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(
index 75ce093af3f8e2767949f6b22eee33fea462ee05..6b7e3be81855f869724349ffd71258892aa565b0 100644 (file)
@@ -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;
index 5f69f1e1e8164e51efbf9923dbb60e966334a35e..7a09abae6175f6e0b8d7fc27d2a2a7487c320a94 100644 (file)
@@ -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[];
 }