[SIEM][Detection Engine] Adds privileges API endpoint
## Summary Adds a privileges API endpoint for the UI and people to query to check to see if their namespaced index is going to have the correct privileges or not. Usage: Testing: Set up your user name and password to a test space for the CLI. Give whatever permissions you want for restricted access to the test-space user, test-space role, and the test-space actual space to ensure everything works out as expected. ```sh export ELASTICSEARCH_USERNAME=test-space export ELASTICSEARCH_PASSWORD=(passwword) export SPACE_URL=/s/test-space ``` Then use it like so API: ```sh GET /api/detection_engine/privileges ``` CLI: ```sh ./get_privileges.sh ``` Return will be something like this: ```sh { "username": "test-space", "has_all_requested": false, "cluster": { "monitor_ml": true, "manage_ccr": false, "manage_index_templates": true, "monitor_watcher": true, "monitor_transform": true, "read_ilm": true, "manage_api_key": false, "manage_security": false, "manage_own_api_key": false, "manage_saml": false, "all": false, "manage_ilm": true, "manage_ingest_pipelines": true, "read_ccr": false, "manage_rollup": true, "monitor": true, "manage_watcher": true, "manage": true, "manage_transform": true, "manage_token": false, "manage_ml": true, "manage_pipeline": true, "monitor_rollup": true, "transport_client": true, "create_snapshot": true }, "index": { ".siem-signals-test-space": { "all": false, "manage_ilm": true, "read": false, "create_index": true, "read_cross_cluster": false, "index": false, "monitor": true, "delete": false, "manage": true, "delete_index": true, "create_doc": false, "view_index_metadata": true, "create": false, "manage_follow_index": true, "manage_leader_index": true, "write": false } }, "application": {} } ``` Example permissions that work for managing all signal indexes across all spaces so that the user in question can create it for each space: <img width="1274" alt="Screen Shot 2019-12-10 at 4 48 19 PM" src="https://user-images.githubusercontent.com/1151048/70579132-f63f8f80-1b6c-11ea-86f7-204fd2163cea.png"> Example permissions that work for managing only a specific signal index: <img width="1234" alt="Screen Shot 2019-12-10 at 3 49 24 PM" src="https://user-images.githubusercontent.com/1151048/70579185-11aa9a80-1b6d-11ea-8a33-311e85ce5dc9.png"> Example permissions that work for an end user using signals across all spaces: <img width="1229" alt="Screen Shot 2019-12-10 at 3 49 41 PM" src="https://user-images.githubusercontent.com/1151048/70579233-31da5980-1b6d-11ea-8b3a-85703ebcc57f.png"> Example permissions that work for an end user using signals for a a specific index: <img width="1234" alt="Screen Shot 2019-12-10 at 3 49 24 PM" src="https://user-images.githubusercontent.com/1151048/70579259-43bbfc80-1b6d-11ea-94c1-8bbc65e621b2.png"> ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
7658e9c631
commit
f53e1a9dbd
|
@ -45,6 +45,7 @@ export const SIGNALS_ID = `${APP_ID}.signals`;
|
|||
*/
|
||||
export const DETECTION_ENGINE_URL = '/api/detection_engine';
|
||||
export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`;
|
||||
export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`;
|
||||
export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`;
|
||||
|
||||
/**
|
||||
|
|
|
@ -19,6 +19,7 @@ import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_s
|
|||
import { ServerFacade } from './types';
|
||||
import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route';
|
||||
import { isAlertExecutor } from './lib/detection_engine/signals/types';
|
||||
import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route';
|
||||
|
||||
const APP_ID = 'siem';
|
||||
|
||||
|
@ -52,4 +53,7 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy
|
|||
createIndexRoute(__legacy);
|
||||
readIndexRoute(__legacy);
|
||||
deleteIndexRoute(__legacy);
|
||||
|
||||
// Privileges API to get the generic user privileges
|
||||
readPrivilegesRoute(__legacy);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
// See the reference(s) below on explanations about why -000001 was chosen and
|
||||
// why the is_write_index is true as well as the bootstrapping step which is needed.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesDeleteParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const deleteAllIndex = async (
|
||||
callWithRequest: CallWithRequest<IndicesDeleteParams, CallClusterOptions, boolean>,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const deletePolicy = async (
|
||||
callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesDeleteTemplateParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const deleteTemplate = async (
|
||||
callWithRequest: CallWithRequest<IndicesDeleteTemplateParams, CallClusterOptions, unknown>,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesExistsParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const getIndexExists = async (
|
||||
callWithRequest: CallWithRequest<IndicesExistsParams, CallClusterOptions, boolean>,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const getPolicyExists = async (
|
||||
callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesExistsTemplateParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const getTemplateExists = async (
|
||||
callWithRequest: CallWithRequest<IndicesExistsTemplateParams, CallClusterOptions, boolean>,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesGetSettingsParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const readIndex = async (
|
||||
callWithRequest: CallWithRequest<IndicesGetSettingsParams, CallClusterOptions, unknown>,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const setPolicy = async (
|
||||
callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { IndicesPutTemplateParams } from 'elasticsearch';
|
||||
import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { CallWithRequest } from './types';
|
||||
import { CallWithRequest } from '../types';
|
||||
|
||||
export const setTemplate = async (
|
||||
callWithRequest: CallWithRequest<IndicesPutTemplateParams, CallClusterOptions, unknown>,
|
||||
|
|
|
@ -1,7 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type CallWithRequest<T, U, V> = (endpoint: string, params: T, options?: U) => Promise<V>;
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { CallWithRequest } from '../types';
|
||||
|
||||
export const readPrivileges = async (
|
||||
callWithRequest: CallWithRequest<unknown, unknown, unknown>,
|
||||
index: string
|
||||
): Promise<unknown> => {
|
||||
return callWithRequest('transport.request', {
|
||||
path: `_security/user/_has_privileges`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: [
|
||||
'all',
|
||||
'create_snapshot',
|
||||
'manage',
|
||||
'manage_api_key',
|
||||
'manage_ccr',
|
||||
'manage_transform',
|
||||
'manage_ilm',
|
||||
'manage_index_templates',
|
||||
'manage_ingest_pipelines',
|
||||
'manage_ml',
|
||||
'manage_own_api_key',
|
||||
'manage_pipeline',
|
||||
'manage_rollup',
|
||||
'manage_saml',
|
||||
'manage_security',
|
||||
'manage_token',
|
||||
'manage_watcher',
|
||||
'monitor',
|
||||
'monitor_transform',
|
||||
'monitor_ml',
|
||||
'monitor_rollup',
|
||||
'monitor_watcher',
|
||||
'read_ccr',
|
||||
'read_ilm',
|
||||
'transport_client',
|
||||
],
|
||||
index: [
|
||||
{
|
||||
names: [index],
|
||||
privileges: [
|
||||
'all',
|
||||
'create',
|
||||
'create_doc',
|
||||
'create_index',
|
||||
'delete',
|
||||
'delete_index',
|
||||
'index',
|
||||
'manage',
|
||||
'manage_follow_index',
|
||||
'manage_ilm',
|
||||
'manage_leader_index',
|
||||
'monitor',
|
||||
'read',
|
||||
'read_cross_cluster',
|
||||
'view_index_metadata',
|
||||
'write',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
|
@ -10,6 +10,7 @@ import { SignalsStatusRestParams, SignalsQueryRestParams } from '../../signals/t
|
|||
import {
|
||||
DETECTION_ENGINE_RULES_URL,
|
||||
DETECTION_ENGINE_SIGNALS_STATUS_URL,
|
||||
DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
DETECTION_ENGINE_QUERY_SIGNALS_URL,
|
||||
} from '../../../../../common/constants';
|
||||
import { RuleAlertType } from '../../rules/types';
|
||||
|
@ -81,6 +82,11 @@ export const getFindRequest = (): ServerInjectOptions => ({
|
|||
url: `${DETECTION_ENGINE_RULES_URL}/_find`,
|
||||
});
|
||||
|
||||
export const getPrivilegeRequest = (): ServerInjectOptions => ({
|
||||
method: 'GET',
|
||||
url: `${DETECTION_ENGINE_PRIVILEGES_URL}`,
|
||||
});
|
||||
|
||||
interface FindHit {
|
||||
page: number;
|
||||
perPage: number;
|
||||
|
@ -225,3 +231,56 @@ export const updateActionResult = (): ActionResult => ({
|
|||
name: '',
|
||||
config: {},
|
||||
});
|
||||
|
||||
export const getMockPrivileges = () => ({
|
||||
username: 'test-space',
|
||||
has_all_requested: false,
|
||||
cluster: {
|
||||
monitor_ml: true,
|
||||
manage_ccr: false,
|
||||
manage_index_templates: true,
|
||||
monitor_watcher: true,
|
||||
monitor_transform: true,
|
||||
read_ilm: true,
|
||||
manage_api_key: false,
|
||||
manage_security: false,
|
||||
manage_own_api_key: false,
|
||||
manage_saml: false,
|
||||
all: false,
|
||||
manage_ilm: true,
|
||||
manage_ingest_pipelines: true,
|
||||
read_ccr: false,
|
||||
manage_rollup: true,
|
||||
monitor: true,
|
||||
manage_watcher: true,
|
||||
manage: true,
|
||||
manage_transform: true,
|
||||
manage_token: false,
|
||||
manage_ml: true,
|
||||
manage_pipeline: true,
|
||||
monitor_rollup: true,
|
||||
transport_client: true,
|
||||
create_snapshot: true,
|
||||
},
|
||||
index: {
|
||||
'.siem-signals-frank-hassanabad-test-space': {
|
||||
all: false,
|
||||
manage_ilm: true,
|
||||
read: false,
|
||||
create_index: true,
|
||||
read_cross_cluster: false,
|
||||
index: false,
|
||||
monitor: true,
|
||||
delete: false,
|
||||
manage: true,
|
||||
delete_index: true,
|
||||
create_doc: false,
|
||||
view_index_metadata: true,
|
||||
create: false,
|
||||
manage_follow_index: true,
|
||||
manage_leader_index: true,
|
||||
write: false,
|
||||
},
|
||||
},
|
||||
application: {},
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
|
|||
const template = getSignalsTemplate(index);
|
||||
await setTemplate(callWithRequest, index, template);
|
||||
}
|
||||
createBootstrapIndex(callWithRequest, index);
|
||||
await createBootstrapIndex(callWithRequest, index);
|
||||
return { acknowledged: true };
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { createMockServer } from '../__mocks__/_mock_server';
|
||||
import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses';
|
||||
import { readPrivilegesRoute } from './read_privileges_route';
|
||||
import * as myUtils from '../utils';
|
||||
|
||||
describe('read_privileges', () => {
|
||||
let { server, elasticsearch } = createMockServer();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex');
|
||||
({ server, elasticsearch } = createMockServer());
|
||||
elasticsearch.getCluster = jest.fn(() => ({
|
||||
callWithRequest: jest.fn(() => getMockPrivileges()),
|
||||
}));
|
||||
readPrivilegesRoute(server);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('normal status codes', () => {
|
||||
test('returns 200 when doing a normal request', async () => {
|
||||
const { statusCode } = await server.inject(getPrivilegeRequest());
|
||||
expect(statusCode).toBe(200);
|
||||
});
|
||||
|
||||
test('returns the payload when doing a normal request', async () => {
|
||||
const { payload } = await server.inject(getPrivilegeRequest());
|
||||
expect(JSON.parse(payload)).toEqual(getMockPrivileges());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 Hapi from 'hapi';
|
||||
import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants';
|
||||
import { RulesRequest } from '../../rules/types';
|
||||
import { ServerFacade } from '../../../../types';
|
||||
import { callWithRequestFactory, transformError, getIndex } from '../utils';
|
||||
import { readPrivileges } from '../../privileges/read_privileges';
|
||||
|
||||
export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.ServerRoute => {
|
||||
return {
|
||||
method: 'GET',
|
||||
path: DETECTION_ENGINE_PRIVILEGES_URL,
|
||||
options: {
|
||||
tags: ['access:siem'],
|
||||
validate: {
|
||||
options: {
|
||||
abortEarly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(request: RulesRequest) {
|
||||
try {
|
||||
const callWithRequest = callWithRequestFactory(request, server);
|
||||
const index = getIndex(request, server);
|
||||
const permissions = await readPrivileges(callWithRequest, index);
|
||||
return permissions;
|
||||
} catch (err) {
|
||||
return transformError(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const readPrivilegesRoute = (server: ServerFacade): void => {
|
||||
server.route(createReadPrivilegesRulesRoute(server));
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
./check_env_variables.sh
|
||||
|
||||
# Example: ./get_privileges.sh
|
||||
curl -s -k \
|
||||
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
|
||||
-X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/privileges | jq .
|
|
@ -65,3 +65,5 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & {
|
|||
created_by: string | undefined | null;
|
||||
updated_by: string | undefined | null;
|
||||
};
|
||||
|
||||
export type CallWithRequest<T, U, V> = (endpoint: string, params: T, options?: U) => Promise<V>;
|
||||
|
|
Loading…
Reference in a new issue