Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable rbac alert security solution ytp #9

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/public/doc_links/doc_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export class DocLinksService {
siem: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`,
gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`,
privileges: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/sec-requirements.html`,
ml: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/machine-learning.html`,
ruleChangeLog: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/prebuilt-rules-changelog.html`,
detectionsReq: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-permissions-section.html`,
Expand Down Expand Up @@ -568,6 +569,7 @@ export interface DocLinksStart {
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,68 +101,4 @@ describe('public search functions', () => {
});
expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy();
});

describe('Detections Alerts deep links', () => {
it('should return alerts link for basic license with only read_alerts capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({
siem: { read_alerts: true, crud_alerts: false },
} as unknown) as Capabilities);

const detectionsDeepLinks =
basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? [];

expect(
detectionsDeepLinks.length &&
detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts)
).toBeTruthy();
});

it('should return alerts link with for basic license with crud_alerts capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({
siem: { read_alerts: true, crud_alerts: true },
} as unknown) as Capabilities);

const detectionsDeepLinks =
basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? [];

expect(
detectionsDeepLinks.length &&
detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts)
).toBeTruthy();
});

it('should NOT return alerts link for basic license with NO read_alerts capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({
siem: { read_alerts: false, crud_alerts: false },
} as unknown) as Capabilities);

const detectionsDeepLinks =
basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? [];

expect(
detectionsDeepLinks.length &&
detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts)
).toBeFalsy();
});

it('should return alerts link for basic license with undefined capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(
mockGlobalState.app.enableExperimental,
basicLicense,
undefined
);

const detectionsDeepLinks =
basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? [];

expect(
detectionsDeepLinks.length &&
detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts)
).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import { EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import {
DetectionsRequirementsLink,
SecuritySolutionRequirementsLink,
} from '../../../../common/components/links_to_docs';
import {
DEFAULT_ITEMS_INDEX,
DEFAULT_LISTS_INDEX,
Expand All @@ -21,6 +17,10 @@ import {
} from '../../../../../common/constants';
import { CommaSeparatedValues } from './comma_separated_values';
import { MissingPrivileges } from './use_missing_privileges';
import {
DetectionsRequirementsLink,
SecuritySolutionRequirementsLink,
} from '../../../../common/components/links_to_docs';

export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle',
Expand All @@ -46,13 +46,13 @@ const CANNOT_EDIT_LISTS = i18n.translate(
const CANNOT_EDIT_ALERTS = i18n.translate(
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditAlerts',
{
defaultMessage: 'Without these privileges, you cannot open or close alerts.',
defaultMessage: 'Without these privileges, you cannot view or change status of alerts.',
}
);

export const missingPrivilegesCallOutBody = ({
indexPrivileges,
featurePrivileges,
featurePrivileges = [],
}: MissingPrivileges) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.messageDetail"
Expand All @@ -77,12 +77,16 @@ export const missingPrivilegesCallOutBody = ({
{indexPrivileges.map(([index, missingPrivileges]) => (
<li key={index}>{missingIndexPrivileges(index, missingPrivileges)}</li>
))}
{featurePrivileges.map(([feature, missingPrivileges]) => (
{
// TODO: Uncomment once RBAC for alerts is reenabled
/* {featurePrivileges.map(([feature, missingPrivileges]) => (
<li key={feature}>{missingFeaturePrivileges(feature, missingPrivileges)}</li>
))}
))} */
}
</ul>
</>
) : null,
// TODO: Uncomment once RBAC for alerts is reenabled
// featurePrivileges:
// featurePrivileges.length > 0 ? (
// <>
Expand Down Expand Up @@ -155,14 +159,15 @@ const missingIndexPrivileges = (index: string, privileges: string[]) => (
/>
);

const missingFeaturePrivileges = (feature: string, privileges: string[]) => (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges"
defaultMessage="Missing {privileges} privileges for the {index} feature. {explanation}"
values={{
privileges: <CommaSeparatedValues values={privileges} />,
index: <EuiCode>{feature}</EuiCode>,
explanation: getPrivilegesExplanation(privileges, feature),
}}
/>
);
// TODO: Uncomment once RBAC for alerts is reenabled
// const missingFeaturePrivileges = (feature: string, privileges: string[]) => (
// <FormattedMessage
// id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges"
// defaultMessage="Missing {privileges} privileges for the {index} feature. {explanation}"
// values={{
// privileges: <CommaSeparatedValues values={privileges} />,
// index: <EuiCode>{feature}</EuiCode>,
// explanation: getPrivilegesExplanation(privileges, feature),
// }}
// />
// );
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,18 @@ export interface MissingPrivileges {
}

export const useMissingPrivileges = (): MissingPrivileges => {
const { listPrivileges } = useUserPrivileges();
const { detectionEnginePrivileges, listPrivileges } = useUserPrivileges();
const [{ canUserCRUD }] = useUserData();

return useMemo<MissingPrivileges>(() => {
const featurePrivileges: MissingFeaturePrivileges[] = [];
const indexPrivileges: MissingIndexPrivileges[] = [];

if (canUserCRUD == null || listPrivileges.result == null) {
if (
canUserCRUD == null ||
listPrivileges.result == null ||
detectionEnginePrivileges.result == null
) {
/**
* Do not check privileges till we get all the data. That helps to reduce
* subsequent layout shift while loading and skip unneeded re-renders.
Expand All @@ -72,9 +76,16 @@ export const useMissingPrivileges = (): MissingPrivileges => {
indexPrivileges.push(missingListsPrivileges);
}

const missingDetectionPrivileges = getMissingIndexPrivileges(
detectionEnginePrivileges.result.index
);
if (missingDetectionPrivileges) {
indexPrivileges.push(missingDetectionPrivileges);
}

return {
featurePrivileges,
indexPrivileges,
};
}, [canUserCRUD, listPrivileges]);
}, [canUserCRUD, listPrivileges, detectionEnginePrivileges]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jest.mock('../../../common/lib/kibana', () => ({
useKibana: jest.fn(),
useGetUserCasesPermissions: jest.fn().mockReturnValue({ crud: true }),
}));
jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({
useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }),
}));
jest.mock('../../../cases/components/use_insert_timeline');

jest.mock('../../../common/hooks/use_experimental_features', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const userPrivilegesInitial: ReturnType<typeof useUserPrivileges> = {
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
};

describe('usePrivilegeUser', () => {
describe('useAlertsPrivileges', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;

beforeEach(() => {
Expand All @@ -113,13 +113,15 @@ describe('usePrivilegeUser', () => {
hasIndexMaintenance: null,
hasIndexWrite: null,
hasIndexUpdateDelete: null,
hasKibanaCRUD: false,
hasKibanaREAD: false,
isAuthenticated: null,
loading: false,
});
});
});

test('if there is an error when fetching user privilege, we should get back false for every properties', async () => {
test('if there is an error when fetching user privilege, we should get back false for all index related properties', async () => {
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
draft.detectionEnginePrivileges.error = new Error('Something went wrong');
});
Expand All @@ -137,6 +139,8 @@ describe('usePrivilegeUser', () => {
hasIndexRead: false,
hasIndexWrite: false,
hasIndexUpdateDelete: false,
hasKibanaCRUD: true,
hasKibanaREAD: true,
isAuthenticated: false,
loading: false,
});
Expand All @@ -162,9 +166,11 @@ describe('usePrivilegeUser', () => {
hasEncryptionKey: true,
hasIndexManage: false,
hasIndexMaintenance: true,
hasIndexRead: false,
hasIndexWrite: false,
hasIndexRead: true,
hasIndexWrite: true,
hasIndexUpdateDelete: true,
hasKibanaCRUD: true,
hasKibanaREAD: true,
isAuthenticated: true,
loading: false,
});
Expand All @@ -187,9 +193,67 @@ describe('usePrivilegeUser', () => {
hasEncryptionKey: true,
hasIndexManage: true,
hasIndexMaintenance: true,
hasIndexRead: false,
hasIndexWrite: false,
hasIndexRead: true,
hasIndexWrite: true,
hasIndexUpdateDelete: true,
hasKibanaCRUD: true,
hasKibanaREAD: true,
isAuthenticated: true,
loading: false,
});
});
});

test('returns "hasKibanaCRUD" as false if user does not have SIEM Kibana "all" privileges', async () => {
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
draft.detectionEnginePrivileges.result = privilege;
draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: true };
});
useUserPrivilegesMock.mockReturnValue(userPrivileges);

await act(async () => {
const { result, waitForNextUpdate } = renderHook<void, UseAlertsPrivelegesReturn>(() =>
useAlertsPrivileges()
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
hasEncryptionKey: true,
hasIndexManage: true,
hasIndexMaintenance: true,
hasIndexRead: true,
hasIndexWrite: true,
hasIndexUpdateDelete: true,
hasKibanaCRUD: false,
hasKibanaREAD: true,
isAuthenticated: true,
loading: false,
});
});
});

test('returns "hasKibanaREAD" as false if user does not have at least SIEM Kibana "read" privileges', async () => {
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
draft.detectionEnginePrivileges.result = privilege;
draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: false };
});
useUserPrivilegesMock.mockReturnValue(userPrivileges);

await act(async () => {
const { result, waitForNextUpdate } = renderHook<void, UseAlertsPrivelegesReturn>(() =>
useAlertsPrivileges()
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
hasEncryptionKey: true,
hasIndexManage: true,
hasIndexMaintenance: true,
hasIndexRead: true,
hasIndexWrite: true,
hasIndexUpdateDelete: true,
hasKibanaCRUD: false,
hasKibanaREAD: false,
isAuthenticated: true,
loading: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useSourcererScope } from '../../../common/containers/sourcerer';
import { createStore, State } from '../../../common/store';
import { mockHistory, Router } from '../../../common/mock/router';
import { mockTimelines } from '../../../common/mock/mock_timelines_plugin';
import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges';

// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
Expand All @@ -34,6 +35,7 @@ jest.mock('../../../common/components/query_bar', () => ({
}));
jest.mock('../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../components/user_info');
jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges');
jest.mock('../../../common/containers/sourcerer');
jest.mock('../../../common/components/link_to');
jest.mock('../../../common/containers/use_global_time', () => ({
Expand Down Expand Up @@ -80,7 +82,7 @@ jest.mock('../../../common/lib/kibana', () => {
docLinks: {
links: {
siem: {
gettingStarted: 'link',
privileges: 'link',
},
},
},
Expand Down Expand Up @@ -109,6 +111,9 @@ describe('DetectionEnginePageComponent', () => {
hasIndexRead: true,
},
]);
(useAlertsPrivileges as jest.Mock).mockReturnValue({
hasKibanaREAD: true,
});
(useSourcererScope as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
Expand Down
Loading