]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Access denied issue for settings icon 70341/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 20 Jul 2026 11:45:53 +0000 (17:15 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Wed, 22 Jul 2026 15:32:11 +0000 (21:02 +0530)
fixes : https://tracker.ceph.com/issues/78407

Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/overview/overview.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/administration/administration.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts

index 19d2bfcf5c3706f31ac278ac47fc67b98c83e8bb..f9443116617ad7926aa7926386873dd536ebfd12 100644 (file)
               size="md"
               (click)="updateSiteName()"
               [title]="editing ? 'Save' : 'Edit'"
+              data-testid="site-name-edit-btn"
             >
-              <svg
-                [cdsIcon]="icons.edit"
-                [size]="icons.size32"
-                class="cds--btn__icon"
-                *ngIf="!editing"
-              ></svg>
-              <svg
-                [cdsIcon]="icons.check"
-                [size]="icons.size32"
-                class="cds--btn__icon"
-                *ngIf="editing"
-              ></svg>
+              @if (!editing) {
+                <svg
+                  [cdsIcon]="icons.edit"
+                  [size]="icons.size32"
+                  class="cds--btn__icon"
+                ></svg>
+              } @else {
+                <svg
+                  [cdsIcon]="icons.check"
+                  [size]="icons.size32"
+                  class="cds--btn__icon"
+                ></svg>
+              }
             </cds-icon-button>
           }
           <cd-copy-2-clipboard-button
index d154d4740aa0eb56c6a6df1bcc2cc6e0dd556557..f784d129e1e5ec30e8693b9689cbf25e759b3c60 100644 (file)
@@ -82,7 +82,8 @@ describe('OverviewComponent', () => {
   });
 
   describe('site name edit button visibility', () => {
-    const editButton = () => fixture.debugElement.query(By.css('cds-icon-button'));
+    const editButton = () =>
+      fixture.debugElement.query(By.css('[data-testid="site-name-edit-btn"]'));
 
     it('should show the edit button when user has update permission', () => {
       component.permission = new Permission(['read', 'update']);
index 804c4dbc566114b5e343b3e6ebfddbd97b0e920f..98747b9debf7fcd9e9f51f874e9ac75a2873b584 100644 (file)
@@ -3,16 +3,18 @@
   [flip]="true"
   description=""
 >
-  <li class="cds--overflow-menu-options__option mb-2">
-    <button
-      routerLink="/user-management"
-      class="cds--overflow-menu-options__btn"
-      type="button"
-      i18n
-    >
-      User management
-    </button>
-  </li>
+  @if (userPermission.create || userPermission.update || userPermission.delete) {
+    <li class="cds--overflow-menu-options__option mb-2">
+      <button
+        routerLink="/user-management"
+        class="cds--overflow-menu-options__btn"
+        type="button"
+        i18n
+      >
+        User management
+      </button>
+    </li>
+  }
   @if (configOptPermission.read) {
     <li class="cds--overflow-menu-options__option mb-2">
       <button
 </cds-overflow-menu>
 
 <ng-template #customTrigger>
-  @if (userPermission.read || configOptPermission.read) {
+  @if (
+    userPermission.create ||
+    userPermission.update ||
+    userPermission.delete ||
+    configOptPermission.read
+  ) {
     <svg
       cdsIcon="settings"
       size="20"
index 515df21749d00cf008ed68a75d617daa07ba7c45..49322996013bab01f3c54fbf3989319a4eac1865 100644 (file)
@@ -1,7 +1,9 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { By } from '@angular/platform-browser';
+import { RouterTestingModule } from '@angular/router/testing';
 
-import { Permission } from '~/app/shared/models/permissions';
+import { Permission, Permissions } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { SharedModule } from '~/app/shared/shared.module';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { AdministrationComponent } from './administration.component';
@@ -9,49 +11,113 @@ import { AdministrationComponent } from './administration.component';
 describe('AdministrationComponent', () => {
   let component: AdministrationComponent;
   let fixture: ComponentFixture<AdministrationComponent>;
+  let permissions: Permissions;
+
+  const createComponent = () => {
+    spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.returnValue(permissions);
+    fixture = TestBed.createComponent(AdministrationComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  };
 
   configureTestBed({
-    imports: [SharedModule],
-    declarations: [AdministrationComponent]
+    imports: [SharedModule, RouterTestingModule],
+    declarations: [AdministrationComponent],
+    providers: [AuthStorageService]
   });
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(AdministrationComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
+    permissions = new Permissions({});
   });
 
   it('should create', () => {
+    createComponent();
     expect(component).toBeTruthy();
   });
 
-  describe('settings icon visibility', () => {
-    const setPermissions = (user: string[], configOpt: string[]) => {
-      component.userPermission = new Permission(user);
-      component.configOptPermission = new Permission(configOpt);
-      fixture.detectChanges();
-    };
+  describe('Settings icon visibility', () => {
+    /**
+     * The settings icon is inside <ng-template #customTrigger> which is not
+     * projected into the DOM by the CDS component in JSDOM. We therefore
+     * verify the guard condition through the component's own Permission objects,
+     * mirroring the *ngIf expression in the template:
+     *   userPermission.create || userPermission.update || userPermission.delete || configOptPermission.read
+     */
+    const iconVisible = (c: AdministrationComponent) =>
+      c['userPermission'].create ||
+      c['userPermission'].update ||
+      c['userPermission'].delete ||
+      c['configOptPermission'].read;
+
+    it('should hide the settings icon for a read-only user', () => {
+      permissions.user = new Permission(['read']);
+      permissions.configOpt = new Permission([]);
+      createComponent();
+      expect(iconVisible(component)).toBeFalsy();
+    });
+
+    it('should show the settings icon when user has create permission', () => {
+      permissions.user = new Permission(['read', 'create']);
+      permissions.configOpt = new Permission([]);
+      createComponent();
+      expect(iconVisible(component)).toBeTruthy();
+    });
 
-    const settingsIcon = () => fixture.debugElement.query(By.css('[cdsIcon="settings"]'));
+    it('should show the settings icon when user has update permission', () => {
+      permissions.user = new Permission(['read', 'update']);
+      permissions.configOpt = new Permission([]);
+      createComponent();
+      expect(iconVisible(component)).toBeTruthy();
+    });
 
-    it('should show the settings icon when user has user.read', () => {
-      setPermissions(['read'], []);
-      expect(settingsIcon()).toBeTruthy();
+    it('should show the settings icon when user has delete permission', () => {
+      permissions.user = new Permission(['read', 'delete']);
+      permissions.configOpt = new Permission([]);
+      createComponent();
+      expect(iconVisible(component)).toBeTruthy();
     });
 
-    it('should show the settings icon when user has configOpt.read', () => {
-      setPermissions([], ['read']);
-      expect(settingsIcon()).toBeTruthy();
+    it('should show the settings icon when configOpt is readable (even with read-only user)', () => {
+      permissions.user = new Permission(['read']);
+      permissions.configOpt = new Permission(['read']);
+      createComponent();
+      expect(iconVisible(component)).toBeTruthy();
     });
+  });
+
+  describe('User Management menu item visibility', () => {
+    it('should hide User Management for a read-only user', () => {
+      permissions.user = new Permission(['read']);
+      createComponent();
+      const buttons = fixture.debugElement.queryAll(By.css('.cds--overflow-menu-options__btn'));
+      const labels = buttons.map((b) => b.nativeElement.textContent.trim());
+      expect(labels).not.toContain('User management');
+    });
+
+    it('should show User Management when user has create permission', () => {
+      permissions.user = new Permission(['read', 'create']);
+      createComponent();
+      const buttons = fixture.debugElement.queryAll(By.css('.cds--overflow-menu-options__btn'));
+      const labels = buttons.map((b) => b.nativeElement.textContent.trim());
+      expect(labels).toContain('User management');
+    });
+  });
 
-    it('should show the settings icon when user has both user.read and configOpt.read', () => {
-      setPermissions(['read'], ['read']);
-      expect(settingsIcon()).toBeTruthy();
+  describe('Telemetry menu item visibility', () => {
+    it('should hide Telemetry configuration when configOpt is not readable', () => {
+      permissions.configOpt = new Permission([]);
+      createComponent();
+      const buttons = fixture.debugElement.queryAll(By.css('.cds--overflow-menu-options__btn'));
+      const labels = buttons.map((b) => b.nativeElement.textContent.trim());
+      expect(labels).not.toContain('Telemetry configuration');
     });
 
-    it('should hide the settings icon when user has neither user.read nor configOpt.read', () => {
-      setPermissions([], []);
-      expect(settingsIcon()).toBeFalsy();
+    it('should show Telemetry configuration when configOpt is readable', () => {
+      permissions.configOpt = new Permission(['read']);
+      createComponent();
+      const buttons = fixture.debugElement.queryAll(By.css('.cds--overflow-menu-options__btn'));
+      const labels = buttons.map((b) => b.nativeElement.textContent.trim());
+      expect(labels).toContain('Telemetry configuration');
     });
   });
 });
index ba607adba7478ad57af087f585dddd5d7e0fff7d..e23340a888e95ba16c32797ed87af82794415966 100644 (file)
       <div class="cds--btn cds--btn--icon-only cds--header__action">
         <cd-dashboard-help></cd-dashboard-help>
       </div>
-      <div
-        class="cds--btn cds--btn--icon-only cds--header__action"
-        *ngIf="permissions.user.read"
-      >
-        <cd-administration></cd-administration>
-      </div>
+      @if (
+        permissions.user.create ||
+        permissions.user.update ||
+        permissions.user.delete ||
+        permissions.configOpt.read
+      ) {
+        <div class="cds--btn cds--btn--icon-only cds--header__action tc_administration">
+          <cd-administration></cd-administration>
+        </div>
+      }
       <div class="cds--btn cds--btn--icon-only cds--header__action">
         <cd-identity></cd-identity>
       </div>
index 1b5e18bccab1842956a33fa783da95c195eeced8..bc7c23cf887cdea2e8ef37bfaca37e6373723c18 100644 (file)
@@ -264,4 +264,31 @@ describe('NavigationComponent', () => {
       }
     });
   });
+
+  describe('Administration settings icon wrapper', () => {
+    it('should hide the wrapper for a read-only user (no write user perms, no configOpt.read)', () => {
+      component.permissions = everythingPermittedExcept(['user', 'configOpt']);
+      fixture.detectChanges();
+      expect(fixture.debugElement.query(By.css('.tc_administration'))).toBeFalsy();
+    });
+
+    it('should show the wrapper when user has write permission', () => {
+      const permissions: Permissions = new Permissions({});
+      Object.keys(permissions).forEach((key) => (permissions[key] = new Permission(['read'])));
+      permissions.user = new Permission(['read', 'create']);
+      component.permissions = permissions;
+      fixture.detectChanges();
+      expect(fixture.debugElement.query(By.css('.tc_administration'))).toBeTruthy();
+    });
+
+    it('should show the wrapper when configOpt is readable', () => {
+      const permissions: Permissions = new Permissions({});
+      Object.keys(permissions).forEach((key) => (permissions[key] = new Permission(['read'])));
+      permissions.user = new Permission(['read']);
+      permissions.configOpt = new Permission(['read']);
+      component.permissions = permissions;
+      fixture.detectChanges();
+      expect(fixture.debugElement.query(By.css('.tc_administration'))).toBeTruthy();
+    });
+  });
 });
index a24005867879b3241862319a144949d21a06e2fb..436adeafdb6a415713c3482eb9759837e61b10cc 100644 (file)
@@ -118,7 +118,7 @@ describe('ApiInterceptorService', () => {
       );
     });
 
-    it('should not redirect 403 for ui-api/ requests', () => {
+    it('should not redirect 403 for unscoped ui-api/ background check requests', () => {
       const uiApiUrl = 'ui-api/prometheus/prometheus-api-host';
       httpClient.get(uiApiUrl).subscribe(
         () => true,
@@ -129,6 +129,20 @@ describe('ApiInterceptorService', () => {
       expect(router.navigate).not.toHaveBeenCalled();
     });
 
+    it('should redirect 403 for scoped ui-api/ requests', () => {
+      const scopedUiApiUrl = 'ui-api/osd/deployment_options';
+      httpClient.get(scopedUiApiUrl).subscribe(
+        () => true,
+        (_resp) => undefined
+      );
+      httpTesting.expectOne(scopedUiApiUrl).error(new ErrorEvent('abc'), { status: 403 });
+      httpTesting.verify();
+      expect(router.navigate).toHaveBeenCalledWith(
+        ['error'],
+        { state: { header: 'Access Denied', icon: 'locked', message: "Sorry, you don't have permission to view this page or resource.", source: 'forbidden' } } // prettier-ignore
+      );
+    });
+
     it('should show notification (error string)', () => {
       runNotificationTest(
         'foobar',
index 9b7b7b8904fe2a32fe143728e0b0f7de33163952..48e7bd3f98ae3b1368cb6946b712d017aa002bc4 100644 (file)
@@ -108,8 +108,12 @@ export class ApiInterceptorService implements HttpInterceptor {
               this.authStorageService.remove();
               this.router.navigate(['/login']);
               break;
-            case 403:
-              if (!request.url.startsWith('ui-api/')) {
+            case 403: {
+              const UNSCOPED_UI_APIS = [
+                'ui-api/prometheus/prometheus-api-host',
+                'ui-api/prometheus/alertmanager-api-host'
+              ];
+              if (!UNSCOPED_UI_APIS.some((path) => request.url.startsWith(path))) {
                 this.router.navigate(['error'], {
                   state: {
                     message: $localize`Sorry, you don't have permission to view this page or resource.`,
@@ -120,6 +124,7 @@ export class ApiInterceptorService implements HttpInterceptor {
                 });
               }
               break;
+            }
             default:
               timeoutId = this.prepareNotification(resp);
           }