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 <adesai@redhat.com>
This commit is contained in:
Abhishek Desai 2026-07-06 16:13:23 +05:30
parent 622483c52e
commit 0d0a241ba7
10 changed files with 64 additions and 21 deletions

View File

@ -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)

View File

@ -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'),

View File

@ -24,14 +24,17 @@
></a
>
</ng-container>
@if (canViewSilences) {
@if (prometheusPermissions.read) {
<ng-container ngbNavItem>
<a class="nav-link"
routerLink="/monitoring/silences"
routerLinkActive="active"
ariaCurrentWhenActive="page"
[routerLinkActiveOptions]="{exact: true}"
i18n>Silences</a>
<a
class="nav-link"
routerLink="/monitoring/silences"
routerLinkActive="active"
ariaCurrentWhenActive="page"
[routerLinkActiveOptions]="{ exact: true }"
i18n
>Silences</a
>
</ng-container>
}
<ng-container ngbNavItem>

View File

@ -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);
});
});

View File

@ -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;
}
}

View File

@ -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 })
);

View File

@ -31,11 +31,17 @@
></svg>
</a>
</li>
<cds-overflow-menu-option (click)="openAboutModal()"
i18n>About</cds-overflow-menu-option>
<cds-overflow-menu-option
(click)="openAboutModal()"
i18n
>About</cds-overflow-menu-option
>
@if (configOptPermission.read) {
<cds-overflow-menu-option (click)="openFeedbackModal()"
i18n>Report an issue...</cds-overflow-menu-option>
<cds-overflow-menu-option
(click)="openFeedbackModal()"
i18n
>Report an issue...</cds-overflow-menu-option
>
}
</cds-overflow-menu>

View File

@ -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 <li> 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 <li> links
expect(options.length).toBe(1);
expect(options[0].nativeElement.textContent.trim()).toBe('About');
});
});

View File

@ -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],

View File

@ -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')