From 528884c5f32cb861f7d11260bd18236ef46d800a Mon Sep 17 00:00:00 2001
From: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com>
Date: Tue, 5 Sep 2023 10:15:21 +0200
Subject: [PATCH] Enabled connector types API (#164689)
Resolves: #163751
This PR adds a new API to allow the other plugins to define an **enabled
connector types list** in the actions registry.
The list is used during the action execution to decide if the action
type is executable (enabled).
This decision logic sits on the existing two other checks:
1- `isActionTypeEnabled` -> if the connector type is in the
`enabledActionTypes` list in the config
2- `isLicenseValidForActionType` -> if the connector type is allowed for
the active license
As the only user of this feature is just security-solutions for now, we
decided to allow the list to be set only once.
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/actions/kibana.jsonc | 3 +-
x-pack/plugins/actions/server/config.ts | 17 +-
x-pack/plugins/actions/server/mocks.ts | 1 +
x-pack/plugins/actions/server/plugin.test.ts | 189 +++++++++++++++++++
x-pack/plugins/actions/server/plugin.ts | 41 +++-
x-pack/plugins/actions/tsconfig.json | 1 +
6 files changed, 240 insertions(+), 12 deletions(-)
diff --git a/x-pack/plugins/actions/kibana.jsonc b/x-pack/plugins/actions/kibana.jsonc
index 9152e64ba898a..78f66742c2a03 100644
--- a/x-pack/plugins/actions/kibana.jsonc
+++ b/x-pack/plugins/actions/kibana.jsonc
@@ -21,7 +21,8 @@
"usageCollection",
"spaces",
"security",
- "monitoringCollection"
+ "monitoringCollection",
+ "serverless"
],
"extraPublicDirs": [
"common"
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index 9a620d1452f23..da45fd40cf925 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -64,6 +64,15 @@ const connectorTypeSchema = schema.object({
maxAttempts: schema.maybe(schema.number({ min: MIN_MAX_ATTEMPTS, max: MAX_MAX_ATTEMPTS })),
});
+// We leverage enabledActionTypes list by allowing the other plugins to overwrite it by using "setEnabledConnectorTypes" in the plugin setup.
+// The list can be overwritten only if it's not already been set in the config.
+const enabledConnectorTypesSchema = schema.arrayOf(
+ schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]),
+ {
+ defaultValue: [AllowedHosts.Any],
+ }
+);
+
export const configSchema = schema.object({
allowedHosts: schema.arrayOf(
schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]),
@@ -71,12 +80,7 @@ export const configSchema = schema.object({
defaultValue: [AllowedHosts.Any],
}
),
- enabledActionTypes: schema.arrayOf(
- schema.oneOf([schema.string(), schema.literal(EnabledActionTypes.Any)]),
- {
- defaultValue: [AllowedHosts.Any],
- }
- ),
+ enabledActionTypes: enabledConnectorTypesSchema,
preconfiguredAlertHistoryEsIndex: schema.boolean({ defaultValue: false }),
preconfigured: schema.recordOf(schema.string(), preconfiguredActionSchema, {
defaultValue: {},
@@ -129,6 +133,7 @@ export const configSchema = schema.object({
});
export type ActionsConfig = TypeOf;
+export type EnabledConnectorTypes = TypeOf;
// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on
// simultaneous usage in the config validator directly, but there's no good way to express
diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts
index ad26114cf7d07..70a2cfd9f8e85 100644
--- a/x-pack/plugins/actions/server/mocks.ts
+++ b/x-pack/plugins/actions/server/mocks.ts
@@ -31,6 +31,7 @@ const createSetupMock = () => {
getCaseConnectorClass: jest.fn(),
getActionsHealth: jest.fn(),
getActionsConfigurationUtilities: jest.fn(),
+ setEnabledConnectorTypes: jest.fn(),
};
return mock;
};
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index 66d75da5fd7cf..d3bc3be1a9deb 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -348,6 +348,163 @@ describe('Actions Plugin', () => {
expect(pluginSetup.isPreconfiguredConnector('anotherConnectorId')).toEqual(false);
});
});
+
+ describe('setEnabledConnectorTypes (works only on serverless)', () => {
+ function setup(config: ActionsConfig) {
+ context = coreMock.createPluginInitializerContext(config);
+ plugin = new ActionsPlugin(context);
+ coreSetup = coreMock.createSetup();
+ pluginsSetup = {
+ taskManager: taskManagerMock.createSetup(),
+ encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
+ licensing: licensingMock.createSetup(),
+ eventLog: eventLogMock.createSetup(),
+ usageCollection: usageCollectionPluginMock.createSetupContract(),
+ features: featuresPluginMock.createSetup(),
+ serverless: {},
+ };
+ }
+
+ it('should set connector type enabled', async () => {
+ setup(getConfig());
+ // coreMock.createSetup doesn't support Plugin generics
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
+ const coreStart = coreMock.createStart();
+ const pluginsStart = {
+ licensing: licensingMock.createStart(),
+ taskManager: taskManagerMock.createStart(),
+ encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
+ eventLog: eventLogMock.createStart(),
+ };
+ const pluginStart = plugin.start(coreStart, pluginsStart);
+
+ pluginSetup.registerType({
+ id: '.server-log',
+ name: 'Server log',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.registerType({
+ id: '.slack',
+ name: 'Slack',
+ minimumLicenseRequired: 'gold',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.setEnabledConnectorTypes(['.server-log']);
+ expect(pluginStart.isActionTypeEnabled('.server-log')).toBeTruthy();
+ expect(pluginStart.isActionTypeEnabled('.slack')).toBeFalsy();
+ });
+
+ it('should set all the connector types enabled when null or ["*"] passed', async () => {
+ setup(getConfig());
+ // coreMock.createSetup doesn't support Plugin generics
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
+ const coreStart = coreMock.createStart();
+ const pluginsStart = {
+ licensing: licensingMock.createStart(),
+ taskManager: taskManagerMock.createStart(),
+ encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
+ eventLog: eventLogMock.createStart(),
+ };
+ const pluginStart = plugin.start(coreStart, pluginsStart);
+
+ pluginSetup.registerType({
+ id: '.server-log',
+ name: 'Server log',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.registerType({
+ id: '.index',
+ name: 'Index',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.setEnabledConnectorTypes(['*']);
+ expect(pluginStart.isActionTypeEnabled('.server-log')).toBeTruthy();
+ expect(pluginStart.isActionTypeEnabled('.index')).toBeTruthy();
+ });
+
+ it('should set all the connector types disabled when [] passed', async () => {
+ setup(getConfig());
+ // coreMock.createSetup doesn't support Plugin generics
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
+ const coreStart = coreMock.createStart();
+ const pluginsStart = {
+ licensing: licensingMock.createStart(),
+ taskManager: taskManagerMock.createStart(),
+ encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
+ eventLog: eventLogMock.createStart(),
+ };
+ const pluginStart = plugin.start(coreStart, pluginsStart);
+
+ pluginSetup.registerType({
+ id: '.server-log',
+ name: 'Server log',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.registerType({
+ id: '.index',
+ name: 'Index',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+ pluginSetup.setEnabledConnectorTypes([]);
+ expect(pluginStart.isActionTypeEnabled('.server-log')).toBeFalsy();
+ expect(pluginStart.isActionTypeEnabled('.index')).toBeFalsy();
+ });
+
+ it('should throw if the enabledActionTypes is already set by the config', async () => {
+ setup({ ...getConfig(), enabledActionTypes: ['.email'] });
+ // coreMock.createSetup doesn't support Plugin generics
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
+
+ expect(() => pluginSetup.setEnabledConnectorTypes(['.index'])).toThrow(
+ "Enabled connector types can be set only if they haven't already been set in the config"
+ );
+ });
+ });
});
describe('start()', () => {
@@ -396,6 +553,38 @@ describe('Actions Plugin', () => {
};
});
+ it('should throw when there is an invalid connector type in enabledActionTypes', async () => {
+ const pluginSetup = await plugin.setup(coreSetup, {
+ ...pluginsSetup,
+ encryptedSavedObjects: {
+ ...pluginsSetup.encryptedSavedObjects,
+ canEncrypt: true,
+ },
+ serverless: {},
+ });
+
+ pluginSetup.registerType({
+ id: '.server-log',
+ name: 'Server log',
+ minimumLicenseRequired: 'basic',
+ supportedFeatureIds: ['alerting'],
+ validate: {
+ config: { schema: schema.object({}) },
+ secrets: { schema: schema.object({}) },
+ params: { schema: schema.object({}) },
+ },
+ executor,
+ });
+
+ pluginSetup.setEnabledConnectorTypes(['.server-log', 'non-existing']);
+
+ await expect(async () =>
+ plugin.start(coreStart, { ...pluginsStart, serverless: {} })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Action type \\"non-existing\\" is not registered."`
+ );
+ });
+
describe('getActionsClientWithRequest()', () => {
it('should not throw error when ESO plugin has encryption key', async () => {
await plugin.setup(coreSetup, {
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index b8b88b05049ca..415a9e36a1c01 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -40,7 +40,8 @@ import {
} from '@kbn/event-log-plugin/server';
import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server';
-import { ActionsConfig, getValidatedConfig } from './config';
+import { ServerlessPluginSetup } from '@kbn/serverless/server';
+import { ActionsConfig, AllowedHosts, EnabledConnectorTypes, getValidatedConfig } from './config';
import { resolveCustomHosts } from './lib/custom_host_settings';
import { ActionsClient } from './actions_client/actions_client';
import { ActionTypeRegistry } from './action_type_registry';
@@ -100,10 +101,8 @@ import { createSubActionConnectorFramework } from './sub_action_framework';
import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework/types';
import { SubActionConnector } from './sub_action_framework/sub_action_connector';
import { CaseConnector } from './sub_action_framework/case';
-import {
- type IUnsecuredActionsClient,
- UnsecuredActionsClient,
-} from './unsecured_actions_client/unsecured_actions_client';
+import type { IUnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client';
+import { UnsecuredActionsClient } from './unsecured_actions_client/unsecured_actions_client';
import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function';
import { createSystemConnectors } from './create_system_actions';
@@ -130,6 +129,7 @@ export interface PluginSetupContract {
getCaseConnectorClass: () => IServiceAbstract;
getActionsHealth: () => { hasPermanentEncryptionKey: boolean };
getActionsConfigurationUtilities: () => ActionsConfigurationUtilities;
+ setEnabledConnectorTypes: (connectorTypes: EnabledConnectorTypes) => void;
}
export interface PluginStartContract {
@@ -169,6 +169,7 @@ export interface ActionsPluginsSetup {
features: FeaturesPluginSetup;
spaces?: SpacesPluginSetup;
monitoringCollection?: MonitoringCollectionSetup;
+ serverless?: ServerlessPluginSetup;
}
export interface ActionsPluginsStart {
@@ -178,6 +179,7 @@ export interface ActionsPluginsStart {
eventLog: IEventLogClientService;
spaces?: SpacesPluginStart;
security?: SecurityPluginStart;
+ serverless?: ServerlessPluginSetup;
}
const includedHiddenTypes = [
@@ -375,6 +377,20 @@ export class ActionsPlugin implements Plugin actionsConfigUtils,
+ setEnabledConnectorTypes: (connectorTypes) => {
+ if (
+ !!plugins.serverless &&
+ this.actionsConfig.enabledActionTypes.length === 1 &&
+ this.actionsConfig.enabledActionTypes[0] === AllowedHosts.Any
+ ) {
+ this.actionsConfig.enabledActionTypes.pop();
+ this.actionsConfig.enabledActionTypes.push(...connectorTypes);
+ } else {
+ throw new Error(
+ "Enabled connector types can be set only if they haven't already been set in the config"
+ );
+ }
+ },
};
}
@@ -542,6 +558,8 @@ export class ActionsPlugin implements Plugin {
return this.actionTypeRegistry!.isActionTypeEnabled(id, options);
@@ -695,6 +713,19 @@ export class ActionsPlugin implements Plugin {
+ if (
+ !!plugins.serverless &&
+ this.actionsConfig.enabledActionTypes.length > 0 &&
+ this.actionsConfig.enabledActionTypes[0] !== AllowedHosts.Any
+ ) {
+ this.actionsConfig.enabledActionTypes.forEach((connectorType) => {
+ // Throws error if action type doesn't exist
+ this.actionTypeRegistry?.get(connectorType);
+ });
+ }
+ };
+
public stop() {
if (this.licenseState) {
this.licenseState.clean();
diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json
index 0f4d2faf03e1a..3beeddef429b2 100644
--- a/x-pack/plugins/actions/tsconfig.json
+++ b/x-pack/plugins/actions/tsconfig.json
@@ -43,6 +43,7 @@
"@kbn/core-saved-objects-api-server-mocks",
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-logging-server-mocks",
+ "@kbn/serverless"
],
"exclude": [
"target/**/*",