From 480886c861f0be318a549df2e9aacdaf5da5d0c9 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet <pierre.gayvallet@elastic.co>
Date: Mon, 6 Jan 2020 17:36:21 +0100
Subject: [PATCH 1/3] [7.x] migrate xsrf / version-check / custom-headers
 handlers to NP (#53684) (#54016)

* migrate xsrf / version-check / custom-headers handlers to NP (#53684)

* migrate xsrf / version-check / custom-headers handlers to NP

* export lifecycleMock to be used by plugins

* move toolkit mock to http_server mock

* remove legacy config tests on xsrf

* fix integration_test http configuration

* remove direct HAPI usages from integration tests

* nits and comments

* add custom headers test in case of server returning error

* resolve merge conflicts

* restore `server.name` to legacy config

* update snapshots
---
 .../__snapshots__/http_config.test.ts.snap    |   6 +
 .../http/cookie_session_storage.test.ts       |   4 +
 src/core/server/http/http_config.test.ts      |  23 ++
 src/core/server/http/http_config.ts           |  19 ++
 src/core/server/http/http_server.mocks.ts     |  13 +
 src/core/server/http/http_server.ts           |   6 +
 .../server/http/http_service.test.mocks.ts    |   4 +
 src/core/server/http/http_service.ts          |  19 +-
 .../lifecycle_handlers.test.ts                | 241 ++++++++++++++++
 .../server/http/lifecycle_handlers.test.ts    | 269 ++++++++++++++++++
 src/core/server/http/lifecycle_handlers.ts    |  93 ++++++
 src/core/server/http/test_utils.ts            |   4 +
 ...gacy_object_to_config_adapter.test.ts.snap |  16 ++
 .../legacy_object_to_config_adapter.test.ts   |  12 +
 .../config/legacy_object_to_config_adapter.ts |   5 +-
 src/legacy/server/config/schema.js            |  15 +-
 src/legacy/server/config/schema.test.js       |  56 ----
 src/legacy/server/http/index.js               |  28 --
 .../integration_tests/version_check.test.js   |  64 -----
 .../http/integration_tests/xsrf.test.js       | 145 ----------
 src/legacy/server/http/version_check.js       |  39 ---
 src/legacy/server/http/xsrf.js                |  47 ---
 src/test_utils/kbn_server.ts                  |   4 +-
 23 files changed, 730 insertions(+), 402 deletions(-)
 create mode 100644 src/core/server/http/integration_tests/lifecycle_handlers.test.ts
 create mode 100644 src/core/server/http/lifecycle_handlers.test.ts
 create mode 100644 src/core/server/http/lifecycle_handlers.ts
 delete mode 100644 src/legacy/server/http/integration_tests/version_check.test.js
 delete mode 100644 src/legacy/server/http/integration_tests/xsrf.test.js
 delete mode 100644 src/legacy/server/http/version_check.js
 delete mode 100644 src/legacy/server/http/xsrf.js

diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap
index 6c690f9da70c3..8856eb95ba722 100644
--- a/src/core/server/http/__snapshots__/http_config.test.ts.snap
+++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap
@@ -31,11 +31,13 @@ Object {
     "enabled": true,
   },
   "cors": false,
+  "customResponseHeaders": Object {},
   "host": "localhost",
   "keepaliveTimeout": 120000,
   "maxPayload": ByteSizeValue {
     "valueInBytes": 1048576,
   },
+  "name": "kibana-hostname",
   "port": 5601,
   "rewriteBasePath": false,
   "socketTimeout": 120000,
@@ -70,6 +72,10 @@ Object {
       "TLSv1.2",
     ],
   },
+  "xsrf": Object {
+    "disableProtection": false,
+    "whitelist": Array [],
+  },
 }
 `;
 
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index 0e4f3972fe9dc..4ce422e1f65c4 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -58,6 +58,10 @@ configService.atPath.mockReturnValue(
       verificationMode: 'none',
     },
     compression: { enabled: true },
+    xsrf: {
+      disableProtection: true,
+      whitelist: [],
+    },
   } as any)
 );
 
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index 082b85ad68add..3dc5fa48bc366 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -23,6 +23,11 @@ import { config, HttpConfig } from '.';
 const validHostnames = ['www.example.com', '8.8.8.8', '::1', 'localhost'];
 const invalidHostname = 'asdf$%^';
 
+jest.mock('os', () => ({
+  ...jest.requireActual('os'),
+  hostname: () => 'kibana-hostname',
+}));
+
 test('has defaults for config', () => {
   const httpSchema = config.schema;
   const obj = {};
@@ -84,6 +89,24 @@ test('accepts only valid uuids for server.uuid', () => {
   );
 });
 
+test('uses os.hostname() as default for server.name', () => {
+  const httpSchema = config.schema;
+  const validated = httpSchema.validate({});
+  expect(validated.name).toEqual('kibana-hostname');
+});
+
+test('throws if xsrf.whitelist element does not start with a slash', () => {
+  const httpSchema = config.schema;
+  const obj = {
+    xsrf: {
+      whitelist: ['/valid-path', 'invalid-path'],
+    },
+  };
+  expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
+    `"[xsrf.whitelist.1]: must start with a slash"`
+  );
+});
+
 describe('with TLS', () => {
   test('throws if TLS is enabled but `key` is not specified', () => {
     const httpSchema = config.schema;
diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts
index 5749eb383f8b9..7d6a6ddb857b1 100644
--- a/src/core/server/http/http_config.ts
+++ b/src/core/server/http/http_config.ts
@@ -18,6 +18,8 @@
  */
 
 import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
+import { hostname } from 'os';
+
 import { CspConfigType, CspConfig, ICspConfig } from '../csp';
 import { SslConfig, sslSchema } from './ssl_config';
 
@@ -33,6 +35,7 @@ export const config = {
   path: 'server',
   schema: schema.object(
     {
+      name: schema.string({ defaultValue: () => hostname() }),
       autoListen: schema.boolean({ defaultValue: true }),
       basePath: schema.maybe(
         schema.string({
@@ -63,6 +66,9 @@ export const config = {
         ),
         schema.boolean({ defaultValue: false })
       ),
+      customResponseHeaders: schema.recordOf(schema.string(), schema.string(), {
+        defaultValue: {},
+      }),
       host: schema.string({
         defaultValue: 'localhost',
         hostname: true,
@@ -97,6 +103,13 @@ export const config = {
           validate: match(uuidRegexp, 'must be a valid uuid'),
         })
       ),
+      xsrf: schema.object({
+        disableProtection: schema.boolean({ defaultValue: false }),
+        whitelist: schema.arrayOf(
+          schema.string({ validate: match(/^\//, 'must start with a slash') }),
+          { defaultValue: [] }
+        ),
+      }),
     },
     {
       validate: rawConfig => {
@@ -125,12 +138,14 @@ export const config = {
 export type HttpConfigType = TypeOf<typeof config.schema>;
 
 export class HttpConfig {
+  public name: string;
   public autoListen: boolean;
   public host: string;
   public keepaliveTimeout: number;
   public socketTimeout: number;
   public port: number;
   public cors: boolean | { origin: string[] };
+  public customResponseHeaders: Record<string, string>;
   public maxPayload: ByteSizeValue;
   public basePath?: string;
   public rewriteBasePath: boolean;
@@ -138,6 +153,7 @@ export class HttpConfig {
   public ssl: SslConfig;
   public compression: { enabled: boolean; referrerWhitelist?: string[] };
   public csp: ICspConfig;
+  public xsrf: { disableProtection: boolean; whitelist: string[] };
 
   /**
    * @internal
@@ -147,7 +163,9 @@ export class HttpConfig {
     this.host = rawHttpConfig.host;
     this.port = rawHttpConfig.port;
     this.cors = rawHttpConfig.cors;
+    this.customResponseHeaders = rawHttpConfig.customResponseHeaders;
     this.maxPayload = rawHttpConfig.maxPayload;
+    this.name = rawHttpConfig.name;
     this.basePath = rawHttpConfig.basePath;
     this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout;
     this.socketTimeout = rawHttpConfig.socketTimeout;
@@ -156,5 +174,6 @@ export class HttpConfig {
     this.defaultRoute = rawHttpConfig.defaultRoute;
     this.compression = rawHttpConfig.compression;
     this.csp = new CspConfig(rawCspConfig);
+    this.xsrf = rawHttpConfig.xsrf;
   }
 }
diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts
index ba742292e9e83..230a229b36888 100644
--- a/src/core/server/http/http_server.mocks.ts
+++ b/src/core/server/http/http_server.mocks.ts
@@ -30,6 +30,9 @@ import {
   RouteMethod,
   KibanaResponseFactory,
 } from './router';
+import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
+import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
+import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
 
 interface RequestFixtureOptions {
   headers?: Record<string, string>;
@@ -137,9 +140,19 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked<LifecycleResponseFact
   customError: jest.fn(),
 });
 
+type ToolkitMock = jest.Mocked<OnPreResponseToolkit & OnPostAuthToolkit & OnPreAuthToolkit>;
+
+const createToolkitMock = (): ToolkitMock => {
+  return {
+    next: jest.fn(),
+    rewriteUrl: jest.fn(),
+  };
+};
+
 export const httpServerMock = {
   createKibanaRequest: createKibanaRequestMock,
   createRawRequest: createRawRequestMock,
   createResponseFactory: createResponseFactoryMock,
   createLifecycleResponseFactory: createLifecycleResponseFactoryMock,
+  createToolkit: createToolkitMock,
 };
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 994a6cced8914..6b978b71c6f2b 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -60,6 +60,12 @@ export interface HttpServerSetup {
   };
 }
 
+/** @internal */
+export type LifecycleRegistrar = Pick<
+  HttpServerSetup,
+  'registerAuth' | 'registerOnPreAuth' | 'registerOnPostAuth' | 'registerOnPreResponse'
+>;
+
 export class HttpServer {
   private server?: Server;
   private config?: HttpConfig;
diff --git a/src/core/server/http/http_service.test.mocks.ts b/src/core/server/http/http_service.test.mocks.ts
index c147944f2b7d8..e18008d3b405d 100644
--- a/src/core/server/http/http_service.test.mocks.ts
+++ b/src/core/server/http/http_service.test.mocks.ts
@@ -27,3 +27,7 @@ jest.mock('./http_server', () => {
     HttpServer: mockHttpServer,
   };
 });
+
+jest.mock('./lifecycle_handlers', () => ({
+  registerCoreHandlers: jest.fn(),
+}));
diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts
index e038443d5c83f..fb12ed80b8e2f 100644
--- a/src/core/server/http/http_service.ts
+++ b/src/core/server/http/http_service.ts
@@ -21,11 +21,10 @@ import { Observable, Subscription, combineLatest } from 'rxjs';
 import { first, map } from 'rxjs/operators';
 import { Server } from 'hapi';
 
-import { LoggerFactory } from '../logging';
 import { CoreService } from '../../types';
-
-import { Logger } from '../logging';
+import { Logger, LoggerFactory } from '../logging';
 import { ContextSetup } from '../context';
+import { Env } from '../config';
 import { CoreContext } from '../core_context';
 import { PluginOpaqueId } from '../plugins';
 import { CspConfigType, config as cspConfig } from '../csp';
@@ -43,6 +42,7 @@ import {
 } from './types';
 
 import { RequestHandlerContext } from '../../server';
+import { registerCoreHandlers } from './lifecycle_handlers';
 
 interface SetupDeps {
   context: ContextSetup;
@@ -57,18 +57,20 @@ export class HttpService implements CoreService<InternalHttpServiceSetup, HttpSe
 
   private readonly logger: LoggerFactory;
   private readonly log: Logger;
+  private readonly env: Env;
   private notReadyServer?: Server;
   private requestHandlerContext?: RequestHandlerContextContainer;
 
   constructor(private readonly coreContext: CoreContext) {
-    const { logger, configService } = coreContext;
+    const { logger, configService, env } = coreContext;
 
     this.logger = logger;
+    this.env = env;
     this.log = logger.get('http');
-    this.config$ = combineLatest(
+    this.config$ = combineLatest([
       configService.atPath<HttpConfigType>(httpConfig.path),
-      configService.atPath<CspConfigType>(cspConfig.path)
-    ).pipe(map(([http, csp]) => new HttpConfig(http, csp)));
+      configService.atPath<CspConfigType>(cspConfig.path),
+    ]).pipe(map(([http, csp]) => new HttpConfig(http, csp)));
     this.httpServer = new HttpServer(logger, 'Kibana');
     this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server'));
   }
@@ -92,6 +94,9 @@ export class HttpService implements CoreService<InternalHttpServiceSetup, HttpSe
     }
 
     const { registerRouter, ...serverContract } = await this.httpServer.setup(config);
+
+    registerCoreHandlers(serverContract, config, this.env);
+
     const contract: InternalHttpServiceSetup = {
       ...serverContract,
 
diff --git a/src/core/server/http/integration_tests/lifecycle_handlers.test.ts b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
new file mode 100644
index 0000000000000..f4c5f16870c7e
--- /dev/null
+++ b/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
@@ -0,0 +1,241 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { resolve } from 'path';
+import supertest from 'supertest';
+import { BehaviorSubject } from 'rxjs';
+import { ByteSizeValue } from '@kbn/config-schema';
+
+import { createHttpServer } from '../test_utils';
+import { HttpService } from '../http_service';
+import { HttpServerSetup } from '../http_server';
+import { IRouter, RouteRegistrar } from '../router';
+
+import { configServiceMock } from '../../config/config_service.mock';
+import { contextServiceMock } from '../../context/context_service.mock';
+
+const pkgPath = resolve(__dirname, '../../../../../package.json');
+const actualVersion = require(pkgPath).version;
+const versionHeader = 'kbn-version';
+const xsrfHeader = 'kbn-xsrf';
+const nameHeader = 'kbn-name';
+const whitelistedTestPath = '/xsrf/test/route/whitelisted';
+const kibanaName = 'my-kibana-name';
+const setupDeps = {
+  context: contextServiceMock.createSetupContract(),
+};
+
+describe('core lifecycle handlers', () => {
+  let server: HttpService;
+  let innerServer: HttpServerSetup['server'];
+  let router: IRouter;
+
+  beforeEach(async () => {
+    const configService = configServiceMock.create();
+    configService.atPath.mockReturnValue(
+      new BehaviorSubject({
+        hosts: ['localhost'],
+        maxPayload: new ByteSizeValue(1024),
+        autoListen: true,
+        ssl: {
+          enabled: false,
+        },
+        compression: { enabled: true },
+        name: kibanaName,
+        customResponseHeaders: {
+          'some-header': 'some-value',
+        },
+        xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] },
+      } as any)
+    );
+    server = createHttpServer({ configService });
+
+    const serverSetup = await server.setup(setupDeps);
+    router = serverSetup.createRouter('/');
+    innerServer = serverSetup.server;
+  }, 30000);
+
+  afterEach(async () => {
+    await server.stop();
+  });
+
+  describe('versionCheck post-auth handler', () => {
+    const testRoute = '/version_check/test/route';
+
+    beforeEach(async () => {
+      router.get({ path: testRoute, validate: false }, (context, req, res) => {
+        return res.ok({ body: 'ok' });
+      });
+      await server.start();
+    });
+
+    it('accepts requests with the correct version passed in the version header', async () => {
+      await supertest(innerServer.listener)
+        .get(testRoute)
+        .set(versionHeader, actualVersion)
+        .expect(200, 'ok');
+    });
+
+    it('accepts requests that do not include a version header', async () => {
+      await supertest(innerServer.listener)
+        .get(testRoute)
+        .expect(200, 'ok');
+    });
+
+    it('rejects requests with an incorrect version passed in the version header', async () => {
+      await supertest(innerServer.listener)
+        .get(testRoute)
+        .set(versionHeader, 'invalid-version')
+        .expect(400, /Browser client is out of date/);
+    });
+  });
+
+  describe('customHeaders pre-response handler', () => {
+    const testRoute = '/custom_headers/test/route';
+    const testErrorRoute = '/custom_headers/test/error_route';
+
+    beforeEach(async () => {
+      router.get({ path: testRoute, validate: false }, (context, req, res) => {
+        return res.ok({ body: 'ok' });
+      });
+      router.get({ path: testErrorRoute, validate: false }, (context, req, res) => {
+        return res.badRequest({ body: 'bad request' });
+      });
+      await server.start();
+    });
+
+    it('adds the kbn-name header', async () => {
+      const result = await supertest(innerServer.listener)
+        .get(testRoute)
+        .expect(200, 'ok');
+      const headers = result.header as Record<string, string>;
+      expect(headers).toEqual(
+        expect.objectContaining({
+          [nameHeader]: kibanaName,
+        })
+      );
+    });
+
+    it('adds the kbn-name header in case of error', async () => {
+      const result = await supertest(innerServer.listener)
+        .get(testErrorRoute)
+        .expect(400);
+      const headers = result.header as Record<string, string>;
+      expect(headers).toEqual(
+        expect.objectContaining({
+          [nameHeader]: kibanaName,
+        })
+      );
+    });
+
+    it('adds the custom headers', async () => {
+      const result = await supertest(innerServer.listener)
+        .get(testRoute)
+        .expect(200, 'ok');
+      const headers = result.header as Record<string, string>;
+      expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
+    });
+
+    it('adds the custom headers in case of error', async () => {
+      const result = await supertest(innerServer.listener)
+        .get(testErrorRoute)
+        .expect(400);
+      const headers = result.header as Record<string, string>;
+      expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
+    });
+  });
+
+  describe('xsrf post-auth handler', () => {
+    const testPath = '/xsrf/test/route';
+    const destructiveMethods = ['POST', 'PUT', 'DELETE'];
+    const nonDestructiveMethods = ['GET', 'HEAD'];
+
+    const getSupertest = (method: string, path: string): supertest.Test => {
+      return (supertest(innerServer.listener) as any)[method.toLowerCase()](path) as supertest.Test;
+    };
+
+    beforeEach(async () => {
+      router.get({ path: testPath, validate: false }, (context, req, res) => {
+        return res.ok({ body: 'ok' });
+      });
+
+      destructiveMethods.forEach(method => {
+        ((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
+          { path: testPath, validate: false },
+          (context, req, res) => {
+            return res.ok({ body: 'ok' });
+          }
+        );
+        ((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
+          { path: whitelistedTestPath, validate: false },
+          (context, req, res) => {
+            return res.ok({ body: 'ok' });
+          }
+        );
+      });
+
+      await server.start();
+    });
+
+    nonDestructiveMethods.forEach(method => {
+      describe(`When using non-destructive ${method} method`, () => {
+        it('accepts requests without a token', async () => {
+          await getSupertest(method.toLowerCase(), testPath).expect(
+            200,
+            method === 'HEAD' ? undefined : 'ok'
+          );
+        });
+
+        it('accepts requests with the xsrf header', async () => {
+          await getSupertest(method.toLowerCase(), testPath)
+            .set(xsrfHeader, 'anything')
+            .expect(200, method === 'HEAD' ? undefined : 'ok');
+        });
+      });
+    });
+
+    destructiveMethods.forEach(method => {
+      describe(`When using destructive ${method} method`, () => {
+        it('accepts requests with the xsrf header', async () => {
+          await getSupertest(method.toLowerCase(), testPath)
+            .set(xsrfHeader, 'anything')
+            .expect(200, 'ok');
+        });
+
+        it('accepts requests with the version header', async () => {
+          await getSupertest(method.toLowerCase(), testPath)
+            .set(versionHeader, actualVersion)
+            .expect(200, 'ok');
+        });
+
+        it('rejects requests without either an xsrf or version header', async () => {
+          await getSupertest(method.toLowerCase(), testPath).expect(400, {
+            statusCode: 400,
+            error: 'Bad Request',
+            message: 'Request must contain a kbn-xsrf header.',
+          });
+        });
+
+        it('accepts whitelisted requests without either an xsrf or version header', async () => {
+          await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok');
+        });
+      });
+    });
+  });
+});
diff --git a/src/core/server/http/lifecycle_handlers.test.ts b/src/core/server/http/lifecycle_handlers.test.ts
new file mode 100644
index 0000000000000..48a6973b741ba
--- /dev/null
+++ b/src/core/server/http/lifecycle_handlers.test.ts
@@ -0,0 +1,269 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  createCustomHeadersPreResponseHandler,
+  createVersionCheckPostAuthHandler,
+  createXsrfPostAuthHandler,
+} from './lifecycle_handlers';
+import { httpServerMock } from './http_server.mocks';
+import { HttpConfig } from './http_config';
+import { KibanaRequest, RouteMethod } from './router';
+
+const createConfig = (partial: Partial<HttpConfig>): HttpConfig => partial as HttpConfig;
+
+const forgeRequest = ({
+  headers = {},
+  path = '/',
+  method = 'get',
+}: Partial<{
+  headers: Record<string, string>;
+  path: string;
+  method: RouteMethod;
+}>): KibanaRequest => {
+  return httpServerMock.createKibanaRequest({ headers, path, method });
+};
+
+describe('xsrf post-auth handler', () => {
+  let toolkit: ReturnType<typeof httpServerMock.createToolkit>;
+  let responseFactory: ReturnType<typeof httpServerMock.createLifecycleResponseFactory>;
+
+  beforeEach(() => {
+    toolkit = httpServerMock.createToolkit();
+    responseFactory = httpServerMock.createLifecycleResponseFactory();
+  });
+
+  describe('non destructive methods', () => {
+    it('accepts requests without version or xsrf header', () => {
+      const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'get', headers: {} });
+
+      toolkit.next.mockReturnValue('next' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(responseFactory.badRequest).not.toHaveBeenCalled();
+      expect(toolkit.next).toHaveBeenCalledTimes(1);
+      expect(result).toEqual('next');
+    });
+  });
+
+  describe('destructive methods', () => {
+    it('accepts requests with xsrf header', () => {
+      const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'post', headers: { 'kbn-xsrf': 'xsrf' } });
+
+      toolkit.next.mockReturnValue('next' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(responseFactory.badRequest).not.toHaveBeenCalled();
+      expect(toolkit.next).toHaveBeenCalledTimes(1);
+      expect(result).toEqual('next');
+    });
+
+    it('accepts requests with version header', () => {
+      const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'post', headers: { 'kbn-version': 'some-version' } });
+
+      toolkit.next.mockReturnValue('next' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(responseFactory.badRequest).not.toHaveBeenCalled();
+      expect(toolkit.next).toHaveBeenCalledTimes(1);
+      expect(result).toEqual('next');
+    });
+
+    it('returns a bad request if called without xsrf or version header', () => {
+      const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'post' });
+
+      responseFactory.badRequest.mockReturnValue('badRequest' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(toolkit.next).not.toHaveBeenCalled();
+      expect(responseFactory.badRequest).toHaveBeenCalledTimes(1);
+      expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(`
+        Object {
+          "body": "Request must contain a kbn-xsrf header.",
+        }
+      `);
+      expect(result).toEqual('badRequest');
+    });
+
+    it('accepts requests if protection is disabled', () => {
+      const config = createConfig({ xsrf: { whitelist: [], disableProtection: true } });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'post', headers: {} });
+
+      toolkit.next.mockReturnValue('next' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(responseFactory.badRequest).not.toHaveBeenCalled();
+      expect(toolkit.next).toHaveBeenCalledTimes(1);
+      expect(result).toEqual('next');
+    });
+
+    it('accepts requests if path is whitelisted', () => {
+      const config = createConfig({
+        xsrf: { whitelist: ['/some-path'], disableProtection: false },
+      });
+      const handler = createXsrfPostAuthHandler(config);
+      const request = forgeRequest({ method: 'post', headers: {}, path: '/some-path' });
+
+      toolkit.next.mockReturnValue('next' as any);
+
+      const result = handler(request, responseFactory, toolkit);
+
+      expect(responseFactory.badRequest).not.toHaveBeenCalled();
+      expect(toolkit.next).toHaveBeenCalledTimes(1);
+      expect(result).toEqual('next');
+    });
+  });
+});
+
+describe('versionCheck post-auth handler', () => {
+  let toolkit: ReturnType<typeof httpServerMock.createToolkit>;
+  let responseFactory: ReturnType<typeof httpServerMock.createLifecycleResponseFactory>;
+
+  beforeEach(() => {
+    toolkit = httpServerMock.createToolkit();
+    responseFactory = httpServerMock.createLifecycleResponseFactory();
+  });
+
+  it('forward the request to the next interceptor if header matches', () => {
+    const handler = createVersionCheckPostAuthHandler('actual-version');
+    const request = forgeRequest({ headers: { 'kbn-version': 'actual-version' } });
+
+    toolkit.next.mockReturnValue('next' as any);
+
+    const result = handler(request, responseFactory, toolkit);
+
+    expect(toolkit.next).toHaveBeenCalledTimes(1);
+    expect(responseFactory.badRequest).not.toHaveBeenCalled();
+    expect(result).toBe('next');
+  });
+
+  it('returns a badRequest error if header does not match', () => {
+    const handler = createVersionCheckPostAuthHandler('actual-version');
+    const request = forgeRequest({ headers: { 'kbn-version': 'another-version' } });
+
+    responseFactory.badRequest.mockReturnValue('badRequest' as any);
+
+    const result = handler(request, responseFactory, toolkit);
+
+    expect(toolkit.next).not.toHaveBeenCalled();
+    expect(responseFactory.badRequest).toHaveBeenCalledTimes(1);
+    expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(`
+      Object {
+        "body": Object {
+          "attributes": Object {
+            "expected": "actual-version",
+            "got": "another-version",
+          },
+          "message": "Browser client is out of date, please refresh the page (\\"kbn-version\\" header was \\"another-version\\" but should be \\"actual-version\\")",
+        },
+      }
+    `);
+    expect(result).toBe('badRequest');
+  });
+
+  it('forward the request to the next interceptor if header is not present', () => {
+    const handler = createVersionCheckPostAuthHandler('actual-version');
+    const request = forgeRequest({ headers: {} });
+
+    toolkit.next.mockReturnValue('next' as any);
+
+    const result = handler(request, responseFactory, toolkit);
+
+    expect(toolkit.next).toHaveBeenCalledTimes(1);
+    expect(responseFactory.badRequest).not.toHaveBeenCalled();
+    expect(result).toBe('next');
+  });
+});
+
+describe('customHeaders pre-response handler', () => {
+  let toolkit: ReturnType<typeof httpServerMock.createToolkit>;
+
+  beforeEach(() => {
+    toolkit = httpServerMock.createToolkit();
+  });
+
+  it('adds the kbn-name header to the response', () => {
+    const config = createConfig({ name: 'my-server-name' });
+    const handler = createCustomHeadersPreResponseHandler(config as HttpConfig);
+
+    handler({} as any, {} as any, toolkit);
+
+    expect(toolkit.next).toHaveBeenCalledTimes(1);
+    expect(toolkit.next).toHaveBeenCalledWith({ headers: { 'kbn-name': 'my-server-name' } });
+  });
+
+  it('adds the custom headers defined in the configuration', () => {
+    const config = createConfig({
+      name: 'my-server-name',
+      customResponseHeaders: {
+        headerA: 'value-A',
+        headerB: 'value-B',
+      },
+    });
+    const handler = createCustomHeadersPreResponseHandler(config as HttpConfig);
+
+    handler({} as any, {} as any, toolkit);
+
+    expect(toolkit.next).toHaveBeenCalledTimes(1);
+    expect(toolkit.next).toHaveBeenCalledWith({
+      headers: {
+        'kbn-name': 'my-server-name',
+        headerA: 'value-A',
+        headerB: 'value-B',
+      },
+    });
+  });
+
+  it('preserve the kbn-name value from server.name if definied in custom headders ', () => {
+    const config = createConfig({
+      name: 'my-server-name',
+      customResponseHeaders: {
+        'kbn-name': 'custom-name',
+        headerA: 'value-A',
+        headerB: 'value-B',
+      },
+    });
+    const handler = createCustomHeadersPreResponseHandler(config as HttpConfig);
+
+    handler({} as any, {} as any, toolkit);
+
+    expect(toolkit.next).toHaveBeenCalledTimes(1);
+    expect(toolkit.next).toHaveBeenCalledWith({
+      headers: {
+        'kbn-name': 'my-server-name',
+        headerA: 'value-A',
+        headerB: 'value-B',
+      },
+    });
+  });
+});
diff --git a/src/core/server/http/lifecycle_handlers.ts b/src/core/server/http/lifecycle_handlers.ts
new file mode 100644
index 0000000000000..ee877ee031a2b
--- /dev/null
+++ b/src/core/server/http/lifecycle_handlers.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { OnPostAuthHandler } from './lifecycle/on_post_auth';
+import { OnPreResponseHandler } from './lifecycle/on_pre_response';
+import { HttpConfig } from './http_config';
+import { Env } from '../config';
+import { LifecycleRegistrar } from './http_server';
+
+const VERSION_HEADER = 'kbn-version';
+const XSRF_HEADER = 'kbn-xsrf';
+const KIBANA_NAME_HEADER = 'kbn-name';
+
+export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler => {
+  const { whitelist, disableProtection } = config.xsrf;
+
+  return (request, response, toolkit) => {
+    if (disableProtection || whitelist.includes(request.route.path)) {
+      return toolkit.next();
+    }
+
+    const isSafeMethod = request.route.method === 'get' || request.route.method === 'head';
+    const hasVersionHeader = VERSION_HEADER in request.headers;
+    const hasXsrfHeader = XSRF_HEADER in request.headers;
+
+    if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
+      return response.badRequest({ body: `Request must contain a ${XSRF_HEADER} header.` });
+    }
+
+    return toolkit.next();
+  };
+};
+
+export const createVersionCheckPostAuthHandler = (kibanaVersion: string): OnPostAuthHandler => {
+  return (request, response, toolkit) => {
+    const requestVersion = request.headers[VERSION_HEADER];
+    if (requestVersion && requestVersion !== kibanaVersion) {
+      return response.badRequest({
+        body: {
+          message:
+            `Browser client is out of date, please refresh the page ` +
+            `("${VERSION_HEADER}" header was "${requestVersion}" but should be "${kibanaVersion}")`,
+          attributes: {
+            expected: kibanaVersion,
+            got: requestVersion,
+          },
+        },
+      });
+    }
+
+    return toolkit.next();
+  };
+};
+
+export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPreResponseHandler => {
+  const serverName = config.name;
+  const customHeaders = config.customResponseHeaders;
+
+  return (request, response, toolkit) => {
+    const additionalHeaders = {
+      ...customHeaders,
+      [KIBANA_NAME_HEADER]: serverName,
+    };
+
+    return toolkit.next({ headers: additionalHeaders });
+  };
+};
+
+export const registerCoreHandlers = (
+  registrar: LifecycleRegistrar,
+  config: HttpConfig,
+  env: Env
+) => {
+  registrar.registerOnPreResponse(createCustomHeadersPreResponseHandler(config));
+  registrar.registerOnPostAuth(createXsrfPostAuthHandler(config));
+  registrar.registerOnPostAuth(createVersionCheckPostAuthHandler(env.packageInfo.version));
+};
diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts
index e0a15cdc6e839..ffdc04d156ca0 100644
--- a/src/core/server/http/test_utils.ts
+++ b/src/core/server/http/test_utils.ts
@@ -41,6 +41,10 @@ configService.atPath.mockReturnValue(
       enabled: false,
     },
     compression: { enabled: true },
+    xsrf: {
+      disableProtection: true,
+      whitelist: [],
+    },
   } as any)
 );
 
diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
index 0ebd8b8371628..ac63f424eabaf 100644
--- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
+++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
@@ -8,10 +8,14 @@ Object {
     "enabled": true,
   },
   "cors": false,
+  "customResponseHeaders": Object {
+    "custom-header": "custom-value",
+  },
   "defaultRoute": undefined,
   "host": "host",
   "keepaliveTimeout": 5000,
   "maxPayload": 1000,
+  "name": "kibana-hostname",
   "port": 1234,
   "rewriteBasePath": false,
   "socketTimeout": 2000,
@@ -21,6 +25,10 @@ Object {
     "someNewValue": "new",
   },
   "uuid": undefined,
+  "xsrf": Object {
+    "disableProtection": false,
+    "whitelist": Array [],
+  },
 }
 `;
 
@@ -32,10 +40,14 @@ Object {
     "enabled": true,
   },
   "cors": false,
+  "customResponseHeaders": Object {
+    "custom-header": "custom-value",
+  },
   "defaultRoute": undefined,
   "host": "host",
   "keepaliveTimeout": 5000,
   "maxPayload": 1000,
+  "name": "kibana-hostname",
   "port": 1234,
   "rewriteBasePath": false,
   "socketTimeout": 2000,
@@ -45,6 +57,10 @@ Object {
     "key": "key",
   },
   "uuid": undefined,
+  "xsrf": Object {
+    "disableProtection": false,
+    "whitelist": Array [],
+  },
 }
 `;
 
diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts
index db2bc117280ca..1c51564187442 100644
--- a/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts
+++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.test.ts
@@ -80,9 +80,11 @@ describe('#get', () => {
   test('correctly handles server config.', () => {
     const configAdapter = new LegacyObjectToConfigAdapter({
       server: {
+        name: 'kibana-hostname',
         autoListen: true,
         basePath: '/abc',
         cors: false,
+        customResponseHeaders: { 'custom-header': 'custom-value' },
         host: 'host',
         maxPayloadBytes: 1000,
         keepaliveTimeout: 5000,
@@ -92,14 +94,20 @@ describe('#get', () => {
         ssl: { enabled: true, keyPassphrase: 'some-phrase', someNewValue: 'new' },
         compression: { enabled: true },
         someNotSupportedValue: 'val',
+        xsrf: {
+          disableProtection: false,
+          whitelist: [],
+        },
       },
     });
 
     const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({
       server: {
+        name: 'kibana-hostname',
         autoListen: true,
         basePath: '/abc',
         cors: false,
+        customResponseHeaders: { 'custom-header': 'custom-value' },
         host: 'host',
         maxPayloadBytes: 1000,
         keepaliveTimeout: 5000,
@@ -109,6 +117,10 @@ describe('#get', () => {
         ssl: { enabled: false, certificate: 'cert', key: 'key' },
         compression: { enabled: true },
         someNotSupportedValue: 'val',
+        xsrf: {
+          disableProtection: false,
+          whitelist: [],
+        },
       },
     });
 
diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
index bdcde8262ef98..397e7a46def58 100644
--- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
+++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts
@@ -60,14 +60,16 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
 
   private static transformServer(configValue: any = {}) {
     // TODO: New platform uses just a subset of `server` config from the legacy platform,
-    // new values will be exposed once we need them (eg. customResponseHeaders or xsrf).
+    // new values will be exposed once we need them
     return {
       autoListen: configValue.autoListen,
       basePath: configValue.basePath,
       defaultRoute: configValue.defaultRoute,
       cors: configValue.cors,
+      customResponseHeaders: configValue.customResponseHeaders,
       host: configValue.host,
       maxPayload: configValue.maxPayloadBytes,
+      name: configValue.name,
       port: configValue.port,
       rewriteBasePath: configValue.rewriteBasePath,
       ssl: configValue.ssl,
@@ -75,6 +77,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter {
       socketTimeout: configValue.socketTimeout,
       compression: configValue.compression,
       uuid: configValue.uuid,
+      xsrf: configValue.xsrf,
     };
   }
 
diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js
index 193011c0b8be9..64e778642aad9 100644
--- a/src/legacy/server/config/schema.js
+++ b/src/legacy/server/config/schema.js
@@ -71,19 +71,6 @@ export default () =>
     server: Joi.object({
       name: Joi.string().default(os.hostname()),
       defaultRoute: Joi.string().regex(/^\//, `start with a slash`),
-      customResponseHeaders: Joi.object()
-        .unknown(true)
-        .default({}),
-      xsrf: Joi.object({
-        disableProtection: Joi.boolean().default(false),
-        whitelist: Joi.array()
-          .items(Joi.string().regex(/^\//, 'start with a slash'))
-          .default([]),
-        token: Joi.string()
-          .optional()
-          .notes('Deprecated'),
-      }).default(),
-
       // keep them for BWC, remove when not used in Legacy.
       // validation should be in sync with one in New platform.
       // https://github.com/elastic/kibana/blob/master/src/core/server/http/http_config.ts
@@ -103,12 +90,14 @@ export default () =>
 
       autoListen: HANDLED_IN_NEW_PLATFORM,
       cors: HANDLED_IN_NEW_PLATFORM,
+      customResponseHeaders: HANDLED_IN_NEW_PLATFORM,
       keepaliveTimeout: HANDLED_IN_NEW_PLATFORM,
       maxPayloadBytes: HANDLED_IN_NEW_PLATFORM,
       socketTimeout: HANDLED_IN_NEW_PLATFORM,
       ssl: HANDLED_IN_NEW_PLATFORM,
       compression: HANDLED_IN_NEW_PLATFORM,
       uuid: HANDLED_IN_NEW_PLATFORM,
+      xsrf: HANDLED_IN_NEW_PLATFORM,
     }).default(),
 
     uiSettings: HANDLED_IN_NEW_PLATFORM,
diff --git a/src/legacy/server/config/schema.test.js b/src/legacy/server/config/schema.test.js
index 1207a05a47497..03d2fe53c2ce7 100644
--- a/src/legacy/server/config/schema.test.js
+++ b/src/legacy/server/config/schema.test.js
@@ -19,7 +19,6 @@
 
 import schemaProvider from './schema';
 import Joi from 'joi';
-import { set } from 'lodash';
 
 describe('Config schema', function() {
   let schema;
@@ -100,60 +99,5 @@ describe('Config schema', function() {
         expect(error.details[0]).toHaveProperty('path', ['server', 'rewriteBasePath']);
       });
     });
-
-    describe('xsrf', () => {
-      it('disableProtection is `false` by default.', () => {
-        const {
-          error,
-          value: {
-            server: {
-              xsrf: { disableProtection },
-            },
-          },
-        } = validate({});
-        expect(error).toBe(null);
-        expect(disableProtection).toBe(false);
-      });
-
-      it('whitelist is empty by default.', () => {
-        const {
-          value: {
-            server: {
-              xsrf: { whitelist },
-            },
-          },
-        } = validate({});
-        expect(whitelist).toBeInstanceOf(Array);
-        expect(whitelist).toHaveLength(0);
-      });
-
-      it('whitelist rejects paths that do not start with a slash.', () => {
-        const config = {};
-        set(config, 'server.xsrf.whitelist', ['path/to']);
-
-        const { error } = validate(config);
-        expect(error).toBeInstanceOf(Object);
-        expect(error).toHaveProperty('details');
-        expect(error.details[0]).toHaveProperty('path', ['server', 'xsrf', 'whitelist', 0]);
-      });
-
-      it('whitelist accepts paths that start with a slash.', () => {
-        const config = {};
-        set(config, 'server.xsrf.whitelist', ['/path/to']);
-
-        const {
-          error,
-          value: {
-            server: {
-              xsrf: { whitelist },
-            },
-          },
-        } = validate(config);
-        expect(error).toBe(null);
-        expect(whitelist).toBeInstanceOf(Array);
-        expect(whitelist).toHaveLength(1);
-        expect(whitelist).toContain('/path/to');
-      });
-    });
   });
 });
diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js
index 9b5ce2046c5d3..265d71e95b301 100644
--- a/src/legacy/server/http/index.js
+++ b/src/legacy/server/http/index.js
@@ -22,11 +22,9 @@ import { resolve } from 'path';
 import _ from 'lodash';
 import Boom from 'boom';
 
-import { setupVersionCheck } from './version_check';
 import { registerHapiPlugins } from './register_hapi_plugins';
 import { setupBasePathProvider } from './setup_base_path_provider';
 import { setupDefaultRouteProvider } from './setup_default_route_provider';
-import { setupXsrf } from './xsrf';
 
 export default async function(kbnServer, server, config) {
   server = kbnServer.server;
@@ -62,29 +60,6 @@ export default async function(kbnServer, server, config) {
     });
   });
 
-  // attach the app name to the server, so we can be sure we are actually talking to kibana
-  server.ext('onPreResponse', function onPreResponse(req, h) {
-    const response = req.response;
-
-    const customHeaders = {
-      ...config.get('server.customResponseHeaders'),
-      'kbn-name': kbnServer.name,
-    };
-
-    if (response.isBoom) {
-      response.output.headers = {
-        ...response.output.headers,
-        ...customHeaders,
-      };
-    } else {
-      Object.keys(customHeaders).forEach(name => {
-        response.header(name, customHeaders[name]);
-      });
-    }
-
-    return h.continue;
-  });
-
   server.route({
     path: '/',
     method: 'GET',
@@ -116,7 +91,4 @@ export default async function(kbnServer, server, config) {
 
   // Expose static assets
   server.exposeStaticDir('/ui/{path*}', resolve(__dirname, '../../ui/public/assets'));
-
-  setupVersionCheck(server, config);
-  setupXsrf(server, config);
 }
diff --git a/src/legacy/server/http/integration_tests/version_check.test.js b/src/legacy/server/http/integration_tests/version_check.test.js
deleted file mode 100644
index 8d71c98d64969..0000000000000
--- a/src/legacy/server/http/integration_tests/version_check.test.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { resolve } from 'path';
-import * as kbnTestServer from '../../../../test_utils/kbn_server';
-
-const src = resolve.bind(null, __dirname, '../../../../../src');
-
-const versionHeader = 'kbn-version';
-const version = require(src('../package.json')).version; // eslint-disable-line import/no-dynamic-require
-
-describe('version_check request filter', function() {
-  let root;
-  beforeAll(async () => {
-    root = kbnTestServer.createRoot();
-
-    await root.setup();
-    await root.start();
-
-    kbnTestServer.getKbnServer(root).server.route({
-      path: '/version_check/test/route',
-      method: 'GET',
-      handler: function() {
-        return 'ok';
-      },
-    });
-  }, 30000);
-
-  afterAll(async () => await root.shutdown());
-
-  it('accepts requests with the correct version passed in the version header', async function() {
-    await kbnTestServer.request
-      .get(root, '/version_check/test/route')
-      .set(versionHeader, version)
-      .expect(200, 'ok');
-  });
-
-  it('rejects requests with an incorrect version passed in the version header', async function() {
-    await kbnTestServer.request
-      .get(root, '/version_check/test/route')
-      .set(versionHeader, `invalid:${version}`)
-      .expect(400, /"Browser client is out of date/);
-  });
-
-  it('accepts requests that do not include a version header', async function() {
-    await kbnTestServer.request.get(root, '/version_check/test/route').expect(200, 'ok');
-  });
-});
diff --git a/src/legacy/server/http/integration_tests/xsrf.test.js b/src/legacy/server/http/integration_tests/xsrf.test.js
deleted file mode 100644
index a06f4eec4fd5c..0000000000000
--- a/src/legacy/server/http/integration_tests/xsrf.test.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { resolve } from 'path';
-import * as kbnTestServer from '../../../../test_utils/kbn_server';
-
-const destructiveMethods = ['POST', 'PUT', 'DELETE'];
-const src = resolve.bind(null, __dirname, '../../../../../src');
-
-const xsrfHeader = 'kbn-xsrf';
-const versionHeader = 'kbn-version';
-const testPath = '/xsrf/test/route';
-const whitelistedTestPath = '/xsrf/test/route/whitelisted';
-const actualVersion = require(src('../package.json')).version; // eslint-disable-line import/no-dynamic-require
-
-describe('xsrf request filter', () => {
-  let root;
-  beforeAll(async () => {
-    root = kbnTestServer.createRoot({
-      server: {
-        xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] },
-      },
-    });
-
-    await root.setup();
-    await root.start();
-
-    const kbnServer = kbnTestServer.getKbnServer(root);
-    kbnServer.server.route({
-      path: testPath,
-      method: 'GET',
-      handler: async function() {
-        return 'ok';
-      },
-    });
-
-    kbnServer.server.route({
-      path: testPath,
-      method: destructiveMethods,
-      config: {
-        // Disable payload parsing to make HapiJS server accept any content-type header.
-        payload: {
-          parse: false,
-        },
-        validate: { payload: null },
-      },
-      handler: async function() {
-        return 'ok';
-      },
-    });
-
-    kbnServer.server.route({
-      path: whitelistedTestPath,
-      method: destructiveMethods,
-      config: {
-        // Disable payload parsing to make HapiJS server accept any content-type header.
-        payload: {
-          parse: false,
-        },
-        validate: { payload: null },
-      },
-      handler: async function() {
-        return 'ok';
-      },
-    });
-  }, 30000);
-
-  afterAll(async () => await root.shutdown());
-
-  describe(`nonDestructiveMethod: GET`, function() {
-    it('accepts requests without a token', async function() {
-      await kbnTestServer.request.get(root, testPath).expect(200, 'ok');
-    });
-
-    it('accepts requests with the xsrf header', async function() {
-      await kbnTestServer.request
-        .get(root, testPath)
-        .set(xsrfHeader, 'anything')
-        .expect(200, 'ok');
-    });
-  });
-
-  describe(`nonDestructiveMethod: HEAD`, function() {
-    it('accepts requests without a token', async function() {
-      await kbnTestServer.request.head(root, testPath).expect(200, undefined);
-    });
-
-    it('accepts requests with the xsrf header', async function() {
-      await kbnTestServer.request
-        .head(root, testPath)
-        .set(xsrfHeader, 'anything')
-        .expect(200, undefined);
-    });
-  });
-
-  for (const method of destructiveMethods) {
-    // eslint-disable-next-line no-loop-func
-    describe(`destructiveMethod: ${method}`, function() {
-      it('accepts requests with the xsrf header', async function() {
-        await kbnTestServer.request[method.toLowerCase()](root, testPath)
-          .set(xsrfHeader, 'anything')
-          .expect(200, 'ok');
-      });
-
-      // this is still valid for existing csrf protection support
-      // it does not actually do any validation on the version value itself
-      it('accepts requests with the version header', async function() {
-        await kbnTestServer.request[method.toLowerCase()](root, testPath)
-          .set(versionHeader, actualVersion)
-          .expect(200, 'ok');
-      });
-
-      it('rejects requests without either an xsrf or version header', async function() {
-        await kbnTestServer.request[method.toLowerCase()](root, testPath).expect(400, {
-          statusCode: 400,
-          error: 'Bad Request',
-          message: 'Request must contain a kbn-xsrf header.',
-        });
-      });
-
-      it('accepts whitelisted requests without either an xsrf or version header', async function() {
-        await kbnTestServer.request[method.toLowerCase()](root, whitelistedTestPath).expect(
-          200,
-          'ok'
-        );
-      });
-    });
-  }
-});
diff --git a/src/legacy/server/http/version_check.js b/src/legacy/server/http/version_check.js
deleted file mode 100644
index 12666c9a0f3f6..0000000000000
--- a/src/legacy/server/http/version_check.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { badRequest } from 'boom';
-
-export function setupVersionCheck(server, config) {
-  const versionHeader = 'kbn-version';
-  const actualVersion = config.get('pkg.version');
-
-  server.ext('onPostAuth', function onPostAuthVersionCheck(req, h) {
-    const versionRequested = req.headers[versionHeader];
-
-    if (versionRequested && versionRequested !== actualVersion) {
-      throw badRequest(
-        `Browser client is out of date, \
-        please refresh the page ("${versionHeader}" header was "${versionRequested}" but should be "${actualVersion}")`,
-        { expected: actualVersion, got: versionRequested }
-      );
-    }
-
-    return h.continue;
-  });
-}
diff --git a/src/legacy/server/http/xsrf.js b/src/legacy/server/http/xsrf.js
deleted file mode 100644
index 79ac3af6d9f90..0000000000000
--- a/src/legacy/server/http/xsrf.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { badRequest } from 'boom';
-
-export function setupXsrf(server, config) {
-  const disabled = config.get('server.xsrf.disableProtection');
-  const whitelist = config.get('server.xsrf.whitelist');
-  const versionHeader = 'kbn-version';
-  const xsrfHeader = 'kbn-xsrf';
-
-  server.ext('onPostAuth', function onPostAuthXsrf(req, h) {
-    if (disabled) {
-      return h.continue;
-    }
-
-    if (whitelist.includes(req.path)) {
-      return h.continue;
-    }
-
-    const isSafeMethod = req.method === 'get' || req.method === 'head';
-    const hasVersionHeader = versionHeader in req.headers;
-    const hasXsrfHeader = xsrfHeader in req.headers;
-
-    if (!isSafeMethod && !hasVersionHeader && !hasXsrfHeader) {
-      throw badRequest(`Request must contain a ${xsrfHeader} header.`);
-    }
-
-    return h.continue;
-  });
-}
diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts
index 370c7554b2cc2..0ed33928ff63c 100644
--- a/src/test_utils/kbn_server.ts
+++ b/src/test_utils/kbn_server.ts
@@ -37,7 +37,7 @@ import { Root } from '../core/server/root';
 import KbnServer from '../legacy/server/kbn_server';
 import { CallCluster } from '../legacy/core_plugins/elasticsearch';
 
-type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put';
+export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put';
 
 const DEFAULTS_SETTINGS = {
   server: {
@@ -97,7 +97,7 @@ export function createRootWithSettings(
  * @param method
  * @param path
  */
-function getSupertest(root: Root, method: HttpMethod, path: string) {
+export function getSupertest(root: Root, method: HttpMethod, path: string) {
   const testUserCredentials = Buffer.from(`${kibanaTestUser.username}:${kibanaTestUser.password}`);
   return supertest((root as any).server.http.httpServer.server.listener)
     [method](path)

From 447c15d0d2be5342c65d9afc7204db77903582fb Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris <github@gidi.io>
Date: Mon, 6 Jan 2020 17:17:38 +0000
Subject: [PATCH 2/3] allows Alerts to recover gracefully from Executor errors
 (#53688) (#54018)

Prevents errors in Alert Executors from forcing their underlying tasks into a zombie state.
---
 .../alert_instance.test.ts                    |   0
 .../{lib => alert_instance}/alert_instance.ts |   2 +-
 .../create_alert_instance_factory.test.ts     |   0
 .../create_alert_instance_factory.ts          |   0
 .../alerting/server/alert_instance/index.ts   |   8 +
 .../server/alert_type_registry.test.ts        |   2 +-
 .../alerting/server/alert_type_registry.ts    |   4 +-
 .../{lib => }/alerts_client_factory.test.ts   |  26 +-
 .../server/{lib => }/alerts_client_factory.ts |  12 +-
 .../plugins/alerting/server/lib/index.ts      |   6 +-
 .../alerting/server/lib/result_type.ts        |  54 +++
 .../server/lib/task_runner_factory.test.ts    | 345 ---------------
 .../server/lib/task_runner_factory.ts         | 190 ---------
 .../legacy/plugins/alerting/server/plugin.ts  |   3 +-
 .../create_execution_handler.test.ts          |   0
 .../create_execution_handler.ts               |   0
 .../get_next_run_at.test.ts                   |   0
 .../{lib => task_runner}/get_next_run_at.ts   |   2 +-
 .../alerting/server/task_runner/index.ts      |   7 +
 .../server/task_runner/task_runner.test.ts    | 400 ++++++++++++++++++
 .../server/task_runner/task_runner.ts         | 241 +++++++++++
 .../task_runner/task_runner_factory.test.ts   |  87 ++++
 .../server/task_runner/task_runner_factory.ts |  46 ++
 .../transform_action_params.test.ts           |   0
 .../transform_action_params.ts                |   0
 .../legacy/plugins/alerting/server/types.ts   |   2 +-
 .../common/lib/alert_utils.ts                 |  47 +-
 .../common/lib/index.ts                       |   1 +
 .../common/lib/test_assertions.ts             |  17 +
 .../tests/alerting/update.ts                  |  15 +-
 .../spaces_only/tests/alerting/alerts.ts      |  40 ++
 31 files changed, 982 insertions(+), 575 deletions(-)
 rename x-pack/legacy/plugins/alerting/server/{lib => alert_instance}/alert_instance.test.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => alert_instance}/alert_instance.ts (97%)
 rename x-pack/legacy/plugins/alerting/server/{lib => alert_instance}/create_alert_instance_factory.test.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => alert_instance}/create_alert_instance_factory.ts (100%)
 create mode 100644 x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
 rename x-pack/legacy/plugins/alerting/server/{lib => }/alerts_client_factory.test.ts (81%)
 rename x-pack/legacy/plugins/alerting/server/{lib => }/alerts_client_factory.ts (92%)
 create mode 100644 x-pack/legacy/plugins/alerting/server/lib/result_type.ts
 delete mode 100644 x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts
 delete mode 100644 x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/create_execution_handler.test.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/create_execution_handler.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/get_next_run_at.test.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/get_next_run_at.ts (92%)
 create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/index.ts
 create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
 create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
 create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
 create mode 100644 x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/transform_action_params.test.ts (100%)
 rename x-pack/legacy/plugins/alerting/server/{lib => task_runner}/transform_action_params.ts (100%)
 create mode 100644 x-pack/test/alerting_api_integration/common/lib/test_assertions.ts

diff --git a/x-pack/legacy/plugins/alerting/server/lib/alert_instance.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/alert_instance.test.ts
rename to x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/alert_instance.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
similarity index 97%
rename from x-pack/legacy/plugins/alerting/server/lib/alert_instance.ts
rename to x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
index 1e2cc26f364ad..a56e2077cdfd8 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/alert_instance.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts
@@ -5,7 +5,7 @@
  */
 
 import { State, Context } from '../types';
-import { parseDuration } from './parse_duration';
+import { parseDuration } from '../lib';
 
 interface Meta {
   lastScheduledActions?: {
diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_alert_instance_factory.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/create_alert_instance_factory.test.ts
rename to x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_alert_instance_factory.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/create_alert_instance_factory.ts
rename to x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.ts
diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
new file mode 100644
index 0000000000000..40ee0874e805c
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { AlertInstance } from './alert_instance';
+export { createAlertInstanceFactory } from './create_alert_instance_factory';
diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
index 57e1b965960e8..8e96ad8dae31c 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { TaskRunnerFactory } from './lib';
+import { TaskRunnerFactory } from './task_runner';
 import { AlertTypeRegistry } from './alert_type_registry';
 import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
 
diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
index b7512864c2a98..2003e810a05b5 100644
--- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
+++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.ts
@@ -6,8 +6,8 @@
 
 import Boom from 'boom';
 import { i18n } from '@kbn/i18n';
-import { TaskRunnerFactory } from './lib';
-import { RunContext } from '../../task_manager/server';
+import { TaskRunnerFactory } from './task_runner';
+import { RunContext } from '../../task_manager';
 import { TaskManagerSetupContract } from './shim';
 import { AlertType } from './types';
 
diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
similarity index 81%
rename from x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts
rename to x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
index 838c567fb2878..519001d07e089 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.test.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.test.ts
@@ -6,13 +6,13 @@
 
 import { Request } from 'hapi';
 import { AlertsClientFactory, ConstructorOpts } from './alerts_client_factory';
-import { alertTypeRegistryMock } from '../alert_type_registry.mock';
-import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
-import { KibanaRequest } from '../../../../../../src/core/server';
-import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
-import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
+import { alertTypeRegistryMock } from './alert_type_registry.mock';
+import { taskManagerMock } from '../../task_manager/server/task_manager.mock';
+import { KibanaRequest } from '../../../../../src/core/server';
+import { loggingServiceMock } from '../../../../../src/core/server/mocks';
+import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks';
 
-jest.mock('../alerts_client');
+jest.mock('./alerts_client');
 
 const savedObjectsClient = jest.fn();
 const securityPluginSetup = {
@@ -55,7 +55,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
   const factory = new AlertsClientFactory(alertsClientFactoryParams);
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
 
-  expect(jest.requireMock('../alerts_client').AlertsClient).toHaveBeenCalledWith({
+  expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
     savedObjectsClient,
     logger: alertsClientFactoryParams.logger,
     taskManager: alertsClientFactoryParams.taskManager,
@@ -72,7 +72,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
 test('getUserName() returns null when security is disabled', async () => {
   const factory = new AlertsClientFactory(alertsClientFactoryParams);
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   const userNameResult = await constructorCall.getUserName();
   expect(userNameResult).toEqual(null);
@@ -84,7 +84,7 @@ test('getUserName() returns a name when security is enabled', async () => {
     securityPluginSetup: securityPluginSetup as any,
   });
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   securityPluginSetup.authc.getCurrentUser.mockResolvedValueOnce({ username: 'bob' });
   const userNameResult = await constructorCall.getUserName();
@@ -94,7 +94,7 @@ test('getUserName() returns a name when security is enabled', async () => {
 test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => {
   const factory = new AlertsClientFactory(alertsClientFactoryParams);
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   const createAPIKeyResult = await constructorCall.createAPIKey();
   expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false });
@@ -103,7 +103,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled
 test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => {
   const factory = new AlertsClientFactory(alertsClientFactoryParams);
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce(null);
   const createAPIKeyResult = await constructorCall.createAPIKey();
@@ -116,7 +116,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
     securityPluginSetup: securityPluginSetup as any,
   });
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce({ api_key: '123', id: 'abc' });
   const createAPIKeyResult = await constructorCall.createAPIKey();
@@ -132,7 +132,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error',
     securityPluginSetup: securityPluginSetup as any,
   });
   factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
-  const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
+  const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
 
   securityPluginSetup.authc.createAPIKey.mockRejectedValueOnce(new Error('TLS disabled'));
   await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot(
diff --git a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
similarity index 92%
rename from x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts
rename to x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
index 026d6c92b0d75..94a396fbaa806 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/alerts_client_factory.ts
+++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts
@@ -6,12 +6,12 @@
 
 import Hapi from 'hapi';
 import uuid from 'uuid';
-import { AlertsClient } from '../alerts_client';
-import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from '../types';
-import { SecurityPluginStartContract, TaskManagerStartContract } from '../shim';
-import { KibanaRequest, Logger } from '../../../../../../src/core/server';
-import { InvalidateAPIKeyParams } from '../../../../../plugins/security/server';
-import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
+import { AlertsClient } from './alerts_client';
+import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
+import { SecurityPluginStartContract, TaskManagerStartContract } from './shim';
+import { KibanaRequest, Logger } from '../../../../../src/core/server';
+import { InvalidateAPIKeyParams } from '../../../../plugins/security/server';
+import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server';
 
 export interface ConstructorOpts {
   logger: Logger;
diff --git a/x-pack/legacy/plugins/alerting/server/lib/index.ts b/x-pack/legacy/plugins/alerting/server/lib/index.ts
index ca4ddf9e11ad2..c41ea4a5998ff 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/index.ts
+++ b/x-pack/legacy/plugins/alerting/server/lib/index.ts
@@ -4,8 +4,6 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-export { AlertInstance } from './alert_instance';
-export { validateAlertTypeParams } from './validate_alert_type_params';
 export { parseDuration, getDurationSchema } from './parse_duration';
-export { AlertsClientFactory } from './alerts_client_factory';
-export { TaskRunnerFactory } from './task_runner_factory';
+export { LicenseState } from './license_state';
+export { validateAlertTypeParams } from './validate_alert_type_params';
diff --git a/x-pack/legacy/plugins/alerting/server/lib/result_type.ts b/x-pack/legacy/plugins/alerting/server/lib/result_type.ts
new file mode 100644
index 0000000000000..644ae51292249
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/lib/result_type.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface Ok<T> {
+  tag: 'ok';
+  value: T;
+}
+
+export interface Err<E> {
+  tag: 'err';
+  error: E;
+}
+export type Result<T, E> = Ok<T> | Err<E>;
+
+export function asOk<T>(value: T): Ok<T> {
+  return {
+    tag: 'ok',
+    value,
+  };
+}
+
+export function asErr<T>(error: T): Err<T> {
+  return {
+    tag: 'err',
+    error,
+  };
+}
+
+export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
+  return result.tag === 'ok';
+}
+
+export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
+  return !isOk(result);
+}
+
+export async function promiseResult<T, E>(future: Promise<T>): Promise<Result<T, E>> {
+  try {
+    return asOk(await future);
+  } catch (e) {
+    return asErr(e);
+  }
+}
+
+export function map<T, E, Resolution>(
+  result: Result<T, E>,
+  onOk: (value: T) => Resolution,
+  onErr: (error: E) => Resolution
+): Resolution {
+  return isOk(result) ? onOk(result.value) : onErr(result.error);
+}
diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts
deleted file mode 100644
index fd13452e04535..0000000000000
--- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import sinon from 'sinon';
-import { schema } from '@kbn/config-schema';
-import { AlertExecutorOptions } from '../types';
-import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server';
-import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
-import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
-import {
-  savedObjectsClientMock,
-  loggingServiceMock,
-} from '../../../../../../src/core/server/mocks';
-
-const alertType = {
-  id: 'test',
-  name: 'My test alert',
-  actionGroups: ['default'],
-  executor: jest.fn(),
-};
-let fakeTimer: sinon.SinonFakeTimers;
-let taskRunnerFactory: TaskRunnerFactory;
-let mockedTaskInstance: ConcreteTaskInstance;
-
-beforeAll(() => {
-  fakeTimer = sinon.useFakeTimers();
-  mockedTaskInstance = {
-    id: '',
-    attempts: 0,
-    status: TaskStatus.Running,
-    version: '123',
-    runAt: new Date(),
-    scheduledAt: new Date(),
-    startedAt: new Date(),
-    retryAt: new Date(Date.now() + 5 * 60 * 1000),
-    state: {
-      startedAt: new Date(Date.now() - 5 * 60 * 1000),
-    },
-    taskType: 'alerting:test',
-    params: {
-      alertId: '1',
-    },
-    ownerId: null,
-  };
-  taskRunnerFactory = new TaskRunnerFactory();
-  taskRunnerFactory.initialize(taskRunnerFactoryInitializerParams);
-});
-
-afterAll(() => fakeTimer.restore());
-
-const savedObjectsClient = savedObjectsClientMock.create();
-const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
-const services = {
-  log: jest.fn(),
-  callCluster: jest.fn(),
-  savedObjectsClient,
-};
-
-const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = {
-  getServices: jest.fn().mockReturnValue(services),
-  executeAction: jest.fn(),
-  encryptedSavedObjectsPlugin,
-  logger: loggingServiceMock.create().get(),
-  spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
-  getBasePath: jest.fn().mockReturnValue(undefined),
-};
-
-const mockedAlertTypeSavedObject = {
-  id: '1',
-  type: 'alert',
-  attributes: {
-    enabled: true,
-    alertTypeId: '123',
-    schedule: { interval: '10s' },
-    mutedInstanceIds: [],
-    params: {
-      bar: true,
-    },
-    actions: [
-      {
-        group: 'default',
-        actionRef: 'action_0',
-        params: {
-          foo: true,
-        },
-      },
-    ],
-  },
-  references: [
-    {
-      name: 'action_0',
-      type: 'action',
-      id: '1',
-    },
-  ],
-};
-
-beforeEach(() => {
-  jest.resetAllMocks();
-  taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
-});
-
-test(`throws an error if factory isn't initialized`, () => {
-  const factory = new TaskRunnerFactory();
-  expect(() =>
-    factory.create(alertType, { taskInstance: mockedTaskInstance })
-  ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`);
-});
-
-test(`throws an error if factory is already initialized`, () => {
-  const factory = new TaskRunnerFactory();
-  factory.initialize(taskRunnerFactoryInitializerParams);
-  expect(() =>
-    factory.initialize(taskRunnerFactoryInitializerParams)
-  ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory already initialized"`);
-});
-
-test('successfully executes the task', async () => {
-  const taskRunner = taskRunnerFactory.create(alertType, {
-    taskInstance: mockedTaskInstance,
-  });
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-  const runnerResult = await taskRunner.run();
-  expect(runnerResult).toMatchInlineSnapshot(`
-                                Object {
-                                  "runAt": 1970-01-01T00:00:10.000Z,
-                                  "state": Object {
-                                    "alertInstances": Object {},
-                                    "alertTypeState": undefined,
-                                    "previousStartedAt": 1970-01-01T00:00:00.000Z,
-                                  },
-                                }
-                `);
-  expect(alertType.executor).toHaveBeenCalledTimes(1);
-  const call = alertType.executor.mock.calls[0][0];
-  expect(call.params).toMatchInlineSnapshot(`
-                                    Object {
-                                      "bar": true,
-                                    }
-                  `);
-  expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`);
-  expect(call.state).toMatchInlineSnapshot(`Object {}`);
-  expect(call.services.alertInstanceFactory).toBeTruthy();
-  expect(call.services.callCluster).toBeTruthy();
-  expect(call.services).toBeTruthy();
-});
-
-test('executeAction is called per alert instance that is scheduled', async () => {
-  alertType.executor.mockImplementation(({ services: executorServices }: AlertExecutorOptions) => {
-    executorServices.alertInstanceFactory('1').scheduleActions('default');
-  });
-  const taskRunner = taskRunnerFactory.create(alertType, {
-    taskInstance: mockedTaskInstance,
-  });
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-  await taskRunner.run();
-  expect(taskRunnerFactoryInitializerParams.executeAction).toHaveBeenCalledTimes(1);
-  expect(taskRunnerFactoryInitializerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
-                Array [
-                  Object {
-                    "apiKey": "MTIzOmFiYw==",
-                    "id": "1",
-                    "params": Object {
-                      "foo": true,
-                    },
-                    "spaceId": undefined,
-                  },
-                ]
-        `);
-});
-
-test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => {
-  alertType.executor.mockImplementation(({ services: executorServices }: AlertExecutorOptions) => {
-    executorServices.alertInstanceFactory('1').scheduleActions('default');
-  });
-  const taskRunner = taskRunnerFactory.create(alertType, {
-    taskInstance: {
-      ...mockedTaskInstance,
-      state: {
-        ...mockedTaskInstance.state,
-        alertInstances: {
-          '1': { meta: {}, state: { bar: false } },
-          '2': { meta: {}, state: { bar: false } },
-        },
-      },
-    },
-  });
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-  const runnerResult = await taskRunner.run();
-  expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(`
-    Object {
-      "1": Object {
-        "meta": Object {
-          "lastScheduledActions": Object {
-            "date": 1970-01-01T00:00:00.000Z,
-            "group": "default",
-          },
-        },
-        "state": Object {
-          "bar": false,
-        },
-      },
-    }
-  `);
-});
-
-test('validates params before executing the alert type', async () => {
-  const taskRunner = taskRunnerFactory.create(
-    {
-      ...alertType,
-      validate: {
-        params: schema.object({
-          param1: schema.string(),
-        }),
-      },
-    },
-    { taskInstance: mockedTaskInstance }
-  );
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-  await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
-    `"params invalid: [param1]: expected value of type [string] but got [undefined]"`
-  );
-});
-
-test('throws error if reference not found', async () => {
-  const taskRunner = taskRunnerFactory.create(alertType, {
-    taskInstance: mockedTaskInstance,
-  });
-  savedObjectsClient.get.mockResolvedValueOnce({
-    ...mockedAlertTypeSavedObject,
-    references: [],
-  });
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-  await expect(taskRunner.run()).rejects.toThrowErrorMatchingInlineSnapshot(
-    `"Action reference \\"action_0\\" not found in alert id: 1"`
-  );
-});
-
-test('uses API key when provided', async () => {
-  const taskRunner = taskRunnerFactory.create(alertType, {
-    taskInstance: mockedTaskInstance,
-  });
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {
-      apiKey: Buffer.from('123:abc').toString('base64'),
-    },
-    references: [],
-  });
-
-  await taskRunner.run();
-  expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
-    getBasePath: expect.anything(),
-    headers: {
-      // base64 encoded "123:abc"
-      authorization: 'ApiKey MTIzOmFiYw==',
-    },
-    path: '/',
-    route: { settings: {} },
-    url: {
-      href: '/',
-    },
-    raw: {
-      req: {
-        url: '/',
-      },
-    },
-  });
-});
-
-test(`doesn't use API key when not provided`, async () => {
-  const factory = new TaskRunnerFactory();
-  factory.initialize(taskRunnerFactoryInitializerParams);
-  const taskRunner = factory.create(alertType, {
-    taskInstance: mockedTaskInstance,
-  });
-  savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
-  encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
-    id: '1',
-    type: 'alert',
-    attributes: {},
-    references: [],
-  });
-
-  await taskRunner.run();
-
-  expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
-    getBasePath: expect.anything(),
-    headers: {},
-    path: '/',
-    route: { settings: {} },
-    url: {
-      href: '/',
-    },
-    raw: {
-      req: {
-        url: '/',
-      },
-    },
-  });
-});
diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts
deleted file mode 100644
index 5614188795ded..0000000000000
--- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Logger } from '../../../../../../src/core/server';
-import { RunContext } from '../../../task_manager/server';
-import { createExecutionHandler } from './create_execution_handler';
-import { createAlertInstanceFactory } from './create_alert_instance_factory';
-import { AlertInstance } from './alert_instance';
-import { getNextRunAt } from './get_next_run_at';
-import { validateAlertTypeParams } from './validate_alert_type_params';
-import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
-import { PluginStartContract as ActionsPluginStartContract } from '../../../actions';
-import {
-  AlertType,
-  AlertServices,
-  GetBasePathFunction,
-  GetServicesFunction,
-  RawAlert,
-  SpaceIdToNamespaceFunction,
-  IntervalSchedule,
-} from '../types';
-
-export interface TaskRunnerContext {
-  logger: Logger;
-  getServices: GetServicesFunction;
-  executeAction: ActionsPluginStartContract['execute'];
-  encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
-  spaceIdToNamespace: SpaceIdToNamespaceFunction;
-  getBasePath: GetBasePathFunction;
-}
-
-export class TaskRunnerFactory {
-  private isInitialized = false;
-  private taskRunnerContext?: TaskRunnerContext;
-
-  public initialize(taskRunnerContext: TaskRunnerContext) {
-    if (this.isInitialized) {
-      throw new Error('TaskRunnerFactory already initialized');
-    }
-    this.isInitialized = true;
-    this.taskRunnerContext = taskRunnerContext;
-  }
-
-  public create(alertType: AlertType, { taskInstance }: RunContext) {
-    if (!this.isInitialized) {
-      throw new Error('TaskRunnerFactory not initialized');
-    }
-
-    const {
-      logger,
-      getServices,
-      executeAction,
-      encryptedSavedObjectsPlugin,
-      spaceIdToNamespace,
-      getBasePath,
-    } = this.taskRunnerContext!;
-
-    return {
-      async run() {
-        const { alertId, spaceId } = taskInstance.params;
-        const requestHeaders: Record<string, string> = {};
-        const namespace = spaceIdToNamespace(spaceId);
-        // Only fetch encrypted attributes here, we'll create a saved objects client
-        // scoped with the API key to fetch the remaining data.
-        const {
-          attributes: { apiKey },
-        } = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAlert>(
-          'alert',
-          alertId,
-          { namespace }
-        );
-
-        if (apiKey) {
-          requestHeaders.authorization = `ApiKey ${apiKey}`;
-        }
-
-        const fakeRequest = {
-          headers: requestHeaders,
-          getBasePath: () => getBasePath(spaceId),
-          path: '/',
-          route: { settings: {} },
-          url: {
-            href: '/',
-          },
-          raw: {
-            req: {
-              url: '/',
-            },
-          },
-        };
-
-        const services = getServices(fakeRequest);
-        // Ensure API key is still valid and user has access
-        const {
-          attributes: { params, actions, schedule, throttle, muteAll, mutedInstanceIds },
-          references,
-        } = await services.savedObjectsClient.get<RawAlert>('alert', alertId);
-
-        // Validate
-        const validatedAlertTypeParams = validateAlertTypeParams(alertType, params);
-
-        // Inject ids into actions
-        const actionsWithIds = actions.map(action => {
-          const actionReference = references.find(obj => obj.name === action.actionRef);
-          if (!actionReference) {
-            throw new Error(
-              `Action reference "${action.actionRef}" not found in alert id: ${alertId}`
-            );
-          }
-          return {
-            ...action,
-            id: actionReference.id,
-          };
-        });
-
-        const executionHandler = createExecutionHandler({
-          alertId,
-          logger,
-          executeAction,
-          apiKey,
-          actions: actionsWithIds,
-          spaceId,
-          alertType,
-        });
-        const alertInstances: Record<string, AlertInstance> = {};
-        const alertInstancesData = taskInstance.state.alertInstances || {};
-        for (const id of Object.keys(alertInstancesData)) {
-          alertInstances[id] = new AlertInstance(alertInstancesData[id]);
-        }
-        const alertInstanceFactory = createAlertInstanceFactory(alertInstances);
-
-        const alertTypeServices: AlertServices = {
-          ...services,
-          alertInstanceFactory,
-        };
-
-        const alertTypeState = await alertType.executor({
-          alertId,
-          services: alertTypeServices,
-          params: validatedAlertTypeParams,
-          state: taskInstance.state.alertTypeState || {},
-          startedAt: taskInstance.startedAt!,
-          previousStartedAt: taskInstance.state.previousStartedAt,
-        });
-
-        await Promise.all(
-          Object.keys(alertInstances).map(alertInstanceId => {
-            const alertInstance = alertInstances[alertInstanceId];
-            if (alertInstance.hasScheduledActions()) {
-              if (
-                alertInstance.isThrottled(throttle) ||
-                muteAll ||
-                mutedInstanceIds.includes(alertInstanceId)
-              ) {
-                return;
-              }
-              const { actionGroup, context, state } = alertInstance.getScheduledActionOptions()!;
-              alertInstance.updateLastScheduledActions(actionGroup);
-              alertInstance.unscheduleActions();
-              return executionHandler({ actionGroup, context, state, alertInstanceId });
-            } else {
-              // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object
-              delete alertInstances[alertInstanceId];
-            }
-          })
-        );
-
-        const nextRunAt = getNextRunAt(
-          new Date(taskInstance.startedAt!),
-          // we do not currently have a good way of returning the type
-          // from SavedObjectsClient, and as we currenrtly require a schedule
-          // and we only support `interval`, we can cast this safely
-          schedule as IntervalSchedule
-        );
-
-        return {
-          state: {
-            alertTypeState,
-            alertInstances,
-            previousStartedAt: taskInstance.startedAt!,
-          },
-          runAt: nextRunAt,
-        };
-      },
-    };
-  }
-}
diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts
index 24d4467dbd807..ede95f76bf811 100644
--- a/x-pack/legacy/plugins/alerting/server/plugin.ts
+++ b/x-pack/legacy/plugins/alerting/server/plugin.ts
@@ -9,7 +9,8 @@ import { first } from 'rxjs/operators';
 import { Services } from './types';
 import { AlertsClient } from './alerts_client';
 import { AlertTypeRegistry } from './alert_type_registry';
-import { AlertsClientFactory, TaskRunnerFactory } from './lib';
+import { TaskRunnerFactory } from './task_runner';
+import { AlertsClientFactory } from './alerts_client_factory';
 import { LicenseState } from './lib/license_state';
 import { IClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server';
 import {
diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.test.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/create_execution_handler.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/get_next_run_at.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/get_next_run_at.test.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts b/x-pack/legacy/plugins/alerting/server/task_runner/get_next_run_at.ts
similarity index 92%
rename from x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/get_next_run_at.ts
index f9867b5372908..cea4584e1f713 100644
--- a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/get_next_run_at.ts
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { parseDuration } from './parse_duration';
+import { parseDuration } from '../lib';
 import { IntervalSchedule } from '../types';
 
 export function getNextRunAt(currentRunAt: Date, schedule: IntervalSchedule) {
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/index.ts b/x-pack/legacy/plugins/alerting/server/task_runner/index.ts
new file mode 100644
index 0000000000000..f5401fbd9cd74
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { TaskRunnerFactory } from './task_runner_factory';
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
new file mode 100644
index 0000000000000..10627c655eca8
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -0,0 +1,400 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon from 'sinon';
+import { schema } from '@kbn/config-schema';
+import { AlertExecutorOptions } from '../types';
+import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager';
+import { TaskRunnerContext } from './task_runner_factory';
+import { TaskRunner } from './task_runner';
+import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
+import {
+  savedObjectsClientMock,
+  loggingServiceMock,
+} from '../../../../../../src/core/server/mocks';
+
+const alertType = {
+  id: 'test',
+  name: 'My test alert',
+  actionGroups: ['default'],
+  executor: jest.fn(),
+};
+let fakeTimer: sinon.SinonFakeTimers;
+
+describe('Task Runner', () => {
+  let mockedTaskInstance: ConcreteTaskInstance;
+
+  beforeAll(() => {
+    fakeTimer = sinon.useFakeTimers();
+    mockedTaskInstance = {
+      id: '',
+      attempts: 0,
+      status: TaskStatus.Running,
+      version: '123',
+      runAt: new Date(),
+      scheduledAt: new Date(),
+      startedAt: new Date(),
+      retryAt: new Date(Date.now() + 5 * 60 * 1000),
+      state: {
+        startedAt: new Date(Date.now() - 5 * 60 * 1000),
+      },
+      taskType: 'alerting:test',
+      params: {
+        alertId: '1',
+      },
+      ownerId: null,
+    };
+  });
+
+  afterAll(() => fakeTimer.restore());
+
+  const savedObjectsClient = savedObjectsClientMock.create();
+  const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
+  const services = {
+    log: jest.fn(),
+    callCluster: jest.fn(),
+    savedObjectsClient,
+  };
+
+  const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = {
+    getServices: jest.fn().mockReturnValue(services),
+    executeAction: jest.fn(),
+    encryptedSavedObjectsPlugin,
+    logger: loggingServiceMock.create().get(),
+    spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
+    getBasePath: jest.fn().mockReturnValue(undefined),
+  };
+
+  const mockedAlertTypeSavedObject = {
+    id: '1',
+    type: 'alert',
+    attributes: {
+      enabled: true,
+      alertTypeId: '123',
+      schedule: { interval: '10s' },
+      mutedInstanceIds: [],
+      params: {
+        bar: true,
+      },
+      actions: [
+        {
+          group: 'default',
+          actionRef: 'action_0',
+          params: {
+            foo: true,
+          },
+        },
+      ],
+    },
+    references: [
+      {
+        name: 'action_0',
+        type: 'action',
+        id: '1',
+      },
+    ],
+  };
+
+  beforeEach(() => {
+    jest.resetAllMocks();
+    taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
+  });
+
+  test('successfully executes the task', async () => {
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+    const runnerResult = await taskRunner.run();
+    expect(runnerResult).toMatchInlineSnapshot(`
+                                  Object {
+                                    "runAt": 1970-01-01T00:00:10.000Z,
+                                    "state": Object {
+                                      "alertInstances": Object {},
+                                      "alertTypeState": undefined,
+                                      "previousStartedAt": 1970-01-01T00:00:00.000Z,
+                                    },
+                                  }
+                  `);
+    expect(alertType.executor).toHaveBeenCalledTimes(1);
+    const call = alertType.executor.mock.calls[0][0];
+    expect(call.params).toMatchInlineSnapshot(`
+                                      Object {
+                                        "bar": true,
+                                      }
+                    `);
+    expect(call.startedAt).toMatchInlineSnapshot(`1970-01-01T00:00:00.000Z`);
+    expect(call.state).toMatchInlineSnapshot(`Object {}`);
+    expect(call.services.alertInstanceFactory).toBeTruthy();
+    expect(call.services.callCluster).toBeTruthy();
+    expect(call.services).toBeTruthy();
+  });
+
+  test('executeAction is called per alert instance that is scheduled', async () => {
+    alertType.executor.mockImplementation(
+      ({ services: executorServices }: AlertExecutorOptions) => {
+        executorServices.alertInstanceFactory('1').scheduleActions('default');
+      }
+    );
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+    await taskRunner.run();
+    expect(taskRunnerFactoryInitializerParams.executeAction).toHaveBeenCalledTimes(1);
+    expect(taskRunnerFactoryInitializerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(`
+                  Array [
+                    Object {
+                      "apiKey": "MTIzOmFiYw==",
+                      "id": "1",
+                      "params": Object {
+                        "foo": true,
+                      },
+                      "spaceId": undefined,
+                    },
+                  ]
+          `);
+  });
+
+  test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => {
+    alertType.executor.mockImplementation(
+      ({ services: executorServices }: AlertExecutorOptions) => {
+        executorServices.alertInstanceFactory('1').scheduleActions('default');
+      }
+    );
+    const taskRunner = new TaskRunner(
+      alertType,
+      {
+        ...mockedTaskInstance,
+        state: {
+          ...mockedTaskInstance.state,
+          alertInstances: {
+            '1': { meta: {}, state: { bar: false } },
+            '2': { meta: {}, state: { bar: false } },
+          },
+        },
+      },
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+    const runnerResult = await taskRunner.run();
+    expect(runnerResult.state.alertInstances).toMatchInlineSnapshot(`
+      Object {
+        "1": Object {
+          "meta": Object {
+            "lastScheduledActions": Object {
+              "date": 1970-01-01T00:00:00.000Z,
+              "group": "default",
+            },
+          },
+          "state": Object {
+            "bar": false,
+          },
+        },
+      }
+    `);
+  });
+
+  test('validates params before executing the alert type', async () => {
+    const taskRunner = new TaskRunner(
+      {
+        ...alertType,
+        validate: {
+          params: schema.object({
+            param1: schema.string(),
+          }),
+        },
+      },
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+    expect(await taskRunner.run()).toMatchInlineSnapshot(`
+      Object {
+        "runAt": 1970-01-01T00:00:10.000Z,
+        "state": Object {
+          "previousStartedAt": 1970-01-01T00:00:00.000Z,
+          "startedAt": 1969-12-31T23:55:00.000Z,
+        },
+      }
+    `);
+    expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith(
+      `Executing Alert \"1\" has resulted in Error: params invalid: [param1]: expected value of type [string] but got [undefined]`
+    );
+  });
+
+  test('throws error if reference not found', async () => {
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce({
+      ...mockedAlertTypeSavedObject,
+      references: [],
+    });
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+    expect(await taskRunner.run()).toMatchInlineSnapshot(`
+      Object {
+        "runAt": 1970-01-01T00:00:10.000Z,
+        "state": Object {
+          "previousStartedAt": 1970-01-01T00:00:00.000Z,
+          "startedAt": 1969-12-31T23:55:00.000Z,
+        },
+      }
+    `);
+    expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith(
+      `Executing Alert \"1\" has resulted in Error: Action reference \"action_0\" not found in alert id: 1`
+    );
+  });
+
+  test('uses API key when provided', async () => {
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+
+    await taskRunner.run();
+    expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
+      getBasePath: expect.anything(),
+      headers: {
+        // base64 encoded "123:abc"
+        authorization: 'ApiKey MTIzOmFiYw==',
+      },
+      path: '/',
+      route: { settings: {} },
+      url: {
+        href: '/',
+      },
+      raw: {
+        req: {
+          url: '/',
+        },
+      },
+    });
+  });
+
+  test(`doesn't use API key when not provided`, async () => {
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {},
+      references: [],
+    });
+
+    await taskRunner.run();
+
+    expect(taskRunnerFactoryInitializerParams.getServices).toHaveBeenCalledWith({
+      getBasePath: expect.anything(),
+      headers: {},
+      path: '/',
+      route: { settings: {} },
+      url: {
+        href: '/',
+      },
+      raw: {
+        req: {
+          url: '/',
+        },
+      },
+    });
+  });
+
+  test('recovers gracefully when the AlertType executor throws an exception', async () => {
+    alertType.executor.mockImplementation(
+      ({ services: executorServices }: AlertExecutorOptions) => {
+        throw new Error('OMG');
+      }
+    );
+
+    const taskRunner = new TaskRunner(
+      alertType,
+      mockedTaskInstance,
+      taskRunnerFactoryInitializerParams
+    );
+
+    savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject);
+    encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({
+      id: '1',
+      type: 'alert',
+      attributes: {
+        apiKey: Buffer.from('123:abc').toString('base64'),
+      },
+      references: [],
+    });
+
+    const runnerResult = await taskRunner.run();
+
+    expect(runnerResult).toMatchInlineSnapshot(`
+      Object {
+        "runAt": 1970-01-01T00:00:10.000Z,
+        "state": Object {
+          "previousStartedAt": 1970-01-01T00:00:00.000Z,
+          "startedAt": 1969-12-31T23:55:00.000Z,
+        },
+      }
+    `);
+  });
+});
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
new file mode 100644
index 0000000000000..2347e9e608ed9
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts
@@ -0,0 +1,241 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { pick, mapValues, omit } from 'lodash';
+import { Logger } from '../../../../../../src/core/server';
+import { SavedObject } from '../../../../../../src/core/server';
+import { TaskRunnerContext } from './task_runner_factory';
+import { ConcreteTaskInstance } from '../../../task_manager';
+import { createExecutionHandler } from './create_execution_handler';
+import { AlertInstance, createAlertInstanceFactory } from '../alert_instance';
+import { getNextRunAt } from './get_next_run_at';
+import { validateAlertTypeParams } from '../lib';
+import { AlertType, RawAlert, IntervalSchedule, Services, State } from '../types';
+import { promiseResult, map } from '../lib/result_type';
+
+type AlertInstances = Record<string, AlertInstance>;
+
+export class TaskRunner {
+  private context: TaskRunnerContext;
+  private logger: Logger;
+  private taskInstance: ConcreteTaskInstance;
+  private alertType: AlertType;
+
+  constructor(
+    alertType: AlertType,
+    taskInstance: ConcreteTaskInstance,
+    context: TaskRunnerContext
+  ) {
+    this.context = context;
+    this.logger = context.logger;
+    this.alertType = alertType;
+    this.taskInstance = taskInstance;
+  }
+
+  async getApiKeyForAlertPermissions(alertId: string, spaceId: string) {
+    const namespace = this.context.spaceIdToNamespace(spaceId);
+    // Only fetch encrypted attributes here, we'll create a saved objects client
+    // scoped with the API key to fetch the remaining data.
+    const {
+      attributes: { apiKey },
+    } = await this.context.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAlert>(
+      'alert',
+      alertId,
+      { namespace }
+    );
+
+    return apiKey;
+  }
+
+  async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) {
+    const requestHeaders: Record<string, string> = {};
+
+    if (apiKey) {
+      requestHeaders.authorization = `ApiKey ${apiKey}`;
+    }
+
+    const fakeRequest = {
+      headers: requestHeaders,
+      getBasePath: () => this.context.getBasePath(spaceId),
+      path: '/',
+      route: { settings: {} },
+      url: {
+        href: '/',
+      },
+      raw: {
+        req: {
+          url: '/',
+        },
+      },
+    };
+
+    return this.context.getServices(fakeRequest);
+  }
+
+  getExecutionHandler(
+    alertId: string,
+    spaceId: string,
+    apiKey: string | null,
+    actions: RawAlert['actions'],
+    references: SavedObject['references']
+  ) {
+    // Inject ids into actions
+    const actionsWithIds = actions.map(action => {
+      const actionReference = references.find(obj => obj.name === action.actionRef);
+      if (!actionReference) {
+        throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);
+      }
+      return {
+        ...action,
+        id: actionReference.id,
+      };
+    });
+
+    return createExecutionHandler({
+      alertId,
+      logger: this.logger,
+      executeAction: this.context.executeAction,
+      apiKey,
+      actions: actionsWithIds,
+      spaceId,
+      alertType: this.alertType,
+    });
+  }
+
+  async executeAlertInstance(
+    alertInstanceId: string,
+    alertInstance: AlertInstance,
+    executionHandler: ReturnType<typeof createExecutionHandler>
+  ) {
+    const { actionGroup, context, state } = alertInstance.getScheduledActionOptions()!;
+    alertInstance.updateLastScheduledActions(actionGroup);
+    alertInstance.unscheduleActions();
+    return executionHandler({ actionGroup, context, state, alertInstanceId });
+  }
+
+  async executeAlertInstances(
+    services: Services,
+    { params, throttle, muteAll, mutedInstanceIds }: SavedObject['attributes'],
+    executionHandler: ReturnType<typeof createExecutionHandler>
+  ): Promise<State> {
+    const {
+      params: { alertId },
+      state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt },
+    } = this.taskInstance;
+
+    const alertInstances = mapValues<AlertInstances>(
+      alertRawInstances,
+      alert => new AlertInstance(alert)
+    );
+
+    const updatedAlertTypeState = await this.alertType.executor({
+      alertId,
+      services: {
+        ...services,
+        alertInstanceFactory: createAlertInstanceFactory(alertInstances),
+      },
+      params,
+      state: alertTypeState,
+      startedAt: this.taskInstance.startedAt!,
+      previousStartedAt,
+    });
+
+    // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object
+    const instancesWithScheduledActions = pick<AlertInstances, AlertInstances>(
+      alertInstances,
+      alertInstance => alertInstance.hasScheduledActions()
+    );
+
+    if (!muteAll) {
+      const enabledAlertInstances = omit<AlertInstances, AlertInstances>(
+        instancesWithScheduledActions,
+        ...mutedInstanceIds
+      );
+
+      await Promise.all(
+        Object.entries(enabledAlertInstances)
+          .filter(
+            ([, alertInstance]: [string, AlertInstance]) => !alertInstance.isThrottled(throttle)
+          )
+          .map(([id, alertInstance]: [string, AlertInstance]) =>
+            this.executeAlertInstance(id, alertInstance, executionHandler)
+          )
+      );
+    }
+
+    return {
+      alertTypeState: updatedAlertTypeState,
+      alertInstances: instancesWithScheduledActions,
+    };
+  }
+
+  async validateAndRunAlert(
+    services: Services,
+    apiKey: string | null,
+    attributes: SavedObject['attributes'],
+    references: SavedObject['references']
+  ) {
+    const {
+      params: { alertId, spaceId },
+    } = this.taskInstance;
+
+    // Validate
+    const params = validateAlertTypeParams(this.alertType, attributes.params);
+    const executionHandler = this.getExecutionHandler(
+      alertId,
+      spaceId,
+      apiKey,
+      attributes.actions,
+      references
+    );
+    return this.executeAlertInstances(services, { ...attributes, params }, executionHandler);
+  }
+
+  async run() {
+    const {
+      params: { alertId, spaceId },
+      startedAt: previousStartedAt,
+      state: originalState,
+    } = this.taskInstance;
+
+    const apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
+    const services = await this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
+
+    // Ensure API key is still valid and user has access
+    const { attributes, references } = await services.savedObjectsClient.get<RawAlert>(
+      'alert',
+      alertId
+    );
+
+    return {
+      state: map<State, Error, State>(
+        await promiseResult<State, Error>(
+          this.validateAndRunAlert(services, apiKey, attributes, references)
+        ),
+        (stateUpdates: State) => {
+          return {
+            ...stateUpdates,
+            previousStartedAt,
+          };
+        },
+        (err: Error) => {
+          this.logger.error(`Executing Alert "${alertId}" has resulted in Error: ${err.message}`);
+          return {
+            ...originalState,
+            previousStartedAt,
+          };
+        }
+      ),
+      runAt: getNextRunAt(
+        new Date(this.taskInstance.startedAt!),
+        // we do not currently have a good way of returning the type
+        // from SavedObjectsClient, and as we currenrtly require a schedule
+        // and we only support `interval`, we can cast this safely
+        attributes.schedule as IntervalSchedule
+      ),
+    };
+  }
+}
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
new file mode 100644
index 0000000000000..2ea1256352bec
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon from 'sinon';
+import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager';
+import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
+import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
+import {
+  savedObjectsClientMock,
+  loggingServiceMock,
+} from '../../../../../../src/core/server/mocks';
+
+const alertType = {
+  id: 'test',
+  name: 'My test alert',
+  actionGroups: ['default'],
+  executor: jest.fn(),
+};
+let fakeTimer: sinon.SinonFakeTimers;
+
+describe('Task Runner Factory', () => {
+  let mockedTaskInstance: ConcreteTaskInstance;
+
+  beforeAll(() => {
+    fakeTimer = sinon.useFakeTimers();
+    mockedTaskInstance = {
+      id: '',
+      attempts: 0,
+      status: TaskStatus.Running,
+      version: '123',
+      runAt: new Date(),
+      scheduledAt: new Date(),
+      startedAt: new Date(),
+      retryAt: new Date(Date.now() + 5 * 60 * 1000),
+      state: {
+        startedAt: new Date(Date.now() - 5 * 60 * 1000),
+      },
+      taskType: 'alerting:test',
+      params: {
+        alertId: '1',
+      },
+      ownerId: null,
+    };
+  });
+
+  afterAll(() => fakeTimer.restore());
+
+  const savedObjectsClient = savedObjectsClientMock.create();
+  const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
+  const services = {
+    log: jest.fn(),
+    callCluster: jest.fn(),
+    savedObjectsClient,
+  };
+
+  const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = {
+    getServices: jest.fn().mockReturnValue(services),
+    executeAction: jest.fn(),
+    encryptedSavedObjectsPlugin,
+    logger: loggingServiceMock.create().get(),
+    spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
+    getBasePath: jest.fn().mockReturnValue(undefined),
+  };
+
+  beforeEach(() => {
+    jest.resetAllMocks();
+    taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services);
+  });
+
+  test(`throws an error if factory isn't initialized`, () => {
+    const factory = new TaskRunnerFactory();
+    expect(() =>
+      factory.create(alertType, { taskInstance: mockedTaskInstance })
+    ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`);
+  });
+
+  test(`throws an error if factory is already initialized`, () => {
+    const factory = new TaskRunnerFactory();
+    factory.initialize(taskRunnerFactoryInitializerParams);
+    expect(() =>
+      factory.initialize(taskRunnerFactoryInitializerParams)
+    ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory already initialized"`);
+  });
+});
diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
new file mode 100644
index 0000000000000..7186e1e729bda
--- /dev/null
+++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Logger } from '../../../../../../src/core/server';
+import { RunContext } from '../../../task_manager';
+import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
+import { PluginStartContract as ActionsPluginStartContract } from '../../../actions';
+import {
+  AlertType,
+  GetBasePathFunction,
+  GetServicesFunction,
+  SpaceIdToNamespaceFunction,
+} from '../types';
+import { TaskRunner } from './task_runner';
+
+export interface TaskRunnerContext {
+  logger: Logger;
+  getServices: GetServicesFunction;
+  executeAction: ActionsPluginStartContract['execute'];
+  encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
+  spaceIdToNamespace: SpaceIdToNamespaceFunction;
+  getBasePath: GetBasePathFunction;
+}
+
+export class TaskRunnerFactory {
+  private isInitialized = false;
+  private taskRunnerContext?: TaskRunnerContext;
+
+  public initialize(taskRunnerContext: TaskRunnerContext) {
+    if (this.isInitialized) {
+      throw new Error('TaskRunnerFactory already initialized');
+    }
+    this.isInitialized = true;
+    this.taskRunnerContext = taskRunnerContext;
+  }
+
+  public create(alertType: AlertType, { taskInstance }: RunContext) {
+    if (!this.isInitialized) {
+      throw new Error('TaskRunnerFactory not initialized');
+    }
+
+    return new TaskRunner(alertType, taskInstance, this.taskRunnerContext!);
+  }
+}
diff --git a/x-pack/legacy/plugins/alerting/server/lib/transform_action_params.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/transform_action_params.test.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/transform_action_params.test.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/transform_action_params.test.ts
diff --git a/x-pack/legacy/plugins/alerting/server/lib/transform_action_params.ts b/x-pack/legacy/plugins/alerting/server/task_runner/transform_action_params.ts
similarity index 100%
rename from x-pack/legacy/plugins/alerting/server/lib/transform_action_params.ts
rename to x-pack/legacy/plugins/alerting/server/task_runner/transform_action_params.ts
diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts
index 62dcf07abb7bd..9b03f9b02aa0a 100644
--- a/x-pack/legacy/plugins/alerting/server/types.ts
+++ b/x-pack/legacy/plugins/alerting/server/types.ts
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { AlertInstance } from './lib';
+import { AlertInstance } from './alert_instance';
 import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
 import { PluginSetupContract, PluginStartContract } from './plugin';
 import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
index 487f396d7a3dc..c47649544f9a7 100644
--- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts
@@ -17,7 +17,7 @@ export interface AlertUtilsOpts {
   objectRemover?: ObjectRemover;
 }
 
-export interface CreateAlwaysFiringActionOpts {
+export interface CreateAlertWithActionOpts {
   indexRecordActionId?: string;
   objectRemover?: ObjectRemover;
   overwrites?: Record<string, any>;
@@ -159,7 +159,7 @@ export class AlertUtils {
     overwrites = {},
     indexRecordActionId,
     reference,
-  }: CreateAlwaysFiringActionOpts) {
+  }: CreateAlertWithActionOpts) {
     const objRemover = objectRemover || this.objectRemover;
     const actionId = indexRecordActionId || this.indexRecordActionId;
 
@@ -207,4 +207,47 @@ export class AlertUtils {
     }
     return response;
   }
+
+  public async createAlwaysFailingAction({
+    objectRemover,
+    overwrites = {},
+    indexRecordActionId,
+    reference,
+  }: CreateAlertWithActionOpts) {
+    const objRemover = objectRemover || this.objectRemover;
+    const actionId = indexRecordActionId || this.indexRecordActionId;
+
+    if (!objRemover) {
+      throw new Error('objectRemover is required');
+    }
+    if (!actionId) {
+      throw new Error('indexRecordActionId is required ');
+    }
+
+    let request = this.supertestWithoutAuth
+      .post(`${getUrlPrefix(this.space.id)}/api/alert`)
+      .set('kbn-xsrf', 'foo');
+    if (this.user) {
+      request = request.auth(this.user.username, this.user.password);
+    }
+    const response = await request.send({
+      enabled: true,
+      name: 'fail',
+      schedule: { interval: '30s' },
+      throttle: '30s',
+      tags: [],
+      alertTypeId: 'test.failing',
+      consumer: 'bar',
+      params: {
+        index: ES_TEST_INDEX_NAME,
+        reference,
+      },
+      actions: [],
+      ...overwrites,
+    });
+    if (response.statusCode === 200) {
+      objRemover.add(this.space.id, response.body.id, 'alert');
+    }
+    return response;
+  }
 }
diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts
index a2f21264634f8..c1e59664f9ce2 100644
--- a/x-pack/test/alerting_api_integration/common/lib/index.ts
+++ b/x-pack/test/alerting_api_integration/common/lib/index.ts
@@ -10,4 +10,5 @@ export { ES_TEST_INDEX_NAME, ESTestIndexTool } from './es_test_index_tool';
 export { getTestAlertData } from './get_test_alert_data';
 export { AlertUtils } from './alert_utils';
 export { TaskManagerUtils } from './task_manager_utils';
+export * from './test_assertions';
 export { checkAAD } from './check_aad';
diff --git a/x-pack/test/alerting_api_integration/common/lib/test_assertions.ts b/x-pack/test/alerting_api_integration/common/lib/test_assertions.ts
new file mode 100644
index 0000000000000..9495dd4cfae82
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/common/lib/test_assertions.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import expect from '@kbn/expect';
+
+export function ensureDatetimeIsWithinRange(
+  date: number,
+  expectedDiff: number,
+  buffer: number = 10000
+) {
+  const diff = date - Date.now();
+  expect(diff).to.be.greaterThan(expectedDiff - buffer);
+  expect(diff).to.be.lessThan(expectedDiff + buffer);
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
index e89b54b1caa55..2a7e0b2203824 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts
@@ -7,7 +7,13 @@
 import expect from '@kbn/expect';
 import { Response as SupertestResponse } from 'supertest';
 import { UserAtSpaceScenarios } from '../../scenarios';
-import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
+import {
+  checkAAD,
+  getUrlPrefix,
+  getTestAlertData,
+  ObjectRemover,
+  ensureDatetimeIsWithinRange,
+} from '../../../common/lib';
 import { FtrProviderContext } from '../../../common/ftr_provider_context';
 
 // eslint-disable-next-line import/no-default-export
@@ -406,10 +412,3 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
     }
   });
 }
-
-function ensureDatetimeIsWithinRange(scheduledRunTime: number, expectedDiff: number) {
-  const buffer = 10000;
-  const diff = scheduledRunTime - Date.now();
-  expect(diff).to.be.greaterThan(expectedDiff - buffer);
-  expect(diff).to.be.lessThan(expectedDiff + buffer);
-}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts
index 03e973194b4e2..032fee15882cf 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts
@@ -5,6 +5,7 @@
  */
 
 import expect from '@kbn/expect';
+import { Response as SupertestResponse } from 'supertest';
 import { Spaces } from '../../scenarios';
 import { FtrProviderContext } from '../../../common/ftr_provider_context';
 import {
@@ -14,6 +15,7 @@ import {
   getTestAlertData,
   ObjectRemover,
   AlertUtils,
+  ensureDatetimeIsWithinRange,
 } from '../../../common/lib';
 
 // eslint-disable-next-line import/no-default-export
@@ -23,6 +25,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
   const retry = getService('retry');
   const esTestIndexTool = new ESTestIndexTool(es, retry);
 
+  function getAlertingTaskById(taskId: string) {
+    return supertestWithoutAuth
+      .get(`/api/alerting_tasks/${taskId}`)
+      .expect(200)
+      .then((response: SupertestResponse) => response.body);
+  }
+
   describe('alerts', () => {
     let alertUtils: AlertUtils;
     let indexRecordActionId: string;
@@ -100,6 +109,37 @@ export default function alertTests({ getService }: FtrProviderContext) {
       });
     });
 
+    it('should reschedule failing alerts using the alerting interval and not the Task Manager retry logic', async () => {
+      /*
+        Alerting does not use the Task Manager schedule and instead implements its own due to a current limitation
+        in TaskManager's ability to update an existing Task.
+        For this reason we need to handle the retry when Alert executors fail, as TaskManager doesn't understand that
+        alerting tasks are recurring tasks.
+      */
+      const alertIntervalInSeconds = 30;
+      const reference = alertUtils.generateReference();
+      const response = await alertUtils.createAlwaysFailingAction({
+        reference,
+        overwrites: { schedule: { interval: `${alertIntervalInSeconds}s` } },
+      });
+
+      expect(response.statusCode).to.eql(200);
+
+      // wait for executor Alert Executor to be run, which means the underlying task is running
+      await esTestIndexTool.waitForDocs('alert:test.failing', reference);
+
+      await retry.try(async () => {
+        const alertTask = (await getAlertingTaskById(response.body.scheduledTaskId)).docs[0];
+        expect(alertTask.status).to.eql('idle');
+        // ensure the alert is rescheduled to a minute from now
+        ensureDatetimeIsWithinRange(
+          Date.parse(alertTask.runAt),
+          alertIntervalInSeconds * 1000,
+          5000
+        );
+      });
+    });
+
     it('should handle custom retry logic', async () => {
       // We'll use this start time to query tasks created after this point
       const testStart = new Date();

From ff7782135ea31f8f23afddb78a4596403b5019c7 Mon Sep 17 00:00:00 2001
From: Nathan Reese <reese.nathan@gmail.com>
Date: Mon, 6 Jan 2020 13:08:19 -0500
Subject: [PATCH 3/3] [Maps] Vector style UI redesign (#53946) (#54026)

* [Maps] style editor update

* update label editor

* update size editor

* update orienation editor

* i18n cleanup

* deconstruct props

* review feedback

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../components/color/dynamic_color_form.js    |  62 ++++++++
 .../color/dynamic_color_selection.js          |  48 ------
 .../components/color/static_color_form.js     |  33 ++++
 .../color/static_color_selection.js           |  30 ----
 .../color/vector_style_color_editor.js        |  34 ++--
 .../components/label/dynamic_label_form.js    |  37 +++++
 .../label/dynamic_label_selector.js           |  24 ---
 .../components/label/static_label_form.js     |  34 ++++
 .../components/label/static_label_selector.js |  28 ----
 .../label/vector_style_label_editor.js        |  18 +--
 .../orientation/dynamic_orientation_form.js   |  40 +++++
 .../dynamic_orientation_selection.js          |  32 ----
 .../orientation/orientation_editor.js         |  22 ++-
 .../orientation/static_orientation_form.js    |  33 ++++
 .../static_orientation_selection.js           |  34 ----
 .../components/size/dynamic_size_form.js      |  63 ++++++++
 .../components/size/dynamic_size_selection.js |  48 ------
 .../components/size/static_size_form.js       |  37 +++++
 .../components/size/static_size_selection.js  |  38 -----
 .../size/vector_style_size_editor.js          |  22 ++-
 .../components/static_dynamic_style_row.js    | 145 ------------------
 .../vector/components/style_prop_editor.js    | 104 +++++++++++++
 .../vector/components/vector_style_editor.js  | 127 +++++++++++----
 .../translations/translations/ja-JP.json      |   2 -
 .../translations/translations/zh-CN.json      |   2 -
 25 files changed, 585 insertions(+), 512 deletions(-)
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js
 delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js
 create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js

diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
new file mode 100644
index 0000000000000..5e0f7434b04d0
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import React, { Fragment } from 'react';
+import { FieldSelect } from '../field_select';
+import { ColorRampSelect } from './color_ramp_select';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+
+export function DynamicColorForm({
+  fields,
+  onDynamicStyleChange,
+  staticDynamicSelect,
+  styleProperty,
+}) {
+  const styleOptions = styleProperty.getOptions();
+
+  const onFieldChange = ({ field }) => {
+    onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
+  };
+
+  const onColorChange = colorOptions => {
+    onDynamicStyleChange(styleProperty.getStyleName(), {
+      ...styleOptions,
+      ...colorOptions,
+    });
+  };
+
+  let colorRampSelect;
+  if (styleOptions.field && styleOptions.field.name) {
+    colorRampSelect = (
+      <ColorRampSelect
+        onChange={onColorChange}
+        color={styleOptions.color}
+        customColorRamp={styleOptions.customColorRamp}
+        useCustomColorRamp={_.get(styleOptions, 'useCustomColorRamp', false)}
+        compressed
+      />
+    );
+  }
+
+  return (
+    <Fragment>
+      <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+        <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+        <EuiFlexItem>
+          <FieldSelect
+            fields={fields}
+            selectedFieldName={_.get(styleOptions, 'field.name')}
+            onChange={onFieldChange}
+            compressed
+          />
+        </EuiFlexItem>
+      </EuiFlexGroup>
+      <EuiSpacer size="s" />
+      {colorRampSelect}
+    </Fragment>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js
deleted file mode 100644
index 84327635f2b65..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_selection.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { dynamicColorShape } from '../style_option_shapes';
-import { FieldSelect, fieldShape } from '../field_select';
-import { ColorRampSelect } from './color_ramp_select';
-import { EuiSpacer } from '@elastic/eui';
-
-export function DynamicColorSelection({ fields, onChange, styleOptions }) {
-  const onFieldChange = ({ field }) => {
-    onChange({ ...styleOptions, field });
-  };
-
-  const onColorChange = colorOptions => {
-    onChange({ ...styleOptions, ...colorOptions });
-  };
-
-  return (
-    <Fragment>
-      <ColorRampSelect
-        onChange={onColorChange}
-        color={styleOptions.color}
-        customColorRamp={styleOptions.customColorRamp}
-        useCustomColorRamp={_.get(styleOptions, 'useCustomColorRamp', false)}
-        compressed
-      />
-      <EuiSpacer size="s" />
-      <FieldSelect
-        fields={fields}
-        selectedFieldName={_.get(styleOptions, 'field.name')}
-        onChange={onFieldChange}
-        compressed
-      />
-    </Fragment>
-  );
-}
-
-DynamicColorSelection.propTypes = {
-  fields: PropTypes.arrayOf(fieldShape).isRequired,
-  styleOptions: dynamicColorShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js
new file mode 100644
index 0000000000000..48befa1ca74c0
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiColorPicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export function StaticColorForm({
+  onStaticStyleChange,
+  staticDynamicSelect,
+  styleProperty,
+  swatches,
+}) {
+  const onColorChange = color => {
+    onStaticStyleChange(styleProperty.getStyleName(), { color });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <EuiColorPicker
+          onChange={onColorChange}
+          color={styleProperty.getOptions().color}
+          swatches={swatches}
+          compressed
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js
deleted file mode 100644
index e42b582dc3929..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/static_color_selection.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { EuiColorPicker } from '@elastic/eui';
-import { staticColorShape } from '../style_option_shapes';
-
-export function StaticColorSelection({ onChange, styleOptions, swatches }) {
-  const onColorChange = color => {
-    onChange({ color });
-  };
-
-  return (
-    <EuiColorPicker
-      onChange={onColorChange}
-      color={styleOptions.color}
-      swatches={swatches}
-      compressed
-    />
-  );
-}
-
-StaticColorSelection.propTypes = {
-  styleOptions: staticColorShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js
index c7745fa69a82f..43e7050b3d1d2 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js
@@ -6,21 +6,29 @@
 
 import React from 'react';
 
-import { StaticDynamicStyleRow } from '../static_dynamic_style_row';
-import { DynamicColorSelection } from './dynamic_color_selection';
-import { StaticColorSelection } from './static_color_selection';
+import { StylePropEditor } from '../style_prop_editor';
+import { DynamicColorForm } from './dynamic_color_form';
+import { StaticColorForm } from './static_color_form';
+import { i18n } from '@kbn/i18n';
 
 export function VectorStyleColorEditor(props) {
+  const colorForm = props.styleProperty.isDynamic() ? (
+    <DynamicColorForm {...props} />
+  ) : (
+    <StaticColorForm {...props} />
+  );
+
   return (
-    <StaticDynamicStyleRow
-      fields={props.fields}
-      styleProperty={props.styleProperty}
-      handlePropertyChange={props.handlePropertyChange}
-      swatches={props.swatches}
-      DynamicSelector={DynamicColorSelection}
-      StaticSelector={StaticColorSelection}
-      defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
-      defaultStaticStyleOptions={props.defaultStaticStyleOptions}
-    />
+    <StylePropEditor
+      {...props}
+      customStaticOptionLabel={i18n.translate(
+        'xpack.maps.styles.color.staticDynamicSelect.staticLabel',
+        {
+          defaultMessage: 'Solid',
+        }
+      )}
+    >
+      {colorForm}
+    </StylePropEditor>
   );
 }
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js
new file mode 100644
index 0000000000000..bad13b487cc29
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FieldSelect } from '../field_select';
+
+export function DynamicLabelForm({
+  fields,
+  onDynamicStyleChange,
+  staticDynamicSelect,
+  styleProperty,
+}) {
+  const styleOptions = styleProperty.getOptions();
+
+  const onFieldChange = ({ field }) => {
+    onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <FieldSelect
+          fields={fields}
+          selectedFieldName={_.get(styleOptions, 'field.name')}
+          onChange={onFieldChange}
+          compressed
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js
deleted file mode 100644
index e393341b90696..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_selector.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import React from 'react';
-import { FieldSelect } from '../field_select';
-
-export function DynamicLabelSelector({ fields, styleOptions, onChange }) {
-  const onFieldChange = ({ field }) => {
-    onChange({ ...styleOptions, field });
-  };
-
-  return (
-    <FieldSelect
-      fields={fields}
-      selectedFieldName={_.get(styleOptions, 'field.name')}
-      onChange={onFieldChange}
-      compressed
-    />
-  );
-}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js
new file mode 100644
index 0000000000000..721487b5d8ff0
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export function StaticLabelForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) {
+  const onValueChange = event => {
+    onStaticStyleChange(styleProperty.getStyleName(), { value: event.target.value });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <EuiFieldText
+          placeholder={i18n.translate('xpack.maps.styles.staticLabel.valuePlaceholder', {
+            defaultMessage: 'symbol label',
+          })}
+          value={styleProperty.getOptions().value}
+          onChange={onValueChange}
+          aria-label={i18n.translate('xpack.maps.styles.staticLabel.valueAriaLabel', {
+            defaultMessage: 'symbol label',
+          })}
+          compressed
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js
deleted file mode 100644
index ea296a3312799..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/static_label_selector.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiFieldText } from '@elastic/eui';
-
-export function StaticLabelSelector({ onChange, styleOptions }) {
-  const onValueChange = event => {
-    onChange({ value: event.target.value });
-  };
-
-  return (
-    <EuiFieldText
-      placeholder={i18n.translate('xpack.maps.styles.staticLabel.valuePlaceholder', {
-        defaultMessage: 'symbol label',
-      })}
-      value={styleOptions.value}
-      onChange={onValueChange}
-      aria-label={i18n.translate('xpack.maps.styles.staticLabel.valueAriaLabel', {
-        defaultMessage: 'symbol label',
-      })}
-    />
-  );
-}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js
index 6bca56425d38d..aaa21ea315f36 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js
@@ -6,16 +6,16 @@
 
 import React from 'react';
 
-import { StaticDynamicStyleRow } from '../static_dynamic_style_row';
-import { DynamicLabelSelector } from './dynamic_label_selector';
-import { StaticLabelSelector } from './static_label_selector';
+import { StylePropEditor } from '../style_prop_editor';
+import { DynamicLabelForm } from './dynamic_label_form';
+import { StaticLabelForm } from './static_label_form';
 
 export function VectorStyleLabelEditor(props) {
-  return (
-    <StaticDynamicStyleRow
-      {...props}
-      DynamicSelector={DynamicLabelSelector}
-      StaticSelector={StaticLabelSelector}
-    />
+  const labelForm = props.styleProperty.isDynamic() ? (
+    <DynamicLabelForm {...props} />
+  ) : (
+    <StaticLabelForm {...props} />
   );
+
+  return <StylePropEditor {...props}>{labelForm}</StylePropEditor>;
 }
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js
new file mode 100644
index 0000000000000..e0b7e7b2865a2
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import React from 'react';
+import { FieldSelect } from '../field_select';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export function DynamicOrientationForm({
+  fields,
+  onDynamicStyleChange,
+  staticDynamicSelect,
+  styleProperty,
+}) {
+  const styleOptions = styleProperty.getOptions();
+
+  const onFieldChange = ({ field }) => {
+    onDynamicStyleChange(styleProperty.getStyleName(), {
+      ...styleOptions,
+      field,
+    });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <FieldSelect
+          fields={fields}
+          selectedFieldName={_.get(styleOptions, 'field.name')}
+          onChange={onFieldChange}
+          compressed
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js
deleted file mode 100644
index 8ad3916ac6509..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_selection.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import React from 'react';
-import PropTypes from 'prop-types';
-import { dynamicOrientationShape } from '../style_option_shapes';
-import { FieldSelect, fieldShape } from '../field_select';
-
-export function DynamicOrientationSelection({ fields, styleOptions, onChange }) {
-  const onFieldChange = ({ field }) => {
-    onChange({ ...styleOptions, field });
-  };
-
-  return (
-    <FieldSelect
-      fields={fields}
-      selectedFieldName={_.get(styleOptions, 'field.name')}
-      onChange={onFieldChange}
-      compressed
-    />
-  );
-}
-
-DynamicOrientationSelection.propTypes = {
-  fields: PropTypes.arrayOf(fieldShape).isRequired,
-  styleOptions: dynamicOrientationShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js
index e97252a5e79da..915fc92c9fb38 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js
@@ -6,20 +6,16 @@
 
 import React from 'react';
 
-import { StaticDynamicStyleRow } from '../static_dynamic_style_row';
-import { DynamicOrientationSelection } from './dynamic_orientation_selection';
-import { StaticOrientationSelection } from './static_orientation_selection';
+import { StylePropEditor } from '../style_prop_editor';
+import { DynamicOrientationForm } from './dynamic_orientation_form';
+import { StaticOrientationForm } from './static_orientation_form';
 
 export function OrientationEditor(props) {
-  return (
-    <StaticDynamicStyleRow
-      fields={props.fields}
-      styleProperty={props.styleProperty}
-      handlePropertyChange={props.handlePropertyChange}
-      DynamicSelector={DynamicOrientationSelection}
-      StaticSelector={StaticOrientationSelection}
-      defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
-      defaultStaticStyleOptions={props.defaultStaticStyleOptions}
-    />
+  const orientationForm = props.styleProperty.isDynamic() ? (
+    <DynamicOrientationForm {...props} />
+  ) : (
+    <StaticOrientationForm {...props} />
   );
+
+  return <StylePropEditor {...props}>{orientationForm}</StylePropEditor>;
 }
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js
new file mode 100644
index 0000000000000..8c4418f95e1d2
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { ValidatedRange } from '../../../../../components/validated_range';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export function StaticOrientationForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) {
+  const onOrientationChange = orientation => {
+    onStaticStyleChange(styleProperty.getStyleName(), { orientation });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <ValidatedRange
+          min={0}
+          max={360}
+          value={styleProperty.getOptions().orientation}
+          onChange={onOrientationChange}
+          showInput="inputWithPopover"
+          showLabels
+          compressed
+          append="°"
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js
deleted file mode 100644
index b5529c6987459..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_selection.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { staticOrientationShape } from '../style_option_shapes';
-import { ValidatedRange } from '../../../../../components/validated_range';
-
-export function StaticOrientationSelection({ onChange, styleOptions }) {
-  const onOrientationChange = orientation => {
-    onChange({ orientation });
-  };
-
-  return (
-    <ValidatedRange
-      min={0}
-      max={360}
-      value={styleOptions.orientation}
-      onChange={onOrientationChange}
-      showInput
-      showLabels
-      compressed
-      append="°"
-    />
-  );
-}
-
-StaticOrientationSelection.propTypes = {
-  styleOptions: staticOrientationShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js
new file mode 100644
index 0000000000000..8b069cd53b731
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import _ from 'lodash';
+import React, { Fragment } from 'react';
+import { FieldSelect } from '../field_select';
+import { SizeRangeSelector } from './size_range_selector';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+
+export function DynamicSizeForm({
+  fields,
+  onDynamicStyleChange,
+  staticDynamicSelect,
+  styleProperty,
+}) {
+  const styleOptions = styleProperty.getOptions();
+
+  const onFieldChange = ({ field }) => {
+    onDynamicStyleChange(styleProperty.getStyleName(), { ...styleOptions, field });
+  };
+
+  const onSizeRangeChange = ({ minSize, maxSize }) => {
+    onDynamicStyleChange(styleProperty.getStyleName(), {
+      ...styleOptions,
+      minSize,
+      maxSize,
+    });
+  };
+
+  let sizeRange;
+  if (styleOptions.field && styleOptions.field.name) {
+    sizeRange = (
+      <SizeRangeSelector
+        onChange={onSizeRangeChange}
+        minSize={styleOptions.minSize}
+        maxSize={styleOptions.maxSize}
+        showLabels
+        compressed
+      />
+    );
+  }
+
+  return (
+    <Fragment>
+      <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+        <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+        <EuiFlexItem>
+          <FieldSelect
+            fields={fields}
+            selectedFieldName={_.get(styleOptions, 'field.name')}
+            onChange={onFieldChange}
+            compressed
+          />
+        </EuiFlexItem>
+      </EuiFlexGroup>
+      <EuiSpacer size="s" />
+      {sizeRange}
+    </Fragment>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js
deleted file mode 100644
index 76c5b97976bbc..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_selection.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { dynamicSizeShape } from '../style_option_shapes';
-import { FieldSelect, fieldShape } from '../field_select';
-import { SizeRangeSelector } from './size_range_selector';
-import { EuiSpacer } from '@elastic/eui';
-
-export function DynamicSizeSelection({ fields, styleOptions, onChange }) {
-  const onFieldChange = ({ field }) => {
-    onChange({ ...styleOptions, field });
-  };
-
-  const onSizeRangeChange = ({ minSize, maxSize }) => {
-    onChange({ ...styleOptions, minSize, maxSize });
-  };
-
-  return (
-    <Fragment>
-      <SizeRangeSelector
-        onChange={onSizeRangeChange}
-        minSize={styleOptions.minSize}
-        maxSize={styleOptions.maxSize}
-        showLabels
-        compressed
-      />
-      <EuiSpacer size="s" />
-      <FieldSelect
-        fields={fields}
-        selectedFieldName={_.get(styleOptions, 'field.name')}
-        onChange={onFieldChange}
-        compressed
-      />
-    </Fragment>
-  );
-}
-
-DynamicSizeSelection.propTypes = {
-  fields: PropTypes.arrayOf(fieldShape).isRequired,
-  styleOptions: dynamicSizeShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js
new file mode 100644
index 0000000000000..d8fe1322db3e3
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { ValidatedRange } from '../../../../../components/validated_range';
+import { i18n } from '@kbn/i18n';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+export function StaticSizeForm({ onStaticStyleChange, staticDynamicSelect, styleProperty }) {
+  const onSizeChange = size => {
+    onStaticStyleChange(styleProperty.getStyleName(), { size });
+  };
+
+  return (
+    <EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
+      <EuiFlexItem grow={false}>{staticDynamicSelect}</EuiFlexItem>
+      <EuiFlexItem>
+        <ValidatedRange
+          min={0}
+          max={100}
+          value={styleProperty.getOptions().size}
+          onChange={onSizeChange}
+          showInput="inputWithPopover"
+          showLabels
+          compressed
+          append={i18n.translate('xpack.maps.vector.size.unitLabel', {
+            defaultMessage: 'px',
+            description: 'Shorthand for pixel',
+          })}
+        />
+      </EuiFlexItem>
+    </EuiFlexGroup>
+  );
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js
deleted file mode 100644
index 38f8fe53d1748..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/static_size_selection.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { staticSizeShape } from '../style_option_shapes';
-import { ValidatedRange } from '../../../../../components/validated_range';
-import { i18n } from '@kbn/i18n';
-
-export function StaticSizeSelection({ onChange, styleOptions }) {
-  const onSizeChange = size => {
-    onChange({ size });
-  };
-
-  return (
-    <ValidatedRange
-      min={0}
-      max={100}
-      value={styleOptions.size}
-      onChange={onSizeChange}
-      showInput
-      showLabels
-      compressed
-      append={i18n.translate('xpack.maps.vector.size.unitLabel', {
-        defaultMessage: 'px',
-        description: 'Shorthand for pixel',
-      })}
-    />
-  );
-}
-
-StaticSizeSelection.propTypes = {
-  styleOptions: staticSizeShape.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js
index 6580bfc00e0ad..e344f72bd429a 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js
@@ -6,20 +6,16 @@
 
 import React from 'react';
 
-import { StaticDynamicStyleRow } from '../static_dynamic_style_row';
-import { DynamicSizeSelection } from './dynamic_size_selection';
-import { StaticSizeSelection } from './static_size_selection';
+import { StylePropEditor } from '../style_prop_editor';
+import { DynamicSizeForm } from './dynamic_size_form';
+import { StaticSizeForm } from './static_size_form';
 
 export function VectorStyleSizeEditor(props) {
-  return (
-    <StaticDynamicStyleRow
-      fields={props.fields}
-      styleProperty={props.styleProperty}
-      handlePropertyChange={props.handlePropertyChange}
-      DynamicSelector={DynamicSizeSelection}
-      StaticSelector={StaticSizeSelection}
-      defaultDynamicStyleOptions={props.defaultDynamicStyleOptions}
-      defaultStaticStyleOptions={props.defaultStaticStyleOptions}
-    />
+  const sizeForm = props.styleProperty.isDynamic() ? (
+    <DynamicSizeForm {...props} />
+  ) : (
+    <StaticSizeForm {...props} />
   );
+
+  return <StylePropEditor {...props}>{sizeForm}</StylePropEditor>;
 }
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js
deleted file mode 100644
index 311406731801a..0000000000000
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { Component, Fragment } from 'react';
-import { VectorStyle } from '../vector_style';
-import { i18n } from '@kbn/i18n';
-import { FieldMetaOptionsPopover } from './field_meta_options_popover';
-import { getVectorStyleLabel } from './get_vector_style_label';
-
-import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiFormRow, EuiButtonToggle } from '@elastic/eui';
-
-export class StaticDynamicStyleRow extends Component {
-  // Store previous options locally so when type is toggled,
-  // previous style options can be used.
-  prevStaticStyleOptions = this.props.defaultStaticStyleOptions;
-  prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions;
-
-  _canBeDynamic() {
-    return this.props.fields.length > 0;
-  }
-
-  _isDynamic() {
-    return this.props.styleProperty.isDynamic();
-  }
-
-  _getStyleOptions() {
-    return this.props.styleProperty.getOptions();
-  }
-
-  _onFieldMetaOptionsChange = fieldMetaOptions => {
-    const styleDescriptor = {
-      type: VectorStyle.STYLE_TYPE.DYNAMIC,
-      options: {
-        ...this._getStyleOptions(),
-        fieldMetaOptions,
-      },
-    };
-    this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor);
-  };
-
-  _onStaticStyleChange = options => {
-    const styleDescriptor = {
-      type: VectorStyle.STYLE_TYPE.STATIC,
-      options,
-    };
-    this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor);
-  };
-
-  _onDynamicStyleChange = options => {
-    const styleDescriptor = {
-      type: VectorStyle.STYLE_TYPE.DYNAMIC,
-      options,
-    };
-    this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor);
-  };
-
-  _onTypeToggle = () => {
-    if (this._isDynamic()) {
-      // preserve current dynmaic style
-      this.prevDynamicStyleOptions = this._getStyleOptions();
-      // toggle to static style
-      this._onStaticStyleChange(this.prevStaticStyleOptions);
-      return;
-    }
-
-    // preserve current static style
-    this.prevStaticStyleOptions = this._getStyleOptions();
-    // toggle to dynamic style
-    this._onDynamicStyleChange(this.prevDynamicStyleOptions);
-  };
-
-  _renderStyleSelector() {
-    if (this._isDynamic()) {
-      const DynamicSelector = this.props.DynamicSelector;
-      return (
-        <Fragment>
-          <DynamicSelector
-            fields={this.props.fields}
-            onChange={this._onDynamicStyleChange}
-            styleOptions={this._getStyleOptions()}
-          />
-          <FieldMetaOptionsPopover
-            styleProperty={this.props.styleProperty}
-            onChange={this._onFieldMetaOptionsChange}
-          />
-        </Fragment>
-      );
-    }
-
-    const StaticSelector = this.props.StaticSelector;
-    return (
-      <StaticSelector
-        onChange={this._onStaticStyleChange}
-        styleOptions={this._getStyleOptions()}
-        swatches={this.props.swatches}
-      />
-    );
-  }
-
-  render() {
-    const isDynamic = this._isDynamic();
-    const dynamicTooltipContent = isDynamic
-      ? i18n.translate('xpack.maps.styles.staticDynamic.staticDescription', {
-          defaultMessage: 'Use static styling properties to symbolize features.',
-        })
-      : i18n.translate('xpack.maps.styles.staticDynamic.dynamicDescription', {
-          defaultMessage: 'Use property values to symbolize features.',
-        });
-
-    return (
-      <EuiFlexGroup gutterSize="xs">
-        <EuiFlexItem
-          className={isDynamic ? 'mapStaticDynamicSylingOption__dynamicSizeHack' : undefined}
-        >
-          <EuiFormRow
-            label={getVectorStyleLabel(this.props.styleProperty.getStyleName())}
-            display="rowCompressed"
-          >
-            {this._renderStyleSelector()}
-          </EuiFormRow>
-        </EuiFlexItem>
-        {this._canBeDynamic() && (
-          <EuiFlexItem grow={false}>
-            <EuiFormRow hasEmptyLabelSpace display="centerCompressed">
-              <EuiToolTip content={dynamicTooltipContent} delay="long">
-                <EuiButtonToggle
-                  size="s"
-                  label={dynamicTooltipContent}
-                  iconType="link"
-                  onChange={this._onTypeToggle}
-                  isEmpty={!isDynamic}
-                  fill={isDynamic}
-                  isIconOnly
-                />
-              </EuiToolTip>
-            </EuiFormRow>
-          </EuiFlexItem>
-        )}
-      </EuiFlexGroup>
-    );
-  }
-}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js
new file mode 100644
index 0000000000000..1ac8edfb2cc69
--- /dev/null
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js
@@ -0,0 +1,104 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Component, Fragment } from 'react';
+import { FieldMetaOptionsPopover } from './field_meta_options_popover';
+import { getVectorStyleLabel } from './get_vector_style_label';
+import { EuiFormRow, EuiSelect } from '@elastic/eui';
+import { VectorStyle } from '../vector_style';
+import { i18n } from '@kbn/i18n';
+
+export class StylePropEditor extends Component {
+  _prevStaticStyleOptions = this.props.defaultStaticStyleOptions;
+  _prevDynamicStyleOptions = this.props.defaultDynamicStyleOptions;
+
+  _onTypeToggle = () => {
+    if (this.props.styleProperty.isDynamic()) {
+      // preserve current dynmaic style
+      this._prevDynamicStyleOptions = this.props.styleProperty.getOptions();
+      // toggle to static style
+      this.props.onStaticStyleChange(
+        this.props.styleProperty.getStyleName(),
+        this._prevStaticStyleOptions
+      );
+    } else {
+      // preserve current static style
+      this._prevStaticStyleOptions = this.props.styleProperty.getOptions();
+      // toggle to dynamic style
+      this.props.onDynamicStyleChange(
+        this.props.styleProperty.getStyleName(),
+        this._prevDynamicStyleOptions
+      );
+    }
+  };
+
+  _onFieldMetaOptionsChange = fieldMetaOptions => {
+    const options = {
+      ...this.props.styleProperty.getOptions(),
+      fieldMetaOptions,
+    };
+    this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options);
+  };
+
+  renderStaticDynamicSelect() {
+    const options = [
+      {
+        value: VectorStyle.STYLE_TYPE.STATIC,
+        text: this.props.customStaticOptionLabel
+          ? this.props.customStaticOptionLabel
+          : i18n.translate('xpack.maps.styles.staticDynamicSelect.staticLabel', {
+              defaultMessage: 'Fixed',
+            }),
+      },
+      {
+        value: VectorStyle.STYLE_TYPE.DYNAMIC,
+        text: i18n.translate('xpack.maps.styles.staticDynamicSelect.dynamicLabel', {
+          defaultMessage: 'By value',
+        }),
+      },
+    ];
+
+    return (
+      <EuiSelect
+        options={options}
+        value={
+          this.props.styleProperty.isDynamic()
+            ? VectorStyle.STYLE_TYPE.DYNAMIC
+            : VectorStyle.STYLE_TYPE.STATIC
+        }
+        onChange={this._onTypeToggle}
+        disabled={this.props.fields.length === 0}
+        aria-label={i18n.translate('xpack.maps.styles.staticDynamicSelect.ariaLabel', {
+          defaultMessage: 'Select to style by fixed value or by data value',
+        })}
+        compressed
+      />
+    );
+  }
+
+  render() {
+    const fieldMetaOptionsPopover = this.props.styleProperty.isDynamic() ? (
+      <FieldMetaOptionsPopover
+        styleProperty={this.props.styleProperty}
+        onChange={this._onFieldMetaOptionsChange}
+      />
+    ) : null;
+
+    return (
+      <EuiFormRow
+        label={getVectorStyleLabel(this.props.styleProperty.getStyleName())}
+        display="rowCompressed"
+      >
+        <Fragment>
+          {React.cloneElement(this.props.children, {
+            staticDynamicSelect: this.renderStaticDynamicSelect(),
+          })}
+          {fieldMetaOptionsPopover}
+        </Fragment>
+      </EuiFormRow>
+    );
+  }
+}
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js
index 44f630db9d890..8e80e036dbb8b 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js
@@ -12,8 +12,13 @@ import { VectorStyleColorEditor } from './color/vector_style_color_editor';
 import { VectorStyleSizeEditor } from './size/vector_style_size_editor';
 import { VectorStyleSymbolEditor } from './vector_style_symbol_editor';
 import { VectorStyleLabelEditor } from './label/vector_style_label_editor';
+import { VectorStyle } from '../vector_style';
 import { OrientationEditor } from './orientation/orientation_editor';
-import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults';
+import {
+  getDefaultDynamicProperties,
+  getDefaultStaticProperties,
+  VECTOR_STYLES,
+} from '../vector_style_defaults';
 import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils';
 import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types';
 import { SYMBOLIZE_AS_ICON } from '../vector_constants';
@@ -128,15 +133,36 @@ export class VectorStyleEditor extends Component {
     this.props.onIsTimeAwareChange(event.target.checked);
   };
 
+  _onStaticStyleChange = (propertyName, options) => {
+    const styleDescriptor = {
+      type: VectorStyle.STYLE_TYPE.STATIC,
+      options,
+    };
+    this.props.handlePropertyChange(propertyName, styleDescriptor);
+  };
+
+  _onDynamicStyleChange = (propertyName, options) => {
+    const styleDescriptor = {
+      type: VectorStyle.STYLE_TYPE.DYNAMIC,
+      options,
+    };
+    this.props.handlePropertyChange(propertyName, styleDescriptor);
+  };
+
   _renderFillColor() {
     return (
       <VectorStyleColorEditor
         swatches={DEFAULT_FILL_COLORS}
-        handlePropertyChange={this.props.handlePropertyChange}
-        styleProperty={this.props.styleProperties.fillColor}
+        onStaticStyleChange={this._onStaticStyleChange}
+        onDynamicStyleChange={this._onDynamicStyleChange}
+        styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]}
         fields={this._getOrdinalFields()}
-        defaultStaticStyleOptions={this.state.defaultStaticProperties.fillColor.options}
-        defaultDynamicStyleOptions={this.state.defaultDynamicProperties.fillColor.options}
+        defaultStaticStyleOptions={
+          this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options
+        }
+        defaultDynamicStyleOptions={
+          this.state.defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options
+        }
       />
     );
   }
@@ -145,11 +171,16 @@ export class VectorStyleEditor extends Component {
     return (
       <VectorStyleColorEditor
         swatches={DEFAULT_LINE_COLORS}
-        handlePropertyChange={this.props.handlePropertyChange}
-        styleProperty={this.props.styleProperties.lineColor}
+        onStaticStyleChange={this._onStaticStyleChange}
+        onDynamicStyleChange={this._onDynamicStyleChange}
+        styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]}
         fields={this._getOrdinalFields()}
-        defaultStaticStyleOptions={this.state.defaultStaticProperties.lineColor.options}
-        defaultDynamicStyleOptions={this.state.defaultDynamicProperties.lineColor.options}
+        defaultStaticStyleOptions={
+          this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options
+        }
+        defaultDynamicStyleOptions={
+          this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options
+        }
       />
     );
   }
@@ -157,11 +188,16 @@ export class VectorStyleEditor extends Component {
   _renderLineWidth() {
     return (
       <VectorStyleSizeEditor
-        handlePropertyChange={this.props.handlePropertyChange}
-        styleProperty={this.props.styleProperties.lineWidth}
+        onStaticStyleChange={this._onStaticStyleChange}
+        onDynamicStyleChange={this._onDynamicStyleChange}
+        styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]}
         fields={this._getOrdinalFields()}
-        defaultStaticStyleOptions={this.state.defaultStaticProperties.lineWidth.options}
-        defaultDynamicStyleOptions={this.state.defaultDynamicProperties.lineWidth.options}
+        defaultStaticStyleOptions={
+          this.state.defaultStaticProperties[VECTOR_STYLES.LINE_WIDTH].options
+        }
+        defaultDynamicStyleOptions={
+          this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options
+        }
       />
     );
   }
@@ -169,11 +205,16 @@ export class VectorStyleEditor extends Component {
   _renderSymbolSize() {
     return (
       <VectorStyleSizeEditor
-        handlePropertyChange={this.props.handlePropertyChange}
-        styleProperty={this.props.styleProperties.iconSize}
+        onStaticStyleChange={this._onStaticStyleChange}
+        onDynamicStyleChange={this._onDynamicStyleChange}
+        styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]}
         fields={this._getOrdinalFields()}
-        defaultStaticStyleOptions={this.state.defaultStaticProperties.iconSize.options}
-        defaultDynamicStyleOptions={this.state.defaultDynamicProperties.iconSize.options}
+        defaultStaticStyleOptions={
+          this.state.defaultStaticProperties[VECTOR_STYLES.ICON_SIZE].options
+        }
+        defaultDynamicStyleOptions={
+          this.state.defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options
+        }
       />
     );
   }
@@ -182,30 +223,45 @@ export class VectorStyleEditor extends Component {
     return (
       <Fragment>
         <VectorStyleLabelEditor
-          handlePropertyChange={this.props.handlePropertyChange}
-          styleProperty={this.props.styleProperties.labelText}
+          onStaticStyleChange={this._onStaticStyleChange}
+          onDynamicStyleChange={this._onDynamicStyleChange}
+          styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT]}
           fields={this.state.fields}
-          defaultStaticStyleOptions={this.state.defaultStaticProperties.labelText.options}
-          defaultDynamicStyleOptions={this.state.defaultDynamicProperties.labelText.options}
+          defaultStaticStyleOptions={
+            this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_TEXT].options
+          }
+          defaultDynamicStyleOptions={
+            this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options
+          }
         />
         <EuiSpacer size="m" />
 
         <VectorStyleColorEditor
           swatches={DEFAULT_LINE_COLORS}
-          handlePropertyChange={this.props.handlePropertyChange}
-          styleProperty={this.props.styleProperties.labelColor}
+          onStaticStyleChange={this._onStaticStyleChange}
+          onDynamicStyleChange={this._onDynamicStyleChange}
+          styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]}
           fields={this._getOrdinalFields()}
-          defaultStaticStyleOptions={this.state.defaultStaticProperties.labelColor.options}
-          defaultDynamicStyleOptions={this.state.defaultDynamicProperties.labelColor.options}
+          defaultStaticStyleOptions={
+            this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options
+          }
+          defaultDynamicStyleOptions={
+            this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_COLOR].options
+          }
         />
         <EuiSpacer size="m" />
 
         <VectorStyleSizeEditor
-          handlePropertyChange={this.props.handlePropertyChange}
-          styleProperty={this.props.styleProperties.labelSize}
+          onStaticStyleChange={this._onStaticStyleChange}
+          onDynamicStyleChange={this._onDynamicStyleChange}
+          styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_SIZE]}
           fields={this._getOrdinalFields()}
-          defaultStaticStyleOptions={this.state.defaultStaticProperties.labelSize.options}
-          defaultDynamicStyleOptions={this.state.defaultDynamicProperties.labelSize.options}
+          defaultStaticStyleOptions={
+            this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_SIZE].options
+          }
+          defaultDynamicStyleOptions={
+            this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_SIZE].options
+          }
         />
         <EuiSpacer size="m" />
       </Fragment>
@@ -217,11 +273,16 @@ export class VectorStyleEditor extends Component {
     if (this.props.symbolDescriptor.options.symbolizeAs === SYMBOLIZE_AS_ICON) {
       iconOrientation = (
         <OrientationEditor
-          handlePropertyChange={this.props.handlePropertyChange}
-          styleProperty={this.props.styleProperties.iconOrientation}
+          onStaticStyleChange={this._onStaticStyleChange}
+          onDynamicStyleChange={this._onDynamicStyleChange}
+          styleProperty={this.props.styleProperties[VECTOR_STYLES.ICON_ORIENTATION]}
           fields={this.state.numberFields}
-          defaultStaticStyleOptions={this.state.defaultStaticProperties.iconOrientation.options}
-          defaultDynamicStyleOptions={this.state.defaultDynamicProperties.iconOrientation.options}
+          defaultStaticStyleOptions={
+            this.state.defaultStaticProperties[VECTOR_STYLES.ICON_ORIENTATION].options
+          }
+          defaultDynamicStyleOptions={
+            this.state.defaultDynamicProperties[VECTOR_STYLES.ICON_ORIENTATION].options
+          }
         />
       );
     }
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cd6286afe7831..8bb3902a34c10 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -6599,8 +6599,6 @@
     "xpack.maps.source.wmsTitle": "ウェブマップサービス",
     "xpack.maps.style.heatmap.displayNameLabel": "ヒートマップスタイル",
     "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "解像度パラメーターが認識されません: {resolution}",
-    "xpack.maps.styles.staticDynamic.dynamicDescription": "プロパティ値で特徴をシンボル化します。",
-    "xpack.maps.styles.staticDynamic.staticDescription": "静的スタイルプロパティで特徴をシンボル化します。",
     "xpack.maps.styles.vector.borderColorLabel": "境界線の色",
     "xpack.maps.styles.vector.borderWidthLabel": "境界線の幅",
     "xpack.maps.styles.vector.fillColorLabel": "塗りつぶす色",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 749306fa9e8a2..8c58c07c03f22 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -6540,8 +6540,6 @@
     "xpack.maps.source.wmsTitle": "Web 地图服务",
     "xpack.maps.style.heatmap.displayNameLabel": "热图样式",
     "xpack.maps.style.heatmap.resolutionStyleErrorMessage": "无法识别分辨率参数:{resolution}",
-    "xpack.maps.styles.staticDynamic.dynamicDescription": "使用属性值代表功能。",
-    "xpack.maps.styles.staticDynamic.staticDescription": "使用静态样式属性代表功能。",
     "xpack.maps.styles.vector.borderColorLabel": "边框颜色",
     "xpack.maps.styles.vector.borderWidthLabel": "边框宽度",
     "xpack.maps.styles.vector.fillColorLabel": "填充颜色",