From 8136c91c215c7afa857369c8c322041a11e61aee Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 12 May 2020 12:40:43 -0400 Subject: [PATCH] [Alerting] refactor action whitelisting functions https://github.com/elastic/kibana/issues/64659 In this PR, the action whitelisting utilities have been refactored to allow them to (eventually) be used in plugins other than the actions plugin. Prior to this, actions required deeper integration with the internal of the actions plugin. This also adds some generic typing to the actionType config, secrets, and params properties, since they are now referred to in multiple places within the actionType. --- .../server/action_type_registry.mock.ts | 2 + .../actions/server/action_type_registry.ts | 13 +++- .../plugins/actions/server/actions_client.ts | 10 ++- .../plugins/actions/server/actions_config.ts | 12 ++- .../server/builtin_action_types/email.test.ts | 67 ++++++++-------- .../server/builtin_action_types/email.ts | 76 +++++++++---------- .../builtin_action_types/es_index.test.ts | 27 ++++--- .../builtin_action_types/pagerduty.test.ts | 41 +++++----- .../builtin_action_types/server_log.test.ts | 32 +++++--- .../server/builtin_action_types/server_log.ts | 11 ++- .../server/builtin_action_types/slack.test.ts | 23 +++--- .../builtin_action_types/webhook.test.ts | 30 ++++---- x-pack/plugins/actions/server/index.ts | 1 + .../actions/server/lib/action_executor.ts | 7 +- .../server/lib/validate_with_schema.test.ts | 47 +++++++----- .../server/lib/validate_with_schema.ts | 69 +++++++++-------- x-pack/plugins/actions/server/mocks.ts | 11 ++- x-pack/plugins/actions/server/types.ts | 63 ++++++++------- 18 files changed, 306 insertions(+), 236 deletions(-) diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index d14d0ca2ddf84..0282b4de7ea65 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -5,6 +5,7 @@ */ import { ActionTypeRegistryContract } from './types'; +import { createValidationServiceMock } from './mocks'; const createActionTypeRegistryMock = () => { const mocked: jest.Mocked = { @@ -15,6 +16,7 @@ const createActionTypeRegistryMock = () => { ensureActionTypeEnabled: jest.fn(), isActionTypeEnabled: jest.fn(), isActionExecutable: jest.fn(), + getValidationService: jest.fn(() => createValidationServiceMock()), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 1f7409fedd2c2..b5616d3030f2c 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,9 +8,9 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType, PreConfiguredAction } from './types'; +import { ActionType, PreConfiguredAction, ActionValidationService } from './types'; import { ActionType as CommonActionType } from '../common'; -import { ActionsConfigurationUtilities } from './actions_config'; +import { ActionsConfigurationUtilities, getValidationService } from './actions_config'; export interface ActionTypeRegistryOpts { taskManager: TaskManagerSetupContract; @@ -27,6 +27,7 @@ export class ActionTypeRegistry { private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly validationService: ActionValidationService; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; @@ -34,6 +35,7 @@ export class ActionTypeRegistry { this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; this.preconfiguredActions = constructorParams.preconfiguredActions; + this.validationService = getValidationService(this.actionsConfigUtils); } /** @@ -43,6 +45,13 @@ export class ActionTypeRegistry { return this.actionTypes.has(id); } + /** + * Return the validation service. + */ + public getValidationService(): ActionValidationService { + return this.validationService; + } + /** * Throws error if action type is not enabled. */ diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index c9052cf53d948..07f2818c92b58 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -75,9 +75,10 @@ export class ActionsClient { */ public async create({ action }: CreateOptions): Promise { const { actionTypeId, name, config, secrets } = action; + const validationService = this.actionTypeRegistry.getValidationService(); const actionType = this.actionTypeRegistry.get(actionTypeId); - const validatedActionTypeConfig = validateConfig(actionType, config); - const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + const validatedActionTypeConfig = validateConfig(actionType, config, validationService); + const validatedActionTypeSecrets = validateSecrets(actionType, secrets, validationService); this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); @@ -119,8 +120,9 @@ export class ActionsClient { const { actionTypeId } = existingObject.attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); - const validatedActionTypeConfig = validateConfig(actionType, config); - const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + const validationService = this.actionTypeRegistry.getValidationService(); + const validatedActionTypeConfig = validateConfig(actionType, config, validationService); + const validatedActionTypeSecrets = validateSecrets(actionType, secrets, validationService); this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b15fe5b4007c5..d0885abe1a9ad 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -10,7 +10,7 @@ import { URL } from 'url'; import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; -import { ActionsConfigType } from './types'; +import { ActionsConfigType, ActionValidationService } from './types'; import { ActionTypeDisabledError } from './lib'; export enum WhitelistedHosts { @@ -109,3 +109,13 @@ export function getActionsConfigurationUtilities( }, }; } + +export function getValidationService( + configUtils: ActionsConfigurationUtilities +): ActionValidationService { + const { isWhitelistedHostname, isWhitelistedUri } = configUtils; + return { + isWhitelistedHostname, + isWhitelistedUri, + }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 1a24622e1cabb..c3f1707f8dd51 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -8,29 +8,21 @@ jest.mock('./lib/send_email', () => ({ sendEmail: jest.fn(), })); -import { Logger } from '../../../../../src/core/server'; - import { ActionType, ActionTypeExecutorOptions } from '../types'; -import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; import { actionsMock } from '../mocks'; -import { - ActionParamsType, - ActionTypeConfigType, - ActionTypeSecretsType, - getActionType, -} from './email'; +import { ActionParamsType, ActionTypeConfigType, ActionTypeSecretsType } from './email'; const sendEmailMock = sendEmail as jest.Mock; const ACTION_TYPE_ID = '.email'; const services = actionsMock.createServices(); +const validationService = actionsMock.createValidationService(); let actionType: ActionType; -let mockedLogger: jest.Mocked; beforeEach(() => { jest.resetAllMocks(); @@ -51,7 +43,8 @@ describe('config validation', () => { service: 'gmail', from: 'bob@example.com', }; - expect(validateConfig(actionType, config)).toEqual({ + validationService.isWhitelistedHostname.mockReturnValue(true); + expect(validateConfig(actionType, config, validationService)).toEqual({ ...config, host: null, port: null, @@ -61,7 +54,7 @@ describe('config validation', () => { delete config.service; config.host = 'elastic.co'; config.port = 8080; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...config, service: null, secure: null, @@ -75,38 +68,42 @@ describe('config validation', () => { // empty object expect(() => { - validateConfig(actionType, {}); + validateConfig(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [from]: expected value of type [string] but got [undefined]"` ); // no service or host/port expect(() => { - validateConfig(actionType, baseConfig); + validateConfig(actionType, baseConfig, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: either [service] or [host]/[port] is required"` ); // host but no port expect(() => { - validateConfig(actionType, { ...baseConfig, host: 'elastic.co' }); + validateConfig(actionType, { ...baseConfig, host: 'elastic.co' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [port] is required if [service] is not provided"` ); // port but no host expect(() => { - validateConfig(actionType, { ...baseConfig, port: 8080 }); + validateConfig(actionType, { ...baseConfig, port: 8080 }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [host] is required if [service] is not provided"` ); // invalid service expect(() => { - validateConfig(actionType, { - ...baseConfig, - service: 'bad-nodemailer-service', - }); + validateConfig( + actionType, + { + ...baseConfig, + service: 'bad-nodemailer-service', + }, + validationService + ); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [service] value 'bad-nodemailer-service' is not valid"` ); @@ -117,13 +114,9 @@ describe('config validation', () => { const NODEMAILER_AOL_SERVICE_HOST = 'smtp.aol.com'; test('config validation handles email host whitelisting', () => { - actionType = getActionType({ - logger: mockedLogger, - configurationUtilities: { - ...actionsConfigMock.create(), - isWhitelistedHostname: (hostname) => hostname === NODEMAILER_AOL_SERVICE_HOST, - }, - }); + validationService.isWhitelistedHostname.mockImplementation( + (hostname) => hostname === NODEMAILER_AOL_SERVICE_HOST + ); const baseConfig = { from: 'bob@example.com', }; @@ -147,23 +140,23 @@ describe('config validation', () => { port: 42, }; - const validatedConfig1 = validateConfig(actionType, whitelistedConfig1); + const validatedConfig1 = validateConfig(actionType, whitelistedConfig1, validationService); expect(validatedConfig1.service).toEqual(whitelistedConfig1.service); expect(validatedConfig1.from).toEqual(whitelistedConfig1.from); - const validatedConfig2 = validateConfig(actionType, whitelistedConfig2); + const validatedConfig2 = validateConfig(actionType, whitelistedConfig2, validationService); expect(validatedConfig2.host).toEqual(whitelistedConfig2.host); expect(validatedConfig2.port).toEqual(whitelistedConfig2.port); expect(validatedConfig2.from).toEqual(whitelistedConfig2.from); expect(() => { - validateConfig(actionType, notWhitelistedConfig1); + validateConfig(actionType, notWhitelistedConfig1, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [service] value 'gmail' resolves to host 'smtp.gmail.com' which is not in the whitelistedHosts configuration"` ); expect(() => { - validateConfig(actionType, notWhitelistedConfig2); + validateConfig(actionType, notWhitelistedConfig2, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [host] value 'smtp.gmail.com' is not in the whitelistedHosts configuration"` ); @@ -176,7 +169,7 @@ describe('secrets validation', () => { user: 'bob', password: 'supersecret', }; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); + expect(validateSecrets(actionType, secrets, validationService)).toEqual(secrets); }); test('secrets validation succeeds when secrets props are null/undefined', () => { @@ -184,9 +177,9 @@ describe('secrets validation', () => { user: null, password: null, }; - expect(validateSecrets(actionType, {})).toEqual(secrets); - expect(validateSecrets(actionType, { user: null })).toEqual(secrets); - expect(validateSecrets(actionType, { password: null })).toEqual(secrets); + expect(validateSecrets(actionType, {}, validationService)).toEqual(secrets); + expect(validateSecrets(actionType, { user: null }, validationService)).toEqual(secrets); + expect(validateSecrets(actionType, { password: null }, validationService)).toEqual(secrets); }); }); @@ -197,7 +190,7 @@ describe('params validation', () => { subject: 'this is a test', message: 'this is the message', }; - expect(validateParams(actionType, params)).toMatchInlineSnapshot(` + expect(validateParams(actionType, params, validationService)).toMatchInlineSnapshot(` Object { "bcc": Array [], "cc": Array [], @@ -213,7 +206,7 @@ describe('params validation', () => { test('params validation fails when params is not valid', () => { // empty object expect(() => { - validateParams(actionType, {}); + validateParams(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [subject]: expected value of type [string] but got [undefined]"` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index 7ddb123a4d780..f61b03d28008e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -12,29 +12,26 @@ import nodemailerGetService from 'nodemailer/lib/well-known'; import { sendEmail, JSON_TRANSPORT_SERVICE, SendEmailOptions, Transport } from './lib/send_email'; import { portSchema } from './lib/schemas'; import { Logger } from '../../../../../src/core/server'; -import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; +import { + ActionType, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, + ActionValidationService, +} from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; // config definition export type ActionTypeConfigType = TypeOf; -const ConfigSchemaProps = { +const ConfigSchema = schema.object({ service: schema.nullable(schema.string()), host: schema.nullable(schema.string()), port: schema.nullable(portSchema()), secure: schema.nullable(schema.boolean()), from: schema.string(), -}; - -const ConfigSchema = schema.object(ConfigSchemaProps); - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: unknown -): string | void { - // avoids circular reference ... - const config = configObject as ActionTypeConfigType; +}); +function validateConfig(config: ActionTypeConfigType, validationService: ActionValidationService) { // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. // Note, not currently making these message translated, as will be @@ -55,7 +52,7 @@ function validateConfig( return '[port] is required if [service] is not provided'; } - if (!configurationUtilities.isWhitelistedHostname(config.host)) { + if (!validationService.isWhitelistedHostname(config.host)) { return `[host] value '${config.host}' is not in the whitelistedHosts configuration`; } } else { @@ -63,7 +60,7 @@ function validateConfig( if (host == null) { return `[service] value '${config.service}' is not valid`; } - if (!configurationUtilities.isWhitelistedHostname(host)) { + if (!validationService.isWhitelistedHostname(host)) { return `[service] value '${config.service}' resolves to host '${host}' which is not in the whitelistedHosts configuration`; } } @@ -82,24 +79,15 @@ const SecretsSchema = schema.object({ export type ActionParamsType = TypeOf; -const ParamsSchema = schema.object( - { - to: schema.arrayOf(schema.string(), { defaultValue: [] }), - cc: schema.arrayOf(schema.string(), { defaultValue: [] }), - bcc: schema.arrayOf(schema.string(), { defaultValue: [] }), - subject: schema.string(), - message: schema.string(), - }, - { - validate: validateParams, - } -); - -function validateParams(paramsObject: unknown): string | void { - // avoids circular reference ... - const params = paramsObject as ActionParamsType; +const ParamsSchema = schema.object({ + to: schema.arrayOf(schema.string(), { defaultValue: [] }), + cc: schema.arrayOf(schema.string(), { defaultValue: [] }), + bcc: schema.arrayOf(schema.string(), { defaultValue: [] }), + subject: schema.string(), + message: schema.string(), +}); - const { to, cc, bcc } = params; +function validateParams({ to, cc, bcc }: ActionParamsType) { const addrs = to.length + cc.length + bcc.length; if (addrs === 0) { @@ -113,8 +101,10 @@ interface GetActionTypeParams { } // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { - const { logger, configurationUtilities } = params; +export function getActionType( + params: GetActionTypeParams +): ActionType { + const { logger } = params; return { id: '.email', minimumLicenseRequired: 'gold', @@ -122,12 +112,14 @@ export function getActionType(params: GetActionTypeParams): ActionType { defaultMessage: 'Email', }), validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), + config: ConfigSchema, secrets: SecretsSchema, params: ParamsSchema, }, + runtimeValidate: { + config: validateConfig, + params: validateParams, + }, executor: curry(executor)({ logger }), }; } @@ -136,13 +128,13 @@ export function getActionType(params: GetActionTypeParams): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions + execOptions: ActionTypeExecutorOptions< + ActionTypeConfigType, + ActionTypeSecretsType, + ActionParamsType + > ): Promise { - const actionId = execOptions.actionId; - const config = execOptions.config as ActionTypeConfigType; - const secrets = execOptions.secrets as ActionTypeSecretsType; - const params = execOptions.params as ActionParamsType; - + const { actionId, config, secrets, params } = execOptions; const transport: Transport = {}; if (secrets.user != null) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index be60f4c2f28af..db403de8f2814 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -17,6 +17,7 @@ import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.index'; const services = actionsMock.createServices(); +const validationService = actionsMock.createValidationService(); let actionType: ActionType; @@ -43,7 +44,7 @@ describe('config validation', () => { refresh: false, }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...config, index: 'testing-123', refresh: false, @@ -51,7 +52,7 @@ describe('config validation', () => { }); config.executionTimeField = 'field-123'; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...config, index: 'testing-123', refresh: false, @@ -59,7 +60,7 @@ describe('config validation', () => { }); config.executionTimeField = null; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...config, index: 'testing-123', refresh: false, @@ -69,14 +70,18 @@ describe('config validation', () => { delete config.index; expect(() => { - validateConfig(actionType, { index: 666 }); + validateConfig(actionType, { index: 666 }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [index]: expected value of type [string] but got [number]"` ); delete config.executionTimeField; expect(() => { - validateConfig(actionType, { index: 'testing-123', executionTimeField: true }); + validateConfig( + actionType, + { index: 'testing-123', executionTimeField: true }, + validationService + ); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [executionTimeField]: types that failed validation: - [executionTimeField.0]: expected value of type [string] but got [boolean] @@ -85,7 +90,7 @@ describe('config validation', () => { delete config.refresh; expect(() => { - validateConfig(actionType, { index: 'testing-123', refresh: 'foo' }); + validateConfig(actionType, { index: 'testing-123', refresh: 'foo' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [refresh]: expected value of type [boolean] but got [string]"` ); @@ -97,7 +102,7 @@ describe('config validation', () => { }; expect(() => { - validateConfig(actionType, baseConfig); + validateConfig(actionType, baseConfig, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [index]: expected value of type [string] but got [undefined]"` ); @@ -109,7 +114,7 @@ describe('params validation', () => { const params: Record = { documents: [{ rando: 'thing' }], }; - expect(validateParams(actionType, params)).toMatchInlineSnapshot(` + expect(validateParams(actionType, params, validationService)).toMatchInlineSnapshot(` Object { "documents": Array [ Object { @@ -122,19 +127,19 @@ describe('params validation', () => { test('params validation fails when params is not valid', () => { expect(() => { - validateParams(actionType, { documents: [{}], jim: 'bob' }); + validateParams(actionType, { documents: [{}], jim: 'bob' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [jim]: definition for this key is missing"` ); expect(() => { - validateParams(actionType, {}); + validateParams(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [documents]: expected value of type [array] but got [undefined]"` ); expect(() => { - validateParams(actionType, { documents: ['should be an object'] }); + validateParams(actionType, { documents: ['should be an object'] }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [documents.0]: could not parse record value from json input"` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index b1ed3728edfae..d49f47c5d7768 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -22,6 +22,7 @@ const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; const services: Services = actionsMock.createServices(); +const validationService = actionsMock.createValidationService(); let actionType: ActionType; let mockedLogger: jest.Mocked; @@ -41,13 +42,15 @@ describe('get()', () => { describe('validateConfig()', () => { test('should validate and pass when config is valid', () => { - expect(validateConfig(actionType, {})).toEqual({ apiUrl: null }); - expect(validateConfig(actionType, { apiUrl: 'bar' })).toEqual({ apiUrl: 'bar' }); + expect(validateConfig(actionType, {}, validationService)).toEqual({ apiUrl: null }); + expect(validateConfig(actionType, { apiUrl: 'bar' }, validationService)).toEqual({ + apiUrl: 'bar', + }); }); test('should validate and throw error when config is invalid', () => { expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); + validateConfig(actionType, { shouldNotBeHere: true }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: [shouldNotBeHere]: definition for this key is missing"` ); @@ -65,7 +68,11 @@ describe('validateConfig()', () => { }); expect( - validateConfig(actionType, { apiUrl: 'https://events.pagerduty.com/v2/enqueue' }) + validateConfig( + actionType, + { apiUrl: 'https://events.pagerduty.com/v2/enqueue' }, + validationService + ) ).toEqual({ apiUrl: 'https://events.pagerduty.com/v2/enqueue' }); }); @@ -81,7 +88,11 @@ describe('validateConfig()', () => { }); expect(() => { - validateConfig(actionType, { apiUrl: 'https://events.pagerduty.com/v2/enqueue' }); + validateConfig( + actionType, + { apiUrl: 'https://events.pagerduty.com/v2/enqueue' }, + validationService + ); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: error configuring pagerduty action: target url is not whitelisted"` ); @@ -91,20 +102,20 @@ describe('validateConfig()', () => { describe('validateSecrets()', () => { test('should validate and pass when secrets is valid', () => { const routingKey = 'super-secret'; - expect(validateSecrets(actionType, { routingKey })).toEqual({ + expect(validateSecrets(actionType, { routingKey }, validationService)).toEqual({ routingKey, }); }); test('should validate and throw error when secrets is invalid', () => { expect(() => { - validateSecrets(actionType, { routingKey: false }); + validateSecrets(actionType, { routingKey: false }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: [routingKey]: expected value of type [string] but got [boolean]"` ); expect(() => { - validateSecrets(actionType, {}); + validateSecrets(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: [routingKey]: expected value of type [string] but got [undefined]"` ); @@ -113,7 +124,7 @@ describe('validateSecrets()', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - expect(validateParams(actionType, {})).toEqual({}); + expect(validateParams(actionType, {}, validationService)).toEqual({}); const params = { eventAction: 'trigger', @@ -126,12 +137,12 @@ describe('validateParams()', () => { group: 'a group', class: 'a class', }; - expect(validateParams(actionType, params)).toEqual(params); + expect(validateParams(actionType, params, validationService)).toEqual(params); }); test('should validate and throw error when params is invalid', () => { expect(() => { - validateParams(actionType, { eventAction: 'ackynollage' }); + validateParams(actionType, { eventAction: 'ackynollage' }, validationService); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [eventAction]: types that failed validation: - [eventAction.0]: expected value to equal [trigger] @@ -144,18 +155,14 @@ describe('validateParams()', () => { const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); const timestamp = ` ${randoDate}`; expect(() => { - validateParams(actionType, { - timestamp, - }); + validateParams(actionType, { timestamp }, validationService); }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); }); test('should validate and throw error when timestamp is invalid', () => { const timestamp = `1963-09-55 90:23:45`; expect(() => { - validateParams(actionType, { - timestamp, - }); + validateParams(actionType, { timestamp }, validationService); }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index d5a9c0cc1ccd2..bc64b23532211 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -12,6 +12,7 @@ import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.server-log'; +const validationService = actionsMock.createValidationService(); let actionType: ActionType; let mockedLogger: jest.Mocked; @@ -31,15 +32,28 @@ describe('get()', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - expect(validateParams(actionType, { message: 'a message', level: 'info' })).toEqual({ + expect( + validateParams( + actionType, + { + message: 'a message', + level: 'info', + }, + validationService + ) + ).toEqual({ message: 'a message', level: 'info', }); expect( - validateParams(actionType, { - message: 'a message', - level: 'info', - }) + validateParams( + actionType, + { + message: 'a message', + level: 'info', + }, + validationService + ) ).toEqual({ message: 'a message', level: 'info', @@ -48,19 +62,19 @@ describe('validateParams()', () => { test('should validate and throw error when params is invalid', () => { expect(() => { - validateParams(actionType, {}); + validateParams(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [message]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateParams(actionType, { message: 1 }); + validateParams(actionType, { message: 1 }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [message]: expected value of type [string] but got [number]"` ); expect(() => { - validateParams(actionType, { message: 'x', level: 2 }); + validateParams(actionType, { message: 'x', level: 2 }, validationService); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [level]: types that failed validation: - [level.0]: expected value to equal [trace] @@ -72,7 +86,7 @@ describe('validateParams()', () => { `); expect(() => { - validateParams(actionType, { message: 'x', level: 'foo' }); + validateParams(actionType, { message: 'x', level: 'foo' }, validationService); }).toThrowErrorMatchingInlineSnapshot(` "error validating action params: [level]: types that failed validation: - [level.0]: expected value to equal [trace] diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index bf8a3d8032cc5..f1713481e1b72 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -32,7 +32,12 @@ const ParamsSchema = schema.object({ }); // action type definition -export function getActionType({ logger }: { logger: Logger }): ActionType { +export function getActionType({ + logger, +}: { + logger: Logger; + // eslint-disable-next-line @typescript-eslint/no-explicit-any +}): ActionType { return { id: '.server-log', minimumLicenseRequired: 'basic', @@ -50,10 +55,10 @@ export function getActionType({ logger }: { logger: Logger }): ActionType { async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions + execOptions: ActionTypeExecutorOptions ): Promise { const actionId = execOptions.actionId; - const params = execOptions.params as ActionParamsType; + const params = execOptions.params; const sanitizedMessage = withoutControlCharacters(params.message); try { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index d1a739c2304f2..c7b720de1469a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -18,6 +18,7 @@ import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); +const validationService = actionsMock.createValidationService(); let actionType: ActionType; @@ -37,20 +38,20 @@ describe('action registeration', () => { describe('validateParams()', () => { test('should validate and pass when params is valid', () => { - expect(validateParams(actionType, { message: 'a message' })).toEqual({ + expect(validateParams(actionType, { message: 'a message' }, validationService)).toEqual({ message: 'a message', }); }); test('should validate and throw error when params is invalid', () => { expect(() => { - validateParams(actionType, {}); + validateParams(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [message]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateParams(actionType, { message: 1 }); + validateParams(actionType, { message: 1 }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [message]: expected value of type [string] but got [number]"` ); @@ -59,26 +60,24 @@ describe('validateParams()', () => { describe('validateActionTypeSecrets()', () => { test('should validate and pass when config is valid', () => { - validateSecrets(actionType, { - webhookUrl: 'https://example.com', - }); + validateSecrets(actionType, { webhookUrl: 'https://example.com' }, validationService); }); test('should validate and throw error when config is invalid', () => { expect(() => { - validateSecrets(actionType, {}); + validateSecrets(actionType, {}, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [undefined]"` ); expect(() => { - validateSecrets(actionType, { webhookUrl: 1 }); + validateSecrets(actionType, { webhookUrl: 1 }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: [webhookUrl]: expected value of type [string] but got [number]"` ); expect(() => { - validateSecrets(actionType, { webhookUrl: 'fee-fi-fo-fum' }); + validateSecrets(actionType, { webhookUrl: 'fee-fi-fo-fum' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: error configuring slack action: unable to parse host name from webhookUrl"` ); @@ -94,7 +93,9 @@ describe('validateActionTypeSecrets()', () => { }, }); - expect(validateSecrets(actionType, { webhookUrl: 'https://api.slack.com/' })).toEqual({ + expect( + validateSecrets(actionType, { webhookUrl: 'https://api.slack.com/' }, validationService) + ).toEqual({ webhookUrl: 'https://api.slack.com/', }); }); @@ -110,7 +111,7 @@ describe('validateActionTypeSecrets()', () => { }); expect(() => { - validateSecrets(actionType, { webhookUrl: 'https://api.slack.com/' }); + validateSecrets(actionType, { webhookUrl: 'https://api.slack.com/' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: error configuring slack action: target hostname is not whitelisted"` ); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 6daf15208f4d9..351d513221260 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -22,6 +22,7 @@ const axiosRequestMock = axios.request as jest.Mock; const ACTION_TYPE_ID = '.webhook'; const services: Services = actionsMock.createServices(); +const validationService = actionsMock.createValidationService(); let actionType: ActionType; let mockedLogger: jest.Mocked; @@ -45,19 +46,22 @@ describe('secrets validation', () => { user: 'bob', password: 'supersecret', }; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); + expect(validateSecrets(actionType, secrets, validationService)).toEqual(secrets); }); test('fails when secret user is provided, but password is omitted', () => { expect(() => { - validateSecrets(actionType, { user: 'bob' }); + validateSecrets(actionType, { user: 'bob' }, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type secrets: both user and password must be specified"` ); }); test('succeeds when basic authentication credentials are omitted', () => { - expect(validateSecrets(actionType, {})).toEqual({ password: null, user: null }); + expect(validateSecrets(actionType, {}, validationService)).toEqual({ + password: null, + user: null, + }); }); }); @@ -71,7 +75,7 @@ describe('config validation', () => { const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...defaultValues, ...config, }); @@ -83,7 +87,7 @@ describe('config validation', () => { url: 'http://mylisteningserver:9200/endpoint', method, }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...defaultValues, ...config, }); @@ -96,7 +100,7 @@ describe('config validation', () => { method: 'https', }; expect(() => { - validateConfig(actionType, config); + validateConfig(actionType, config, validationService); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [method]: types that failed validation: - [method.0]: expected value to equal [post] @@ -108,7 +112,7 @@ describe('config validation', () => { const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...defaultValues, ...config, }); @@ -123,7 +127,7 @@ describe('config validation', () => { 'Content-Type': 'application/json', }, }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...defaultValues, ...config, }); @@ -135,7 +139,7 @@ describe('config validation', () => { headers: 'application/json', }; expect(() => { - validateConfig(actionType, config); + validateConfig(actionType, config, validationService); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [headers]: types that failed validation: - [headers.0]: could not parse record value from json input @@ -153,7 +157,7 @@ describe('config validation', () => { }, }; - expect(validateConfig(actionType, config)).toEqual({ + expect(validateConfig(actionType, config, validationService)).toEqual({ ...defaultValues, ...config, }); @@ -180,7 +184,7 @@ describe('config validation', () => { }; expect(() => { - validateConfig(actionType, config); + validateConfig(actionType, config, validationService); }).toThrowErrorMatchingInlineSnapshot( `"error validating action type config: error configuring webhook action: target url is not whitelisted"` ); @@ -190,14 +194,14 @@ describe('config validation', () => { describe('params validation', () => { test('param validation passes when no fields are provided as none are required', () => { const params: Record = {}; - expect(validateParams(actionType, params)).toEqual({}); + expect(validateParams(actionType, params, validationService)).toEqual({}); }); test('params validation passes when a valid body is provided', () => { const params: Record = { body: 'count: {{ctx.payload.hits.total}}', }; - expect(validateParams(actionType, params)).toEqual({ + expect(validateParams(actionType, params, validationService)).toEqual({ ...params, }); }); diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 88553c314112f..9e16e035fbf50 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -17,6 +17,7 @@ export { ActionTypeExecutorOptions, ActionType, PreConfiguredAction, + ActionValidationService, } from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 250bfc2752f1b..32170c34c7f9f 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -95,15 +95,16 @@ export class ActionExecutor { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } const actionType = actionTypeRegistry.get(actionTypeId); + const validationService = actionTypeRegistry.getValidationService(); let validatedParams: Record; let validatedConfig: Record; let validatedSecrets: Record; try { - validatedParams = validateParams(actionType, params); - validatedConfig = validateConfig(actionType, config); - validatedSecrets = validateSecrets(actionType, secrets); + validatedParams = validateParams(actionType, params, validationService); + validatedConfig = validateConfig(actionType, config, validationService); + validatedSecrets = validateSecrets(actionType, secrets, validationService); } catch (err) { return { status: 'error', actionId, message: err.message, retry: false }; } diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index 03ae7a9b35a81..411ed21dd1aea 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -8,6 +8,9 @@ import { schema } from '@kbn/config-schema'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionType, ExecutorType } from '../types'; +import { actionsMock } from '../mocks'; + +const validationService = actionsMock.createValidationService(); const executor: ExecutorType = async (options) => { return { status: 'ok', actionId: options.actionId }; @@ -22,7 +25,7 @@ test('should validate when there are no validators', () => { }; const testValue = { any: ['old', 'thing'] }; - const result = validateConfig(actionType, testValue); + const result = validateConfig(actionType, testValue, validationService); expect(result).toEqual(testValue); }); @@ -38,13 +41,13 @@ test('should validate when there are no individual validators', () => { let result; const testValue = { any: ['old', 'thing'] }; - result = validateParams(actionType, testValue); + result = validateParams(actionType, testValue, validationService); expect(result).toEqual(testValue); - result = validateConfig(actionType, testValue); + result = validateConfig(actionType, testValue, validationService); expect(result).toEqual(testValue); - result = validateSecrets(actionType, testValue); + result = validateSecrets(actionType, testValue, validationService); expect(result).toEqual(testValue); }); @@ -65,13 +68,13 @@ test('should validate when validators return incoming value', () => { let result; const testValue = { any: ['old', 'thing'] }; - result = validateParams(actionType, testValue); + result = validateParams(actionType, testValue, validationService); expect(result).toEqual(testValue); - result = validateConfig(actionType, testValue); + result = validateConfig(actionType, testValue, validationService); expect(result).toEqual(testValue); - result = validateSecrets(actionType, testValue); + result = validateSecrets(actionType, testValue, validationService); expect(result).toEqual(testValue); }); @@ -93,13 +96,13 @@ test('should validate when validators return different values', () => { let result; const testValue = { any: ['old', 'thing'] }; - result = validateParams(actionType, testValue); + result = validateParams(actionType, testValue, validationService); expect(result).toEqual(returnedValue); - result = validateConfig(actionType, testValue); + result = validateConfig(actionType, testValue, validationService); expect(result).toEqual(returnedValue); - result = validateSecrets(actionType, testValue); + result = validateSecrets(actionType, testValue, validationService); expect(result).toEqual(returnedValue); }); @@ -123,17 +126,17 @@ test('should throw with expected error when validators fail', () => { const testValue = { any: ['old', 'thing'] }; - expect(() => validateParams(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: test error"` - ); + expect(() => + validateParams(actionType, testValue, validationService) + ).toThrowErrorMatchingInlineSnapshot(`"error validating action params: test error"`); - expect(() => validateConfig(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: test error"` - ); + expect(() => + validateConfig(actionType, testValue, validationService) + ).toThrowErrorMatchingInlineSnapshot(`"error validating action type config: test error"`); - expect(() => validateSecrets(actionType, testValue)).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: test error"` - ); + expect(() => + validateSecrets(actionType, testValue, validationService) + ).toThrowErrorMatchingInlineSnapshot(`"error validating action type secrets: test error"`); }); test('should work with @kbn/config-schema', () => { @@ -150,10 +153,12 @@ test('should work with @kbn/config-schema', () => { }, }; - const result = validateParams(actionType, { foo: 'bar' }); + const result = validateParams(actionType, { foo: 'bar' }, validationService); expect(result).toEqual({ foo: 'bar' }); - expect(() => validateParams(actionType, { bar: 2 })).toThrowErrorMatchingInlineSnapshot( + expect(() => + validateParams(actionType, { bar: 2 }, validationService) + ).toThrowErrorMatchingInlineSnapshot( `"error validating action params: [foo]: expected value of type [string] but got [undefined]"` ); }); diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 021c460f4c815..68b33fd2b9409 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -5,53 +5,52 @@ */ import Boom from 'boom'; -import { ActionType } from '../types'; +import { ActionType, ActionValidationService } from '../types'; -export function validateParams(actionType: ActionType, value: unknown) { - return validateWithSchema(actionType, 'params', value); -} +type ValidKeys = 'params' | 'config' | 'secrets'; -export function validateConfig(actionType: ActionType, value: unknown) { - return validateWithSchema(actionType, 'config', value); +export function validateParams( + actionType: ActionType, + value: unknown, + validationService: ActionValidationService +) { + return validateWithSchema(actionType, 'action params', 'params', value, validationService); } -export function validateSecrets(actionType: ActionType, value: unknown) { - return validateWithSchema(actionType, 'secrets', value); +export function validateConfig( + actionType: ActionType, + value: unknown, + validationService: ActionValidationService +) { + return validateWithSchema(actionType, 'action type config', 'config', value, validationService); } -type ValidKeys = 'params' | 'config' | 'secrets'; +export function validateSecrets( + actionType: ActionType, + value: unknown, + validationService: ActionValidationService +) { + return validateWithSchema(actionType, 'action type secrets', 'secrets', value, validationService); +} function validateWithSchema( actionType: ActionType, + name: string, key: ValidKeys, - value: unknown + value: unknown, + validationService: ActionValidationService ): Record { - if (actionType.validate) { - let name; + const validator = actionType?.validate?.[key]; + const runtimeValidator = actionType.runtimeValidate?.[key]; + + if (validator) { try { - switch (key) { - case 'params': - name = 'action params'; - if (actionType.validate.params) { - return actionType.validate.params.validate(value); - } - break; - case 'config': - name = 'action type config'; - if (actionType.validate.config) { - return actionType.validate.config.validate(value); - } - - break; - case 'secrets': - name = 'action type secrets'; - if (actionType.validate.secrets) { - return actionType.validate.secrets.validate(value); - } - break; - default: - // should never happen, but left here for future-proofing - throw new Error(`invalid actionType validate key: ${key}`); + value = validator.validate(value); + if (runtimeValidator) { + const message = runtimeValidator(value, validationService); + if (message) { + throw new Error(message); + } } } catch (err) { // we can't really i18n this yet, since the err.message isn't i18n'd itself diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 09bc137da736f..7559eb8cdea20 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -6,7 +6,7 @@ import { actionsClientMock } from './actions_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { Services } from './types'; +import { Services, ActionValidationService } from './types'; import { elasticsearchServiceMock, savedObjectsClientMock, @@ -45,8 +45,17 @@ const createServicesMock = () => { return mock; }; +export const createValidationServiceMock = () => { + const mock: jest.Mocked = { + isWhitelistedHostname: jest.fn(), + isWhitelistedUri: jest.fn(), + }; + return mock; +}; + export const actionsMock = { createServices: createServicesMock, createSetup: createSetupMock, createStart: createStartMock, + createValidationService: createValidationServiceMock, }; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 093d22c2c1a71..abe9e3ee8ab1e 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -49,32 +49,27 @@ export interface ActionsConfigType { } // the parameters passed to an action type executor function -export interface ActionTypeExecutorOptions { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ActionTypeExecutorOptions { actionId: string; services: Services; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: Record; + config: Config; + secrets: Secrets; + params: Params; } -export interface ActionResult { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ActionResult { id: string; actionTypeId: string; name: string; - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - config?: Record; + config?: Config; isPreconfigured: boolean; } -export interface PreConfiguredAction extends ActionResult { - // This will have to remain `any` until we can extend Action Executors with generics - // eslint-disable-next-line @typescript-eslint/no-explicit-any - secrets: Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface PreConfiguredAction extends ActionResult { + secrets: Secrets; } export interface FindActionResult extends ActionResult { @@ -94,26 +89,42 @@ export interface ActionTypeExecutorResult { } // signature of the action type executor function -export type ExecutorType = ( - options: ActionTypeExecutorOptions +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ExecutorType = ( + options: ActionTypeExecutorOptions ) => Promise; -interface ValidatorType { - validate(value: unknown): Record; +interface ValidatorType { + validate(value: unknown): Type; +} + +export interface ActionValidationService { + isWhitelistedHostname(hostname: string): boolean; + isWhitelistedUri(uri: string): boolean; } -export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; -export interface ActionType { +type RuntimeValidatorType = ( + value: Type, + validationService: ActionValidationService +) => void | string; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ActionType { id: string; name: string; maxAttempts?: number; minimumLicenseRequired: LicenseType; validate?: { - params?: ValidatorType; - config?: ValidatorType; - secrets?: ValidatorType; + params?: ValidatorType; + config?: ValidatorType; + secrets?: ValidatorType; + }; + runtimeValidate?: { + params?: RuntimeValidatorType; + config?: RuntimeValidatorType; + secrets?: RuntimeValidatorType; }; - executor: ExecutorType; + executor: ExecutorType; } export interface RawAction extends SavedObjectAttributes {