Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add security support for alerts and actions #41389

Merged
merged 64 commits into from
Aug 20, 2019
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8fc2509
Initial work
mikecote Aug 7, 2019
07b923f
Cleanup add createAPIKey fn pt1
mikecote Aug 8, 2019
5954064
Change getServices function to take request as parameter
mikecote Aug 8, 2019
1e821f2
Use API key when executing alerts
mikecote Aug 8, 2019
37d1be5
Revert task manager using encrypted saved objects
mikecote Aug 9, 2019
28b2ae6
Store fired actions within a saved object to encrypt API keys
mikecote Aug 9, 2019
c8d742e
Merge with master
mikecote Aug 12, 2019
6e9a6fe
Fix fireActionId
mikecote Aug 12, 2019
5700426
Cleanup code, fix type check error
mikecote Aug 12, 2019
c0205a2
Add a type for getScopedSavedObjectsClient
mikecote Aug 12, 2019
b756589
Fix getBasePath and spaceIdToNamespace functions
mikecote Aug 12, 2019
3c296b5
Add safety check for API key and action
mikecote Aug 12, 2019
06a4c40
Merge branch 'master' of github.com:elastic/kibana into alerting/secu…
mikecote Aug 12, 2019
59f0807
Fix integration tests
mikecote Aug 12, 2019
c55cea5
Fix broken jest tests
mikecote Aug 12, 2019
72e0a87
Cleanup
mikecote Aug 12, 2019
b7f43d4
Rename generatedApiKey to apiKeyValue
mikecote Aug 12, 2019
32c5efc
Ensure access to action record
mikecote Aug 12, 2019
389510f
Cleanup
mikecote Aug 12, 2019
be69d1c
Add unit tests
mikecote Aug 12, 2019
416eda8
Fix variable conflict
mikecote Aug 12, 2019
c0d9fa6
Revert task manager specific code (no longer needed)
mikecote Aug 13, 2019
5bacfe2
Remove fire terminology
mikecote Aug 13, 2019
3ed5892
Move tests to spaces and security folder
mikecote Aug 13, 2019
79deb91
Use ES Archiver to remove spaces (empty_kibana)
mikecote Aug 13, 2019
e3cf792
Merge with master
mikecote Aug 13, 2019
3becd91
Fix missing pieces
mikecote Aug 13, 2019
d41855b
Convert action tests to run per user
mikecote Aug 14, 2019
ddc325a
Convert alerting tests to run per user
mikecote Aug 14, 2019
550ec16
Fix type check issue
mikecote Aug 14, 2019
ed16564
Merge branch 'master' of github.com:elastic/kibana into alerting/secu…
mikecote Aug 14, 2019
ffe0949
Fix failing test
mikecote Aug 15, 2019
7f26259
Add callCluster and savedObjectsClient authorization tests
mikecote Aug 15, 2019
ae5aca1
Make savedObjectsClient return 403 for authorization tests
mikecote Aug 15, 2019
51aab13
Cleanup
mikecote Aug 15, 2019
0f9f72e
Fix test failure
mikecote Aug 15, 2019
0875123
Common function to get data from test index
mikecote Aug 15, 2019
9ddd6e7
Create ObjectRemover
mikecote Aug 15, 2019
7bdbfa4
Merge with master
mikecote Aug 15, 2019
160ad25
Cleanup
mikecote Aug 15, 2019
9e2007f
Merge branch 'master' of github.com:elastic/kibana into alerting/secu…
mikecote Aug 16, 2019
f6a8273
useApiKey now provided to functions instead of relying on condition o…
mikecote Aug 16, 2019
6f3dfe1
Fix typo
mikecote Aug 16, 2019
d2b2215
Make tests it(...) start with should
mikecote Aug 16, 2019
6a1e9a5
Rename useApiKey to isSecurityEnabled
mikecote Aug 16, 2019
5ef6ebf
Merge apiKeyId and apiKeyValue into one
mikecote Aug 16, 2019
a7dcdfb
Update docs
mikecote Aug 16, 2019
62a1c00
Merge branch 'master' of github.com:elastic/kibana into alerting/secu…
mikecote Aug 16, 2019
d5dcd97
Merge with master
mikecote Aug 19, 2019
59495f3
Use feature controls for list alert / action types API
mikecote Aug 19, 2019
7ef8cce
Remove need to add ! in TypeScript for required plugins
mikecote Aug 19, 2019
4d1e2e9
Fix ESLint issue
mikecote Aug 19, 2019
b1a40d5
Include actions and alertTypeParams into AAD and genereate new API ke…
mikecote Aug 19, 2019
bdef9ed
Generate random id for API key name attribute
mikecote Aug 19, 2019
888337a
Merge branch 'master' of github.com:elastic/kibana into alerting/secu…
mikecote Aug 20, 2019
b2f67c7
Include interval in AAD
mikecote Aug 20, 2019
66fac87
Send pre-encoded string
mikecote Aug 20, 2019
21e5f1f
Fix ExecutorError
mikecote Aug 20, 2019
422174c
Fix apiKey snapshot
mikecote Aug 20, 2019
e505a65
Fix 'default' typo
mikecote Aug 20, 2019
243890e
De-compose apiKey
mikecote Aug 20, 2019
f99ab74
Refresh API key when enabling / disabling an alert
mikecote Aug 20, 2019
c6d5589
Add updatedBy
mikecote Aug 20, 2019
f5eb263
Make unauthorized APIs return 404
mikecote Aug 20, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion x-pack/legacy/plugins/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This is the primary function for an action type. Whenever the action needs to ex
|config|The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type.|
|params|Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function.|
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.|
|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>**NOTE**: This currently only works when security is disabled. A future PR will add support for enabling security using Elasticsearch API tokens.|
|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled).|
|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)|

### Example
Expand Down Expand Up @@ -146,6 +146,7 @@ The following table describes the properties of the `options` object.
|id|The id of the action you want to execute.|string|
|params|The `params` value to give the action type executor.|object|
|spaceId|The space id the action is within.|string|
|apiKey|The Elasticsearch API key to use for context. (Note: only required and used when security is enabled).|string|

### Example

Expand Down
14 changes: 14 additions & 0 deletions x-pack/legacy/plugins/actions/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,19 @@
"type": "binary"
}
}
},
"action_task_params": {
"properties": {
"actionId": {
"type": "keyword"
},
"params": {
"enabled": false,
"type": "object"
},
"apiKey": {
"type": "binary"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function getServices() {
}
const actionTypeRegistryParams = {
getServices,
isSecurityEnabled: true,
taskManager: mockTaskManager,
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.create(),
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down Expand Up @@ -64,11 +65,14 @@ describe('register()', () => {
},
]
`);
expect(getCreateTaskRunnerFunction).toHaveBeenCalledTimes(1);
const call = getCreateTaskRunnerFunction.mock.calls[0][0];
expect(call.actionTypeRegistry).toBeTruthy();
expect(call.encryptedSavedObjectsPlugin).toBeTruthy();
expect(call.getServices).toBeTruthy();
expect(getCreateTaskRunnerFunction).toHaveBeenCalledWith({
actionTypeRegistry,
isSecurityEnabled: true,
encryptedSavedObjectsPlugin: actionTypeRegistryParams.encryptedSavedObjectsPlugin,
getServices: actionTypeRegistryParams.getServices,
getBasePath: actionTypeRegistryParams.getBasePath,
spaceIdToNamespace: actionTypeRegistryParams.spaceIdToNamespace,
});
});

test('throws error if action type already registered', () => {
Expand Down
15 changes: 11 additions & 4 deletions x-pack/legacy/plugins/actions/server/action_type_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@

import Boom from 'boom';
import { i18n } from '@kbn/i18n';
import { ActionType, GetServicesFunction } from './types';
import { TaskManager, TaskRunCreatorFunction } from '../../task_manager';
import { getCreateTaskRunnerFunction, ExecutorError } from './lib';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import { SpacesPlugin } from '../../spaces';
import {
ActionType,
GetBasePathFunction,
GetServicesFunction,
SpaceIdToNamespaceFunction,
} from './types';

interface ConstructorOptions {
isSecurityEnabled: boolean;
taskManager: TaskManager;
getServices: GetServicesFunction;
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPlugin;
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
getBasePath: SpacesPlugin['getBasePath'];
spaceIdToNamespace: SpaceIdToNamespaceFunction;
getBasePath: GetBasePathFunction;
}

export class ActionTypeRegistry {
Expand All @@ -31,9 +36,11 @@ export class ActionTypeRegistry {
encryptedSavedObjectsPlugin,
spaceIdToNamespace,
getBasePath,
isSecurityEnabled,
}: ConstructorOptions) {
this.taskManager = taskManager;
this.taskRunCreatorFunction = getCreateTaskRunnerFunction({
isSecurityEnabled,
getServices,
actionTypeRegistry: this,
encryptedSavedObjectsPlugin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function getServices() {

const actionTypeRegistryParams = {
getServices,
isSecurityEnabled: true,
taskManager: mockTaskManager,
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
beforeAll(() => {
actionTypeRegistry = new ActionTypeRegistry({
getServices,
isSecurityEnabled: true,
taskManager: taskManagerMock.create(),
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
beforeAll(() => {
actionTypeRegistry = new ActionTypeRegistry({
getServices,
isSecurityEnabled: true,
taskManager: taskManagerMock.create(),
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const mockEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
beforeAll(() => {
actionTypeRegistry = new ActionTypeRegistry({
getServices,
isSecurityEnabled: true,
taskManager: taskManagerMock.create(),
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise<an
beforeAll(() => {
actionTypeRegistry = new ActionTypeRegistry({
getServices,
isSecurityEnabled: true,
taskManager: taskManagerMock.create(),
encryptedSavedObjectsPlugin: mockEncryptedSavedObjectsPlugin,
spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
Expand Down
143 changes: 116 additions & 27 deletions x-pack/legacy/plugins/actions/server/create_execute_function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import { SavedObjectsClientMock } from '../../../../../src/core/server/mocks';

const mockTaskManager = taskManagerMock.create();
const savedObjectsClient = SavedObjectsClientMock.create();
const spaceIdToNamespace = jest.fn();
const getBasePath = jest.fn();

beforeEach(() => jest.resetAllMocks());

describe('execute()', () => {
test('schedules the action with all given parameters', async () => {
const executeFn = createExecuteFunction({
getBasePath,
isSecurityEnabled: true,
taskManager: mockTaskManager,
internalSavedObjectsRepository: savedObjectsClient,
spaceIdToNamespace,
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
Expand All @@ -29,41 +30,129 @@ describe('execute()', () => {
},
references: [],
});
spaceIdToNamespace.mockReturnValueOnce('namespace1');
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
attributes: {},
references: [],
});
await executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: 'MTIzOmFiYw==',
mikecote marked this conversation as resolved.
Show resolved Hide resolved
});
expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1);
expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"params": Object {
"id": "123",
"params": Object {
"baz": false,
},
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:mock-action",
},
]
`);
expect(savedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"action",
"123",
Object {
"namespace": "namespace1",
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:mock-action",
},
]
`);
expect(spaceIdToNamespace).toHaveBeenCalledWith('default');
expect(savedObjectsClient.get).toHaveBeenCalledWith('action', '123');
expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', {
actionId: '123',
params: { baz: false },
apiKey: 'MTIzOmFiYw==',
});
});

test('uses API key when provided', async () => {
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
const executeFn = createExecuteFunction({
getBasePath,
taskManager: mockTaskManager,
getScopedSavedObjectsClient,
isSecurityEnabled: true,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: 'action',
attributes: {
actionTypeId: 'mock-action',
},
references: [],
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
attributes: {},
references: [],
});

await executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: 'MTIzOmFiYw==',
});
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
getBasePath: expect.anything(),
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
});
});

test(`doesn't use API keys when not provided`, async () => {
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
const executeFn = createExecuteFunction({
isSecurityEnabled: false,
getBasePath,
taskManager: mockTaskManager,
getScopedSavedObjectsClient,
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: 'action',
attributes: {
actionTypeId: 'mock-action',
},
references: [],
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
attributes: {},
references: [],
});

await executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
});
expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({
getBasePath: expect.anything(),
headers: {},
});
});

test(`throws an error when isSecurityEnabled is true and key not passed in`, async () => {
const executeFn = createExecuteFunction({
getBasePath,
taskManager: mockTaskManager,
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
isSecurityEnabled: true,
});
await expect(
executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"API key is required. The attribute \\"apiKey\\" is missing."`
);
});
});
43 changes: 33 additions & 10 deletions x-pack/legacy/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,57 @@

import { SavedObjectsClientContract } from 'src/core/server';
import { TaskManager } from '../../task_manager';
import { SpacesPlugin } from '../../spaces';
import { GetBasePathFunction } from './types';

interface CreateExecuteFunctionOptions {
isSecurityEnabled: boolean;
taskManager: TaskManager;
internalSavedObjectsRepository: SavedObjectsClientContract;
spaceIdToNamespace: SpacesPlugin['spaceIdToNamespace'];
getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract;
getBasePath: GetBasePathFunction;
}

export interface ExecuteOptions {
id: string;
params: Record<string, any>;
spaceId: string;
apiKey?: string;
}

export function createExecuteFunction({
getBasePath,
taskManager,
internalSavedObjectsRepository,
spaceIdToNamespace,
isSecurityEnabled,
getScopedSavedObjectsClient,
}: CreateExecuteFunctionOptions) {
return async function execute({ id, params, spaceId }: ExecuteOptions) {
const namespace = spaceIdToNamespace(spaceId);
const actionSavedObject = await internalSavedObjectsRepository.get('action', id, { namespace });
return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) {
const requestHeaders: Record<string, string> = {};

if (isSecurityEnabled && !apiKey) {
throw new Error('API key is required. The attribute "apiKey" is missing.');
mikecote marked this conversation as resolved.
Show resolved Hide resolved
} else if (isSecurityEnabled) {
requestHeaders.authorization = `ApiKey ${apiKey}`;
}

// Since we're using API keys and accessing elasticsearch can only be done
// via a request, we're faking one with the proper authorization headers.
const fakeRequest: any = {
headers: requestHeaders,
getBasePath: () => getBasePath(spaceId),
};

const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest);
const actionSavedObject = await savedObjectsClient.get('action', id);
const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
mikecote marked this conversation as resolved.
Show resolved Hide resolved
actionId: id,
params,
apiKey,
});

await taskManager.schedule({
taskType: `actions:${actionSavedObject.attributes.actionTypeId}`,
params: {
id,
spaceId,
params,
actionTaskParamsId: actionTaskParamsRecord.id,
},
state: {},
scope: ['actions'],
Expand Down
Loading