mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
mgr/dashboard : Access denied issue for settings icon
fixes : https://tracker.ceph.com/issues/78407 Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
This commit is contained in:
parent
21e7419557
commit
481d6e4699
@ -29,19 +29,21 @@
|
||||
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
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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
|
||||
@ -28,7 +30,12 @@
|
||||
</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"
|
||||
|
||||
@ -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;
|
||||
|
||||
configureTestBed({
|
||||
imports: [SharedModule],
|
||||
declarations: [AdministrationComponent]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const createComponent = () => {
|
||||
spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.returnValue(permissions);
|
||||
fixture = TestBed.createComponent(AdministrationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
configureTestBed({
|
||||
imports: [SharedModule, RouterTestingModule],
|
||||
declarations: [AdministrationComponent],
|
||||
providers: [AuthStorageService]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
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;
|
||||
|
||||
const settingsIcon = () => fixture.debugElement.query(By.css('[cdsIcon="settings"]'));
|
||||
|
||||
it('should show the settings icon when user has user.read', () => {
|
||||
setPermissions(['read'], []);
|
||||
expect(settingsIcon()).toBeTruthy();
|
||||
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 configOpt.read', () => {
|
||||
setPermissions([], ['read']);
|
||||
expect(settingsIcon()).toBeTruthy();
|
||||
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();
|
||||
});
|
||||
|
||||
it('should show the settings icon when user has both user.read and configOpt.read', () => {
|
||||
setPermissions(['read'], ['read']);
|
||||
expect(settingsIcon()).toBeTruthy();
|
||||
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 hide the settings icon when user has neither user.read nor configOpt.read', () => {
|
||||
setPermissions([], []);
|
||||
expect(settingsIcon()).toBeFalsy();
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
|
||||
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 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -57,12 +57,16 @@
|
||||
<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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user