[Response Ops][Connectors] New xpack.actions.email.services.enabled Kibana setting (#223363)

## Summary

Closes #220288

## Release note
New kibana setting `xpack.actions.email.services.enabled` to
enable/disable email services for email connector.
This commit is contained in:
Julian Gernun 2025-06-20 16:10:43 +02:00 committed by GitHub
parent 028660e4e1
commit 411ab215a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 608 additions and 63 deletions

View file

@ -198,7 +198,8 @@ enabled:
- x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/config.ts
- x-pack/test/functional_with_es_ssl/apps/embeddable_alerts_table/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_aws_ses_kibana_config/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_email_aws_ses_kbn_config/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/with_email_services_enabled_kbn_config/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/shared/config.ts
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors/webhook_disabled_ssl_pfx/config.ts
- x-pack/test/functional/apps/advanced_settings/config.ts

View file

@ -150,6 +150,12 @@ $$$action-config-email-domain-allowlist$$$
Data type: `int`
Default: `465`
`xpack.actions.email.services.enabled` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")
: An array of strings indicating all email services that are enabled. Available options are `elastic-cloud`, `google-mail`, `microsoft-outlook`, `amazon-ses`, `microsoft-exchange`, and `other`. If the array is empty, no email services are enabled. The default value is `["*"]`, which enables all email services.
Data type: `string`
Default: `["*"]`
`xpack.actions.enableFooterInEmail` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")
: A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions.

View file

@ -326,6 +326,28 @@ groups:
ess: all
# example: |
- setting: xpack.actions.email.services.enabled
id: action-config-email-services-
description: |
An array of strings indicating all email services that are enabled. Available options are `elastic-cloud`, `google-mail`, `microsoft-outlook`, `amazon-ses`, `microsoft-exchange`, and `other`. If the array is empty, no email services are enabled. The default value is `["*"]`, which enables all email services.
# state: deprecated/hidden/tech-preview
# deprecation_details: ""
# note: ""
# tip: ""
# warning: ""
# important: ""
datatype: enum
default: ["*"]
# options:
# - option:
# description: ""
# type: static/dynamic
applies_to:
deployment:
self: all
ess: all
# example: |
- setting: xpack.actions.enableFooterInEmail
# id:
description: |

View file

@ -214,6 +214,7 @@ kibana_vars=(
xpack.actions.email.domain_allowlist
xpack.actions.email.services.ses.host
xpack.actions.email.services.ses.port
xpack.actions.email.services.enabled
xpack.actions.enableFooterInEmail
xpack.actions.enabledActionTypes
xpack.actions.maxResponseContentLength

View file

@ -203,6 +203,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'vis_type_xy.readOnly (boolean?|never)',
'vis_type_vega.enableExternalUrls (boolean?)',
'xpack.actions.email.domain_allowlist (array?)',
'xpack.actions.email.services.enabled (array?)',
'xpack.actions.webhook.ssl.pfx.enabled (boolean?)',
'xpack.apm.serviceMapEnabled (boolean?)',
'xpack.apm.ui.enabled (boolean?)',

View file

@ -14,12 +14,16 @@ export interface ActionsPublicPluginSetup {
emails: string[],
options?: ValidateEmailAddressesOptions
): ValidatedEmail[];
enabledEmailServices: string[];
isWebhookSslWithPfxEnabled?: boolean;
}
export interface Config {
email: {
domain_allowlist: string[];
services: {
enabled: string[];
};
};
webhook: {
ssl: {
@ -32,11 +36,13 @@ export interface Config {
export class Plugin implements CorePlugin<ActionsPublicPluginSetup> {
private readonly allowedEmailDomains: string[] | null = null;
private readonly enabledEmailServices: string[];
private readonly webhookSslWithPfxEnabled: boolean;
constructor(ctx: PluginInitializerContext<Config>) {
const config = ctx.config.get();
this.allowedEmailDomains = config.email?.domain_allowlist || null;
this.enabledEmailServices = Array.from(new Set(config.email?.services?.enabled || ['*']));
this.webhookSslWithPfxEnabled = config.webhook?.ssl.pfx.enabled ?? true;
}
@ -44,6 +50,7 @@ export class Plugin implements CorePlugin<ActionsPublicPluginSetup> {
return {
validateEmailAddresses: (emails: string[], options: ValidateEmailAddressesOptions) =>
validateEmails(this.allowedEmailDomains, emails, options),
enabledEmailServices: this.enabledEmailServices,
isWebhookSslWithPfxEnabled: this.webhookSslWithPfxEnabled,
};
}

View file

@ -44,6 +44,7 @@ const createActionsConfigMock = () => {
},
}),
getAwsSesConfig: jest.fn().mockReturnValue(null),
getEnabledEmailServices: jest.fn().mockReturnValue(['*']),
};
return mocked;
};

View file

@ -633,6 +633,14 @@ describe('getAwsSesConfig()', () => {
expect(acu.getAwsSesConfig()).toEqual(null);
});
test('returns null when no email.services.ses config set', () => {
const acu = getActionsConfigurationUtilities({
...defaultActionsConfig,
email: { services: {} },
});
expect(acu.getAwsSesConfig()).toEqual(null);
});
test('returns config if set', () => {
const acu = getActionsConfigurationUtilities({
...defaultActionsConfig,
@ -652,3 +660,47 @@ describe('getAwsSesConfig()', () => {
});
});
});
describe('getEnabledEmailServices()', () => {
test('returns all services when no email config set', () => {
const acu = getActionsConfigurationUtilities(defaultActionsConfig);
expect(acu.getEnabledEmailServices()).toEqual(['*']);
});
test('returns all services when no email.services config set', () => {
const acu = getActionsConfigurationUtilities({ ...defaultActionsConfig, email: {} });
expect(acu.getEnabledEmailServices()).toEqual(['*']);
});
test('returns all services when no email.services.enabled config set', () => {
const acu = getActionsConfigurationUtilities({
...defaultActionsConfig,
email: { services: {} },
});
expect(acu.getEnabledEmailServices()).toEqual(['*']);
});
test('returns only enabled services', () => {
const acu = getActionsConfigurationUtilities({
...defaultActionsConfig,
email: {
services: {
enabled: ['google-mail', 'microsoft-exchange'],
},
},
});
expect(acu.getEnabledEmailServices()).toEqual(['google-mail', 'microsoft-exchange']);
});
test('returns all services when enabled is set to "*" in config', () => {
const acu = getActionsConfigurationUtilities({
...defaultActionsConfig,
email: {
services: {
enabled: ['*'],
},
},
});
expect(acu.getEnabledEmailServices()).toEqual(['*']);
});
});

View file

@ -63,6 +63,7 @@ export interface ActionsConfigurationUtilities {
};
};
getAwsSesConfig: () => AwsSesConfig;
getEnabledEmailServices: () => string[];
}
function allowListErrorMessage(field: AllowListingField, value: string) {
@ -243,15 +244,23 @@ export function getActionsConfigurationUtilities(
};
},
getAwsSesConfig: () => {
if (config.email?.services?.ses.host && config.email?.services?.ses.port) {
if (config.email?.services?.ses?.host && config.email?.services?.ses?.port) {
return {
host: config.email?.services?.ses.host,
port: config.email?.services?.ses.port,
host: config.email?.services?.ses?.host,
port: config.email?.services?.ses?.port,
secure: true,
};
}
return null;
},
getEnabledEmailServices() {
const emailServices = config.email?.services?.enabled;
if (emailServices) {
return Array.from(new Set(Array.from(emailServices)));
}
return ['*'];
},
};
}

View file

@ -238,7 +238,7 @@ describe('config validation', () => {
config.email = {};
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"`
`"[email]: email.domain_allowlist or email.services must be defined"`
);
config.email = { domain_allowlist: [] };
@ -285,35 +285,35 @@ describe('config validation', () => {
test('validates empty email config', () => {
config.email = {};
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"`
`"[email]: email.domain_allowlist or email.services must be defined"`
);
});
test('validates email config with empty services', () => {
config.email = { services: {} };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"`
`"[email.services]: email.services.enabled or email.services.ses must be defined"`
);
});
test('validates email config with empty ses service', () => {
config.email = { services: { ses: {} } };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires either domain_allowlist or services.ses to be specified"`
`"[email.services.ses.host]: expected value of type [string] but got [undefined]"`
);
});
test('validates ses config with host only', () => {
config.email = { services: { ses: { host: 'ses.host.com' } } };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires both services.ses.host and services.ses.port to be specified"`
`"[email.services.ses.port]: expected value of type [number] but got [undefined]"`
);
});
test('validates ses config with port only', () => {
config.email = { services: { ses: { port: 1 } } };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email]: Email configuration requires both services.ses.host and services.ses.port to be specified"`
`"[email.services.ses.host]: expected value of type [string] but got [undefined]"`
);
});
@ -323,6 +323,43 @@ describe('config validation', () => {
expect(result.email?.services?.ses).toEqual({ host: 'ses.host.com', port: 1 });
});
});
describe('email.services.enabled', () => {
const config: Record<string, unknown> = {};
test('validates email config with empty enabled services', () => {
config.email = { services: { enabled: [] } };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(
`"[email.services.enabled]: array size is [0], but cannot be smaller than [1]"`
);
});
test('validates email config with enabled services', () => {
config.email = { services: { enabled: ['elastic-cloud', 'amazon-ses'] } };
const result = configSchema.validate(config);
expect(result.email?.services?.enabled).toEqual(['elastic-cloud', 'amazon-ses']);
});
test('validates email config with unexistend service', () => {
config.email = { services: { enabled: ['fake-service'] } };
expect(() => configSchema.validate(config)).toThrowErrorMatchingInlineSnapshot(`
"[email.services.enabled.0]: types that failed validation:
- [email.services.enabled.0.0]: expected value to equal [google-mail]
- [email.services.enabled.0.1]: expected value to equal [microsoft-exchange]
- [email.services.enabled.0.2]: expected value to equal [microsoft-outlook]
- [email.services.enabled.0.3]: expected value to equal [amazon-ses]
- [email.services.enabled.0.4]: expected value to equal [elastic-cloud]
- [email.services.enabled.0.5]: expected value to equal [other]
- [email.services.enabled.0.6]: expected value to equal [*]"
`);
});
test('validates enabled services but no ses service', () => {
config.email = { services: { enabled: ['google-mail', 'amazon-ses'] } };
const result = configSchema.validate(config);
expect(result.email?.services?.enabled).toEqual(['google-mail', 'amazon-ses']);
expect(result.email?.services?.ses).toBeUndefined();
});
});
});
// object creator that ensures we can create a property named __proto__ on an

View file

@ -125,22 +125,43 @@ export const configSchema = schema.object({
{
domain_allowlist: schema.maybe(schema.arrayOf(schema.string())),
services: schema.maybe(
schema.object({
ses: schema.object({
host: schema.maybe(schema.string({ minLength: 1 })),
port: schema.maybe(schema.number({ min: 1, max: 65535 })),
}),
})
schema.object(
{
enabled: schema.maybe(
schema.arrayOf(
schema.oneOf([
schema.literal('google-mail'),
schema.literal('microsoft-exchange'),
schema.literal('microsoft-outlook'),
schema.literal('amazon-ses'),
schema.literal('elastic-cloud'),
schema.literal('other'),
schema.literal('*'),
]),
{ minSize: 1 }
)
),
ses: schema.maybe(
schema.object({
host: schema.string({ minLength: 1 }),
port: schema.number({ min: 1, max: 65535 }),
})
),
},
{
validate: (obj) => {
if (obj && Object.keys(obj).length === 0) {
return 'email.services.enabled or email.services.ses must be defined';
}
},
}
)
),
},
{
validate: (obj) => {
if (!obj.domain_allowlist && !obj.services?.ses.host && !obj.services?.ses.port) {
return 'Email configuration requires either domain_allowlist or services.ses to be specified';
}
if (obj.services?.ses && (!obj.services.ses.host || !obj.services.ses.port)) {
return 'Email configuration requires both services.ses.host and services.ses.port to be specified';
if (obj && Object.keys(obj).length === 0) {
return 'email.domain_allowlist or email.services must be defined';
}
},
}

View file

@ -50,7 +50,7 @@ export type { ServiceParams } from './sub_action_framework/types';
export const config: PluginConfigDescriptor<ActionsConfig> = {
schema: configSchema,
exposeToBrowser: {
email: { domain_allowlist: true },
email: { domain_allowlist: true, services: { enabled: true } },
webhook: { ssl: { pfx: { enabled: true } } },
},
};

View 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.
*/
export const serviceParamValueToKbnSettingMap = {
gmail: 'google-mail',
outlook365: 'microsoft-outlook',
ses: 'amazon-ses',
elastic_cloud: 'elastic-cloud',
exchange_server: 'microsoft-exchange',
other: 'other',
} as const;

View file

@ -8,7 +8,7 @@
import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
import { registerConnectorTypes } from '..';
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
import { getEmailServices } from './email';
import { emailServices, getEmailServices } from './email';
import {
ValidatedEmail,
InvalidEmailReason,
@ -17,6 +17,7 @@ import {
} from '@kbn/actions-plugin/common';
import { experimentalFeaturesMock } from '../../mocks';
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
import { serviceParamValueToKbnSettingMap } from '../../../common/email/constants';
const CONNECTOR_TYPE_ID = '.email';
let connectorTypeModel: ConnectorTypeModel;
@ -65,14 +66,65 @@ describe('connectorTypeRegistry.get() works', () => {
describe('getEmailServices', () => {
test('should return elastic cloud service if isCloudEnabled is true', () => {
const services = getEmailServices(true);
const services = getEmailServices(true, ['*']);
expect(services.find((service) => service.value === 'elastic_cloud')).toBeTruthy();
});
test('should not return elastic cloud service if isCloudEnabled is false', () => {
const services = getEmailServices(false);
const services = getEmailServices(false, ['*']);
expect(services.find((service) => service.value === 'elastic_cloud')).toBeFalsy();
});
test('should return all services if enabledEmailsServices is *', () => {
const services = getEmailServices(true, ['*']);
expect(services).toEqual(emailServices);
});
test('should return only specified services if enabledEmailsServices is not empty', () => {
const services = getEmailServices(true, [
serviceParamValueToKbnSettingMap.gmail,
serviceParamValueToKbnSettingMap.outlook365,
]);
expect(services).toEqual([
{
['kbn-setting-value']: 'google-mail',
text: 'Gmail',
value: 'gmail',
},
{
['kbn-setting-value']: 'microsoft-outlook',
text: 'Outlook',
value: 'outlook365',
},
]);
});
test('should return enabled services and the current service if specified', () => {
const services = getEmailServices(
true,
[serviceParamValueToKbnSettingMap.gmail, serviceParamValueToKbnSettingMap.outlook365],
serviceParamValueToKbnSettingMap.other
);
expect(services).toEqual([
{
['kbn-setting-value']: 'google-mail',
text: 'Gmail',
value: 'gmail',
},
{
['kbn-setting-value']: 'microsoft-outlook',
text: 'Outlook',
value: 'outlook365',
},
{
['kbn-setting-value']: 'other',
text: 'Other',
value: 'other',
},
]);
});
});
describe('action params validation', () => {

View file

@ -16,50 +16,76 @@ import type {
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { EmailActionParams, EmailConfig, EmailSecrets } from '../types';
import { RegistrationServices } from '..';
import { serviceParamValueToKbnSettingMap as emailKbnSettings } from '../../../common/email/constants';
const emailServices: EuiSelectOption[] = [
export const emailServices: Array<EuiSelectOption & { 'kbn-setting-value': string }> = [
{
text: i18n.translate('xpack.stackConnectors.components.email.gmailServerTypeLabel', {
defaultMessage: 'Gmail',
}),
value: 'gmail',
['kbn-setting-value']: emailKbnSettings.gmail,
},
{
text: i18n.translate('xpack.stackConnectors.components.email.outlookServerTypeLabel', {
defaultMessage: 'Outlook',
}),
value: 'outlook365',
['kbn-setting-value']: emailKbnSettings.outlook365,
},
{
text: i18n.translate('xpack.stackConnectors.components.email.amazonSesServerTypeLabel', {
defaultMessage: 'Amazon SES',
}),
value: 'ses',
['kbn-setting-value']: emailKbnSettings.ses,
},
{
text: i18n.translate('xpack.stackConnectors.components.email.elasticCloudServerTypeLabel', {
defaultMessage: 'Elastic Cloud',
}),
value: 'elastic_cloud',
['kbn-setting-value']: emailKbnSettings.elastic_cloud,
},
{
text: i18n.translate('xpack.stackConnectors.components.email.exchangeServerTypeLabel', {
defaultMessage: 'MS Exchange Server',
}),
value: 'exchange_server',
['kbn-setting-value']: emailKbnSettings.exchange_server,
},
{
text: i18n.translate('xpack.stackConnectors.components.email.otherServerTypeLabel', {
defaultMessage: 'Other',
}),
value: 'other',
['kbn-setting-value']: emailKbnSettings.other,
},
];
export function getEmailServices(isCloudEnabled: boolean) {
return isCloudEnabled
// Return the current service regardless of its enabled state to allow users to:
// 1. View the current service in the dropdown UI
// 2. Update the service configuration if needed
// Note: The connector update endpoint will reject updates where the service
// remains unchanged but is disabled.
export function getEmailServices(
isCloudEnabled: boolean,
enabledEmailsServices: string[],
currentService?: string
): Array<EuiSelectOption & { 'kbn-setting-value': string }> {
const allEmailServices = isCloudEnabled
? emailServices
: emailServices.filter((service) => service.value !== 'elastic_cloud');
if (enabledEmailsServices.includes('*')) {
return allEmailServices;
}
return allEmailServices.filter(
(service) =>
service.value === currentService ||
enabledEmailsServices.includes(service['kbn-setting-value'])
);
}
export function getConnectorType(

View file

@ -7,22 +7,34 @@
import React, { Suspense } from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { act } from '@testing-library/react';
import { act, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
import EmailActionConnectorFields from './email_connector';
import * as hooks from './use_email_config';
import {
AppMockRenderer,
ConnectorFormTestProvider,
createAppMockRenderer,
waitForComponentToUpdate,
} from '../lib/test_utils';
import { getServiceConfig } from './api';
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('./api', () => {
return {
getServiceConfig: jest.fn(),
};
});
describe('EmailActionConnectorFields', () => {
const enabledEmailServices = ['*'];
afterEach(() => {
jest.clearAllMocks();
});
test('all connector fields are rendered', async () => {
const actionConnector = {
secrets: {
@ -170,12 +182,11 @@ describe('EmailActionConnectorFields', () => {
});
test('host, port and secure fields should be disabled when service field is set to well known service', async () => {
const getEmailServiceConfig = jest
.fn()
.mockResolvedValue({ host: 'https://example.com', port: 80, secure: false });
jest
.spyOn(hooks, 'useEmailConfig')
.mockImplementation(() => ({ isLoading: false, getEmailServiceConfig }));
(getServiceConfig as jest.Mock).mockResolvedValue({
host: 'https://example.com',
port: 80,
secure: false,
});
const actionConnector = {
secrets: {
@ -214,12 +225,11 @@ describe('EmailActionConnectorFields', () => {
});
test('host, port and secure fields should not be disabled when service field is set to other', async () => {
const getEmailServiceConfig = jest
.fn()
.mockResolvedValue({ host: 'https://example.com', port: 80, secure: false });
jest
.spyOn(hooks, 'useEmailConfig')
.mockImplementation(() => ({ isLoading: false, getEmailServiceConfig }));
(getServiceConfig as jest.Mock).mockResolvedValue({
host: 'https://example.com',
port: 80,
secure: false,
});
const actionConnector = {
secrets: {
@ -292,7 +302,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -356,7 +366,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -415,7 +425,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -463,7 +473,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -509,7 +519,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -554,7 +564,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<Suspense fallback={null}>
<EmailActionConnectorFields
@ -603,7 +613,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -649,7 +659,7 @@ describe('EmailActionConnectorFields', () => {
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses }}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
@ -689,3 +699,102 @@ describe('EmailActionConnectorFields', () => {
});
});
});
describe('when not all email services are enabled', () => {
const enabledEmailServices = ['amazon-ses', 'other', 'microsoft-exchange'];
let appMockRenderer: AppMockRenderer;
const onSubmit = jest.fn();
const validateEmailAddresses = jest.fn();
beforeEach(() => {
appMockRenderer = createAppMockRenderer();
validateEmailAddresses.mockReturnValue([{ valid: true }]);
(getServiceConfig as jest.Mock).mockResolvedValue({
host: 'https://example.com',
port: 2255,
secure: true,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('only allows enabled services to be selected only', async () => {
const actionConnector = {
secrets: {
user: 'user',
password: 'pass',
},
id: 'test',
actionTypeId: '.email',
name: 'email',
config: {},
isDeprecated: false,
};
appMockRenderer.render(
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
const emailServiceSelect = screen.getByTestId('emailServiceSelectInput') as HTMLSelectElement;
const options = within(emailServiceSelect).getAllByRole('option');
expect(options).toHaveLength(3);
expect(options[0].textContent).toBe('Amazon SES');
expect(options[1].textContent).toBe('MS Exchange Server');
expect(options[2].textContent).toBe('Other');
});
it('adds the current connector service to the service list even if not enabled', async () => {
const actionConnector = {
secrets: {
user: 'user',
password: 'pass',
},
id: 'test',
actionTypeId: '.email',
name: 'email',
config: {
from: 'test@test.com',
test: 'test',
service: 'gmail', // not enabled
secure: true,
},
isDeprecated: false,
};
appMockRenderer.render(
<ConnectorFormTestProvider
connector={actionConnector}
onSubmit={onSubmit}
connectorServices={{ validateEmailAddresses, enabledEmailServices }}
>
<EmailActionConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
const emailServiceSelect = screen.getByTestId('emailServiceSelectInput') as HTMLSelectElement;
const options = within(emailServiceSelect).getAllByRole('option');
expect(options).toHaveLength(4);
expect(options[0].textContent).toBe('Gmail');
expect(options[1].textContent).toBe('Amazon SES');
expect(options[2].textContent).toBe('MS Exchange Server');
expect(options[3].textContent).toBe('Other');
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { lazy, useEffect, useMemo } from 'react';
import React, { lazy, useEffect, useMemo, useRef } from 'react';
import { isEmpty } from 'lodash';
import { EuiFlexItem, EuiFlexGroup, EuiTitle, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
@ -102,7 +102,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<ActionConnector
notifications: { toasts },
} = useKibana().services;
const {
services: { validateEmailAddresses },
services: { validateEmailAddresses, enabledEmailServices },
} = useConnectorContext();
const form = useFormContext();
@ -119,6 +119,15 @@ export const EmailActionConnectorFields: React.FunctionComponent<ActionConnector
const { service = null, hasAuth = false } = config ?? {};
const disableServiceConfig = shouldDisableEmailConfiguration(service);
const { isLoading, getEmailServiceConfig } = useEmailConfig({ http, toasts });
const initialService = useRef(service);
if (!initialService.current && service) {
initialService.current = service;
}
const availableEmailServices = getEmailServices(
isCloud,
enabledEmailServices,
initialService.current
);
useEffect(() => {
async function fetchConfig() {
@ -173,7 +182,7 @@ export const EmailActionConnectorFields: React.FunctionComponent<ActionConnector
componentProps={{
euiFieldProps: {
'data-test-subj': 'emailServiceSelectInput',
options: getEmailServices(isCloud),
options: availableEmailServices,
fullWidth: true,
hasNoInitialSelection: true,
disabled: readOnly || isLoading,

View file

@ -74,6 +74,7 @@ const FormTestProviderComponent: React.FC<FormTestProviderProps> = ({
connectorServices = {
validateEmailAddresses: jest.fn(),
isWebhookSslWithPfxEnabled: true,
enabledEmailServices: ['*'],
},
}) => {
const { form } = useForm({ defaultValue });

View file

@ -34,6 +34,7 @@ import { getConnectorType } from '.';
import type { ValidateEmailAddressesOptions } from '@kbn/actions-plugin/common';
import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types';
import { AdditionalEmailServices } from '../../../common';
import { serviceParamValueToKbnSettingMap } from '../../../common/email/constants';
const sendEmailMock = sendEmail as jest.Mock;
@ -503,6 +504,75 @@ describe('params validation', () => {
);
}).not.toThrowError();
});
test('error when using a service that is not enabled', async () => {
const configUtils = actionsConfigMock.create();
configUtils.getEnabledEmailServices = jest
.fn()
.mockReturnValue([
serviceParamValueToKbnSettingMap.gmail,
serviceParamValueToKbnSettingMap.elastic_cloud,
]);
expect(() =>
validateConfig(
connectorType,
{
service: 'other',
from: 'bob@example.com',
host: 'wrong-host',
port: 123,
secure: true,
hasAuth: true,
},
{ configurationUtilities: configUtils }
)
).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [service]: \\"other\\" is not in the list of enabled email services: google-mail,elastic-cloud"`
);
});
test('no error using enabled services = *', async () => {
const configUtils = actionsConfigMock.create();
configUtils.getEnabledEmailServices = jest.fn().mockReturnValue(['*']);
expect(() =>
validateConfig(
connectorType,
{
service: 'other',
from: 'bob@example.com',
host: 'wrong-host',
port: 123,
secure: true,
hasAuth: true,
},
{ configurationUtilities: configUtils }
)
).not.toThrowError();
});
test('does not throw when fetching service enabled in config', () => {
const configUtils = actionsConfigMock.create();
configUtils.getEnabledEmailServices = jest
.fn()
.mockReturnValue([serviceParamValueToKbnSettingMap.elastic_cloud]);
expect(() =>
validateConfig(
connectorType,
{
service: 'elastic_cloud',
from: 'bob@example.com',
host: 'dockerhost',
port: 10025,
secure: false,
hasAuth: false,
},
{ configurationUtilities: configUtils }
)
).not.toThrowError();
});
});
describe('execute()', () => {

View file

@ -34,6 +34,7 @@ import { AdditionalEmailServices } from '../../../common';
import type { SendEmailOptions, Transport } from './send_email';
import { sendEmail, JSON_TRANSPORT_SERVICE } from './send_email';
import { portSchema } from '../lib/schemas';
import { serviceParamValueToKbnSettingMap as emailKbnSettings } from '../../../common/email/constants';
export type EmailConnectorType = ConnectorType<
ConnectorTypeConfigType,
@ -82,6 +83,20 @@ function validateConfig(
const config = configObject;
const { configurationUtilities } = validatorServices;
const awsSesConfig = configurationUtilities.getAwsSesConfig();
const enabledServices = configurationUtilities.getEnabledEmailServices();
const serviceKey = config.service as keyof typeof emailKbnSettings;
if (
!enabledServices.includes('*') &&
config.service in emailKbnSettings &&
!enabledServices.includes(emailKbnSettings[serviceKey])
) {
throw new Error(
`[service]: "${
emailKbnSettings[serviceKey]
}" is not in the list of enabled email services: ${enabledServices.join(',')}`
);
}
const emails = [config.from];
const invalidEmailsMessage = configurationUtilities.validateEmailAddresses(emails);

View file

@ -24,7 +24,7 @@ const FormTestProviderComponent: React.FC<FormTestProviderProps> = ({
children,
defaultValue,
onSubmit,
connectorServices = { validateEmailAddresses: jest.fn() },
connectorServices = { validateEmailAddresses: jest.fn(), enabledEmailServices: ['*'] },
}) => {
const { form } = useForm({ defaultValue });
const { submit } = form;

View file

@ -90,11 +90,15 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
const {
actions: { validateEmailAddresses, isWebhookSslWithPfxEnabled },
actions: { validateEmailAddresses, enabledEmailServices, isWebhookSslWithPfxEnabled },
} = useKibana().services;
return (
<ConnectorProvider value={{ services: { validateEmailAddresses, isWebhookSslWithPfxEnabled } }}>
<ConnectorProvider
value={{
services: { validateEmailAddresses, enabledEmailServices, isWebhookSslWithPfxEnabled },
}}
>
<Routes>
<Route
path={`/:section(${sectionsRegex})`}

View file

@ -14,13 +14,13 @@ const style = {
export const RulesListSandbox = () => {
const {
services: { validateEmailAddresses },
services: { validateEmailAddresses, enabledEmailServices },
} = useConnectorContext();
return (
<div style={style}>
{getRulesListLazy({
connectorServices: { validateEmailAddresses },
connectorServices: { validateEmailAddresses, enabledEmailServices },
rulesListProps: {},
})}
</div>

View file

@ -120,12 +120,12 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
const {
actions: { validateEmailAddresses },
actions: { validateEmailAddresses, enabledEmailServices },
application: { navigateToApp },
} = useKibana().services;
return (
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
<ConnectorProvider value={{ services: { validateEmailAddresses, enabledEmailServices } }}>
<Routes>
<Route
exact

View file

@ -27,7 +27,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
const licensingPluginMock = licensingMock.createStart();
return {
...core,
actions: { validateEmailAddresses: jest.fn() },
actions: { validateEmailAddresses: jest.fn(), enabledEmailServices: ['*'] },
ruleTypeRegistry: {
has: jest.fn(),
register: jest.fn(),

View file

@ -49,7 +49,7 @@ import { getUntrackModalLazy } from './common/get_untrack_modal';
function createStartMock(): TriggersAndActionsUIPublicPluginStart {
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
const ruleTypeRegistry = new TypeRegistry<RuleTypeModel>();
const connectorServices = { validateEmailAddresses: jest.fn() };
const connectorServices = { validateEmailAddresses: jest.fn(), enabledEmailServices: ['*'] };
return {
actionTypeRegistry,
ruleTypeRegistry,

View file

@ -205,6 +205,7 @@ export class Plugin
const ruleTypeRegistry = this.ruleTypeRegistry;
this.connectorServices = {
validateEmailAddresses: plugins.actions.validateEmailAddresses,
enabledEmailServices: plugins.actions.enabledEmailServices,
};
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });

View file

@ -400,6 +400,7 @@ export interface SnoozeSchedule {
export interface ConnectorServices {
validateEmailAddresses: ActionsPublicPluginSetup['validateEmailAddresses'];
enabledEmailServices: ActionsPublicPluginSetup['enabledEmailServices'];
isWebhookSslWithPfxEnabled?: ActionsPublicPluginSetup['isWebhookSslWithPfxEnabled'];
}

View 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.
*/
export const emailEnabledServices = ['google-mail', 'amazon-ses'];
import { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const baseConfig = await readConfigFile(require.resolve('../../../../config.base.ts'));
return {
...baseConfig.getAll(),
testFiles: [require.resolve('.')],
junit: {
reportName:
'Chrome X-Pack UI Functional Tests with ES SSL - Email services enabled Kibana config',
},
kbnTestServer: {
...baseConfig.getAll().kbnTestServer,
serverArgs: [
...baseConfig.getAll().kbnTestServer.serverArgs,
`--xpack.actions.email.services.enabled=${JSON.stringify(emailEnabledServices)}`,
],
},
};
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const find = getService('find');
describe('Email - with multiple enabled services config', () => {
beforeEach(async () => {
await pageObjects.common.navigateToApp('triggersActionsConnectors');
});
it('should use the kibana config for enabled services', async () => {
await pageObjects.triggersActionsUI.clickCreateConnectorButton();
await testSubjects.click('.email-card');
const emailServicesOptions = await find.allByCssSelector(
'[data-test-subj="emailServiceSelectInput"] > option'
);
expect(emailServicesOptions.length).to.be(3);
expect(await emailServicesOptions[0].getVisibleText()).to.be(' '); // empty option
expect(await emailServicesOptions[1].getVisibleText()).to.be('Gmail');
expect(await emailServicesOptions[2].getVisibleText()).to.be('Amazon SES');
});
});
};

View file

@ -0,0 +1,21 @@
/*
* 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 { FtrProviderContext } from '@kbn/test-suites-xpack-platform/alerting_api_integration/common/ftr_provider_context';
import {
buildUp,
tearDown,
} from '@kbn/test-suites-xpack-platform/alerting_api_integration/spaces_only/tests/helpers';
export default function actionsTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Connectors with email services enabled Kibana config', () => {
before(async () => buildUp(getService));
after(async () => tearDown(getService));
loadTestFile(require.resolve('./email'));
});
}