Notify the response ops when there is change on connector config (#175981)

Resolves: #175018

This Pr adds an integration test to check the changes on connectorTypes
config, secrets and params schemas.
I used `validate.schema` field as all the connector types have it.

ConnectorTypes has config, secrets and params schemas on
`validate.schema` whereas SubActionConnectorTypes has only config and
secrets.

They have multiple params schema as well but only registered and used
during action execution.
e.g.
https://github.com/ersin-erdal/kibana/blob/main/x-pack/plugins/stack_connectors/server/connector_types/bedrock/bedrock.ts#L57

And here is the explanation why they are not listed in a definition:

https://github.com/ersin-erdal/kibana/blob/main/x-pack/plugins/actions/server/sub_action_framework/validators.ts#L38

We need to do some refactoring to list those schemas on the connector
types.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ersin Erdal 2024-02-15 18:25:20 +01:00 committed by GitHub
parent 68d6ab2135
commit 9488c93b3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 11400 additions and 14 deletions

View file

@ -6,7 +6,7 @@
*/
module.exports = {
preset: '@kbn/test/jest_integration_node',
preset: '@kbn/test/jest_integration',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/actions'],
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { TestElasticsearchUtils, TestKibanaUtils } from '@kbn/core-test-helpers-kbn-server';
import { ActionTypeRegistry } from '../action_type_registry';
import { setupTestServers } from './lib';
import { connectorTypes } from './mocks/connector_types';
jest.mock('../action_type_registry', () => {
const actual = jest.requireActual('../action_type_registry');
return {
...actual,
ActionTypeRegistry: jest.fn().mockImplementation((opts) => {
return new actual.ActionTypeRegistry(opts);
}),
};
});
describe('Connector type config checks', () => {
let esServer: TestElasticsearchUtils;
let kibanaServer: TestKibanaUtils;
let actionTypeRegistry: ActionTypeRegistry;
beforeAll(async () => {
const setupResult = await setupTestServers();
esServer = setupResult.esServer;
kibanaServer = setupResult.kibanaServer;
const mockedActionTypeRegistry = jest.requireMock('../action_type_registry');
expect(mockedActionTypeRegistry.ActionTypeRegistry).toHaveBeenCalledTimes(1);
actionTypeRegistry = mockedActionTypeRegistry.ActionTypeRegistry.mock.results[0].value;
});
afterAll(async () => {
if (kibanaServer) {
await kibanaServer.stop();
}
if (esServer) {
await esServer.stop();
}
});
test('ensure connector types list up to date', () => {
expect(connectorTypes).toEqual(actionTypeRegistry.getAllTypes());
});
for (const connectorTypeId of connectorTypes) {
test(`detect connector type changes for: ${connectorTypeId}`, async () => {
const connectorType = actionTypeRegistry.get(connectorTypeId);
expect(connectorType?.validate.config.schema.getSchema!().describe()).toMatchSnapshot();
expect(connectorType.validate.secrets.schema.getSchema!().describe()).toMatchSnapshot();
expect(connectorType.validate.params.schema.getSchema!().describe()).toMatchSnapshot();
});
}
});

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { setupTestServers } from './setup_test_servers';

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createTestServers, createRootWithCorePlugins } from '@kbn/core-test-helpers-kbn-server';
export async function setupTestServers(settings = {}) {
const { startES } = createTestServers({
adjustTimeout: (t) => jest.setTimeout(t),
settings: {
es: {
license: 'trial',
},
},
});
const esServer = await startES();
const root = createRootWithCorePlugins(settings, { oss: false });
await root.preboot();
const coreSetup = await root.setup();
const coreStart = await root.start();
return {
esServer,
kibanaServer: {
root,
coreSetup,
coreStart,
stop: async () => await root.shutdown(),
},
};
}

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const connectorTypes: string[] = [
'.email',
'.index',
'.pagerduty',
'.swimlane',
'.server-log',
'.slack',
'.slack_api',
'.webhook',
'.cases-webhook',
'.xmatters',
'.servicenow',
'.servicenow-sir',
'.servicenow-itom',
'.jira',
'.resilient',
'.teams',
'.torq',
'.opsgenie',
'.tines',
'.gen-ai',
'.bedrock',
'.d3security',
'.sentinelone',
];

View file

@ -16,6 +16,7 @@ import {
SavedObjectReference,
Logger,
} from '@kbn/core/server';
import { AnySchema } from 'joi';
import { ActionTypeRegistry } from './action_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { ActionsClient } from './actions_client';
@ -101,11 +102,12 @@ export type ExecutorType<
options: ActionTypeExecutorOptions<Config, Secrets, Params>
) => Promise<ActionTypeExecutorResult<ResultData>>;
export interface ValidatorType<Type> {
export interface ValidatorType<T> {
schema: {
validate(value: unknown): Type;
validate(value: unknown): T;
getSchema?: () => AnySchema;
};
customValidator?: (value: Type, validatorServices: ValidatorServices) => void;
customValidator?: (value: T, validatorServices: ValidatorServices) => void;
}
export interface ValidatorServices {

View file

@ -44,7 +44,8 @@
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-logging-server-mocks",
"@kbn/serverless",
"@kbn/actions-types"
"@kbn/actions-types",
"@kbn/core-test-helpers-kbn-server"
],
"exclude": [
"target/**/*",

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { RuleTaskState } from '../../types';
import { taskInstanceToAlertTaskInstance } from '../../task_runner/alert_task_instance';
import { ReadOperations, AlertingAuthorizationEntity } from '../../authorization';
@ -18,18 +19,28 @@ export async function getAlertState(
context: RulesClientContext,
{ id }: GetAlertStateParams
): Promise<RuleTaskState | void> {
const alert = await get(context, { id });
const rule = await get(context, { id });
await context.authorization.ensureAuthorized({
ruleTypeId: alert.alertTypeId,
consumer: alert.consumer,
ruleTypeId: rule.alertTypeId,
consumer: rule.consumer,
operation: ReadOperations.GetRuleState,
entity: AlertingAuthorizationEntity.Rule,
});
if (alert.scheduledTaskId) {
const { state } = taskInstanceToAlertTaskInstance(
await context.taskManager.get(alert.scheduledTaskId),
alert
);
return state;
if (rule.scheduledTaskId) {
try {
const { state } = taskInstanceToAlertTaskInstance(
await context.taskManager.get(rule.scheduledTaskId),
rule
);
return state;
} catch (e) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
context.logger.warn(`Task (${rule.scheduledTaskId}) not found`);
} else {
context.logger.warn(
`An error occurred when getting the task state for (${rule.scheduledTaskId})`
);
}
}
}
}

View file

@ -22,6 +22,7 @@ import { AlertingAuthorization } from '../../authorization/alerting_authorizatio
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { getBeforeSetup } from './lib';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
@ -175,6 +176,70 @@ describe('getAlertState()', () => {
expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId);
});
test('logs a warning if the task not found', async () => {
const rulesClient = new RulesClient(rulesClientParams);
const scheduledTaskId = 'task-123';
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: RULE_SAVED_OBJECT_TYPE,
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
},
actions: [],
enabled: true,
scheduledTaskId,
mutedInstanceIds: [],
muteAll: true,
},
references: [],
});
taskManager.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError());
await rulesClient.getAlertState({ id: '1' });
expect(rulesClientParams.logger.warn).toHaveBeenCalledTimes(1);
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith('Task (task-123) not found');
});
test('logs a warning if the taskManager throws an error', async () => {
const rulesClient = new RulesClient(rulesClientParams);
const scheduledTaskId = 'task-123';
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: RULE_SAVED_OBJECT_TYPE,
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
},
actions: [],
enabled: true,
scheduledTaskId,
mutedInstanceIds: [],
muteAll: true,
},
references: [],
});
taskManager.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createBadRequestError());
await rulesClient.getAlertState({ id: '1' });
expect(rulesClientParams.logger.warn).toHaveBeenCalledTimes(1);
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith(
'An error occurred when getting the task state for (task-123)'
);
});
describe('authorization', () => {
beforeEach(() => {
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({