Skip to content

Commit

Permalink
[SecuritySolution] Check visualize.save on added to library (elastic#…
Browse files Browse the repository at this point in the history
…194689)

## Summary

Previously we use `canUseEditor` to check if we should disable the
`Added to Library` action. It's not ideal as it checks
`core.application.capabilities.visualize?.show` under the hood.
We should use `application.capabilities.visualize?.save` to make sure
`Added to Library` is disabled when they have no rights to save a
visualization.

Steps to verify:

1. Create a role with read visualization privilege, and assign it to a
user:
<img width="2556" alt="Screenshot 2024-10-02 at 13 20 44"
src="https://github.com/user-attachments/assets/1ab9ddcf-96fd-4fd1-bdad-7382573350fb">

2. Login with the user and check `Add to Library` should be disabled:
<img width="2556" alt="Screenshot 2024-10-02 at 13 20 11"
src="https://github.com/user-attachments/assets/9681b121-77e6-47c1-9a99-57d53f5d0b07">


### Checklist

Delete any items that are not applicable to this PR.


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
angorayc and kibanamachine authored Oct 2, 2024
1 parent fe83c0d commit b2d821a
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@

import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import { useKibana } from '../../lib/kibana/kibana_react';
import { mockAttributes } from './mocks';
import { DEFAULT_ACTIONS, useActions } from './use_actions';
import { coreMock } from '@kbn/core/public/mocks';
import { TestProviders } from '../../mock';

jest.mock('./use_add_to_existing_case', () => {
Expand All @@ -30,25 +27,20 @@ jest.mock('./use_add_to_new_case', () => {
}),
};
});

jest.mock('./use_redirect_to_dashboard_from_lens', () => ({
useRedirectToDashboardFromLens: jest.fn().mockReturnValue({
redirectTo: jest.fn(),
getEditOrCreateDashboardPath: jest.fn().mockReturnValue('mockDashboardPath'),
}),
}));

jest.mock('../../lib/kibana/kibana_react', () => {
return {
useKibana: jest.fn(),
};
});

const coreStart = coreMock.createStart();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<TestProviders>
<NavigationProvider core={coreStart}>{children}</NavigationProvider>
</TestProviders>
);
describe(`useActions`, () => {
const mockNavigateToPrefilledEditor = jest.fn();
beforeAll(() => {
(useKibana as jest.Mock).mockReturnValue({
useKibana: jest.fn().mockReturnValue({
services: {
lens: {
navigateToPrefilledEditor: mockNavigateToPrefilledEditor,
navigateToPrefilledEditor: jest.fn(),
canUseEditor: jest.fn().mockReturnValue(true),
SaveModalComponent: jest
.fn()
Expand All @@ -59,33 +51,34 @@ describe(`useActions`, () => {
addWarning: jest.fn(),
},
},
application: { capabilities: { visualize: { save: true } } },
},
});
});
}),
};
});

const props = {
withActions: DEFAULT_ACTIONS,
attributes: mockAttributes,
timeRange: {
from: '2022-10-26T23:00:00.000Z',
to: '2022-11-03T15:16:50.053Z',
},
inspectActionProps: {
handleInspectClick: jest.fn(),
isInspectButtonDisabled: false,
},
};

describe(`useActions`, () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render actions', () => {
const { result } = renderHook(
() =>
useActions({
withActions: DEFAULT_ACTIONS,
attributes: mockAttributes,
timeRange: {
from: '2022-10-26T23:00:00.000Z',
to: '2022-11-03T15:16:50.053Z',
},
inspectActionProps: {
handleInspectClick: jest.fn(),
isInspectButtonDisabled: false,
},
}),
{
wrapper,
}
);
const { result } = renderHook(() => useActions(props), {
wrapper: TestProviders,
});
expect(result.current[0].id).toEqual('inspect');
expect(result.current[0].order).toEqual(4);
expect(result.current[1].id).toEqual('addToNewCase');
Expand Down Expand Up @@ -119,20 +112,11 @@ describe(`useActions`, () => {
const { result } = renderHook(
() =>
useActions({
withActions: DEFAULT_ACTIONS,
attributes: mockAttributes,
timeRange: {
from: '2022-10-26T23:00:00.000Z',
to: '2022-11-03T15:16:50.053Z',
},
inspectActionProps: {
handleInspectClick: jest.fn(),
isInspectButtonDisabled: false,
},
...props,
extraActions: mockExtraAction,
}),
{
wrapper,
wrapper: TestProviders,
}
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { useSaveToLibrary } from './use_save_to_library';
import { useKibana } from '../../lib/kibana';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';

jest.mock('../../lib/kibana', () => ({
useKibana: jest.fn(),
}));

jest.mock('./use_redirect_to_dashboard_from_lens', () => ({
useRedirectToDashboardFromLens: jest.fn().mockReturnValue({
redirectTo: jest.fn(),
getEditOrCreateDashboardPath: jest.fn().mockReturnValue('mockDashboardPath'),
}),
}));

jest.mock('../link_to', () => ({
useGetSecuritySolutionUrl: jest.fn(),
}));

jest.mock('@kbn/react-kibana-mount', () => ({
toMountPoint: jest.fn().mockReturnValue(jest.fn()),
}));

const mockUseKibana = useKibana as jest.Mock;

describe('useSaveToLibrary hook', () => {
const mockStartServices = {
application: { capabilities: { visualize: { save: true } } },
lens: { SaveModalComponent: jest.fn() },
};

beforeEach(() => {
jest.clearAllMocks();
mockUseKibana.mockReturnValue({ services: mockStartServices });
});

it('should open the save visualization flyout when openSaveVisualizationFlyout is called', () => {
const { result } = renderHook(() =>
useSaveToLibrary({ attributes: kpiHostMetricLensAttributes })
);

act(() => {
result.current.openSaveVisualizationFlyout();
});

expect(toMountPoint).toHaveBeenCalled();
});

it('should disable visualizations if user cannot save', () => {
const noSaveCapabilities = {
...mockStartServices,
application: { capabilities: { visualize: { save: false } } },
};
mockUseKibana.mockReturnValue({ services: noSaveCapabilities });

const { result } = renderHook(() =>
useSaveToLibrary({ attributes: kpiHostMetricLensAttributes })
);
expect(result.current.disableVisualizations).toBe(true);
});

it('should disable visualizations if attributes are missing', () => {
mockUseKibana.mockReturnValue({ services: mockStartServices });
const { result } = renderHook(() => useSaveToLibrary({ attributes: null }));
expect(result.current.disableVisualizations).toBe(true);
});

it('should enable visualizations if user can save and attributes are present', () => {
const { result } = renderHook(() =>
useSaveToLibrary({ attributes: kpiHostMetricLensAttributes })
);
expect(result.current.disableVisualizations).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const useSaveToLibrary = ({
attributes: LensAttributes | undefined | null;
}) => {
const startServices = useKibana().services;
const { SaveModalComponent, canUseEditor } = startServices.lens;
const canSaveVisualization = !!startServices.application.capabilities.visualize?.save;
const { SaveModalComponent } = startServices.lens;
const getSecuritySolutionUrl = useGetSecuritySolutionUrl();
const { redirectTo, getEditOrCreateDashboardPath } = useRedirectToDashboardFromLens({
getSecuritySolutionUrl,
Expand Down Expand Up @@ -49,8 +50,8 @@ export const useSaveToLibrary = ({
}, [SaveModalComponent, attributes, getEditOrCreateDashboardPath, redirectTo, startServices]);

const disableVisualizations = useMemo(
() => !canUseEditor() || attributes == null,
[attributes, canUseEditor]
() => !canSaveVisualization || attributes == null,
[attributes, canSaveVisualization]
);

return { openSaveVisualizationFlyout, disableVisualizations };
Expand Down

0 comments on commit b2d821a

Please sign in to comment.