Skip to content

Commit

Permalink
Merge pull request #9 from yctercero/disable-rbac-alert-security-solu…
Browse files Browse the repository at this point in the history
…tion-ytp

Disable rbac alert security solution ytp
  • Loading branch information
XavierM authored Aug 31, 2021
2 parents da68c5d + 1bb3327 commit ee04db9
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 94 deletions.
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

0 comments on commit ee04db9

Please sign in to comment.