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 {