mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Add notifications plugin, offering basic email service (#143303)
* Misc enhancements following PR comments * Adding functional tests * Fixing types * Fixing tests * Removing unnecessary Promise.all * Cleanup * Misc fixes and simplifications * Add missing tsconfig.json * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * Add dependency to Actions plugin in tsconfig.json * Separate setup logic from start logic * Fix bulkEnqueueExecution params structure * Update README * Add UTs * Check license type >platinum for email notifications * Fix incorrect UTs * Import types when possible * Misc enhancements and code cleanup * Transform factory => provider, update start contract * Code cleanup, update README * Fix TS error * Fix CI types error * Address PR remarks * Address PR remarks #2 Co-authored-by: Ying Mao <ying.mao@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e5271bd250
commit
88815398e8
27 changed files with 1108 additions and 5 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -89,6 +89,7 @@
|
|||
/x-pack/test/search_sessions_integration/ @elastic/kibana-app-services
|
||||
/test/plugin_functional/test_suites/panel_actions @elastic/kibana-app-services
|
||||
/test/plugin_functional/test_suites/data_plugin @elastic/kibana-app-services
|
||||
/x-pack/plugins/notifications/ @elastic/kibana-app-services
|
||||
|
||||
### Observability Plugins
|
||||
|
||||
|
|
|
@ -589,6 +589,10 @@ Elastic.
|
|||
|This plugin allows for other plugins to add data to Kibana stack monitoring documents.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/notifications/README.md[notifications]
|
||||
|The Notifications plugin provides a set of services to help Solutions and plugins send notifications to users.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/observability/README.md[observability]
|
||||
|This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI.
|
||||
|
||||
|
|
|
@ -51,14 +51,14 @@ To use the exposed plugin start and setup contracts:
|
|||
|
||||
import { TelemetryPluginsStart } from '../telemetry/server`;
|
||||
|
||||
interface MyPlyginStartDeps {
|
||||
interface MyPluginStartDeps {
|
||||
telemetry?: TelemetryPluginsStart;
|
||||
}
|
||||
|
||||
class MyPlugin {
|
||||
public async start(
|
||||
core: CoreStart,
|
||||
{ telemetry }: MyPlyginStartDeps
|
||||
{ telemetry }: MyPluginStartDeps
|
||||
) {
|
||||
const isOptedIn = await telemetry?.getIsOptedIn();
|
||||
...
|
||||
|
|
|
@ -1104,6 +1104,8 @@
|
|||
"@kbn/monitoring-collection-plugin/*": ["x-pack/plugins/monitoring_collection/*"],
|
||||
"@kbn/monitoring-plugin": ["x-pack/plugins/monitoring"],
|
||||
"@kbn/monitoring-plugin/*": ["x-pack/plugins/monitoring/*"],
|
||||
"@kbn/notifications-plugin": ["x-pack/plugins/notifications"],
|
||||
"@kbn/notifications-plugin/*": ["x-pack/plugins/notifications/*"],
|
||||
"@kbn/observability-plugin": ["x-pack/plugins/observability"],
|
||||
"@kbn/observability-plugin/*": ["x-pack/plugins/observability/*"],
|
||||
"@kbn/osquery-plugin": ["x-pack/plugins/osquery"],
|
||||
|
|
|
@ -39,7 +39,7 @@ const createStartMock = () => {
|
|||
isActionTypeEnabled: jest.fn(),
|
||||
isActionExecutable: jest.fn(),
|
||||
getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()),
|
||||
getUnsecuredActionsClient: jest.fn().mockResolvedValue(unsecuredActionsClientMock.create()),
|
||||
getUnsecuredActionsClient: jest.fn().mockReturnValue(unsecuredActionsClientMock.create()),
|
||||
getActionsAuthorizationWithRequest: jest
|
||||
.fn()
|
||||
.mockReturnValue(actionsAuthorizationMock.create()),
|
||||
|
|
|
@ -102,7 +102,7 @@ import { IServiceAbstract, SubActionConnectorType } from './sub_action_framework
|
|||
import { SubActionConnector } from './sub_action_framework/sub_action_connector';
|
||||
import { CaseConnector } from './sub_action_framework/case';
|
||||
import {
|
||||
IUnsecuredActionsClient,
|
||||
type IUnsecuredActionsClient,
|
||||
UnsecuredActionsClient,
|
||||
} from './unsecured_actions_client/unsecured_actions_client';
|
||||
import { createBulkUnsecuredExecutionEnqueuerFunction } from './create_unsecured_execute_function';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IUnsecuredActionsClient } from './unsecured_actions_client';
|
||||
import type { IUnsecuredActionsClient } from './unsecured_actions_client';
|
||||
|
||||
export type UnsecuredActionsClientMock = jest.Mocked<IUnsecuredActionsClient>;
|
||||
|
||||
|
|
70
x-pack/plugins/notifications/README.md
Executable file
70
x-pack/plugins/notifications/README.md
Executable file
|
@ -0,0 +1,70 @@
|
|||
# Kibana Notifications Plugin
|
||||
|
||||
The Notifications plugin provides a set of services to help Solutions and plugins send notifications to users.
|
||||
|
||||
## Notifications Plugin public API
|
||||
|
||||
### Start
|
||||
|
||||
The `start` function exposes the following interface:
|
||||
|
||||
- `isEmailServiceAvailable(): boolean`:
|
||||
A function to check whether the deployment is properly configured and the EmailService can be correctly retrieved.
|
||||
- `getEmailService(): EmailService`:
|
||||
- A function to get the basic EmailService, which can be used to send plain text emails. If the EmailService is not available, trying to retrieve it will result in an Exception.
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
To use the exposed plugin start contract:
|
||||
|
||||
1. Make sure `notifications` is in your `optionalPlugins` in the `kibana.json` file:
|
||||
|
||||
```json5
|
||||
// <plugin>/kibana.json
|
||||
{
|
||||
"id": "...",
|
||||
"requiredPlugins": ["notifications"]
|
||||
}
|
||||
```
|
||||
|
||||
2. Use the exposed contract:
|
||||
|
||||
```ts
|
||||
// <plugin>/server/plugin.ts
|
||||
import { NotificationsPluginStart } from '../notifications/server`;
|
||||
|
||||
interface MyPluginStartDeps {
|
||||
notifications?: NotificationsPluginStart;
|
||||
}
|
||||
|
||||
class MyPlugin {
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ notifications }: MyPluginStartDeps
|
||||
) {
|
||||
if (notifications.isEmailServiceAvailable()) {
|
||||
const emailService = notifications.getEmailService();
|
||||
emailService.sendPlainTextEmail({
|
||||
to: 'foo@bar.com',
|
||||
subject: 'Some subject',
|
||||
message: 'Hello world!',
|
||||
});
|
||||
}
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
- This plugin currently depends on the `'actions'` plugin, as it uses `Connectors` under the hood.
|
||||
- Note also that for each notification channel the corresponding connector must be preconfigured. E.g. to enable email notifications, an `Email` connector must exist in the system.
|
||||
- Once the appropriate connectors are preconfigured in `kibana.yaml`, you can configure the `'notifications'` plugin by adding:
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
connectors:
|
||||
default:
|
||||
email: elastic-cloud-email # The identifier of the configured connector
|
||||
```
|
8
x-pack/plugins/notifications/common/index.ts
Normal file
8
x-pack/plugins/notifications/common/index.ts
Normal 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 const PLUGIN_ID = 'notifications';
|
15
x-pack/plugins/notifications/jest.config.js
Normal file
15
x-pack/plugins/notifications/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/notifications'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/notifications',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/plugins/notifications/{common,server}/**/*.{js,ts,tsx}'],
|
||||
};
|
12
x-pack/plugins/notifications/kibana.json
Executable file
12
x-pack/plugins/notifications/kibana.json
Executable file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "notifications",
|
||||
"owner": {
|
||||
"name": "App Services",
|
||||
"githubTeam": "kibana-app-services"
|
||||
},
|
||||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": false,
|
||||
"requiredPlugins": ["actions", "licensing"],
|
||||
"optionalPlugins": []
|
||||
}
|
29
x-pack/plugins/notifications/server/config/config.ts
Normal file
29
x-pack/plugins/notifications/server/config/config.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { schema, type TypeOf } from '@kbn/config-schema';
|
||||
import type { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
|
||||
export const configSchema = schema.object(
|
||||
{
|
||||
connectors: schema.maybe(
|
||||
schema.object({
|
||||
default: schema.maybe(
|
||||
schema.object({
|
||||
email: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
},
|
||||
{ defaultValue: {} }
|
||||
);
|
||||
export type NotificationsConfigType = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<NotificationsConfigType> = {
|
||||
schema: configSchema,
|
||||
};
|
8
x-pack/plugins/notifications/server/config/index.ts
Normal file
8
x-pack/plugins/notifications/server/config/index.ts
Normal 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 { type NotificationsConfigType, config } from './config';
|
18
x-pack/plugins/notifications/server/index.ts
Executable file
18
x-pack/plugins/notifications/server/index.ts
Executable file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { PluginInitializerContext } from '@kbn/core/server';
|
||||
import { NotificationsPlugin } from './plugin';
|
||||
export { config } from './config';
|
||||
|
||||
// This exports static code and TypeScript types,
|
||||
// as well as, Kibana Platform `plugin()` initializer.
|
||||
export type { NotificationsPluginStart } from './types';
|
||||
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new NotificationsPlugin(initializerContext);
|
||||
}
|
52
x-pack/plugins/notifications/server/mocks.ts
Normal file
52
x-pack/plugins/notifications/server/mocks.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { EmailService } from './services';
|
||||
import type { NotificationsPluginStart } from './types';
|
||||
import type { NotificationsPlugin } from './plugin';
|
||||
|
||||
const emailServiceMock: jest.Mocked<EmailService> = {
|
||||
sendPlainTextEmail: jest.fn(),
|
||||
};
|
||||
|
||||
const createEmailServiceMock = () => {
|
||||
return emailServiceMock;
|
||||
};
|
||||
|
||||
const startMock: jest.Mocked<NotificationsPluginStart> = {
|
||||
isEmailServiceAvailable: jest.fn(),
|
||||
getEmailService: jest.fn(createEmailServiceMock),
|
||||
};
|
||||
|
||||
const createStartMock = () => {
|
||||
return startMock;
|
||||
};
|
||||
|
||||
const notificationsPluginMock: jest.Mocked<PublicMethodsOf<NotificationsPlugin>> = {
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(createStartMock) as jest.Mock<NotificationsPluginStart>,
|
||||
stop: jest.fn(),
|
||||
};
|
||||
|
||||
const createNotificationsPluginMock = () => {
|
||||
return notificationsPluginMock;
|
||||
};
|
||||
|
||||
export const notificationsMock = {
|
||||
createNotificationsPlugin: createNotificationsPluginMock,
|
||||
createEmailService: createEmailServiceMock,
|
||||
createStart: createStartMock,
|
||||
clear: () => {
|
||||
emailServiceMock.sendPlainTextEmail.mockClear();
|
||||
startMock.getEmailService.mockClear();
|
||||
startMock.isEmailServiceAvailable.mockClear();
|
||||
notificationsPluginMock.setup.mockClear();
|
||||
notificationsPluginMock.start.mockClear();
|
||||
notificationsPluginMock.stop.mockClear();
|
||||
},
|
||||
};
|
107
x-pack/plugins/notifications/server/plugin.test.ts
Normal file
107
x-pack/plugins/notifications/server/plugin.test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { coreMock } from '@kbn/core/server/mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import type { NotificationsConfigType } from './config';
|
||||
import { NotificationsPlugin } from './plugin';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { EmailServiceProvider } from './services/connectors_email_service_provider';
|
||||
import { EmailServiceStart } from './services';
|
||||
|
||||
jest.mock('./services/connectors_email_service_provider');
|
||||
|
||||
const emailServiceProviderMock = EmailServiceProvider as jest.MockedClass<
|
||||
typeof EmailServiceProvider
|
||||
>;
|
||||
|
||||
const validConnectorConfig = {
|
||||
connectors: {
|
||||
default: {
|
||||
email: 'validConnectorId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createNotificationsPlugin = (config: NotificationsConfigType) => {
|
||||
const context = coreMock.createPluginInitializerContext<NotificationsConfigType>(config);
|
||||
const plugin = new NotificationsPlugin(context);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
const actionsSetup = actionsMock.createSetup();
|
||||
actionsSetup.isPreconfiguredConnector.mockImplementationOnce(
|
||||
(connectorId) => connectorId === 'validConnectorId'
|
||||
);
|
||||
const pluginSetup = {
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
};
|
||||
|
||||
const actionsStart = actionsMock.createStart();
|
||||
const pluginStart = {
|
||||
actions: actionsStart,
|
||||
licensing: licensingMock.createStart(),
|
||||
};
|
||||
|
||||
return {
|
||||
context,
|
||||
logger: context.logger.get(),
|
||||
plugin,
|
||||
coreSetup,
|
||||
coreStart,
|
||||
actionsSetup,
|
||||
pluginSetup,
|
||||
actionsStart,
|
||||
pluginStart,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Notifications Plugin', () => {
|
||||
beforeEach(() => emailServiceProviderMock.mockClear());
|
||||
|
||||
it('should create an EmailServiceProvider passing in the configuration and logger from the initializer context', () => {
|
||||
const { logger } = createNotificationsPlugin(validConnectorConfig);
|
||||
expect(emailServiceProviderMock).toHaveBeenCalledTimes(1);
|
||||
expect(emailServiceProviderMock).toHaveBeenCalledWith(validConnectorConfig, logger);
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
it('should call setup() on the created EmailServiceProvider, passing in the setup plugin dependencies', () => {
|
||||
const { plugin, coreSetup, pluginSetup } = createNotificationsPlugin(validConnectorConfig);
|
||||
plugin.setup(coreSetup, pluginSetup);
|
||||
expect(emailServiceProviderMock.mock.instances[0].setup).toHaveBeenCalledTimes(1);
|
||||
expect(emailServiceProviderMock.mock.instances[0].setup).toBeCalledWith(pluginSetup);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start()', () => {
|
||||
it('should call start() on the created EmailServiceProvider, passing in the setup plugin dependencies', () => {
|
||||
const { plugin, coreStart, pluginStart } = createNotificationsPlugin(validConnectorConfig);
|
||||
plugin.start(coreStart, pluginStart);
|
||||
expect(emailServiceProviderMock.mock.instances[0].start).toHaveBeenCalledTimes(1);
|
||||
expect(emailServiceProviderMock.mock.instances[0].start).toBeCalledWith(pluginStart);
|
||||
});
|
||||
|
||||
it('should return EmailServiceProvider.start() contract as part of its contract', () => {
|
||||
const { plugin, coreStart, pluginStart } = createNotificationsPlugin(validConnectorConfig);
|
||||
|
||||
const emailStart: EmailServiceStart = {
|
||||
getEmailService: jest.fn(),
|
||||
isEmailServiceAvailable: jest.fn(),
|
||||
};
|
||||
|
||||
const providerMock = emailServiceProviderMock.mock
|
||||
.instances[0] as jest.Mocked<EmailServiceProvider>;
|
||||
providerMock.start.mockReturnValue(emailStart);
|
||||
const start = plugin.start(coreStart, pluginStart);
|
||||
expect(emailServiceProviderMock.mock.instances[0].start).toHaveBeenCalledTimes(1);
|
||||
expect(emailServiceProviderMock.mock.instances[0].start).toBeCalledWith(pluginStart);
|
||||
expect(start).toEqual(expect.objectContaining(emailStart));
|
||||
});
|
||||
});
|
||||
});
|
40
x-pack/plugins/notifications/server/plugin.ts
Executable file
40
x-pack/plugins/notifications/server/plugin.ts
Executable file
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
|
||||
import type {
|
||||
NotificationsPluginSetupDeps,
|
||||
NotificationsPluginStartDeps,
|
||||
NotificationsPluginStart,
|
||||
} from './types';
|
||||
import type { NotificationsConfigType } from './config';
|
||||
import { EmailServiceProvider } from './services/connectors_email_service_provider';
|
||||
|
||||
export class NotificationsPlugin implements Plugin<void, NotificationsPluginStart> {
|
||||
private emailServiceProvider: EmailServiceProvider;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<NotificationsConfigType>) {
|
||||
this.emailServiceProvider = new EmailServiceProvider(
|
||||
initializerContext.config.get(),
|
||||
initializerContext.logger.get()
|
||||
);
|
||||
}
|
||||
|
||||
public setup(_core: CoreSetup, plugins: NotificationsPluginSetupDeps) {
|
||||
this.emailServiceProvider.setup(plugins);
|
||||
}
|
||||
|
||||
public start(_core: CoreStart, plugins: NotificationsPluginStartDeps) {
|
||||
const emailStartContract = this.emailServiceProvider.start(plugins);
|
||||
|
||||
return {
|
||||
...emailStartContract,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { unsecuredActionsClientMock } from '@kbn/actions-plugin/server/unsecured_actions_client/unsecured_actions_client.mock';
|
||||
import { ConnectorsEmailService } from './connectors_email_service';
|
||||
import type { PlainTextEmail } from './types';
|
||||
|
||||
const REQUESTER_ID = 'requesterId';
|
||||
const CONNECTOR_ID = 'connectorId';
|
||||
|
||||
describe('sendPlainTextEmail()', () => {
|
||||
describe('calls the provided ActionsClient#bulkEnqueueExecution() with the appropriate params', () => {
|
||||
it(`omits the 'relatedSavedObjects' field if no context is provided`, () => {
|
||||
const actionsClient = unsecuredActionsClientMock.create();
|
||||
const email = new ConnectorsEmailService(REQUESTER_ID, CONNECTOR_ID, actionsClient);
|
||||
const payload: PlainTextEmail = {
|
||||
to: ['user1@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
};
|
||||
|
||||
email.sendPlainTextEmail(payload);
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith(REQUESTER_ID, [
|
||||
{
|
||||
id: CONNECTOR_ID,
|
||||
params: {
|
||||
to: ['user1@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`populates the 'relatedSavedObjects' field if context is provided`, () => {
|
||||
const actionsClient = unsecuredActionsClientMock.create();
|
||||
const email = new ConnectorsEmailService(REQUESTER_ID, CONNECTOR_ID, actionsClient);
|
||||
const payload: PlainTextEmail = {
|
||||
to: ['user1@email.com', 'user2@email.com', 'user3@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
context: {
|
||||
relatedObjects: [
|
||||
{
|
||||
id: '9c9456a4-c160-46f5-96f7-e9ac734d0d9b',
|
||||
type: 'cases',
|
||||
namespace: 'space1',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
email.sendPlainTextEmail(payload);
|
||||
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith(REQUESTER_ID, [
|
||||
{
|
||||
id: CONNECTOR_ID,
|
||||
params: {
|
||||
to: ['user1@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
},
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: '9c9456a4-c160-46f5-96f7-e9ac734d0d9b',
|
||||
type: 'cases',
|
||||
namespace: 'space1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: CONNECTOR_ID,
|
||||
params: {
|
||||
to: ['user2@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
},
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: '9c9456a4-c160-46f5-96f7-e9ac734d0d9b',
|
||||
type: 'cases',
|
||||
namespace: 'space1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: CONNECTOR_ID,
|
||||
params: {
|
||||
to: ['user3@email.com'],
|
||||
subject: 'This is a notification email',
|
||||
message: 'With some contents inside.',
|
||||
},
|
||||
relatedSavedObjects: [
|
||||
{
|
||||
id: '9c9456a4-c160-46f5-96f7-e9ac734d0d9b',
|
||||
type: 'cases',
|
||||
namespace: 'space1',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
30
x-pack/plugins/notifications/server/services/connectors_email_service.ts
Executable file
30
x-pack/plugins/notifications/server/services/connectors_email_service.ts
Executable file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { IUnsecuredActionsClient } from '@kbn/actions-plugin/server';
|
||||
import type { EmailService, PlainTextEmail } from './types';
|
||||
|
||||
export class ConnectorsEmailService implements EmailService {
|
||||
constructor(
|
||||
private requesterId: string,
|
||||
private connectorId: string,
|
||||
private actionsClient: IUnsecuredActionsClient
|
||||
) {}
|
||||
|
||||
async sendPlainTextEmail(params: PlainTextEmail): Promise<void> {
|
||||
const actions = params.to.map((to) => ({
|
||||
id: this.connectorId,
|
||||
params: {
|
||||
to: [to],
|
||||
subject: params.subject,
|
||||
message: params.message,
|
||||
},
|
||||
relatedSavedObjects: params.context?.relatedObjects,
|
||||
}));
|
||||
return await this.actionsClient.bulkEnqueueExecution(this.requesterId, actions);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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 { loggerMock } from '@kbn/logging-mocks';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { LicensedEmailService } from './licensed_email_service';
|
||||
import { EmailServiceProvider } from './connectors_email_service_provider';
|
||||
import { ConnectorsEmailService } from './connectors_email_service';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
|
||||
jest.mock('./licensed_email_service');
|
||||
jest.mock('./connectors_email_service');
|
||||
|
||||
const licensedEmailServiceMock = LicensedEmailService as jest.MockedClass<
|
||||
typeof LicensedEmailService
|
||||
>;
|
||||
const connectorsEmailServiceMock = ConnectorsEmailService as jest.MockedClass<
|
||||
typeof ConnectorsEmailService
|
||||
>;
|
||||
|
||||
const missingConnectorConfig = {
|
||||
connectors: {
|
||||
default: {},
|
||||
},
|
||||
};
|
||||
|
||||
const invalidConnectorConfig = {
|
||||
connectors: {
|
||||
default: {
|
||||
email: 'someUnexistingConnectorId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validConnectorConfig = {
|
||||
connectors: {
|
||||
default: {
|
||||
email: 'validConnectorId',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('ConnectorsEmailServiceProvider', () => {
|
||||
const logger = loggerMock.create();
|
||||
const actionsSetup = actionsMock.createSetup();
|
||||
actionsSetup.isPreconfiguredConnector.mockImplementation(
|
||||
(connectorId) => connectorId === 'validConnectorId'
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
loggerMock.clear(logger);
|
||||
licensedEmailServiceMock.mockClear();
|
||||
connectorsEmailServiceMock.mockClear();
|
||||
});
|
||||
|
||||
it('implements the IEmailServiceProvider interface', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, loggerMock.create());
|
||||
expect(serviceProvider.setup).toBeInstanceOf(Function);
|
||||
expect(serviceProvider.start).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
describe('setup()', () => {
|
||||
it('should log a warning if Actions or Licensing plugins are not available', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, logger);
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Email Service Error: 'actions' and 'licensing' plugins are required.`
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(serviceProvider['setupSuccessful']).toEqual(false);
|
||||
});
|
||||
|
||||
it('should log a warning if no default email connector has been defined', () => {
|
||||
const serviceProvider = new EmailServiceProvider(missingConnectorConfig, logger);
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Email Service Error: Email connector not specified.`
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(serviceProvider['setupSuccessful']).toEqual(false);
|
||||
});
|
||||
|
||||
it('should log a warning if the specified email connector is not a preconfigured connector', () => {
|
||||
const serviceProvider = new EmailServiceProvider(invalidConnectorConfig, logger);
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Email Service Error: Unexisting email connector 'someUnexistingConnectorId' specified.`
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(serviceProvider['setupSuccessful']).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not log a warning if required plugins are present and the specified email connector is valid', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, logger);
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(serviceProvider['setupSuccessful']).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start()', () => {
|
||||
it('returns an object that implements the EmailServiceStart contract', () => {
|
||||
const serviceProvider = new EmailServiceProvider(missingConnectorConfig, logger);
|
||||
const start = serviceProvider.start({});
|
||||
expect(start.getEmailService).toBeInstanceOf(Function);
|
||||
expect(start.isEmailServiceAvailable).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
describe('if setup has not been run', () => {
|
||||
it('the start contract methods fail accordingly', () => {
|
||||
const serviceProvider = new EmailServiceProvider(missingConnectorConfig, logger);
|
||||
const start = serviceProvider.start({});
|
||||
expect(start.isEmailServiceAvailable()).toEqual(false);
|
||||
expect(() => {
|
||||
start.getEmailService();
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Email Service Error: setup() has not been run"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if setup() did not complete successfully', () => {
|
||||
it('the start contract methods fail accordingly', () => {
|
||||
const serviceProvider = new EmailServiceProvider(invalidConnectorConfig, logger);
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
const start = serviceProvider.start({
|
||||
actions: actionsMock.createStart(),
|
||||
licensing: licensingMock.createStart(),
|
||||
});
|
||||
expect(start.isEmailServiceAvailable()).toEqual(false);
|
||||
expect(() => {
|
||||
start.getEmailService();
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Email Service Error: Unexisting email connector 'someUnexistingConnectorId' specified."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if setup() did complete successfully and Action and Licensing plugin start contracts are available', () => {
|
||||
it('attempts to build an UnsecuredActionsClient', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, logger);
|
||||
const actionsStart = actionsMock.createStart();
|
||||
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
serviceProvider.start({
|
||||
actions: actionsStart,
|
||||
licensing: licensingMock.createStart(),
|
||||
});
|
||||
expect(actionsStart.getUnsecuredActionsClient).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('if getUnsecuredActionsClient() throws an Exception', () => {
|
||||
it('catches the exception, and the start contract methods fail accordingly', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, logger);
|
||||
const actionsStart = actionsMock.createStart();
|
||||
actionsStart.getUnsecuredActionsClient.mockImplementation(() => {
|
||||
throw new Error('Something went terribly wrong.');
|
||||
});
|
||||
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
const start = serviceProvider.start({
|
||||
actions: actionsStart,
|
||||
licensing: licensingMock.createStart(),
|
||||
});
|
||||
|
||||
expect(start.isEmailServiceAvailable()).toEqual(false);
|
||||
expect(() => {
|
||||
start.getEmailService();
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Email Service Error: Something went terribly wrong."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if getUnsecuredActionsClient() returns an UnsecuredActionsClient', () => {
|
||||
it('returns a start contract that provides valid EmailService', () => {
|
||||
const serviceProvider = new EmailServiceProvider(validConnectorConfig, logger);
|
||||
const licensingStart = licensingMock.createStart();
|
||||
const actionsStart = actionsMock.createStart();
|
||||
|
||||
serviceProvider.setup({
|
||||
actions: actionsSetup,
|
||||
licensing: licensingMock.createSetup(),
|
||||
});
|
||||
const start = serviceProvider.start({
|
||||
actions: actionsStart,
|
||||
licensing: licensingStart,
|
||||
});
|
||||
|
||||
expect(start.isEmailServiceAvailable()).toEqual(true);
|
||||
const email = start.getEmailService();
|
||||
expect(email).toBeInstanceOf(LicensedEmailService);
|
||||
expect(licensedEmailServiceMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(licensedEmailServiceMock).toHaveBeenCalledWith(
|
||||
connectorsEmailServiceMock.mock.instances[0],
|
||||
licensingStart.license$,
|
||||
'platinum',
|
||||
expect.objectContaining({ debug: expect.any(Function), warn: expect.any(Function) })
|
||||
);
|
||||
|
||||
expect(connectorsEmailServiceMock).toHaveBeenCalledTimes(1);
|
||||
expect(connectorsEmailServiceMock).toHaveBeenCalledWith(
|
||||
PLUGIN_ID,
|
||||
validConnectorConfig.connectors.default.email,
|
||||
actionsStart.getUnsecuredActionsClient()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/core/server';
|
||||
import { PluginSetupContract, PluginStartContract } from '@kbn/actions-plugin/server';
|
||||
import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||
import type { EmailService, EmailServiceStart, IEmailServiceProvider } from './types';
|
||||
import type { NotificationsConfigType } from '../config';
|
||||
import { LicensedEmailService } from './licensed_email_service';
|
||||
import { ConnectorsEmailService } from './connectors_email_service';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
|
||||
const MINIMUM_LICENSE = 'platinum';
|
||||
|
||||
export interface EmailServiceSetupDeps {
|
||||
actions?: PluginSetupContract;
|
||||
licensing?: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface EmailServiceStartDeps {
|
||||
actions?: PluginStartContract;
|
||||
licensing?: LicensingPluginStart;
|
||||
}
|
||||
|
||||
export class EmailServiceProvider
|
||||
implements IEmailServiceProvider<EmailServiceSetupDeps, EmailServiceStartDeps>
|
||||
{
|
||||
private setupSuccessful: boolean;
|
||||
private setupError: string;
|
||||
|
||||
constructor(private config: NotificationsConfigType, private logger: Logger) {
|
||||
this.setupSuccessful = false;
|
||||
this.setupError = 'Email Service Error: setup() has not been run';
|
||||
}
|
||||
|
||||
public setup(plugins: EmailServiceSetupDeps) {
|
||||
const { actions, licensing } = plugins;
|
||||
|
||||
if (!actions || !licensing) {
|
||||
return this._registerServiceError(`Error: 'actions' and 'licensing' plugins are required.`);
|
||||
}
|
||||
|
||||
const emailConnector = this.config.connectors?.default?.email;
|
||||
if (!emailConnector) {
|
||||
return this._registerServiceError('Error: Email connector not specified.');
|
||||
}
|
||||
|
||||
if (!actions.isPreconfiguredConnector(emailConnector)) {
|
||||
return this._registerServiceError(
|
||||
`Error: Unexisting email connector '${emailConnector}' specified.`
|
||||
);
|
||||
}
|
||||
|
||||
this.setupSuccessful = true;
|
||||
this.setupError = '';
|
||||
}
|
||||
|
||||
public start(plugins: EmailServiceStartDeps): EmailServiceStart {
|
||||
const { actions, licensing } = plugins;
|
||||
|
||||
let email: EmailService;
|
||||
if (this.setupSuccessful && actions && licensing) {
|
||||
const emailConnector = this.config.connectors!.default!.email!;
|
||||
|
||||
try {
|
||||
const unsecuredActionsClient = actions.getUnsecuredActionsClient();
|
||||
email = new LicensedEmailService(
|
||||
new ConnectorsEmailService(PLUGIN_ID, emailConnector, unsecuredActionsClient),
|
||||
licensing.license$,
|
||||
MINIMUM_LICENSE,
|
||||
this.logger
|
||||
);
|
||||
} catch (err) {
|
||||
this._registerServiceError(err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isEmailServiceAvailable: () => !!email,
|
||||
getEmailService: () => {
|
||||
if (!email) {
|
||||
throw new Error(this.setupError);
|
||||
}
|
||||
return email;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _registerServiceError(error: string) {
|
||||
const message = `Email Service ${error}`;
|
||||
this.setupError = message;
|
||||
this.logger.warn(message);
|
||||
}
|
||||
}
|
12
x-pack/plugins/notifications/server/services/index.ts
Normal file
12
x-pack/plugins/notifications/server/services/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 type { EmailService, EmailServiceStart, PlainTextEmail } from './types';
|
||||
export type {
|
||||
EmailServiceSetupDeps,
|
||||
EmailServiceStartDeps,
|
||||
} from './connectors_email_service_provider';
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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 { Subject } from 'rxjs';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { LicensedEmailService } from './licensed_email_service';
|
||||
import type { ILicense } from '@kbn/licensing-plugin/server';
|
||||
import type { EmailService, PlainTextEmail } from './types';
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const emailServiceMock: EmailService = {
|
||||
sendPlainTextEmail: jest.fn(),
|
||||
};
|
||||
|
||||
const validLicense = licensingMock.createLicenseMock();
|
||||
const invalidLicense = licensingMock.createLicenseMock();
|
||||
invalidLicense.type = 'basic';
|
||||
invalidLicense.check = jest.fn(() => ({
|
||||
state: 'invalid',
|
||||
message: 'This is an invalid testing license',
|
||||
})) as unknown as any;
|
||||
|
||||
const someEmail: PlainTextEmail = {
|
||||
to: ['user1@email.com'],
|
||||
subject: 'Some subject',
|
||||
message: 'Some message',
|
||||
};
|
||||
|
||||
describe('LicensedEmailService', () => {
|
||||
const logger = loggerMock.create();
|
||||
|
||||
beforeEach(() => loggerMock.clear(logger));
|
||||
it('observes license$ changes and logs info or warning messages accordingly', () => {
|
||||
const license$ = new Subject<ILicense>();
|
||||
new LicensedEmailService(emailServiceMock, license$, 'platinum', logger);
|
||||
license$.next(invalidLicense);
|
||||
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledWith('This is an invalid testing license');
|
||||
|
||||
license$.next(validLicense);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(logger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'Your current license allows sending email notifications'
|
||||
);
|
||||
});
|
||||
|
||||
describe('sendPlainTextEmail()', () => {
|
||||
it('does not call the underlying email service until the license is determined and valid', async () => {
|
||||
const license$ = new Subject<ILicense>();
|
||||
const email = new LicensedEmailService(emailServiceMock, license$, 'platinum', logger);
|
||||
|
||||
email.sendPlainTextEmail(someEmail);
|
||||
expect(emailServiceMock.sendPlainTextEmail).not.toHaveBeenCalled();
|
||||
license$.next(validLicense);
|
||||
|
||||
await delay(1);
|
||||
|
||||
expect(emailServiceMock.sendPlainTextEmail).toHaveBeenCalledTimes(1);
|
||||
expect(emailServiceMock.sendPlainTextEmail).toHaveBeenCalledWith(someEmail);
|
||||
});
|
||||
|
||||
it('does not call the underlying email service if the license is invalid', async () => {
|
||||
const license$ = new Subject<ILicense>();
|
||||
const email = new LicensedEmailService(emailServiceMock, license$, 'platinum', logger);
|
||||
license$.next(invalidLicense);
|
||||
|
||||
try {
|
||||
await email.sendPlainTextEmail(someEmail);
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
'The current license does not allow sending email notifications'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
expect('it should have thrown').toEqual('but it did not');
|
||||
});
|
||||
|
||||
it('does not log a warning for every email attempt, but rather for every license change', async () => {
|
||||
const license$ = new Subject<ILicense>();
|
||||
const email = new LicensedEmailService(emailServiceMock, license$, 'platinum', logger);
|
||||
license$.next(invalidLicense);
|
||||
license$.next(validLicense);
|
||||
license$.next(invalidLicense);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(2);
|
||||
|
||||
let emailsOk = 0;
|
||||
let emailsKo = 0;
|
||||
const silentSend = async () => {
|
||||
try {
|
||||
await email.sendPlainTextEmail(someEmail);
|
||||
emailsOk++;
|
||||
} catch (err) {
|
||||
emailsKo++;
|
||||
}
|
||||
};
|
||||
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
license$.next(validLicense);
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
await silentSend();
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledTimes(2);
|
||||
expect(logger.warn).toHaveBeenCalledTimes(2);
|
||||
expect(emailsKo).toEqual(4);
|
||||
expect(emailsOk).toEqual(4);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/logging';
|
||||
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/server';
|
||||
import { firstValueFrom, map, type Observable, ReplaySubject, type Subject } from 'rxjs';
|
||||
import type { EmailService, PlainTextEmail } from './types';
|
||||
import { PLUGIN_ID } from '../../common';
|
||||
|
||||
export class LicensedEmailService implements EmailService {
|
||||
private validLicense$: Subject<boolean> = new ReplaySubject(1);
|
||||
|
||||
constructor(
|
||||
private emailService: EmailService,
|
||||
license$: Observable<ILicense>,
|
||||
private minimumLicense: LicenseType,
|
||||
private logger: Logger
|
||||
) {
|
||||
// no need to explicitly unsubscribe as the license$ observable already completes on stop()
|
||||
license$.pipe(map((license) => this.checkValidLicense(license))).subscribe(this.validLicense$);
|
||||
}
|
||||
|
||||
async sendPlainTextEmail(payload: PlainTextEmail): Promise<void> {
|
||||
if (await firstValueFrom(this.validLicense$, { defaultValue: false })) {
|
||||
await this.emailService.sendPlainTextEmail(payload);
|
||||
} else {
|
||||
throw new Error('The current license does not allow sending email notifications');
|
||||
}
|
||||
}
|
||||
|
||||
private checkValidLicense(license: ILicense): boolean {
|
||||
const licenseCheck = license.check(PLUGIN_ID, this.minimumLicense);
|
||||
|
||||
if (licenseCheck.state === 'valid') {
|
||||
this.logger.debug('Your current license allows sending email notifications');
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
licenseCheck.message || 'The current license does not allow sending email notifications'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
35
x-pack/plugins/notifications/server/services/types.ts
Executable file
35
x-pack/plugins/notifications/server/services/types.ts
Executable file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 interface EmailService {
|
||||
sendPlainTextEmail(payload: PlainTextEmail): Promise<void>;
|
||||
}
|
||||
|
||||
export interface EmailServiceStart {
|
||||
isEmailServiceAvailable(): boolean;
|
||||
getEmailService(): EmailService;
|
||||
}
|
||||
|
||||
export interface IEmailServiceProvider<T, U> {
|
||||
setup(setupDeps: T): void;
|
||||
start(startDeps: U): EmailServiceStart;
|
||||
}
|
||||
|
||||
export interface RelatedSavedObject {
|
||||
id: string;
|
||||
type: string;
|
||||
namespace?: string; // namespace is undefined for the spaceId 'default'
|
||||
}
|
||||
|
||||
export interface PlainTextEmail {
|
||||
to: string[];
|
||||
subject: string;
|
||||
message: string;
|
||||
context?: {
|
||||
relatedObjects?: RelatedSavedObject[];
|
||||
};
|
||||
}
|
14
x-pack/plugins/notifications/server/types.ts
Executable file
14
x-pack/plugins/notifications/server/types.ts
Executable file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { EmailServiceStart, EmailServiceSetupDeps, EmailServiceStartDeps } from './services';
|
||||
|
||||
// The 'notifications' plugin is currently only exposing an email service.
|
||||
// If we want to expose other services in the future, we should update these types accordingly
|
||||
export type NotificationsPluginSetupDeps = EmailServiceSetupDeps;
|
||||
export type NotificationsPluginStartDeps = EmailServiceStartDeps;
|
||||
export type NotificationsPluginStart = EmailServiceStart;
|
21
x-pack/plugins/notifications/tsconfig.json
Normal file
21
x-pack/plugins/notifications/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types",
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"server/**/*",
|
||||
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
|
||||
"server/**/*.json",
|
||||
"public/**/*",
|
||||
"common/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../actions/tsconfig.json" },
|
||||
{ "path": "../licensing/tsconfig.json" }
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue