mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Cloud Posture] Subscription gating (#140894)
This commit is contained in:
parent
140f720710
commit
5a81f0b559
20 changed files with 384 additions and 30 deletions
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 type { LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import { isSubscriptionAllowed } from './subscription';
|
||||
import { licenseMock } from '@kbn/licensing-plugin/common/licensing.mock';
|
||||
|
||||
const ON_PREM_ALLOWED_LICENSES: readonly LicenseType[] = ['enterprise', 'trial'];
|
||||
const ON_PREM_NOT_ALLOWED_LICENSES: readonly LicenseType[] = ['basic', 'gold', 'platinum'];
|
||||
const ALL_LICENSE_TYPES: readonly LicenseType[] = [
|
||||
'standard',
|
||||
...ON_PREM_NOT_ALLOWED_LICENSES,
|
||||
...ON_PREM_NOT_ALLOWED_LICENSES,
|
||||
];
|
||||
|
||||
describe('isSubscriptionAllowed', () => {
|
||||
it('should allow any cloud subscription', () => {
|
||||
const isCloudEnabled = true;
|
||||
ALL_LICENSE_TYPES.forEach((licenseType) => {
|
||||
const license = licenseMock.createLicense({ license: { type: licenseType } });
|
||||
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow enterprise and trial licenses for on-prem', () => {
|
||||
const isCloudEnabled = false;
|
||||
ON_PREM_ALLOWED_LICENSES.forEach((licenseType) => {
|
||||
const license = licenseMock.createLicense({ license: { type: licenseType } });
|
||||
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow enterprise and trial licenses for on-prem', () => {
|
||||
const isCloudEnabled = false;
|
||||
ON_PREM_NOT_ALLOWED_LICENSES.forEach((licenseType) => {
|
||||
const license = licenseMock.createLicense({ license: { type: licenseType } });
|
||||
expect(isSubscriptionAllowed(isCloudEnabled, license)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import { PLUGIN_NAME } from '..';
|
||||
|
||||
const MINIMUM_NON_CLOUD_LICENSE_TYPE: LicenseType = 'enterprise';
|
||||
|
||||
export const isSubscriptionAllowed = (isCloudEnabled?: boolean, license?: ILicense): boolean => {
|
||||
if (isCloudEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!license) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const licenseCheck = license.check(PLUGIN_NAME, MINIMUM_NON_CLOUD_LICENSE_TYPE);
|
||||
return licenseCheck.state === 'valid';
|
||||
};
|
|
@ -10,6 +10,17 @@
|
|||
"description": "The cloud security posture plugin",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["navigation", "data", "fleet", "unifiedSearch", "taskManager", "security", "charts", "discover"],
|
||||
"requiredPlugins": [
|
||||
"navigation",
|
||||
"data",
|
||||
"fleet",
|
||||
"unifiedSearch",
|
||||
"taskManager",
|
||||
"security",
|
||||
"charts",
|
||||
"discover",
|
||||
"cloud",
|
||||
"licensing"
|
||||
],
|
||||
"requiredBundles": ["kibanaReact"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { createContext } from 'react';
|
||||
|
||||
interface SetupContextValue {
|
||||
isCloudEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility to pass data from the plugin setup lifecycle stage to application components
|
||||
*/
|
||||
export const SetupContext = createContext<SetupContextValue>({});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { useContext } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SetupContext } from '../../application/setup_context';
|
||||
import { isSubscriptionAllowed } from '../../../common/utils/subscription';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
const SUBSCRIPTION_QUERY_KEY = 'csp_subscription_query_key';
|
||||
|
||||
export const useSubscriptionStatus = () => {
|
||||
const { licensing } = useKibana().services;
|
||||
const { isCloudEnabled } = useContext(SetupContext);
|
||||
return useQuery([SUBSCRIPTION_QUERY_KEY], async () => {
|
||||
const license = await licensing.refresh();
|
||||
return isSubscriptionAllowed(isCloudEnabled, license);
|
||||
});
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
|
||||
import Chance from 'chance';
|
||||
import {
|
||||
DEFAULT_NO_DATA_TEST_SUBJECT,
|
||||
|
@ -12,6 +13,7 @@ import {
|
|||
isCommonError,
|
||||
LOADING_STATE_TEST_SUBJECT,
|
||||
PACKAGE_NOT_INSTALLED_TEST_SUBJECT,
|
||||
SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT,
|
||||
} from './cloud_posture_page';
|
||||
import { createReactQueryResponse } from '../test/fixtures/react_query';
|
||||
import { TestProvider } from '../test/test_provider';
|
||||
|
@ -27,6 +29,7 @@ import { useCISIntegrationLink } from '../common/navigation/use_navigate_to_cis_
|
|||
const chance = new Chance();
|
||||
jest.mock('../common/api/use_setup_status_api');
|
||||
jest.mock('../common/navigation/use_navigate_to_cis_integration');
|
||||
jest.mock('../common/hooks/use_subscription_status');
|
||||
|
||||
describe('<CloudPosturePage />', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -37,6 +40,13 @@ describe('<CloudPosturePage />', () => {
|
|||
data: { status: 'indexed' },
|
||||
})
|
||||
);
|
||||
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const renderCloudPosturePage = (
|
||||
|
@ -69,10 +79,66 @@ describe('<CloudPosturePage />', () => {
|
|||
|
||||
expect(screen.getByText(children)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default loading state when the subscription query is loading', () => {
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(
|
||||
() =>
|
||||
createReactQueryResponse({
|
||||
status: 'loading',
|
||||
}) as unknown as UseQueryResult
|
||||
);
|
||||
|
||||
const children = chance.sentence();
|
||||
renderCloudPosturePage({ children });
|
||||
|
||||
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default error state when the subscription query has an error', () => {
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(
|
||||
() =>
|
||||
createReactQueryResponse({
|
||||
status: 'error',
|
||||
error: new Error('error'),
|
||||
}) as unknown as UseQueryResult
|
||||
);
|
||||
|
||||
const children = chance.sentence();
|
||||
renderCloudPosturePage({ children });
|
||||
|
||||
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders subscription not allowed prompt if subscription is not installed', () => {
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: false,
|
||||
})
|
||||
);
|
||||
|
||||
const children = chance.sentence();
|
||||
renderCloudPosturePage({ children });
|
||||
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders integrations installation prompt if integration is not installed', () => {
|
||||
(useCspSetupStatusApi as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
|
@ -88,6 +154,7 @@ describe('<CloudPosturePage />', () => {
|
|||
expect(screen.getByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -105,6 +172,7 @@ describe('<CloudPosturePage />', () => {
|
|||
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -122,6 +190,7 @@ describe('<CloudPosturePage />', () => {
|
|||
|
||||
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -135,6 +204,7 @@ describe('<CloudPosturePage />', () => {
|
|||
renderCloudPosturePage({ children, query });
|
||||
|
||||
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
|
@ -149,6 +219,7 @@ describe('<CloudPosturePage />', () => {
|
|||
renderCloudPosturePage({ children, query });
|
||||
|
||||
expect(screen.getByTestId(LOADING_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
|
@ -177,6 +248,7 @@ describe('<CloudPosturePage />', () => {
|
|||
expect(screen.getByText(text, { exact: false })).toBeInTheDocument()
|
||||
);
|
||||
expect(screen.getByTestId(ERROR_STATE_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
|
@ -209,6 +281,7 @@ describe('<CloudPosturePage />', () => {
|
|||
[error, statusCode].forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument());
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -230,6 +303,7 @@ describe('<CloudPosturePage />', () => {
|
|||
expect(screen.getByText(loading)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -245,6 +319,7 @@ describe('<CloudPosturePage />', () => {
|
|||
|
||||
expect(screen.getByTestId(DEFAULT_NO_DATA_TEST_SUBJECT)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
|
@ -273,6 +348,7 @@ describe('<CloudPosturePage />', () => {
|
|||
expect(screen.getByText(pageTitle)).toBeInTheDocument();
|
||||
expect(screen.getAllByText(solution, { exact: false })[0]).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(LOADING_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(children)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ERROR_STATE_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(PACKAGE_NOT_INSTALLED_TEST_SUBJECT)).not.toBeInTheDocument();
|
||||
|
|
|
@ -11,6 +11,8 @@ import { EuiEmptyPrompt } from '@elastic/eui';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { NoDataPage } from '@kbn/kibana-react-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import { SubscriptionNotAllowed } from './subscription_not_allowed';
|
||||
import { useSubscriptionStatus } from '../common/hooks/use_subscription_status';
|
||||
import { FullSizeCenteredPage } from './full_size_centered_page';
|
||||
import { useCspSetupStatusApi } from '../common/api/use_setup_status_api';
|
||||
import { CspLoadingState } from './csp_loading_state';
|
||||
|
@ -20,6 +22,7 @@ export const LOADING_STATE_TEST_SUBJECT = 'cloud_posture_page_loading';
|
|||
export const ERROR_STATE_TEST_SUBJECT = 'cloud_posture_page_error';
|
||||
export const PACKAGE_NOT_INSTALLED_TEST_SUBJECT = 'cloud_posture_page_package_not_installed';
|
||||
export const DEFAULT_NO_DATA_TEST_SUBJECT = 'cloud_posture_page_no_data';
|
||||
export const SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT = 'cloud_posture_page_subscription_not_allowed';
|
||||
|
||||
interface CommonError {
|
||||
body: {
|
||||
|
@ -120,28 +123,29 @@ const defaultErrorRenderer = (error: unknown) => (
|
|||
</FullSizeCenteredPage>
|
||||
);
|
||||
|
||||
const defaultNoDataRenderer = () => {
|
||||
return (
|
||||
<FullSizeCenteredPage>
|
||||
<NoDataPage
|
||||
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
|
||||
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
|
||||
defaultMessage: 'No data found',
|
||||
})}
|
||||
solution={i18n.translate(
|
||||
'xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel',
|
||||
{
|
||||
defaultMessage: 'Cloud Security Posture',
|
||||
}
|
||||
)}
|
||||
// TODO: Add real docs link once we have it
|
||||
docsLink={'https://www.elastic.co/guide/index.html'}
|
||||
logo={'logoSecurity'}
|
||||
actions={{}}
|
||||
/>
|
||||
</FullSizeCenteredPage>
|
||||
);
|
||||
};
|
||||
const defaultNoDataRenderer = () => (
|
||||
<FullSizeCenteredPage>
|
||||
<NoDataPage
|
||||
data-test-subj={DEFAULT_NO_DATA_TEST_SUBJECT}
|
||||
pageTitle={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.pageTitle', {
|
||||
defaultMessage: 'No data found',
|
||||
})}
|
||||
solution={i18n.translate('xpack.csp.cloudPosturePage.defaultNoDataConfig.solutionNameLabel', {
|
||||
defaultMessage: 'Cloud Security Posture',
|
||||
})}
|
||||
// TODO: Add real docs link once we have it
|
||||
docsLink={'https://www.elastic.co/guide/index.html'}
|
||||
logo={'logoSecurity'}
|
||||
actions={{}}
|
||||
/>
|
||||
</FullSizeCenteredPage>
|
||||
);
|
||||
|
||||
const subscriptionNotAllowedRenderer = () => (
|
||||
<FullSizeCenteredPage data-test-subj={SUBSCRIPTION_NOT_ALLOWED_TEST_SUBJECT}>
|
||||
<SubscriptionNotAllowed />
|
||||
</FullSizeCenteredPage>
|
||||
);
|
||||
|
||||
interface CloudPosturePageProps<TData, TError> {
|
||||
children: React.ReactNode;
|
||||
|
@ -158,10 +162,23 @@ export const CloudPosturePage = <TData, TError>({
|
|||
errorRender = defaultErrorRenderer,
|
||||
noDataRenderer = defaultNoDataRenderer,
|
||||
}: CloudPosturePageProps<TData, TError>) => {
|
||||
const subscriptionStatus = useSubscriptionStatus();
|
||||
const getSetupStatus = useCspSetupStatusApi();
|
||||
const cisIntegrationLink = useCISIntegrationLink();
|
||||
|
||||
const render = () => {
|
||||
if (subscriptionStatus.isError) {
|
||||
return defaultErrorRenderer(subscriptionStatus.error);
|
||||
}
|
||||
|
||||
if (subscriptionStatus.isLoading) {
|
||||
return defaultLoadingRenderer();
|
||||
}
|
||||
|
||||
if (!subscriptionStatus.data) {
|
||||
return subscriptionNotAllowedRenderer();
|
||||
}
|
||||
|
||||
if (getSetupStatus.isError) {
|
||||
return defaultErrorRenderer(getSetupStatus.error);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiPageSection, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../common/hooks/use_kibana';
|
||||
|
||||
export const SubscriptionNotAllowed = () => {
|
||||
const { application } = useKibana().services;
|
||||
return (
|
||||
<EuiPageSection color="danger" alignment="center">
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.csp.subscriptionNotAllowed.promptTitle"
|
||||
defaultMessage="Upgrade for subscription features"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.csp.subscriptionNotAllowed.promptDescription"
|
||||
defaultMessage="To use these cloud security features, you must {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href={application.getUrlForApp('management', {
|
||||
path: 'stack/license_management/home',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.csp.subscriptionNotAllowed.promptLinkText"
|
||||
defaultMessage="start a trial or upgrade your subscription"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</EuiPageSection>
|
||||
);
|
||||
};
|
|
@ -15,10 +15,12 @@ import { Benchmarks } from './benchmarks';
|
|||
import * as TEST_SUBJ from './test_subjects';
|
||||
import { useCspBenchmarkIntegrations } from './use_csp_benchmark_integrations';
|
||||
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
|
||||
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
|
||||
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
|
||||
|
||||
jest.mock('./use_csp_benchmark_integrations');
|
||||
jest.mock('../../common/api/use_setup_status_api');
|
||||
jest.mock('../../common/hooks/use_subscription_status');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
|
||||
const chance = new Chance();
|
||||
|
||||
|
@ -31,6 +33,14 @@ describe('<Benchmarks />', () => {
|
|||
data: { status: 'indexed' },
|
||||
})
|
||||
);
|
||||
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
|
||||
(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { render } from '@testing-library/react';
|
|||
import { TestProvider } from '../../test/test_provider';
|
||||
import { ComplianceDashboard } from '.';
|
||||
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
|
||||
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
|
||||
import { useComplianceDashboardDataApi } from '../../common/api/use_compliance_dashboard_data_api';
|
||||
import { DASHBOARD_CONTAINER } from './test_subjects';
|
||||
import { createReactQueryResponse } from '../../test/fixtures/react_query';
|
||||
|
@ -22,6 +23,7 @@ import { expectIdsInDoc } from '../../test/utils';
|
|||
|
||||
jest.mock('../../common/api/use_setup_status_api');
|
||||
jest.mock('../../common/api/use_compliance_dashboard_data_api');
|
||||
jest.mock('../../common/hooks/use_subscription_status');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
|
||||
const chance = new Chance();
|
||||
|
@ -179,6 +181,13 @@ describe('<ComplianceDashboard />', () => {
|
|||
data: { status: 'indexed' },
|
||||
})
|
||||
);
|
||||
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const renderComplianceDashboardPage = () => {
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { DataView } from '@kbn/data-plugin/common';
|
|||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
|
||||
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
|
||||
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
|
||||
import { createReactQueryResponse } from '../../test/fixtures/react_query';
|
||||
import { useCISIntegrationPoliciesLink } from '../../common/navigation/use_navigate_to_cis_integration_policies';
|
||||
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
|
||||
|
@ -28,9 +29,11 @@ import { render } from '@testing-library/react';
|
|||
import { useFindingsEsPit } from './es_pit/use_findings_es_pit';
|
||||
import { expectIdsInDoc } from '../../test/utils';
|
||||
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
|
||||
jest.mock('../../common/api/use_latest_findings_data_view');
|
||||
jest.mock('../../common/api/use_setup_status_api');
|
||||
jest.mock('../../common/hooks/use_subscription_status');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
|
||||
jest.mock('./es_pit/use_findings_es_pit');
|
||||
|
@ -46,6 +49,13 @@ beforeEach(() => {
|
|||
setPitId: () => {},
|
||||
pitIdRef: chance.guid(),
|
||||
}));
|
||||
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const renderFindingsPage = () => {
|
||||
|
@ -57,6 +67,7 @@ const renderFindingsPage = () => {
|
|||
charts: chartPluginMock.createStartContract(),
|
||||
discover: discoverPluginMock.createStartContract(),
|
||||
fleet: fleetMock.createStartMock(),
|
||||
licensing: licensingMock.createStart(),
|
||||
}}
|
||||
>
|
||||
<Findings />
|
||||
|
|
|
@ -24,6 +24,7 @@ import { FindingsEsPitContext } from '../es_pit/findings_es_pit_context';
|
|||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
|
||||
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
|
||||
jest.mock('../../../common/api/use_latest_findings_data_view');
|
||||
jest.mock('../../../common/api/use_cis_kubernetes_integration');
|
||||
|
@ -70,6 +71,7 @@ describe('<LatestFindingsContainer />', () => {
|
|||
charts: chartPluginMock.createStartContract(),
|
||||
discover: discoverPluginMock.createStartContract(),
|
||||
fleet: fleetMock.createStartMock(),
|
||||
licensing: licensingMock.createStart(),
|
||||
}}
|
||||
>
|
||||
<FindingsEsPitContext.Provider value={{ setPitId, pitIdRef, pitQuery }}>
|
||||
|
|
|
@ -18,12 +18,14 @@ import * as TEST_SUBJECTS from './test_subjects';
|
|||
import { createReactQueryResponse } from '../../test/fixtures/react_query';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api';
|
||||
import { useSubscriptionStatus } from '../../common/hooks/use_subscription_status';
|
||||
import { useCISIntegrationLink } from '../../common/navigation/use_navigate_to_cis_integration';
|
||||
|
||||
jest.mock('./use_csp_integration', () => ({
|
||||
useCspIntegrationInfo: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../common/api/use_setup_status_api');
|
||||
jest.mock('../../common/hooks/use_subscription_status');
|
||||
jest.mock('../../common/navigation/use_navigate_to_cis_integration');
|
||||
const chance = new Chance();
|
||||
|
||||
|
@ -68,6 +70,14 @@ describe('<Rules />', () => {
|
|||
data: { status: 'indexed' },
|
||||
})
|
||||
);
|
||||
|
||||
(useSubscriptionStatus as jest.Mock).mockImplementation(() =>
|
||||
createReactQueryResponse({
|
||||
status: 'success',
|
||||
data: true,
|
||||
})
|
||||
);
|
||||
|
||||
(useCISIntegrationLink as jest.Mock).mockImplementation(() => chance.url());
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
|||
CspClientPluginStartDeps,
|
||||
} from './types';
|
||||
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants';
|
||||
import { SetupContext } from './application/setup_context';
|
||||
|
||||
const LazyCspEditPolicy = lazy(() => import('./components/fleet_extensions/policy_extension_edit'));
|
||||
const LazyCspCreatePolicy = lazy(
|
||||
|
@ -42,10 +43,13 @@ export class CspPlugin
|
|||
CspClientPluginStartDeps
|
||||
>
|
||||
{
|
||||
private isCloudEnabled?: boolean;
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
|
||||
plugins: CspClientPluginSetupDeps
|
||||
): CspClientPluginSetup {
|
||||
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
}
|
||||
|
@ -74,7 +78,9 @@ export class CspPlugin
|
|||
(
|
||||
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||
<RedirectAppLinks coreStart={core}>
|
||||
<CspRouter {...props} />
|
||||
<SetupContext.Provider value={{ isCloudEnabled: this.isCloudEnabled }}>
|
||||
<CspRouter {...props} />
|
||||
</SetupContext.Provider>
|
||||
</RedirectAppLinks>
|
||||
</KibanaContextProvider>
|
||||
),
|
||||
|
|
|
@ -17,6 +17,7 @@ import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
|||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
|
||||
import { fleetMock } from '@kbn/fleet-plugin/public/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
|
||||
import type { CspClientPluginStartDeps } from '../types';
|
||||
|
||||
interface CspAppDeps {
|
||||
|
@ -33,6 +34,7 @@ export const TestProvider: React.FC<Partial<CspAppDeps>> = ({
|
|||
charts: chartPluginMock.createStartContract(),
|
||||
discover: discoverPluginMock.createStartContract(),
|
||||
fleet: fleetMock.createStartMock(),
|
||||
licensing: licensingMock.createStart(),
|
||||
},
|
||||
params = coreMock.createAppMountParameters(),
|
||||
children,
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
@ -32,6 +34,7 @@ export interface CspClientPluginSetupDeps {
|
|||
// required
|
||||
data: DataPublicPluginSetup;
|
||||
fleet: FleetSetup;
|
||||
cloud: CloudSetup;
|
||||
// optional
|
||||
}
|
||||
|
||||
|
@ -42,6 +45,7 @@ export interface CspClientPluginStartDeps {
|
|||
charts: ChartsPluginStart;
|
||||
discover: DiscoverStart;
|
||||
fleet: FleetStart;
|
||||
licensing: LicensingPluginStart;
|
||||
// optional
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { DeepReadonly } from 'utility-types';
|
||||
import { createCspRuleSearchFilterByPackagePolicy } from '../../common/utils/helpers';
|
||||
import {
|
||||
CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
|
||||
CLOUDBEAT_VANILLA,
|
||||
CSP_RULE_SAVED_OBJECT_TYPE,
|
||||
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
|
@ -127,6 +128,9 @@ export const isCspPackageInstalled = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const isCspPackage = (packageName?: string) =>
|
||||
packageName === CLOUD_SECURITY_POSTURE_PACKAGE_NAME;
|
||||
|
||||
const generateRulesFromTemplates = (
|
||||
packagePolicyId: string,
|
||||
policyId: string,
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
|
@ -76,6 +77,7 @@ describe('Cloud Security Posture Plugin', () => {
|
|||
data: dataPluginMock.createStartContract(),
|
||||
taskManager: taskManagerMock.createStart(),
|
||||
security: securityMock.createStart(),
|
||||
licensing: licensingMock.createStart(),
|
||||
};
|
||||
|
||||
const contextMock = coreMock.createCustomRequestHandlerContext(mockRouteContext);
|
||||
|
|
|
@ -14,12 +14,17 @@ import type {
|
|||
Plugin,
|
||||
Logger,
|
||||
} from '@kbn/core/server';
|
||||
import { DeepReadonly } from 'utility-types';
|
||||
import { DeletePackagePoliciesResponse, PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import {
|
||||
import type { DeepReadonly } from 'utility-types';
|
||||
import type {
|
||||
DeletePackagePoliciesResponse,
|
||||
PackagePolicy,
|
||||
NewPackagePolicy,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import type {
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import { isSubscriptionAllowed } from '../common/utils/subscription';
|
||||
import type {
|
||||
CspServerPluginSetup,
|
||||
CspServerPluginStart,
|
||||
|
@ -32,6 +37,7 @@ import { setupSavedObjects } from './saved_objects';
|
|||
import { initializeCspIndices } from './create_indices/create_indices';
|
||||
import { initializeCspTransforms } from './create_transforms/create_transforms';
|
||||
import {
|
||||
isCspPackage,
|
||||
isCspPackageInstalled,
|
||||
onPackagePolicyPostCreateCallback,
|
||||
removeCspRulesInstancesCallback,
|
||||
|
@ -41,7 +47,6 @@ import {
|
|||
updatePackagePolicyRuntimeCfgVar,
|
||||
getCspRulesSO,
|
||||
} from './routes/configuration/update_rules_configuration';
|
||||
|
||||
import {
|
||||
removeFindingsStatsTask,
|
||||
scheduleFindingsStatsTask,
|
||||
|
@ -58,6 +63,7 @@ export class CspPlugin
|
|||
>
|
||||
{
|
||||
private readonly logger: Logger;
|
||||
private isCloudEnabled?: boolean;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
|
@ -77,6 +83,8 @@ export class CspPlugin
|
|||
const coreStartServices = core.getStartServices();
|
||||
this.setupCspTasks(plugins.taskManager, coreStartServices, this.logger);
|
||||
|
||||
this.isCloudEnabled = plugins.cloud.isCloudEnabled;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -92,6 +100,26 @@ export class CspPlugin
|
|||
this.initialize(core, plugins.taskManager);
|
||||
}
|
||||
|
||||
plugins.fleet.registerExternalCallback(
|
||||
'packagePolicyCreate',
|
||||
async (
|
||||
packagePolicy: NewPackagePolicy,
|
||||
_context: RequestHandlerContext,
|
||||
_request: KibanaRequest
|
||||
): Promise<NewPackagePolicy> => {
|
||||
const license = await plugins.licensing.refresh();
|
||||
if (isCspPackage(packagePolicy.package?.name)) {
|
||||
if (!isSubscriptionAllowed(this.isCloudEnabled, license)) {
|
||||
throw new Error(
|
||||
'To use this feature you must upgrade your subscription or start a trial'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return packagePolicy;
|
||||
}
|
||||
);
|
||||
|
||||
plugins.fleet.registerExternalCallback(
|
||||
'packagePolicyPostCreate',
|
||||
async (
|
||||
|
@ -99,7 +127,7 @@ export class CspPlugin
|
|||
context: RequestHandlerContext,
|
||||
request: KibanaRequest
|
||||
): Promise<PackagePolicy> => {
|
||||
if (packagePolicy.package?.name === CLOUD_SECURITY_POSTURE_PACKAGE_NAME) {
|
||||
if (isCspPackage(packagePolicy.package?.name)) {
|
||||
await this.initialize(core, plugins.taskManager);
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
|
@ -128,7 +156,7 @@ export class CspPlugin
|
|||
'postPackagePolicyDelete',
|
||||
async (deletedPackagePolicies: DeepReadonly<DeletePackagePoliciesResponse>) => {
|
||||
for (const deletedPackagePolicy of deletedPackagePolicies) {
|
||||
if (deletedPackagePolicy.package?.name === CLOUD_SECURITY_POSTURE_PACKAGE_NAME) {
|
||||
if (isCspPackage(deletedPackagePolicy.package?.name)) {
|
||||
const soClient = core.savedObjects.createInternalRepository();
|
||||
await removeCspRulesInstancesCallback(deletedPackagePolicy, soClient, this.logger);
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import type {
|
||||
PluginSetup as DataPluginSetup,
|
||||
PluginStart as DataPluginStart,
|
||||
} from '@kbn/data-plugin/server';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||
import {
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
|
@ -42,6 +44,7 @@ export interface CspServerPluginSetupDeps {
|
|||
data: DataPluginSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
security: SecurityPluginSetup;
|
||||
cloud: CloudSetup;
|
||||
// optional
|
||||
}
|
||||
|
||||
|
@ -51,6 +54,7 @@ export interface CspServerPluginStartDeps {
|
|||
fleet: FleetStartContract;
|
||||
taskManager: TaskManagerStartContract;
|
||||
security: SecurityPluginStart;
|
||||
licensing: LicensingPluginStart;
|
||||
}
|
||||
|
||||
export type CspServerPluginStartServices = Promise<
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue