[Security Solution] Check Dashboard capabilities at dashboards landing page (#137186)

* new useCapabilities kibana hook

* check dashboard capabilities on security dashboard landing table and button
This commit is contained in:
Sergi Massaneda 2022-07-26 18:24:14 +02:00 committed by GitHub
parent 17a0f7fcfd
commit 5124d6c94d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 95 additions and 41 deletions

View file

@ -13,6 +13,7 @@
"alerting",
"cases",
"cloudSecurityPosture",
"dashboard",
"data",
"embeddable",
"eventLog",
@ -35,7 +36,6 @@
"encryptedSavedObjects",
"fleet",
"ml",
"dashboard",
"newsfeed",
"security",
"spaces",

View file

@ -90,3 +90,9 @@ export const useNavigateTo = jest.fn().mockReturnValue({
}
}),
});
export const useCapabilities = jest.fn((featureId?: string) =>
featureId
? mockStartServicesMock.application.capabilities[featureId]
: mockStartServicesMock.application.capabilities
);

View file

@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { camelCase, isArray, isObject } from 'lodash';
import { set } from '@elastic/safer-lodash-set';
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
import type { NavigateToAppOptions } from '@kbn/core/public';
import type { Capabilities, NavigateToAppOptions } from '@kbn/core/public';
import type { CasesPermissions } from '@kbn/cases-plugin/common/ui';
import {
APP_UI_ID,
@ -182,18 +182,16 @@ export const useGetUserCasesPermissions = () => {
return casesPermissions;
};
/**
* Returns a full URL to the provided page path by using
* kibana's `getUrlForApp()`
*/
export type GetAppUrl = (param: {
appId?: string;
deepLinkId?: string;
path?: string;
absolute?: boolean;
}) => string;
/**
* The `getAppUrl` function returns a full URL to the provided page path by using
* kibana's `getUrlForApp()`
*/
export const useAppUrl = () => {
const { getUrlForApp } = useKibana().services.application;
@ -204,18 +202,16 @@ export const useAppUrl = () => {
return { getAppUrl };
};
/**
* Navigate to any app using kibana's `navigateToApp()`
* or by url using `navigateToUrl()`
*/
export type NavigateTo = (
param: {
url?: string;
appId?: string;
} & NavigateToAppOptions
) => void;
/**
* The `navigateTo` function navigates to any app using kibana's `navigateToApp()`.
* When the `{ url: string }` parameter is passed it will navigate using `navigateToUrl()`.
*/
export const useNavigateTo = () => {
const { navigateToApp, navigateToUrl } = useKibana().services.application;
@ -233,11 +229,27 @@ export const useNavigateTo = () => {
};
/**
* Returns navigateTo and getAppUrl navigation hooks
*
* Returns `navigateTo` and `getAppUrl` navigation hooks
*/
export const useNavigation = () => {
const { navigateTo } = useNavigateTo();
const { getAppUrl } = useAppUrl();
return { navigateTo, getAppUrl };
};
// Get the type for any feature capability
export type FeatureCapability = Capabilities[string];
interface UseCapabilities {
(): Capabilities;
<T extends FeatureCapability = FeatureCapability>(featureId: string): T;
}
/**
* Returns the feature capability when the `featureId` parameter is defined,
* or the entire kibana `Capabilities` object when the parameter is omitted.
*/
export const useCapabilities: UseCapabilities = <T extends FeatureCapability = FeatureCapability>(
featureId?: string
) => {
const { capabilities } = useKibana().services.application;
return featureId ? (capabilities[featureId] as T) : capabilities;
};

View file

@ -11,12 +11,18 @@ import { SecurityPageName } from '../../app/types';
import { TestProviders } from '../../common/mock';
import { DashboardsLandingPage } from './dashboards';
import type { NavLinkItem } from '../../common/components/navigation/types';
import { useCapabilities } from '../../common/lib/kibana';
jest.mock('../../common/lib/kibana');
jest.mock('../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null }));
jest.mock('../../common/components/dashboards/dashboards_table', () => ({
DashboardsTable: () => <span data-test-subj="dashboardsTable" />,
}));
const DEFAULT_DASHBOARD_CAPABILITIES = { show: true, createNew: true };
const mockUseCapabilities = useCapabilities as jest.Mock;
mockUseCapabilities.mockReturnValue(DEFAULT_DASHBOARD_CAPABILITIES);
const OVERVIEW_ITEM_LABEL = 'Overview';
const DETECTION_RESPONSE_ITEM_LABEL = 'Detection & Response';
@ -99,6 +105,16 @@ describe('Dashboards landing', () => {
expect(result.getByTestId('dashboardsTable')).toBeInTheDocument();
});
it('should not render dashboards table if no read capability', () => {
mockUseCapabilities.mockReturnValueOnce({
...DEFAULT_DASHBOARD_CAPABILITIES,
show: false,
});
const result = renderDashboardLanding();
expect(result.queryByTestId('dashboardsTable')).not.toBeInTheDocument();
});
describe('Create Security Dashboard button', () => {
it('should render', () => {
const result = renderDashboardLanding();
@ -106,6 +122,16 @@ describe('Dashboards landing', () => {
expect(result.getByTestId('createDashboardButton')).toBeInTheDocument();
});
it('should not render if no write capability', () => {
mockUseCapabilities.mockReturnValueOnce({
...DEFAULT_DASHBOARD_CAPABILITIES,
createNew: false,
});
const result = renderDashboardLanding();
expect(result.queryByTestId('createDashboardButton')).not.toBeInTheDocument();
});
it('should be enabled when link loaded', () => {
const result = renderDashboardLanding();

View file

@ -13,19 +13,21 @@ import {
EuiTitle,
} from '@elastic/eui';
import React from 'react';
import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types';
import { DashboardConstants } from '@kbn/dashboard-plugin/public';
import { SecurityPageName } from '../../app/types';
import { DashboardsTable } from '../../common/components/dashboards/dashboards_table';
import { Title } from '../../common/components/header_page/title';
import { useAppRootNavLink } from '../../common/components/navigation/nav_links';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { useCreateSecurityDashboardLink } from '../../common/containers/dashboards/use_create_security_dashboard_link';
import { useNavigateTo } from '../../common/lib/kibana';
import { useCapabilities, useNavigateTo } from '../../common/lib/kibana';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { LandingImageCards } from '../components/landing_links_images';
import * as i18n from './translations';
/* eslint-disable @elastic/eui/href-or-on-click */
const Header = () => {
const Header: React.FC<{ canCreateDashboard: boolean }> = ({ canCreateDashboard }) => {
const { isLoading, url } = useCreateSecurityDashboardLink();
const { navigateTo } = useNavigateTo();
return (
@ -33,32 +35,36 @@ const Header = () => {
<EuiFlexItem>
<Title title={i18n.DASHBOARDS_PAGE_TITLE} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={isLoading}
color="primary"
fill
iconType="plusInCircle"
href={url}
onClick={(ev) => {
ev.preventDefault();
navigateTo({ url });
}}
data-test-subj="createDashboardButton"
>
{i18n.DASHBOARDS_PAGE_CREATE_BUTTON}
</EuiButton>
</EuiFlexItem>
{canCreateDashboard && (
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={isLoading}
color="primary"
fill
iconType="plusInCircle"
href={url}
onClick={(ev) => {
ev.preventDefault();
navigateTo({ url });
}}
data-test-subj="createDashboardButton"
>
{i18n.DASHBOARDS_PAGE_CREATE_BUTTON}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
export const DashboardsLandingPage = () => {
const dashboardLinks = useAppRootNavLink(SecurityPageName.dashboardsLanding)?.links ?? [];
const { show: canReadDashboard, createNew: canCreateDashboard } =
useCapabilities<DashboardCapabilities>(DashboardConstants.DASHBOARD_ID);
return (
<SecuritySolutionPageWrapper>
<Header />
<Header canCreateDashboard={canCreateDashboard} />
<EuiSpacer size="xl" />
<EuiTitle size="xxxs">
@ -68,12 +74,16 @@ export const DashboardsLandingPage = () => {
<LandingImageCards items={dashboardLinks} />
<EuiSpacer size="xxl" />
<EuiTitle size="xxxs">
<h2>{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}</h2>
</EuiTitle>
<EuiHorizontalRule margin="s" />
<EuiSpacer size="m" />
<DashboardsTable />
{canReadDashboard && (
<>
<EuiTitle size="xxxs">
<h2>{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}</h2>
</EuiTitle>
<EuiHorizontalRule margin="s" />
<EuiSpacer size="m" />
<DashboardsTable />
</>
)}
<SpyRoute pageName={SecurityPageName.dashboardsLanding} />
</SecuritySolutionPageWrapper>