Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[SIEM] [Detection engine] Permission II (elastic#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
XavierM authored and jkelastic committed Jan 17, 2020

Verified

This commit was signed with the committer’s verified signature.
1 parent 21f18dd commit 368d931
Showing 34 changed files with 953 additions and 339 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
`;

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
@@ -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(
(
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
@@ -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) {
@@ -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
@@ -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;

@@ -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) {
@@ -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 });
}
}
}
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 368d931

Please sign in to comment.