mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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. <img width="890" alt="Screenshot 2023-08-31 at 12 00 10" src="896ab104
-6f75-4eee-8f19-9f2eb04bb149"> <img width="592" alt="Screenshot 2023-08-31 at 12 13 38" src="533a1cfd
-947a-4506-b078-149780346088"> <img width="1513" alt="Screenshot 2023-08-31 at 12 00 01" src="91169dfe
-b7f8-4fef-b734-56857a75dbdc"> --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0f40b6dbc1
commit
528884c5f3
6 changed files with 240 additions and 12 deletions
|
@ -21,7 +21,8 @@
|
|||
"usageCollection",
|
||||
"spaces",
|
||||
"security",
|
||||
"monitoringCollection"
|
||||
"monitoringCollection",
|
||||
"serverless"
|
||||
],
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
|
|
|
@ -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<typeof configSchema>;
|
||||
export type EnabledConnectorTypes = TypeOf<typeof enabledConnectorTypesSchema>;
|
||||
|
||||
// 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
|
||||
|
|
|
@ -31,6 +31,7 @@ const createSetupMock = () => {
|
|||
getCaseConnectorClass: jest.fn(),
|
||||
getActionsHealth: jest.fn(),
|
||||
getActionsConfigurationUtilities: jest.fn(),
|
||||
setEnabledConnectorTypes: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -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<ActionsConfig>(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, {
|
||||
|
|
|
@ -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: <Config, Secrets>() => IServiceAbstract<Config, Secrets>;
|
||||
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<PluginSetupContract, PluginStartCon
|
|||
};
|
||||
},
|
||||
getActionsConfigurationUtilities: () => 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<PluginSetupContract, PluginStartCon
|
|||
});
|
||||
}
|
||||
|
||||
this.validateEnabledConnectorTypes(plugins);
|
||||
|
||||
return {
|
||||
isActionTypeEnabled: (id, options = { notifyUsage: false }) => {
|
||||
return this.actionTypeRegistry!.isActionTypeEnabled(id, options);
|
||||
|
@ -695,6 +713,19 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
};
|
||||
};
|
||||
|
||||
private validateEnabledConnectorTypes = (plugins: ActionsPluginsStart) => {
|
||||
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();
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue