Skip to content

Commit

Permalink
[SIEM] [Detection engine] Permission II (#54292)
Browse files Browse the repository at this point in the history
* allow read only user with no CRUD

* use ../../lib/kibana

* fix timeline-template

* add re-routing on page

* bug

* cleanup

* review I

* review II

* a pretty shameful bug I will live thanks Frank

* bug select rule

* only activate deactivate if user has the manage permission

* add permissions rule with manage api key

* bug on batch action for rules

* add permissions to write status on signal
  • Loading branch information
XavierM authored Jan 11, 2020
1 parent 10733b5 commit b057f18
Show file tree
Hide file tree
Showing 34 changed files with 953 additions and 339 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as i18n from './translations';

const InspectContainer = styled.div<{ showInspect: boolean }>`
.euiButtonIcon {
${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0')}
${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0;')}
transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease;
}
`;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -149,20 +149,23 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
);
}, []);

const handleTimelineChange = useCallback(options => {
const selectedTimeline = options.filter(
(option: { checked: string }) => option.checked === 'on'
);
if (selectedTimeline != null && selectedTimeline.length > 0 && onTimelineChange != null) {
onTimelineChange(
isEmpty(selectedTimeline[0].title)
? i18nTimeline.UNTITLED_TIMELINE
: selectedTimeline[0].title,
selectedTimeline[0].id
const handleTimelineChange = useCallback(
options => {
const selectedTimeline = options.filter(
(option: { checked: string }) => option.checked === 'on'
);
}
setIsPopoverOpen(false);
}, []);
if (selectedTimeline != null && selectedTimeline.length > 0) {
onTimelineChange(
isEmpty(selectedTimeline[0].title)
? i18nTimeline.UNTITLED_TIMELINE
: selectedTimeline[0].title,
selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id
);
}
setIsPopoverOpen(false);
},
[onTimelineChange]
);

const handleOnScroll = useCallback(
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import {
NewRule,
Rule,
FetchRuleProps,
BasicFetchProps,
} from './types';
import { throwIfNotOk } from '../../../hooks/api/api';
import { DETECTION_ENGINE_RULES_URL } from '../../../../common/constants';
import {
DETECTION_ENGINE_RULES_URL,
DETECTION_ENGINE_PREPACKAGED_URL,
} from '../../../../common/constants';

/**
* Add provided Rule
Expand Down Expand Up @@ -199,3 +203,22 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru
responses.map<Promise<Rule>>(response => response.json())
);
};

/**
* Create Prepackaged Rules
*
* @param signal AbortSignal for cancelling request
*/
export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => {
const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, {
method: 'PUT',
credentials: 'same-origin',
headers: {
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
signal,
});
await throwIfNotOk(response);
return true;
};
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,7 @@ export interface DeleteRulesProps {
export interface DuplicateRulesProps {
rules: Rules;
}

export interface BasicFetchProps {
signal: AbortSignal;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,24 @@ export const SIGNAL_FETCH_FAILURE = i18n.translate(
defaultMessage: 'Failed to query signals',
}
);

export const PRIVILEGE_FETCH_FAILURE = i18n.translate(
'xpack.siem.containers.detectionEngine.signals.errorFetchingSignalsDescription',
{
defaultMessage: 'Failed to query signals',
}
);

export const SIGNAL_GET_NAME_FAILURE = i18n.translate(
'xpack.siem.containers.detectionEngine.signals.errorGetSignalDescription',
{
defaultMessage: 'Failed to get signal index name',
}
);

export const SIGNAL_POST_FAILURE = i18n.translate(
'xpack.siem.containers.detectionEngine.signals.errorPostSignalDescription',
{
defaultMessage: 'Failed to create signal index',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,29 @@

import { useEffect, useState } from 'react';

import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
import { getUserPrivilege } from './api';
import * as i18n from './translations';

type Return = [boolean, boolean | null, boolean | null];

interface Return {
loading: boolean;
isAuthenticated: boolean | null;
hasIndexManage: boolean | null;
hasManageApiKey: boolean | null;
hasIndexWrite: boolean | null;
}
/**
* Hook to get user privilege from
*
*/
export const usePrivilegeUser = (): Return => {
const [loading, setLoading] = useState(true);
const [isAuthenticated, setAuthenticated] = useState<boolean | null>(null);
const [hasWrite, setHasWrite] = useState<boolean | null>(null);
const [hasIndexManage, setHasIndexManage] = useState<boolean | null>(null);
const [hasIndexWrite, setHasIndexWrite] = useState<boolean | null>(null);
const [hasManageApiKey, setHasManageApiKey] = useState<boolean | null>(null);
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
let isSubscribed = true;
Expand All @@ -34,13 +45,21 @@ export const usePrivilegeUser = (): Return => {
setAuthenticated(privilege.isAuthenticated);
if (privilege.index != null && Object.keys(privilege.index).length > 0) {
const indexName = Object.keys(privilege.index)[0];
setHasWrite(privilege.index[indexName].create_index);
setHasIndexManage(privilege.index[indexName].manage);
setHasIndexWrite(privilege.index[indexName].write);
setHasManageApiKey(
privilege.cluster.manage_security ||
privilege.cluster.manage_api_key ||
privilege.cluster.manage_own_api_key
);
}
}
} catch (error) {
if (isSubscribed) {
setAuthenticated(false);
setHasWrite(false);
setHasIndexManage(false);
setHasIndexWrite(false);
errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster });
}
}
if (isSubscribed) {
Expand All @@ -55,5 +74,5 @@ export const usePrivilegeUser = (): Return => {
};
}, []);

return [loading, isAuthenticated, hasWrite];
return { loading, isAuthenticated, hasIndexManage, hasManageApiKey, hasIndexWrite };
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { useEffect, useState, useRef } from 'react';

import { errorToToaster } from '../../../components/ml/api/error_to_toaster';
import { useStateToaster } from '../../../components/toasters';
import { createPrepackagedRules } from '../rules';
import { createSignalIndex, getSignalIndex } from './api';
import * as i18n from './translations';
import { PostSignalError } from './types';
import { PostSignalError, SignalIndexError } from './types';

type Func = () => void;

Expand Down Expand Up @@ -40,11 +41,15 @@ export const useSignalIndex = (): Return => {
if (isSubscribed && signal != null) {
setSignalIndexName(signal.name);
setSignalIndexExists(true);
createPrepackagedRules({ signal: abortCtrl.signal });
}
} catch (error) {
if (isSubscribed) {
setSignalIndexName(null);
setSignalIndexExists(false);
if (error instanceof SignalIndexError && error.statusCode !== 404) {
errorToToaster({ title: i18n.SIGNAL_GET_NAME_FAILURE, error, dispatchToaster });
}
}
}
if (isSubscribed) {
Expand All @@ -69,7 +74,7 @@ export const useSignalIndex = (): Return => {
} else {
setSignalIndexName(null);
setSignalIndexExists(false);
errorToToaster({ title: i18n.SIGNAL_FETCH_FAILURE, error, dispatchToaster });
errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster });
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiCallOut, EuiButton } from '@elastic/eui';
import React, { memo, useCallback, useState } from 'react';

import * as i18n from './translations';

const NoWriteSignalsCallOutComponent = () => {
const [showCallOut, setShowCallOut] = useState(true);
const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);

return showCallOut ? (
<EuiCallOut title={i18n.NO_WRITE_SIGNALS_CALLOUT_TITLE} color="warning" iconType="alert">
<p>{i18n.NO_WRITE_SIGNALS_CALLOUT_MSG}</p>
<EuiButton color="warning" onClick={handleCallOut}>
{i18n.DISMISS_CALLOUT}
</EuiButton>
</EuiCallOut>
) : null;
};

export const NoWriteSignalsCallOut = memo(NoWriteSignalsCallOutComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';

export const NO_WRITE_SIGNALS_CALLOUT_TITLE = i18n.translate(
'xpack.siem.detectionEngine.noWriteSignalsCallOutTitle',
{
defaultMessage: 'Signals index permissions required',
}
);

export const NO_WRITE_SIGNALS_CALLOUT_MSG = i18n.translate(
'xpack.siem.detectionEngine.noWriteSignalsCallOutMsg',
{
defaultMessage:
'You are currently missing the required permissions to update signals. Please contact your administrator for further assistance.',
}
);

export const DISMISS_CALLOUT = i18n.translate(
'xpack.siem.detectionEngine.dismissNoWriteSignalButton',
{
defaultMessage: 'Dismiss',
}
);
Loading

0 comments on commit b057f18

Please sign in to comment.