mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Implement missing privileges callout component (#98125)
This commit is contained in:
parent
1322eee98e
commit
24734a39d1
40 changed files with 923 additions and 699 deletions
|
@ -15,7 +15,7 @@ export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate(
|
|||
'xpack.cases.readOnlySavedObjectDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'You only have permissions to view cases. If you need to open and update cases, contact your Kibana administrator.',
|
||||
'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults
|
|||
export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults';
|
||||
export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults';
|
||||
export const DEFAULT_SIGNALS_INDEX = '.siem-signals';
|
||||
export const DEFAULT_LISTS_INDEX = '.lists';
|
||||
export const DEFAULT_ITEMS_INDEX = '.items';
|
||||
// The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts`
|
||||
// If either changes, engineer should ensure both values are updated
|
||||
export const DEFAULT_MAX_SIGNALS = 100;
|
||||
|
@ -50,6 +52,7 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_ON = true;
|
|||
export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms
|
||||
export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms
|
||||
export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100;
|
||||
export const SAVED_OBJECTS_MANAGEMENT_FEATURE_ID = 'Saved Objects Management';
|
||||
|
||||
// Document path where threat indicator fields are expected. Fields are used
|
||||
// to enrich signals, and are copied to threat.indicator.
|
||||
|
|
|
@ -41,8 +41,7 @@ const waitForPageTitleToBeShown = () => {
|
|||
};
|
||||
|
||||
describe('Detections > Callouts', () => {
|
||||
const ALERTS_CALLOUT = 'read-only-access-to-alerts';
|
||||
const RULES_CALLOUT = 'read-only-access-to-rules';
|
||||
const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges';
|
||||
|
||||
before(() => {
|
||||
// First, we have to open the app on behalf of a privileged user in order to initialize it.
|
||||
|
@ -62,15 +61,15 @@ describe('Detections > Callouts', () => {
|
|||
});
|
||||
|
||||
it('We show one primary callout', () => {
|
||||
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
});
|
||||
|
||||
context('When a user clicks Dismiss on the callout', () => {
|
||||
it('We hide it and persist the dismissal', () => {
|
||||
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
|
||||
dismissCallOut(ALERTS_CALLOUT);
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
dismissCallOut(MISSING_PRIVILEGES_CALLOUT);
|
||||
reloadPage();
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -81,15 +80,15 @@ describe('Detections > Callouts', () => {
|
|||
});
|
||||
|
||||
it('We show one primary callout', () => {
|
||||
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
});
|
||||
|
||||
context('When a user clicks Dismiss on the callout', () => {
|
||||
it('We hide it and persist the dismissal', () => {
|
||||
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
|
||||
dismissCallOut(RULES_CALLOUT);
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
dismissCallOut(MISSING_PRIVILEGES_CALLOUT);
|
||||
reloadPage();
|
||||
getCallOut(RULES_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -106,27 +105,18 @@ describe('Detections > Callouts', () => {
|
|||
deleteCustomRule();
|
||||
});
|
||||
|
||||
it('We show two primary callouts', () => {
|
||||
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
|
||||
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
|
||||
it('We show one primary callout', () => {
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
});
|
||||
|
||||
context('When a user clicks Dismiss on the callouts', () => {
|
||||
it('We hide them and persist the dismissal', () => {
|
||||
waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary');
|
||||
waitForCallOutToBeShown(RULES_CALLOUT, 'primary');
|
||||
waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary');
|
||||
|
||||
dismissCallOut(ALERTS_CALLOUT);
|
||||
dismissCallOut(MISSING_PRIVILEGES_CALLOUT);
|
||||
reloadPage();
|
||||
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(RULES_CALLOUT).should('be.visible');
|
||||
|
||||
dismissCallOut(RULES_CALLOUT);
|
||||
reloadPage();
|
||||
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(RULES_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -139,8 +129,7 @@ describe('Detections > Callouts', () => {
|
|||
});
|
||||
|
||||
it('We show no callout', () => {
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(RULES_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -150,8 +139,7 @@ describe('Detections > Callouts', () => {
|
|||
});
|
||||
|
||||
it('We show no callout', () => {
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(RULES_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -168,8 +156,7 @@ describe('Detections > Callouts', () => {
|
|||
});
|
||||
|
||||
it('We show no callouts', () => {
|
||||
getCallOut(ALERTS_CALLOUT).should('not.exist');
|
||||
getCallOut(RULES_CALLOUT).should('not.exist');
|
||||
getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
export const CALLOUT = '[data-test-subj^="callout-"]';
|
||||
|
||||
export const callOutWithId = (id: string) => `[data-test-subj="callout-${id}"]`;
|
||||
export const callOutWithId = (id: string) => `[data-test-subj^="callout-${id}"]`;
|
||||
|
||||
export const CALLOUT_DISMISS_BTN = '[data-test-subj^="callout-dismiss-"]';
|
||||
|
|
|
@ -25,6 +25,7 @@ import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
|
|||
import { StartServices } from '../types';
|
||||
import { PageRouter } from './routes';
|
||||
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
|
||||
import { UserPrivilegesProvider } from '../detections/components/user_privileges';
|
||||
|
||||
interface StartAppComponent {
|
||||
children: React.ReactNode;
|
||||
|
@ -45,11 +46,13 @@ const StartAppComponent: FC<StartAppComponent> = ({ children, history, onAppLeav
|
|||
<ReduxStoreProvider store={store}>
|
||||
<EuiThemeProvider darkMode={darkMode}>
|
||||
<MlCapabilitiesProvider>
|
||||
<ManageUserInfo>
|
||||
<PageRouter history={history} onAppLeave={onAppLeave}>
|
||||
{children}
|
||||
</PageRouter>
|
||||
</ManageUserInfo>
|
||||
<UserPrivilegesProvider>
|
||||
<ManageUserInfo>
|
||||
<PageRouter history={history} onAppLeave={onAppLeave}>
|
||||
{children}
|
||||
</PageRouter>
|
||||
</ManageUserInfo>
|
||||
</UserPrivilegesProvider>
|
||||
</MlCapabilitiesProvider>
|
||||
</EuiThemeProvider>
|
||||
<ErrorToastDispatcher />
|
||||
|
|
|
@ -18,7 +18,7 @@ export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate(
|
|||
'xpack.securitySolution.cases.readOnlySavedObjectDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'You only have permissions to view cases. If you need to open and update cases, contact your Kibana administrator.',
|
||||
'You only have privileges to view cases. If you need to open and update cases, contact your Kibana administrator.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export const CasesPage = React.memo(() => {
|
|||
{userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
|
||||
<CaseCallOut
|
||||
title={savedObjectReadOnlyErrorMessage.title}
|
||||
messages={[{ ...savedObjectReadOnlyErrorMessage }]}
|
||||
messages={[{ ...savedObjectReadOnlyErrorMessage, title: '' }]}
|
||||
/>
|
||||
)}
|
||||
<AllCases userCanCrud={userPermissions?.crud ?? false} />
|
||||
|
|
|
@ -38,7 +38,7 @@ export const CaseDetailsPage = React.memo(() => {
|
|||
{userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
|
||||
<CaseCallOut
|
||||
title={savedObjectReadOnlyErrorMessage.title}
|
||||
messages={[{ ...savedObjectReadOnlyErrorMessage }]}
|
||||
messages={[{ ...savedObjectReadOnlyErrorMessage, title: '' }]}
|
||||
/>
|
||||
)}
|
||||
<CaseView
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { difference, fromPairs, identity } from 'lodash/fp';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { difference, fromPairs, identity, intersection, isEqual } from 'lodash/fp';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import useMap from 'react-use/lib/useMap';
|
||||
import { useMessagesStorage } from '../../containers/local_storage/use_messages_storage';
|
||||
import { CallOutMessage } from './callout_types';
|
||||
|
@ -24,8 +23,7 @@ export const useCallOutStorage = (
|
|||
): CallOutStorage => {
|
||||
const { getMessages, addMessage } = useMessagesStorage();
|
||||
|
||||
const visibilityStateInitial = useMemo(() => createInitialVisibilityState(messages), [messages]);
|
||||
const [visibilityState, setVisibilityState] = useMap(visibilityStateInitial);
|
||||
const [visibilityState, setVisibilityState] = useMap<Record<string, boolean>>({});
|
||||
|
||||
const dismissedMessagesKey = getDismissedMessagesStorageKey(namespace);
|
||||
|
||||
|
@ -58,16 +56,27 @@ export const useCallOutStorage = (
|
|||
[setVisibilityState, addMessage, dismissedMessagesKey]
|
||||
);
|
||||
|
||||
useEffectOnce(() => {
|
||||
const idsAll = Object.keys(visibilityState);
|
||||
const idsDismissed = getMessages(dismissedMessagesKey);
|
||||
const idsToMakeVisible = difference(idsAll)(idsDismissed);
|
||||
const populateVisibilityState = useCallback(
|
||||
(ids: string[]) => {
|
||||
const idsDismissed = getMessages(dismissedMessagesKey);
|
||||
const idsToShow = difference(ids, idsDismissed);
|
||||
const idsToHide = intersection(ids, idsDismissed);
|
||||
|
||||
setVisibilityState.setAll({
|
||||
...createVisibilityState(idsToMakeVisible, true),
|
||||
...createVisibilityState(idsDismissed, false),
|
||||
});
|
||||
});
|
||||
setVisibilityState.setAll({
|
||||
...createVisibilityState(idsToShow, true),
|
||||
...createVisibilityState(idsToHide, false),
|
||||
});
|
||||
},
|
||||
[getMessages, dismissedMessagesKey, setVisibilityState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const idsFromProps = messages.map((m) => m.id);
|
||||
const idsFromState = Object.keys(visibilityState);
|
||||
if (!isEqual(idsFromProps, idsFromState)) {
|
||||
populateVisibilityState(idsFromProps);
|
||||
}
|
||||
}, [messages, visibilityState, populateVisibilityState]);
|
||||
|
||||
return {
|
||||
getVisibleMessageIds,
|
||||
|
@ -79,12 +88,6 @@ export const useCallOutStorage = (
|
|||
const getDismissedMessagesStorageKey = (namespace: string) =>
|
||||
`kibana.securitySolution.${namespace}.callouts.dismissed`;
|
||||
|
||||
const createInitialVisibilityState = (messages: CallOutMessage[]) =>
|
||||
createVisibilityState(
|
||||
messages.map((m) => m.id),
|
||||
false
|
||||
);
|
||||
|
||||
const createVisibilityState = (messageIds: string[], isVisible: boolean) =>
|
||||
mapToObject(messageIds, identity, () => isVisible);
|
||||
|
||||
|
|
|
@ -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 { EuiCode } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface CommaSeparatedValuesProps {
|
||||
values: React.ReactNode[];
|
||||
}
|
||||
|
||||
export const CommaSeparatedValues = ({ values }: CommaSeparatedValuesProps) => (
|
||||
<>
|
||||
{values.map((value, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<EuiCode>{value}</EuiCode>
|
||||
{i < values.length - 1 ? ', ' : ''}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 React, { memo, useMemo } from 'react';
|
||||
import hash from 'object-hash';
|
||||
import { CallOutMessage, CallOutSwitcher } from '../../../../common/components/callouts';
|
||||
import * as i18n from './translations';
|
||||
import { useMissingPrivileges } from './use_missing_privileges';
|
||||
|
||||
const MissingPrivilegesCallOutComponent = () => {
|
||||
const missingPrivileges = useMissingPrivileges();
|
||||
|
||||
const MissingPrivilegesMessage: CallOutMessage | null = useMemo(() => {
|
||||
const hasMissingPrivileges =
|
||||
missingPrivileges.indexPrivileges.length > 0 ||
|
||||
missingPrivileges.featurePrivileges.length > 0;
|
||||
|
||||
if (!hasMissingPrivileges) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const missingPrivilegesHash = hash(missingPrivileges);
|
||||
return {
|
||||
type: 'primary',
|
||||
/**
|
||||
* Use privileges hash as a part of the message id.
|
||||
* We want to make sure that the user will see the
|
||||
* callout message in case his privileges change.
|
||||
* The previous click on Dismiss should not affect that.
|
||||
*/
|
||||
id: `missing-user-privileges-${missingPrivilegesHash}`,
|
||||
title: i18n.MISSING_PRIVILEGES_CALLOUT_TITLE,
|
||||
description: i18n.missingPrivilegesCallOutBody(missingPrivileges),
|
||||
};
|
||||
}, [missingPrivileges]);
|
||||
|
||||
return (
|
||||
MissingPrivilegesMessage && (
|
||||
<CallOutSwitcher namespace="detections" condition={true} message={MissingPrivilegesMessage} />
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const MissingPrivilegesCallOut = memo(MissingPrivilegesCallOutComponent);
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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 { 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,
|
||||
DEFAULT_SIGNALS_INDEX,
|
||||
SAVED_OBJECTS_MANAGEMENT_FEATURE_ID,
|
||||
} from '../../../../../common/constants';
|
||||
import { CommaSeparatedValues } from './comma_separated_values';
|
||||
import { MissingPrivileges } from './use_missing_privileges';
|
||||
|
||||
export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle',
|
||||
{
|
||||
defaultMessage: 'Insufficient privileges',
|
||||
}
|
||||
);
|
||||
|
||||
const CANNOT_EDIT_RULES = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditRules',
|
||||
{
|
||||
defaultMessage: 'Without that privilege you cannot create or edit detection engine rules.',
|
||||
}
|
||||
);
|
||||
|
||||
const CANNOT_EDIT_LISTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditLists',
|
||||
{
|
||||
defaultMessage: 'Without these privileges, you cannot create or edit value lists.',
|
||||
}
|
||||
);
|
||||
|
||||
const CANNOT_EDIT_ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditAlerts',
|
||||
{
|
||||
defaultMessage: 'Without these privileges, you cannot open or close alerts.',
|
||||
}
|
||||
);
|
||||
|
||||
export const missingPrivilegesCallOutBody = ({
|
||||
indexPrivileges,
|
||||
featurePrivileges,
|
||||
}: MissingPrivileges) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.messageDetail"
|
||||
defaultMessage="{essence} Missing privileges: {privileges} Related documentation: {docs}"
|
||||
values={{
|
||||
essence: (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.essenceDescription"
|
||||
defaultMessage="You need the following privileges to fully access this functionality. Contact your administrator for further assistance."
|
||||
/>
|
||||
</p>
|
||||
),
|
||||
privileges: (
|
||||
<ul>
|
||||
{indexPrivileges.map(([index, missingPrivileges]) => (
|
||||
<li key={index}>{missingIndexPrivileges(index, missingPrivileges)}</li>
|
||||
))}
|
||||
{featurePrivileges.map(([feature, missingPrivileges]) => (
|
||||
<li key={feature}>{missingFeaturePrivileges(feature, missingPrivileges)}</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
docs: (
|
||||
<ul>
|
||||
<li>
|
||||
<DetectionsRequirementsLink />
|
||||
</li>
|
||||
<li>
|
||||
<SecuritySolutionRequirementsLink />
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
interface PrivilegeExplanations {
|
||||
[key: string]: {
|
||||
[privilegeName: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
const PRIVILEGE_EXPLANATIONS: PrivilegeExplanations = {
|
||||
[SAVED_OBJECTS_MANAGEMENT_FEATURE_ID]: {
|
||||
all: CANNOT_EDIT_RULES,
|
||||
},
|
||||
[DEFAULT_SIGNALS_INDEX]: {
|
||||
write: CANNOT_EDIT_ALERTS,
|
||||
},
|
||||
[DEFAULT_LISTS_INDEX]: {
|
||||
write: CANNOT_EDIT_LISTS,
|
||||
},
|
||||
[DEFAULT_ITEMS_INDEX]: {
|
||||
write: CANNOT_EDIT_LISTS,
|
||||
},
|
||||
};
|
||||
|
||||
const getPrivilegesExplanation = (missingPrivileges: string[], index: string) => {
|
||||
const explanationsByPrivilege = Object.entries(PRIVILEGE_EXPLANATIONS).find(([key]) =>
|
||||
index.startsWith(key)
|
||||
)?.[1];
|
||||
|
||||
return missingPrivileges
|
||||
.map((privilege) => explanationsByPrivilege?.[privilege])
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const missingIndexPrivileges = (index: string, privileges: string[]) => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingIndexPrivileges"
|
||||
defaultMessage="Missing {privileges} privileges for the {index} index. {explanation}"
|
||||
values={{
|
||||
privileges: <CommaSeparatedValues values={privileges} />,
|
||||
index: <EuiCode>{index}</EuiCode>,
|
||||
explanation: getPrivilegesExplanation(privileges, index),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
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),
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { SAVED_OBJECTS_MANAGEMENT_FEATURE_ID } from '../../../../../common/constants';
|
||||
import { Privilege } from '../../../containers/detection_engine/alerts/types';
|
||||
import { useUserData } from '../../user_info';
|
||||
import { useUserPrivileges } from '../../user_privileges';
|
||||
|
||||
const REQUIRED_INDEX_PRIVILIGES = ['read', 'write', 'view_index_metadata', 'maintenance'] as const;
|
||||
|
||||
const getIndexName = (indexPrivileges: Privilege['index']) => {
|
||||
const [indexName] = Object.keys(indexPrivileges);
|
||||
|
||||
return indexName;
|
||||
};
|
||||
|
||||
const getMissingIndexPrivileges = (
|
||||
indexPrivileges: Privilege['index']
|
||||
): MissingIndexPrivileges | undefined => {
|
||||
const indexName = getIndexName(indexPrivileges);
|
||||
const privileges = indexPrivileges[indexName];
|
||||
const missingPrivileges = REQUIRED_INDEX_PRIVILIGES.filter((privelege) => !privileges[privelege]);
|
||||
|
||||
if (missingPrivileges.length) {
|
||||
return [indexName, missingPrivileges];
|
||||
}
|
||||
};
|
||||
|
||||
export type MissingFeaturePrivileges = [feature: string, privileges: string[]];
|
||||
export type MissingIndexPrivileges = [indexName: string, privileges: string[]];
|
||||
|
||||
export interface MissingPrivileges {
|
||||
featurePrivileges: MissingFeaturePrivileges[];
|
||||
indexPrivileges: MissingIndexPrivileges[];
|
||||
}
|
||||
|
||||
export const useMissingPrivileges = (): MissingPrivileges => {
|
||||
const { detectionEnginePrivileges, listPrivileges } = useUserPrivileges();
|
||||
const [{ canUserCRUD }] = useUserData();
|
||||
|
||||
return useMemo<MissingPrivileges>(() => {
|
||||
const featurePrivileges: MissingFeaturePrivileges[] = [];
|
||||
const indexPrivileges: MissingIndexPrivileges[] = [];
|
||||
|
||||
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.
|
||||
*/
|
||||
return {
|
||||
featurePrivileges,
|
||||
indexPrivileges,
|
||||
};
|
||||
}
|
||||
|
||||
if (canUserCRUD === false) {
|
||||
featurePrivileges.push([SAVED_OBJECTS_MANAGEMENT_FEATURE_ID, ['all']]);
|
||||
}
|
||||
|
||||
const missingItemsPrivileges = getMissingIndexPrivileges(listPrivileges.result.listItems.index);
|
||||
if (missingItemsPrivileges) {
|
||||
indexPrivileges.push(missingItemsPrivileges);
|
||||
}
|
||||
|
||||
const missingListsPrivileges = getMissingIndexPrivileges(listPrivileges.result.lists.index);
|
||||
if (missingListsPrivileges) {
|
||||
indexPrivileges.push(missingListsPrivileges);
|
||||
}
|
||||
|
||||
const missingDetectionPrivileges = getMissingIndexPrivileges(
|
||||
detectionEnginePrivileges.result.index
|
||||
);
|
||||
if (missingDetectionPrivileges) {
|
||||
indexPrivileges.push(missingDetectionPrivileges);
|
||||
}
|
||||
|
||||
return {
|
||||
featurePrivileges,
|
||||
indexPrivileges,
|
||||
};
|
||||
}, [canUserCRUD, listPrivileges, detectionEnginePrivileges]);
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { CallOutMessage, CallOutSwitcher } from '../../../../common/components/callouts';
|
||||
import { useUserData } from '../../user_info';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const readOnlyAccessToAlertsMessage: CallOutMessage = {
|
||||
type: 'primary',
|
||||
id: 'read-only-access-to-alerts',
|
||||
title: i18n.READ_ONLY_ALERTS_CALLOUT_TITLE,
|
||||
description: i18n.readOnlyAlertsCallOutBody(),
|
||||
};
|
||||
|
||||
const ReadOnlyAlertsCallOutComponent = () => {
|
||||
const [{ hasIndexUpdateDelete }] = useUserData();
|
||||
|
||||
return (
|
||||
<CallOutSwitcher
|
||||
namespace="detections"
|
||||
condition={hasIndexUpdateDelete != null && !hasIndexUpdateDelete}
|
||||
message={readOnlyAccessToAlertsMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReadOnlyAlertsCallOut = memo(ReadOnlyAlertsCallOutComponent);
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
SecuritySolutionRequirementsLink,
|
||||
DetectionsRequirementsLink,
|
||||
} from '../../../../common/components/links_to_docs';
|
||||
|
||||
export const READ_ONLY_ALERTS_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageTitle',
|
||||
{
|
||||
defaultMessage: 'You cannot change alert states',
|
||||
}
|
||||
);
|
||||
|
||||
export const readOnlyAlertsCallOutBody = () => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.messageDetail"
|
||||
defaultMessage="{essence} Related documentation: {docs}"
|
||||
values={{
|
||||
essence: (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.essenceDescription"
|
||||
defaultMessage="You only have permissions to view alerts. If you need to update alert states (open or close alerts), contact your Kibana administrator."
|
||||
/>
|
||||
</p>
|
||||
),
|
||||
docs: (
|
||||
<ul>
|
||||
<li>
|
||||
<DetectionsRequirementsLink />
|
||||
</li>
|
||||
<li>
|
||||
<SecuritySolutionRequirementsLink />
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { CallOutMessage, CallOutSwitcher } from '../../../../common/components/callouts';
|
||||
import { useUserData } from '../../user_info';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const readOnlyAccessToRulesMessage: CallOutMessage = {
|
||||
type: 'primary',
|
||||
id: 'read-only-access-to-rules',
|
||||
title: i18n.READ_ONLY_RULES_CALLOUT_TITLE,
|
||||
description: i18n.readOnlyRulesCallOutBody(),
|
||||
};
|
||||
|
||||
const ReadOnlyRulesCallOutComponent = () => {
|
||||
const [{ canUserCRUD }] = useUserData();
|
||||
|
||||
return (
|
||||
<CallOutSwitcher
|
||||
namespace="detections"
|
||||
condition={canUserCRUD != null && !canUserCRUD}
|
||||
message={readOnlyAccessToRulesMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReadOnlyRulesCallOut = memo(ReadOnlyRulesCallOutComponent);
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
SecuritySolutionRequirementsLink,
|
||||
DetectionsRequirementsLink,
|
||||
} from '../../../../common/components/links_to_docs';
|
||||
|
||||
export const READ_ONLY_RULES_CALLOUT_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageTitle',
|
||||
{
|
||||
defaultMessage: 'Rule permissions required',
|
||||
}
|
||||
);
|
||||
|
||||
export const readOnlyRulesCallOutBody = () => (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.messageDetail"
|
||||
defaultMessage="{essence} Related documentation: {docs}"
|
||||
values={{
|
||||
essence: (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.essenceDescription"
|
||||
defaultMessage="You are currently missing the required permissions to create/edit detection engine rule. Please contact your administrator for further assistance."
|
||||
/>
|
||||
</p>
|
||||
),
|
||||
docs: (
|
||||
<ul>
|
||||
<li>
|
||||
<DetectionsRequirementsLink />
|
||||
</li>
|
||||
<li>
|
||||
<SecuritySolutionRequirementsLink />
|
||||
</li>
|
||||
</ul>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useUserInfo, ManageUserInfo } from './index';
|
||||
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import * as api from '../../containers/detection_engine/alerts/api';
|
||||
import { TestProviders } from '../../../common/mock/test_providers';
|
||||
import React from 'react';
|
||||
import { UserPrivilegesProvider } from '../user_privileges';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../containers/detection_engine/alerts/api');
|
||||
|
@ -63,7 +64,9 @@ describe('useUserInfo', () => {
|
|||
});
|
||||
const wrapper = ({ children }: { children: JSX.Element }) => (
|
||||
<TestProviders>
|
||||
<ManageUserInfo>{children}</ManageUserInfo>
|
||||
<UserPrivilegesProvider>
|
||||
<ManageUserInfo>{children}</ManageUserInfo>
|
||||
</UserPrivilegesProvider>
|
||||
</TestProviders>
|
||||
);
|
||||
await act(async () => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { noop } from 'lodash/fp';
|
||||
import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react';
|
||||
|
||||
import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user';
|
||||
import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges';
|
||||
import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { useCreateTransforms } from '../../../transforms/containers/use_create_transforms';
|
||||
|
@ -196,7 +196,7 @@ export const useUserInfo = (): State => {
|
|||
hasIndexMaintenance: hasApiIndexMaintenance,
|
||||
hasIndexWrite: hasApiIndexWrite,
|
||||
hasIndexUpdateDelete: hasApiIndexUpdateDelete,
|
||||
} = usePrivilegeUser();
|
||||
} = useAlertsPrivileges();
|
||||
const {
|
||||
loading: indexNameLoading,
|
||||
signalIndexExists: isApiSignalIndexExists,
|
||||
|
@ -208,8 +208,7 @@ export const useUserInfo = (): State => {
|
|||
const { createTransforms } = useCreateTransforms();
|
||||
|
||||
const uiCapabilities = useKibana().services.application.capabilities;
|
||||
const capabilitiesCanUserCRUD: boolean =
|
||||
typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false;
|
||||
const capabilitiesCanUserCRUD: boolean = uiCapabilities.siem.crud === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== (privilegeLoading || indexNameLoading)) {
|
||||
|
@ -275,7 +274,7 @@ export const useUserInfo = (): State => {
|
|||
}, [dispatch, loading, hasEncryptionKey, isApiEncryptionKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) {
|
||||
if (!loading && canUserCRUD !== capabilitiesCanUserCRUD) {
|
||||
dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD });
|
||||
}
|
||||
}, [dispatch, loading, canUserCRUD, capabilitiesCanUserCRUD]);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 React, { createContext, useContext } from 'react';
|
||||
import { useFetchDetectionEnginePrivileges } from './use_fetch_detection_engine_privileges';
|
||||
import { useFetchListPrivileges } from './use_fetch_list_privileges';
|
||||
|
||||
export interface UserPrivilegesState {
|
||||
listPrivileges: ReturnType<typeof useFetchListPrivileges>;
|
||||
detectionEnginePrivileges: ReturnType<typeof useFetchDetectionEnginePrivileges>;
|
||||
}
|
||||
|
||||
const UserPrivilegesContext = createContext<UserPrivilegesState>({
|
||||
listPrivileges: { loading: false, error: undefined, result: undefined },
|
||||
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
|
||||
});
|
||||
|
||||
interface UserPrivilegesProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const UserPrivilegesProvider = ({ children }: UserPrivilegesProviderProps) => {
|
||||
const listPrivileges = useFetchListPrivileges();
|
||||
const detectionEnginePrivileges = useFetchDetectionEnginePrivileges();
|
||||
|
||||
return (
|
||||
<UserPrivilegesContext.Provider
|
||||
value={{
|
||||
listPrivileges,
|
||||
detectionEnginePrivileges,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserPrivilegesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUserPrivileges = () => useContext(UserPrivilegesContext);
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LISTS_PRIVILEGES_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.alerts.readListsPrivileges.errorDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to retrieve lists privileges',
|
||||
}
|
||||
);
|
||||
|
||||
export const DETECTION_ENGINE_PRIVILEGES_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.alerts.detectionEnginePrivileges.errorFetching',
|
||||
{
|
||||
defaultMessage: 'Failed to retreive detection engine privileges',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { useFetchDetectionEnginePrivileges } from './use_fetch_detection_engine_privileges';
|
||||
|
||||
export const useFetchDetectionEnginePrivilegesMock: () => jest.Mocked<
|
||||
ReturnType<typeof useFetchDetectionEnginePrivileges>
|
||||
> = () => ({ loading: false, error: undefined, result: undefined });
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useAsync, withOptionalSignal } from '../../../shared_imports';
|
||||
import { getUserPrivilege } from '../../containers/detection_engine/alerts/api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const useFetchPrivileges = () => useAsync(withOptionalSignal(getUserPrivilege));
|
||||
|
||||
export const useFetchDetectionEnginePrivileges = () => {
|
||||
const { start, ...detectionEnginePrivileges } = useFetchPrivileges();
|
||||
const { addError } = useAppToasts();
|
||||
const abortCtrlRef = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
const { loading, result, error } = detectionEnginePrivileges;
|
||||
|
||||
if (!loading && !(result || error)) {
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
start({ signal: abortCtrlRef.current.signal });
|
||||
}
|
||||
}, [start, detectionEnginePrivileges]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortCtrlRef.current.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const error = detectionEnginePrivileges.error;
|
||||
if (error != null) {
|
||||
addError(error, {
|
||||
title: i18n.DETECTION_ENGINE_PRIVILEGES_FETCH_FAILURE,
|
||||
});
|
||||
}
|
||||
}, [addError, detectionEnginePrivileges.error]);
|
||||
|
||||
return detectionEnginePrivileges;
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
|
||||
import { useHttp, useKibana } from '../../../common/lib/kibana';
|
||||
import { useReadListPrivileges } from '../../../shared_imports';
|
||||
import { Privilege } from '../../containers/detection_engine/alerts/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface ListPrivileges {
|
||||
is_authenticated: boolean;
|
||||
lists: {
|
||||
index: Privilege['index'];
|
||||
};
|
||||
listItems: {
|
||||
index: Privilege['index'];
|
||||
};
|
||||
}
|
||||
|
||||
export const useFetchListPrivileges = () => {
|
||||
const http = useHttp();
|
||||
const { lists } = useKibana().services;
|
||||
const { start: fetchListPrivileges, ...listPrivileges } = useReadListPrivileges();
|
||||
const { addError } = useAppToasts();
|
||||
const abortCtrlRef = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
const { loading, result, error } = listPrivileges;
|
||||
|
||||
if (lists && !loading && !(result || error)) {
|
||||
abortCtrlRef.current.abort();
|
||||
abortCtrlRef.current = new AbortController();
|
||||
fetchListPrivileges({ http, signal: abortCtrlRef.current.signal });
|
||||
}
|
||||
}, [http, lists, fetchListPrivileges, listPrivileges]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortCtrlRef.current.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const error = listPrivileges.error;
|
||||
if (error != null) {
|
||||
addError(error, {
|
||||
title: i18n.LISTS_PRIVILEGES_FETCH_FAILURE,
|
||||
});
|
||||
}
|
||||
}, [addError, listPrivileges.error]);
|
||||
|
||||
return {
|
||||
loading: listPrivileges.loading,
|
||||
error: listPrivileges.error,
|
||||
result: listPrivileges.result as ListPrivileges | undefined,
|
||||
};
|
||||
};
|
|
@ -14,13 +14,6 @@ export const ALERT_FETCH_FAILURE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const PRIVILEGE_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.alerts.errorFetchingAlertsDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to query alerts',
|
||||
}
|
||||
);
|
||||
|
||||
export const SIGNAL_GET_NAME_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.alerts.errorGetAlertDescription',
|
||||
{
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
import produce from 'immer';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
|
||||
import { useUserPrivileges } from '../../../components/user_privileges';
|
||||
import { Privilege } from './types';
|
||||
import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../../../components/user_privileges');
|
||||
|
||||
const useUserPrivilegesMock = useUserPrivileges as jest.Mock<ReturnType<typeof useUserPrivileges>>;
|
||||
|
||||
const privilege: Privilege = {
|
||||
username: 'soc_manager',
|
||||
has_all_requested: false,
|
||||
cluster: {
|
||||
monitor_ml: false,
|
||||
manage_ccr: false,
|
||||
manage_index_templates: false,
|
||||
monitor_watcher: false,
|
||||
monitor_transform: false,
|
||||
read_ilm: false,
|
||||
manage_api_key: false,
|
||||
manage_security: false,
|
||||
manage_own_api_key: false,
|
||||
manage_saml: false,
|
||||
all: false,
|
||||
manage_ilm: false,
|
||||
manage_ingest_pipelines: false,
|
||||
read_ccr: false,
|
||||
manage_rollup: false,
|
||||
monitor: false,
|
||||
manage_watcher: false,
|
||||
manage: true,
|
||||
manage_transform: false,
|
||||
manage_token: false,
|
||||
manage_ml: false,
|
||||
manage_pipeline: false,
|
||||
monitor_rollup: false,
|
||||
transport_client: false,
|
||||
create_snapshot: false,
|
||||
},
|
||||
index: {
|
||||
'.siem-signals-default': {
|
||||
all: false,
|
||||
manage_ilm: true,
|
||||
read: true,
|
||||
create_index: true,
|
||||
read_cross_cluster: false,
|
||||
index: true,
|
||||
monitor: true,
|
||||
delete: true,
|
||||
manage: true,
|
||||
delete_index: true,
|
||||
create_doc: true,
|
||||
view_index_metadata: true,
|
||||
create: true,
|
||||
manage_follow_index: true,
|
||||
manage_leader_index: true,
|
||||
maintenance: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
application: {},
|
||||
is_authenticated: true,
|
||||
has_encryption_key: true,
|
||||
};
|
||||
|
||||
const userPrivilegesInitial: ReturnType<typeof useUserPrivileges> = {
|
||||
detectionEnginePrivileges: {
|
||||
loading: false,
|
||||
result: undefined,
|
||||
error: undefined,
|
||||
},
|
||||
listPrivileges: {
|
||||
loading: false,
|
||||
result: undefined,
|
||||
error: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
describe('usePrivilegeUser', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
useUserPrivilegesMock.mockReturnValue(userPrivilegesInitial);
|
||||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, UseAlertsPrivelegesReturn>(() =>
|
||||
useAlertsPrivileges()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexRead: null,
|
||||
hasIndexMaintenance: null,
|
||||
hasIndexWrite: null,
|
||||
hasIndexUpdateDelete: null,
|
||||
isAuthenticated: null,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if there is an error when fetching user privilege, we should get back false for every properties', async () => {
|
||||
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
|
||||
draft.detectionEnginePrivileges.error = new Error('Something went wrong');
|
||||
});
|
||||
useUserPrivilegesMock.mockReturnValue(userPrivileges);
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, UseAlertsPrivelegesReturn>(() =>
|
||||
useAlertsPrivileges()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: false,
|
||||
hasIndexManage: false,
|
||||
hasIndexMaintenance: false,
|
||||
hasIndexRead: false,
|
||||
hasIndexWrite: false,
|
||||
hasIndexUpdateDelete: false,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('returns "hasIndexManage" is false if the privilege does not have cluster manage', async () => {
|
||||
const privilegeWithClusterManage = produce(privilege, (draft) => {
|
||||
draft.cluster.manage = false;
|
||||
});
|
||||
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
|
||||
draft.detectionEnginePrivileges.result = privilegeWithClusterManage;
|
||||
});
|
||||
useUserPrivilegesMock.mockReturnValue(userPrivileges);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, UseAlertsPrivelegesReturn>(() =>
|
||||
useAlertsPrivileges()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: true,
|
||||
hasIndexManage: false,
|
||||
hasIndexMaintenance: true,
|
||||
hasIndexRead: true,
|
||||
hasIndexWrite: true,
|
||||
hasIndexUpdateDelete: true,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('returns "hasIndexManage" is true if the privilege has cluster manage', async () => {
|
||||
const userPrivileges = produce(userPrivilegesInitial, (draft) => {
|
||||
draft.detectionEnginePrivileges.result = privilege;
|
||||
});
|
||||
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,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useUserPrivileges } from '../../../components/user_privileges';
|
||||
|
||||
export interface UseAlertsPrivelegesReturn extends AlertsPrivelegesState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface AlertsPrivelegesState {
|
||||
isAuthenticated: boolean | null;
|
||||
hasEncryptionKey: boolean | null;
|
||||
hasIndexManage: boolean | null;
|
||||
hasIndexWrite: boolean | null;
|
||||
hasIndexUpdateDelete: boolean | null;
|
||||
hasIndexMaintenance: boolean | null;
|
||||
hasIndexRead: boolean | null;
|
||||
}
|
||||
/**
|
||||
* Hook to get user privilege from
|
||||
*
|
||||
*/
|
||||
export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => {
|
||||
const [privileges, setPrivileges] = useState<AlertsPrivelegesState>({
|
||||
isAuthenticated: null,
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexRead: null,
|
||||
hasIndexWrite: null,
|
||||
hasIndexUpdateDelete: null,
|
||||
hasIndexMaintenance: null,
|
||||
});
|
||||
const { detectionEnginePrivileges } = useUserPrivileges();
|
||||
|
||||
useEffect(() => {
|
||||
if (detectionEnginePrivileges.error != null) {
|
||||
setPrivileges({
|
||||
isAuthenticated: false,
|
||||
hasEncryptionKey: false,
|
||||
hasIndexManage: false,
|
||||
hasIndexRead: false,
|
||||
hasIndexWrite: false,
|
||||
hasIndexUpdateDelete: false,
|
||||
hasIndexMaintenance: false,
|
||||
});
|
||||
}
|
||||
}, [detectionEnginePrivileges.error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (detectionEnginePrivileges.result != null) {
|
||||
const privilege = detectionEnginePrivileges.result;
|
||||
|
||||
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
|
||||
const indexName = Object.keys(privilege.index)[0];
|
||||
setPrivileges({
|
||||
isAuthenticated: privilege.is_authenticated,
|
||||
hasEncryptionKey: privilege.has_encryption_key,
|
||||
hasIndexManage: privilege.index[indexName].manage && privilege.cluster.manage,
|
||||
hasIndexMaintenance: privilege.index[indexName].maintenance,
|
||||
hasIndexRead: privilege.index[indexName].read,
|
||||
hasIndexWrite:
|
||||
privilege.index[indexName].create ||
|
||||
privilege.index[indexName].create_doc ||
|
||||
privilege.index[indexName].index ||
|
||||
privilege.index[indexName].write,
|
||||
hasIndexUpdateDelete: privilege.index[indexName].write,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [detectionEnginePrivileges.result]);
|
||||
|
||||
return { loading: detectionEnginePrivileges.loading, ...privileges };
|
||||
};
|
|
@ -1,238 +0,0 @@
|
|||
/*
|
||||
* 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 { usePrivilegeUser, ReturnPrivilegeUser } from './use_privilege_user';
|
||||
import * as api from './api';
|
||||
import { Privilege } from './types';
|
||||
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
|
||||
describe('usePrivilegeUser', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
});
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPrivilegeUser>(() =>
|
||||
usePrivilegeUser()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexMaintenance: null,
|
||||
hasIndexWrite: null,
|
||||
hasIndexUpdateDelete: null,
|
||||
isAuthenticated: null,
|
||||
loading: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch user privilege', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPrivilegeUser>(() =>
|
||||
usePrivilegeUser()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: true,
|
||||
hasIndexManage: true,
|
||||
hasIndexMaintenance: true,
|
||||
hasIndexWrite: true,
|
||||
hasIndexUpdateDelete: true,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('if there is an error when fetching user privilege, we should get back false for every properties', async () => {
|
||||
const spyOnGetUserPrivilege = jest.spyOn(api, 'getUserPrivilege');
|
||||
spyOnGetUserPrivilege.mockImplementation(() => {
|
||||
throw new Error('Something went wrong, let see what happen');
|
||||
});
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPrivilegeUser>(() =>
|
||||
usePrivilegeUser()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: false,
|
||||
hasIndexManage: false,
|
||||
hasIndexMaintenance: false,
|
||||
hasIndexWrite: false,
|
||||
hasIndexUpdateDelete: false,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('returns "hasIndexManage" is false if the privilege does not have cluster manage', async () => {
|
||||
const privilege: Privilege = {
|
||||
username: 'soc_manager',
|
||||
has_all_requested: false,
|
||||
cluster: {
|
||||
monitor_ml: false,
|
||||
manage_ccr: false,
|
||||
manage_index_templates: false,
|
||||
monitor_watcher: false,
|
||||
monitor_transform: false,
|
||||
read_ilm: false,
|
||||
manage_api_key: false,
|
||||
manage_security: false,
|
||||
manage_own_api_key: false,
|
||||
manage_saml: false,
|
||||
all: false,
|
||||
manage_ilm: false,
|
||||
manage_ingest_pipelines: false,
|
||||
read_ccr: false,
|
||||
manage_rollup: false,
|
||||
monitor: false,
|
||||
manage_watcher: false,
|
||||
manage: false,
|
||||
manage_transform: false,
|
||||
manage_token: false,
|
||||
manage_ml: false,
|
||||
manage_pipeline: false,
|
||||
monitor_rollup: false,
|
||||
transport_client: false,
|
||||
create_snapshot: false,
|
||||
},
|
||||
index: {
|
||||
'.siem-signals-default': {
|
||||
all: false,
|
||||
manage_ilm: true,
|
||||
read: true,
|
||||
create_index: true,
|
||||
read_cross_cluster: false,
|
||||
index: true,
|
||||
monitor: true,
|
||||
delete: true,
|
||||
manage: true,
|
||||
delete_index: true,
|
||||
create_doc: true,
|
||||
view_index_metadata: true,
|
||||
create: true,
|
||||
manage_follow_index: true,
|
||||
manage_leader_index: true,
|
||||
maintenance: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
application: {},
|
||||
is_authenticated: true,
|
||||
has_encryption_key: true,
|
||||
};
|
||||
const spyOnGetUserPrivilege = jest.spyOn(api, 'getUserPrivilege');
|
||||
spyOnGetUserPrivilege.mockImplementation(() => Promise.resolve(privilege));
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPrivilegeUser>(() =>
|
||||
usePrivilegeUser()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: true,
|
||||
hasIndexManage: false,
|
||||
hasIndexMaintenance: true,
|
||||
hasIndexWrite: true,
|
||||
hasIndexUpdateDelete: true,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('returns "hasIndexManage" is true if the privilege has cluster manage', async () => {
|
||||
const privilege: Privilege = {
|
||||
username: 'soc_manager',
|
||||
has_all_requested: false,
|
||||
cluster: {
|
||||
monitor_ml: false,
|
||||
manage_ccr: false,
|
||||
manage_index_templates: false,
|
||||
monitor_watcher: false,
|
||||
monitor_transform: false,
|
||||
read_ilm: false,
|
||||
manage_api_key: false,
|
||||
manage_security: false,
|
||||
manage_own_api_key: false,
|
||||
manage_saml: false,
|
||||
all: false,
|
||||
manage_ilm: false,
|
||||
manage_ingest_pipelines: false,
|
||||
read_ccr: false,
|
||||
manage_rollup: false,
|
||||
monitor: false,
|
||||
manage_watcher: false,
|
||||
manage: true,
|
||||
manage_transform: false,
|
||||
manage_token: false,
|
||||
manage_ml: false,
|
||||
manage_pipeline: false,
|
||||
monitor_rollup: false,
|
||||
transport_client: false,
|
||||
create_snapshot: false,
|
||||
},
|
||||
index: {
|
||||
'.siem-signals-default': {
|
||||
all: false,
|
||||
manage_ilm: true,
|
||||
read: true,
|
||||
create_index: true,
|
||||
read_cross_cluster: false,
|
||||
index: true,
|
||||
monitor: true,
|
||||
delete: true,
|
||||
manage: true,
|
||||
delete_index: true,
|
||||
create_doc: true,
|
||||
view_index_metadata: true,
|
||||
create: true,
|
||||
manage_follow_index: true,
|
||||
manage_leader_index: true,
|
||||
maintenance: true,
|
||||
write: true,
|
||||
},
|
||||
},
|
||||
application: {},
|
||||
is_authenticated: true,
|
||||
has_encryption_key: true,
|
||||
};
|
||||
const spyOnGetUserPrivilege = jest.spyOn(api, 'getUserPrivilege');
|
||||
spyOnGetUserPrivilege.mockImplementation(() => Promise.resolve(privilege));
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPrivilegeUser>(() =>
|
||||
usePrivilegeUser()
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
hasEncryptionKey: true,
|
||||
hasIndexManage: true,
|
||||
hasIndexMaintenance: true,
|
||||
hasIndexWrite: true,
|
||||
hasIndexUpdateDelete: true,
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
|
||||
import { getUserPrivilege } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface ReturnPrivilegeUser {
|
||||
loading: boolean;
|
||||
isAuthenticated: boolean | null;
|
||||
hasEncryptionKey: boolean | null;
|
||||
hasIndexManage: boolean | null;
|
||||
hasIndexWrite: boolean | null;
|
||||
hasIndexUpdateDelete: boolean | null;
|
||||
hasIndexMaintenance: boolean | null;
|
||||
}
|
||||
/**
|
||||
* Hook to get user privilege from
|
||||
*
|
||||
*/
|
||||
export const usePrivilegeUser = (): ReturnPrivilegeUser => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [privilegeUser, setPrivilegeUser] = useState<
|
||||
Pick<
|
||||
ReturnPrivilegeUser,
|
||||
| 'isAuthenticated'
|
||||
| 'hasEncryptionKey'
|
||||
| 'hasIndexManage'
|
||||
| 'hasIndexWrite'
|
||||
| 'hasIndexUpdateDelete'
|
||||
| 'hasIndexMaintenance'
|
||||
>
|
||||
>({
|
||||
isAuthenticated: null,
|
||||
hasEncryptionKey: null,
|
||||
hasIndexManage: null,
|
||||
hasIndexWrite: null,
|
||||
hasIndexUpdateDelete: null,
|
||||
hasIndexMaintenance: null,
|
||||
});
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
setLoading(true);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const privilege = await getUserPrivilege({
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
if (isSubscribed && privilege != null) {
|
||||
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
|
||||
const indexName = Object.keys(privilege.index)[0];
|
||||
setPrivilegeUser({
|
||||
isAuthenticated: privilege.is_authenticated,
|
||||
hasEncryptionKey: privilege.has_encryption_key,
|
||||
hasIndexManage: privilege.index[indexName].manage && privilege.cluster.manage,
|
||||
hasIndexMaintenance: privilege.index[indexName].maintenance,
|
||||
hasIndexWrite:
|
||||
privilege.index[indexName].create ||
|
||||
privilege.index[indexName].create_doc ||
|
||||
privilege.index[indexName].index ||
|
||||
privilege.index[indexName].write,
|
||||
hasIndexUpdateDelete: privilege.index[indexName].write,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
setPrivilegeUser({
|
||||
isAuthenticated: false,
|
||||
hasEncryptionKey: false,
|
||||
hasIndexManage: false,
|
||||
hasIndexWrite: false,
|
||||
hasIndexUpdateDelete: false,
|
||||
hasIndexMaintenance: false,
|
||||
});
|
||||
addError(error, { title: i18n.PRIVILEGE_FETCH_FAILURE });
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [addError]);
|
||||
|
||||
return { loading, ...privilegeUser };
|
||||
};
|
|
@ -4,16 +4,21 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useSignalIndex, ReturnSignalIndex } from './use_signal_index';
|
||||
import * as api from './api';
|
||||
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { UserPrivilegesProvider } from '../../../components/user_privileges';
|
||||
|
||||
jest.mock('./api');
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<UserPrivilegesProvider>{children}</UserPrivilegesProvider>
|
||||
);
|
||||
|
||||
describe('useSignalIndex', () => {
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
|
@ -26,8 +31,9 @@ describe('useSignalIndex', () => {
|
|||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
|
@ -42,11 +48,13 @@ describe('useSignalIndex', () => {
|
|||
|
||||
test('fetch alerts info', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
createDeSignalIndex: result.current.createDeSignalIndex,
|
||||
loading: false,
|
||||
|
@ -59,11 +67,13 @@ describe('useSignalIndex', () => {
|
|||
|
||||
test('make sure that createSignalIndex is giving back the signal info', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
if (result.current.createDeSignalIndex != null) {
|
||||
await result.current.createDeSignalIndex();
|
||||
}
|
||||
|
@ -81,11 +91,13 @@ describe('useSignalIndex', () => {
|
|||
test('make sure that createSignalIndex have been called when trying to create signal index', async () => {
|
||||
const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex');
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
if (result.current.createDeSignalIndex != null) {
|
||||
await result.current.createDeSignalIndex();
|
||||
}
|
||||
|
@ -100,11 +112,13 @@ describe('useSignalIndex', () => {
|
|||
throw new Error('Something went wrong, let see what happen');
|
||||
});
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
if (result.current.createDeSignalIndex != null) {
|
||||
await result.current.createDeSignalIndex();
|
||||
}
|
||||
|
@ -124,11 +138,13 @@ describe('useSignalIndex', () => {
|
|||
throw new Error('Something went wrong, let see what happen');
|
||||
});
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(() =>
|
||||
useSignalIndex()
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnSignalIndex>(
|
||||
() => useSignalIndex(),
|
||||
{ wrapper: Wrapper }
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
createDeSignalIndex: result.current.createDeSignalIndex,
|
||||
loading: false,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
|||
import { createSignalIndex, getSignalIndex } from './api';
|
||||
import * as i18n from './translations';
|
||||
import { isSecurityAppError } from '../../../../common/utils/api';
|
||||
import { useAlertsPrivileges } from './use_alerts_privileges';
|
||||
|
||||
type Func = () => Promise<void>;
|
||||
|
||||
|
@ -36,6 +37,7 @@ export const useSignalIndex = (): ReturnSignalIndex => {
|
|||
createDeSignalIndex: null,
|
||||
});
|
||||
const { addError } = useAppToasts();
|
||||
const { hasIndexRead } = useAlertsPrivileges();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
@ -102,12 +104,18 @@ export const useSignalIndex = (): ReturnSignalIndex => {
|
|||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
if (hasIndexRead) {
|
||||
fetchData();
|
||||
} else {
|
||||
// Skip data fetching as the current user doesn't have enough priviliges.
|
||||
// Attempt to get the signal index will result in 500 error.
|
||||
setLoading(false);
|
||||
}
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [addError]);
|
||||
}, [addError, hasIndexRead]);
|
||||
|
||||
return { loading, ...signalIndex };
|
||||
};
|
||||
|
|
|
@ -20,10 +20,3 @@ export const LISTS_INDEX_CREATE_FAILURE = i18n.translate(
|
|||
defaultMessage: 'Failed to create the lists index',
|
||||
}
|
||||
);
|
||||
|
||||
export const LISTS_PRIVILEGES_READ_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.alerts.readListsPrivileges.errorDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to retrieve lists privileges',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useHttp, useKibana } from '../../../../common/lib/kibana';
|
|||
import { isSecurityAppError } from '../../../../common/utils/api';
|
||||
import * as i18n from './translations';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useListsPrivileges } from './use_lists_privileges';
|
||||
|
||||
export interface UseListsIndexReturn {
|
||||
createIndex: () => void;
|
||||
|
@ -26,6 +27,7 @@ export const useListsIndex = (): UseListsIndexReturn => {
|
|||
const { lists } = useKibana().services;
|
||||
const http = useHttp();
|
||||
const { addError } = useAppToasts();
|
||||
const { canReadIndex } = useListsPrivileges();
|
||||
const { loading: readLoading, start: readListIndex, ...readListIndexState } = useReadListIndex();
|
||||
const {
|
||||
loading: createLoading,
|
||||
|
@ -35,10 +37,10 @@ export const useListsIndex = (): UseListsIndexReturn => {
|
|||
const loading = readLoading || createLoading;
|
||||
|
||||
const readIndex = useCallback(() => {
|
||||
if (lists) {
|
||||
if (lists && canReadIndex) {
|
||||
readListIndex({ http });
|
||||
}
|
||||
}, [http, lists, readListIndex]);
|
||||
}, [http, lists, readListIndex, canReadIndex]);
|
||||
|
||||
const createIndex = useCallback(() => {
|
||||
if (lists) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { UseListsPrivilegesReturn } from './use_lists_privileges';
|
|||
|
||||
export const getUseListsPrivilegesMock: () => jest.Mocked<UseListsPrivilegesReturn> = () => ({
|
||||
isAuthenticated: null,
|
||||
canReadIndex: null,
|
||||
canManageIndex: null,
|
||||
canWriteIndex: null,
|
||||
loading: false,
|
||||
|
|
|
@ -5,16 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { useReadListPrivileges } from '../../../../shared_imports';
|
||||
import { useHttp, useKibana } from '../../../../common/lib/kibana';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useUserPrivileges } from '../../../components/user_privileges';
|
||||
import { Privilege } from '../alerts/types';
|
||||
|
||||
export interface UseListsPrivilegesState {
|
||||
isAuthenticated: boolean | null;
|
||||
canManageIndex: boolean | null;
|
||||
canReadIndex: boolean | null;
|
||||
canWriteIndex: boolean | null;
|
||||
}
|
||||
|
||||
|
@ -22,38 +20,7 @@ export interface UseListsPrivilegesReturn extends UseListsPrivilegesState {
|
|||
loading: boolean;
|
||||
}
|
||||
|
||||
interface ListIndexPrivileges {
|
||||
[indexName: string]: {
|
||||
all: boolean;
|
||||
create: boolean;
|
||||
create_doc: boolean;
|
||||
create_index: boolean;
|
||||
delete: boolean;
|
||||
delete_index: boolean;
|
||||
index: boolean;
|
||||
manage: boolean;
|
||||
manage_follow_index: boolean;
|
||||
manage_ilm: boolean;
|
||||
manage_leader_index: boolean;
|
||||
monitor: boolean;
|
||||
read: boolean;
|
||||
read_cross_cluster: boolean;
|
||||
view_index_metadata: boolean;
|
||||
write: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface ListPrivileges {
|
||||
is_authenticated: boolean;
|
||||
lists: {
|
||||
index: ListIndexPrivileges;
|
||||
};
|
||||
listItems: {
|
||||
index: ListIndexPrivileges;
|
||||
};
|
||||
}
|
||||
|
||||
const canManageIndex = (indexPrivileges: ListIndexPrivileges): boolean => {
|
||||
const canManageIndex = (indexPrivileges: Privilege['index']): boolean => {
|
||||
const [indexName] = Object.keys(indexPrivileges);
|
||||
const privileges = indexPrivileges[indexName];
|
||||
if (privileges == null) {
|
||||
|
@ -62,7 +29,17 @@ const canManageIndex = (indexPrivileges: ListIndexPrivileges): boolean => {
|
|||
return privileges.manage;
|
||||
};
|
||||
|
||||
const canWriteIndex = (indexPrivileges: ListIndexPrivileges): boolean => {
|
||||
const canReadIndex = (indexPrivileges: Privilege['index']): boolean => {
|
||||
const [indexName] = Object.keys(indexPrivileges);
|
||||
const privileges = indexPrivileges[indexName];
|
||||
if (privileges == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return privileges.read;
|
||||
};
|
||||
|
||||
const canWriteIndex = (indexPrivileges: Privilege['index']): boolean => {
|
||||
const [indexName] = Object.keys(indexPrivileges);
|
||||
const privileges = indexPrivileges[indexName];
|
||||
if (privileges == null) {
|
||||
|
@ -76,57 +53,41 @@ export const useListsPrivileges = (): UseListsPrivilegesReturn => {
|
|||
const [state, setState] = useState<UseListsPrivilegesState>({
|
||||
isAuthenticated: null,
|
||||
canManageIndex: null,
|
||||
canReadIndex: null,
|
||||
canWriteIndex: null,
|
||||
});
|
||||
const { lists } = useKibana().services;
|
||||
const http = useHttp();
|
||||
const { addError } = useAppToasts();
|
||||
const { loading, start: readListPrivileges, ...readState } = useReadListPrivileges();
|
||||
|
||||
const readPrivileges = useCallback(() => {
|
||||
if (lists) {
|
||||
readListPrivileges({ http });
|
||||
}
|
||||
}, [http, lists, readListPrivileges]);
|
||||
|
||||
// initRead
|
||||
useEffect(() => {
|
||||
if (!loading && !readState.error && state.isAuthenticated === null) {
|
||||
readPrivileges();
|
||||
}
|
||||
}, [loading, readState.error, readPrivileges, state.isAuthenticated]);
|
||||
const { listPrivileges } = useUserPrivileges();
|
||||
|
||||
// handleReadResult
|
||||
useEffect(() => {
|
||||
if (readState.result != null) {
|
||||
try {
|
||||
const {
|
||||
is_authenticated: isAuthenticated,
|
||||
lists: { index: listsPrivileges },
|
||||
listItems: { index: listItemsPrivileges },
|
||||
} = readState.result as ListPrivileges;
|
||||
if (listPrivileges.result != null) {
|
||||
const {
|
||||
is_authenticated: isAuthenticated,
|
||||
lists: { index: listsPrivileges },
|
||||
listItems: { index: listItemsPrivileges },
|
||||
} = listPrivileges.result;
|
||||
|
||||
setState({
|
||||
isAuthenticated,
|
||||
canManageIndex: canManageIndex(listsPrivileges) && canManageIndex(listItemsPrivileges),
|
||||
canWriteIndex: canWriteIndex(listsPrivileges) && canWriteIndex(listItemsPrivileges),
|
||||
});
|
||||
} catch (e) {
|
||||
setState({ isAuthenticated: null, canManageIndex: false, canWriteIndex: false });
|
||||
}
|
||||
setState({
|
||||
isAuthenticated,
|
||||
canReadIndex: canReadIndex(listsPrivileges) && canReadIndex(listItemsPrivileges),
|
||||
canManageIndex: canManageIndex(listsPrivileges) && canManageIndex(listItemsPrivileges),
|
||||
canWriteIndex: canWriteIndex(listsPrivileges) && canWriteIndex(listItemsPrivileges),
|
||||
});
|
||||
}
|
||||
}, [readState.result]);
|
||||
}, [listPrivileges.result]);
|
||||
|
||||
// handleReadError
|
||||
useEffect(() => {
|
||||
const error = readState.error;
|
||||
if (error != null) {
|
||||
setState({ isAuthenticated: false, canManageIndex: false, canWriteIndex: false });
|
||||
addError(error, {
|
||||
title: i18n.LISTS_PRIVILEGES_READ_FAILURE,
|
||||
if (listPrivileges.error != null) {
|
||||
setState({
|
||||
isAuthenticated: false,
|
||||
canManageIndex: false,
|
||||
canReadIndex: false,
|
||||
canWriteIndex: false,
|
||||
});
|
||||
}
|
||||
}, [addError, readState.error]);
|
||||
}, [listPrivileges.error]);
|
||||
|
||||
return { loading, ...state };
|
||||
return { loading: listPrivileges.loading, ...state };
|
||||
};
|
||||
|
|
|
@ -28,7 +28,6 @@ import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
|||
import { useAlertInfo } from '../../components/alerts_info';
|
||||
import { AlertsTable } from '../../components/alerts_table';
|
||||
import { NoApiIntegrationKeyCallOut } from '../../components/callouts/no_api_integration_callout';
|
||||
import { ReadOnlyAlertsCallOut } from '../../components/callouts/read_only_alerts_callout';
|
||||
import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel';
|
||||
import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config';
|
||||
import { useUserData } from '../../components/user_info';
|
||||
|
@ -57,6 +56,7 @@ import {
|
|||
import { useSourcererScope } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { NeedAdminForUpdateRulesCallOut } from '../../components/callouts/need_admin_for_update_callout';
|
||||
import { MissingPrivilegesCallOut } from '../../components/callouts/missing_privileges_callout';
|
||||
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
|
@ -211,8 +211,8 @@ const DetectionEnginePageComponent = () => {
|
|||
return (
|
||||
<>
|
||||
{hasEncryptionKey != null && !hasEncryptionKey && <NoApiIntegrationKeyCallOut />}
|
||||
<ReadOnlyAlertsCallOut />
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
{indicesExist ? (
|
||||
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
|
|
|
@ -61,8 +61,6 @@ import {
|
|||
buildShowBuildingBlockFilter,
|
||||
buildThreatMatchFilter,
|
||||
} from '../../../../components/alerts_table/default_config';
|
||||
import { ReadOnlyAlertsCallOut } from '../../../../components/callouts/read_only_alerts_callout';
|
||||
import { ReadOnlyRulesCallOut } from '../../../../components/callouts/read_only_rules_callout';
|
||||
import { RuleSwitch } from '../../../../components/rules/rule_switch';
|
||||
import { StepPanel } from '../../../../components/rules/step_panel';
|
||||
import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers';
|
||||
|
@ -109,6 +107,7 @@ import * as i18n from './translations';
|
|||
import { isTab } from '../../../../../common/components/accessibility/helpers';
|
||||
import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout';
|
||||
import { getRuleStatusText } from '../../../../../../common/detection_engine/utils';
|
||||
import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout';
|
||||
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
|
@ -528,8 +527,7 @@ const RuleDetailsPageComponent = () => {
|
|||
return (
|
||||
<>
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<ReadOnlyAlertsCallOut />
|
||||
<ReadOnlyRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
{indicesExist ? (
|
||||
<StyledFullHeightContainer onKeyDown={onKeyDown} ref={containerElement}>
|
||||
<EuiWindowEvent event="resize" handler={noop} />
|
||||
|
|
|
@ -22,7 +22,6 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes';
|
|||
import { useUserData } from '../../../components/user_info';
|
||||
import { AllRules } from './all';
|
||||
import { ImportDataModal } from '../../../../common/components/import_data_modal';
|
||||
import { ReadOnlyRulesCallOut } from '../../../components/callouts/read_only_rules_callout';
|
||||
import { ValueListsModal } from '../../../components/value_lists_management_modal';
|
||||
import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout';
|
||||
import {
|
||||
|
@ -37,6 +36,7 @@ import { LinkButton } from '../../../../common/components/links';
|
|||
import { useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { NeedAdminForUpdateRulesCallOut } from '../../../components/callouts/need_admin_for_update_callout';
|
||||
import { MlJobCompatibilityCallout } from '../../../components/callouts/ml_job_compatibility_callout';
|
||||
import { MissingPrivilegesCallOut } from '../../../components/callouts/missing_privileges_callout';
|
||||
|
||||
type Func = () => Promise<void>;
|
||||
|
||||
|
@ -161,7 +161,7 @@ const RulesPageComponent: React.FC = () => {
|
|||
return (
|
||||
<>
|
||||
<NeedAdminForUpdateRulesCallOut />
|
||||
<ReadOnlyRulesCallOut />
|
||||
<MissingPrivilegesCallOut />
|
||||
<MlJobCompatibilityCallout />
|
||||
<ValueListsModal
|
||||
showModal={showValueListsModal}
|
||||
|
|
|
@ -18750,12 +18750,6 @@
|
|||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "クエリ結果をプレビューするデータのタイムフレームを選択します",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "クイッククエリプレビュー",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading": "...loading",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.essenceDescription": "アラートを表示する権限のみが付与されています。アラート状態を更新 (アラートを開く、アラートを閉じる) 必要がある場合は、Kibana管理者に連絡してください。",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.messageDetail": "{essence} 関連ドキュメント:{docs}",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageTitle": "アラート状態を変更することはできません",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.essenceDescription": "現在、検出エンジンルールを作成/編集するための必要な権限がありません。サポートについては、管理者にお問い合わせください。",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.messageDetail": "{essence} 関連ドキュメント:{docs}",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageTitle": "ルールアクセス権が必要です",
|
||||
"xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "{countError, plural, one {このタブ} other {これらのタブ}}に無効な入力があります:{tabHasError}",
|
||||
"xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription": "開始",
|
||||
"xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription": "停止",
|
||||
|
|
|
@ -19018,12 +19018,6 @@
|
|||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewHelpText": "选择数据的时间范围以预览查询结果",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewLabel": "快速查询预览",
|
||||
"xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading": "...正在加载",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.essenceDescription": "您仅有权查看告警。如果您需要更新告警状态 (打开或关闭告警) ,请联系您的 Kibana 管理员。",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageBody.messageDetail": "{essence} 相关文档:{docs}",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyAlertsCallOut.messageTitle": "您无法更改告警状态",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.essenceDescription": "您当前缺少所需的权限,无法创建/编辑检测引擎规则。有关进一步帮助,请联系您的管理员。",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageBody.messageDetail": "{essence} 相关文档:{docs}",
|
||||
"xpack.securitySolution.detectionEngine.readOnlyRulesCallOut.messageTitle": "需要规则权限",
|
||||
"xpack.securitySolution.detectionEngine.rule.editRule.errorMsgDescription": "您在{countError, plural, other {以下选项卡}}中的输入无效:{tabHasError}",
|
||||
"xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription": "已启动",
|
||||
"xpack.securitySolution.detectionEngine.ruleDescription.mlJobStoppedDescription": "已停止",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue