Skip to content

Commit

Permalink
[Alerting] Export rules and connectors (elastic#98802)
Browse files Browse the repository at this point in the history
* Adding importableAndExportable but hidden saved object types to saved object feature privilege

* Adding helper function for transforming rule for export. Added audit logging

* Adding helper function for transforming rule for export. Added audit logging

* Adding unit test for transforming rules for export

* Exporting connectors

* Removing auditing during export

* Adding import/export to docs

* PR fixes

* Using action type validation onExport

* Fixing logic for connectors with optional secrets

* Fixing logic for connectors with optional secrets

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
ymao1 and kibanamachine committed May 5, 2021
1 parent 9f9c417 commit 909fa63
Show file tree
Hide file tree
Showing 11 changed files with 495 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/management/action-types.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ New connectors can be created by clicking the *Create connector* button, which w
[role="screenshot"]
image::images/connector-select-type.png[Connector select type]

[float]
[[importing-and-exporting-connectors]]
=== Importing and exporting connectors

To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.

[float]
[[create-connectors]]
=== Preconfigured connectors
Expand Down
6 changes: 6 additions & 0 deletions docs/user/alerting/rule-management.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ These operations can also be performed in bulk by multi-selecting rules and clic
[role="screenshot"]
image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk]

[float]
[[importing-and-exporting-rules]]
=== Importing and exporting rules

To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.

[float]
=== Required permissions

Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
}

plugins.features.registerKibanaFeature(ACTIONS_FEATURE);
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);

this.eventLogService = plugins.eventLog;
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
Expand Down Expand Up @@ -228,6 +227,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
this.actionExecutor = actionExecutor;
this.security = plugins.security;

setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects, this.actionTypeRegistry!);

registerBuiltInActionTypes({
logger: this.logger,
actionTypeRegistry,
Expand Down
17 changes: 15 additions & 2 deletions x-pack/plugins/actions/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@
* 2.0.
*/

import { SavedObject, SavedObjectsServiceSetup } from 'kibana/server';
import {
SavedObject,
SavedObjectsExportTransformContext,
SavedObjectsServiceSetup,
} from 'kibana/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import mappings from './mappings.json';
import { getMigrations } from './migrations';
import { RawAction } from '../types';
import { getImportResultMessage, GO_TO_CONNECTORS_BUTTON_LABLE } from './get_import_result_message';
import { transformConnectorsForExport } from './transform_connectors_for_export';
import { ActionTypeRegistry } from '../action_type_registry';

export const ACTION_SAVED_OBJECT_TYPE = 'action';
export const ALERT_SAVED_OBJECT_TYPE = 'alert';
export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params';

export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
actionTypeRegistry: ActionTypeRegistry
) {
savedObjects.registerType({
name: ACTION_SAVED_OBJECT_TYPE,
Expand All @@ -32,6 +39,12 @@ export function setupSavedObjects(
getTitle(obj) {
return `Connector: [${obj.attributes.name}]`;
},
onExport<RawAction>(
context: SavedObjectsExportTransformContext,
objects: Array<SavedObject<RawAction>>
) {
return transformConnectorsForExport(objects, actionTypeRegistry);
},
onImport(connectors) {
return {
warnings: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/*
* 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 { transformConnectorsForExport } from './transform_connectors_for_export';
import { ActionTypeRegistry, ActionTypeRegistryOpts } from '../action_type_registry';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../actions_config.mock';
import { licensingMock } from '../../../licensing/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { taskManagerMock } from '../../../task_manager/server/mocks';
import { ActionExecutor, TaskRunnerFactory } from '../lib';
import { registerBuiltInActionTypes } from '../builtin_action_types';

describe('transform connector for export', () => {
const actionTypeRegistryParams: ActionTypeRegistryOpts = {
licensing: licensingMock.createSetup(),
taskManager: taskManagerMock.createSetup(),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
actionsConfigUtils: actionsConfigMock.create(),
licenseState: licenseStateMock.create(),
preconfiguredActions: [],
};
const actionTypeRegistry: ActionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);

registerBuiltInActionTypes({
logger: loggingSystemMock.create().get(),
actionTypeRegistry,
actionsConfigUtils: actionsConfigMock.create(),
});

const connectorsWithNoSecrets = [
{
id: '1',
type: 'action',
attributes: {
actionTypeId: '.email',
name: 'email connector without auth',
isMissingSecrets: false,
config: {
hasAuth: false,
from: 'me@me.com',
host: 'host',
port: 22,
service: null,
secure: null,
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '2',
type: 'action',
attributes: {
actionTypeId: '.index',
name: 'index connector',
isMissingSecrets: false,
config: {
index: 'test-index',
refresh: false,
executionTimeField: null,
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '3',
type: 'action',
attributes: {
actionTypeId: '.server-log',
name: 'server log connector',
isMissingSecrets: false,
config: {},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '4',
type: 'action',
attributes: {
actionTypeId: '.webhook',
name: 'webhook connector without auth',
isMissingSecrets: false,
config: {
method: 'post',
hasAuth: false,
url: 'https://webhook',
headers: {},
},
secrets: 'asbqw4tqbef',
},
references: [],
},
];
const connectorsWithSecrets = [
{
id: '1',
type: 'action',
attributes: {
actionTypeId: '.email',
name: 'email connector with auth',
isMissingSecrets: false,
config: {
hasAuth: true,
from: 'me@me.com',
host: 'host',
port: 22,
service: null,
secure: null,
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '2',
type: 'action',
attributes: {
actionTypeId: '.resilient',
name: 'resilient connector',
isMissingSecrets: false,
config: {
apiUrl: 'https://resilient',
orgId: 'origId',
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '3',
type: 'action',
attributes: {
actionTypeId: '.servicenow',
name: 'servicenow itsm connector',
isMissingSecrets: false,
config: {
apiUrl: 'https://servicenow',
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '4',
type: 'action',
attributes: {
actionTypeId: '.pagerduty',
name: 'pagerduty connector',
isMissingSecrets: false,
config: {
apiUrl: 'https://pagerduty',
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '5',
type: 'action',
attributes: {
actionTypeId: '.jira',
name: 'jira connector',
isMissingSecrets: false,
config: {
apiUrl: 'https://jira',
projectKey: 'foo',
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '6',
type: 'action',
attributes: {
actionTypeId: '.teams',
name: 'teams connector',
isMissingSecrets: false,
config: {},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '7',
type: 'action',
attributes: {
actionTypeId: '.slack',
name: 'slack connector',
isMissingSecrets: false,
config: {},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '8',
type: 'action',
attributes: {
actionTypeId: '.servicenow-sir',
name: 'servicenow sir connector',
isMissingSecrets: false,
config: {
apiUrl: 'https://servicenow-sir',
},
secrets: 'asbqw4tqbef',
},
references: [],
},
{
id: '8',
type: 'action',
attributes: {
actionTypeId: '.webhook',
name: 'webhook connector with auth',
isMissingSecrets: false,
config: {
method: 'post',
hasAuth: true,
url: 'https://webhook',
headers: {},
},
secrets: 'asbqw4tqbef',
},
references: [],
},
];

it('should not change connectors without secrets', () => {
expect(transformConnectorsForExport(connectorsWithNoSecrets, actionTypeRegistry)).toEqual(
connectorsWithNoSecrets
);
});

it('should remove secrets for connectors with secrets', () => {
expect(transformConnectorsForExport(connectorsWithSecrets, actionTypeRegistry)).toEqual(
connectorsWithSecrets.map((connector) => ({
...connector,
attributes: {
...connector.attributes,
isMissingSecrets: true,
},
}))
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { SavedObject } from 'kibana/server';
import { ActionTypeRegistry } from '../action_type_registry';
import { validateSecrets } from '../lib';
import { RawAction, ActionType } from '../types';

export function transformConnectorsForExport(
connectors: SavedObject[],
actionTypeRegistry: ActionTypeRegistry
): Array<SavedObject<RawAction>> {
return connectors.map((c) => {
const connector = c as SavedObject<RawAction>;
return transformConnectorForExport(
connector,
actionTypeRegistry.get(connector.attributes.actionTypeId)
);
});
}

function transformConnectorForExport(
connector: SavedObject<RawAction>,
actionType: ActionType
): SavedObject<RawAction> {
let isMissingSecrets = false;
try {
// If connector requires secrets, this will throw an error
validateSecrets(actionType, {});

// If connector has optional (or no) secrets, set isMissingSecrets value to value of hasAuth
// If connector doesn't have hasAuth value, default to isMissingSecrets: false
isMissingSecrets = (connector?.attributes?.config?.hasAuth as boolean) ?? false;
} catch (err) {
isMissingSecrets = true;
}

// Skip connectors
return {
...connector,
attributes: {
...connector.attributes,
isMissingSecrets,
},
};
}
Loading

0 comments on commit 909fa63

Please sign in to comment.