mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Actions][Connectors] Modify email connector UI flyout to support OAuth 2.0 Client Credentials flow for MS Exchange provider (#112375)
* [Actions][Connectors] Modify email connector UI flyout to support OAuth 2.0 Client Credentials flow for MS Exchange provider * fixed test * added unit test * added validation unit test * fixed fn test * fixed prettier * - * Update email_connector.test.tsx * Update use_email_config.test.ts * fixed due to comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
03007d0150
commit
03cd9e8886
14 changed files with 570 additions and 203 deletions
|
@ -15,3 +15,10 @@ export * from './rewrite_request_case';
|
|||
export const BASE_ACTION_API_PATH = '/api/actions';
|
||||
export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions';
|
||||
export const ACTIONS_FEATURE_ID = 'actions';
|
||||
|
||||
// supported values for `service` in addition to nodemailer's list of well-known services
|
||||
export enum AdditionalEmailServices {
|
||||
ELASTIC_CLOUD = 'elastic_cloud',
|
||||
EXCHANGE = 'exchange_server',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { Logger } from '../../../../../src/core/server';
|
|||
import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { renderMustacheString, renderMustacheObject } from '../lib/mustache_renderer';
|
||||
import { AdditionalEmailServices } from '../../common';
|
||||
|
||||
export type EmailActionType = ActionType<
|
||||
ActionTypeConfigType,
|
||||
|
@ -33,13 +34,6 @@ export type EmailActionTypeExecutorOptions = ActionTypeExecutorOptions<
|
|||
// config definition
|
||||
export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
|
||||
|
||||
// supported values for `service` in addition to nodemailer's list of well-known services
|
||||
export enum AdditionalEmailServices {
|
||||
ELASTIC_CLOUD = 'elastic_cloud',
|
||||
EXCHANGE = 'exchange_server',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
// these values for `service` require users to fill in host/port/secure
|
||||
export const CUSTOM_HOST_PORT_SERVICES: string[] = [AdditionalEmailServices.OTHER];
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import qs from 'query-string';
|
||||
import axios from 'axios';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { request } from './axios_utils';
|
||||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
|
@ -59,7 +60,7 @@ export async function requestOAuthClientCredentialsToken(
|
|||
expiresIn: res.data.expires_in,
|
||||
};
|
||||
} else {
|
||||
const errString = JSON.stringify(res.data);
|
||||
const errString = stringify(res.data);
|
||||
logger.warn(
|
||||
`error thrown getting the access token from ${tokenUrl} for clientID: ${clientId}: ${errString}`
|
||||
);
|
||||
|
|
|
@ -13,10 +13,10 @@ import { Logger } from '../../../../../../src/core/server';
|
|||
import { ActionsConfigurationUtilities } from '../../actions_config';
|
||||
import { CustomHostSettings } from '../../config';
|
||||
import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
|
||||
import { AdditionalEmailServices } from '../email';
|
||||
import { sendEmailGraphApi } from './send_email_graph_api';
|
||||
import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token';
|
||||
import { ProxySettings } from '../../types';
|
||||
import { AdditionalEmailServices } from '../../../common';
|
||||
|
||||
// an email "service" which doesn't actually send, just returns what it would send
|
||||
export const JSON_TRANSPORT_SERVICE = '__json';
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// @ts-expect-error missing type def
|
||||
import stringify from 'json-stringify-safe';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { Logger } from '../../../../../../src/core/server';
|
||||
import { request } from './axios_utils';
|
||||
|
@ -41,9 +43,9 @@ export async function sendEmailGraphApi(
|
|||
validateStatus: () => true,
|
||||
});
|
||||
if (res.status === 202) {
|
||||
return res;
|
||||
return res.data;
|
||||
}
|
||||
const errString = JSON.stringify(res.data);
|
||||
const errString = stringify(res.data);
|
||||
logger.warn(
|
||||
`error thrown sending Microsoft Exchange email for clientID: ${sendEmailOptions.options.transport.clientId}: ${errString}`
|
||||
);
|
||||
|
|
|
@ -10,10 +10,10 @@ import { IRouter } from 'kibana/server';
|
|||
import nodemailerGetService from 'nodemailer/lib/well-known';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { INTERNAL_BASE_ACTION_API_PATH } from '../../common';
|
||||
import { AdditionalEmailServices, INTERNAL_BASE_ACTION_API_PATH } from '../../common';
|
||||
import { ActionsRequestHandlerContext } from '../types';
|
||||
import { verifyAccessAndContext } from './verify_access_and_context';
|
||||
import { AdditionalEmailServices, ELASTIC_CLOUD_SERVICE } from '../builtin_action_types/email';
|
||||
import { ELASTIC_CLOUD_SERVICE } from '../builtin_action_types/email';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
service: schema.string(),
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('connector validation', () => {
|
|||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
|
@ -70,12 +71,15 @@ describe('connector validation', () => {
|
|||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -86,6 +90,7 @@ describe('connector validation', () => {
|
|||
secrets: {
|
||||
user: null,
|
||||
password: null,
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
|
@ -108,12 +113,15 @@ describe('connector validation', () => {
|
|||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -141,12 +149,15 @@ describe('connector validation', () => {
|
|||
port: ['Port is required.'],
|
||||
host: ['Host is required.'],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -156,6 +167,7 @@ describe('connector validation', () => {
|
|||
secrets: {
|
||||
user: 'user',
|
||||
password: null,
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
|
@ -178,12 +190,15 @@ describe('connector validation', () => {
|
|||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: ['Password is required when username is used.'],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -193,6 +208,7 @@ describe('connector validation', () => {
|
|||
secrets: {
|
||||
user: null,
|
||||
password: 'password',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
|
@ -215,12 +231,15 @@ describe('connector validation', () => {
|
|||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: ['Username is required when password is used.'],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -253,12 +272,53 @@ describe('connector validation', () => {
|
|||
port: [],
|
||||
host: [],
|
||||
service: ['Service is required.'],
|
||||
clientId: [],
|
||||
tenantId: [],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
user: [],
|
||||
password: [],
|
||||
clientSecret: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
user: 'user',
|
||||
password: 'pass',
|
||||
clientSecret: null,
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
isPreconfigured: false,
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
|
||||
expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({
|
||||
config: {
|
||||
errors: {
|
||||
from: [],
|
||||
port: [],
|
||||
host: [],
|
||||
service: [],
|
||||
clientId: ['Client ID is required.'],
|
||||
tenantId: ['Tenant ID is required.'],
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
errors: {
|
||||
clientSecret: ['Client Secret is required.'],
|
||||
password: [],
|
||||
user: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
GenericValidationResult,
|
||||
} from '../../../../types';
|
||||
import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types';
|
||||
import { AdditionalEmailServices } from '../../../../../../actions/common';
|
||||
|
||||
const emailServices: EuiSelectOption[] = [
|
||||
{
|
||||
|
@ -106,10 +107,13 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
port: new Array<string>(),
|
||||
host: new Array<string>(),
|
||||
service: new Array<string>(),
|
||||
clientId: new Array<string>(),
|
||||
tenantId: new Array<string>(),
|
||||
};
|
||||
const secretsErrors = {
|
||||
user: new Array<string>(),
|
||||
password: new Array<string>(),
|
||||
clientSecret: new Array<string>(),
|
||||
};
|
||||
|
||||
const validationResult = {
|
||||
|
@ -122,17 +126,29 @@ export function getActionType(): ActionTypeModel<EmailConfig, EmailSecrets, Emai
|
|||
if (action.config.from && !action.config.from.trim().match(mailformat)) {
|
||||
configErrors.from.push(translations.SENDER_NOT_VALID);
|
||||
}
|
||||
if (!action.config.port) {
|
||||
configErrors.port.push(translations.PORT_REQUIRED);
|
||||
}
|
||||
if (!action.config.host) {
|
||||
configErrors.host.push(translations.HOST_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
if (action.config.service !== AdditionalEmailServices.EXCHANGE) {
|
||||
if (!action.config.port) {
|
||||
configErrors.port.push(translations.PORT_REQUIRED);
|
||||
}
|
||||
if (!action.config.host) {
|
||||
configErrors.host.push(translations.HOST_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.user.push(translations.USERNAME_REQUIRED);
|
||||
}
|
||||
if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) {
|
||||
secretsErrors.password.push(translations.PASSWORD_REQUIRED);
|
||||
}
|
||||
} else {
|
||||
if (!action.config.clientId) {
|
||||
configErrors.clientId.push(translations.CLIENT_ID_REQUIRED);
|
||||
}
|
||||
if (!action.config.tenantId) {
|
||||
configErrors.tenantId.push(translations.TENANT_ID_REQUIRED);
|
||||
}
|
||||
if (!action.secrets.clientSecret) {
|
||||
secretsErrors.clientSecret.push(translations.CLIENT_SECRET_REQUIRED);
|
||||
}
|
||||
}
|
||||
if (!action.config.service) {
|
||||
configErrors.service.push(translations.SERVICE_REQUIRED);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { lazy, useEffect } from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexItem,
|
||||
|
@ -27,7 +27,9 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
import { getEmailServices } from './email';
|
||||
import { useEmailConfig } from './use_email_config';
|
||||
import { AdditionalEmailServices } from '../../../../../../actions/common';
|
||||
|
||||
const ExchangeFormFields = lazy(() => import('./exchange_form'));
|
||||
export const EmailActionConnectorFields: React.FunctionComponent<
|
||||
ActionConnectorFieldsProps<EmailActionConnector>
|
||||
> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => {
|
||||
|
@ -61,6 +63,88 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
password !== undefined && errors.password !== undefined && errors.password.length > 0;
|
||||
const isUserInvalid: boolean =
|
||||
user !== undefined && errors.user !== undefined && errors.user.length > 0;
|
||||
|
||||
const authForm = (
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Username and password are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailUser"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={isUserInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
data-test-subj="emailUserInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('user', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!user) {
|
||||
editActionSecrets('user', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
name="password"
|
||||
value={password || ''}
|
||||
data-test-subj="emailPasswordInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('password', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
|
@ -130,214 +214,149 @@ export const EmailActionConnectorFields: React.FunctionComponent<
|
|||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailHost"
|
||||
fullWidth
|
||||
error={errors.host}
|
||||
isInvalid={isHostInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Host',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
isInvalid={isHostInvalid}
|
||||
name="host"
|
||||
value={host || ''}
|
||||
data-test-subj="emailHostInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('host', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!host) {
|
||||
editActionConfig('host', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPort"
|
||||
fullWidth
|
||||
placeholder="587"
|
||||
error={errors.port}
|
||||
isInvalid={isPortInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Port',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
prepend=":"
|
||||
isInvalid={isPortInvalid}
|
||||
fullWidth
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
name="port"
|
||||
value={port || ''}
|
||||
data-test-subj="emailPortInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('port', parseInt(e.target.value, 10));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!port) {
|
||||
editActionConfig('port', 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.secureSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Secure',
|
||||
}
|
||||
)}
|
||||
data-test-subj="emailSecureSwitch"
|
||||
disabled={readOnly || !emailServiceConfigurable}
|
||||
checked={secure || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('secure', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel"
|
||||
defaultMessage="Authentication"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this server',
|
||||
}
|
||||
)}
|
||||
disabled={readOnly}
|
||||
checked={hasAuth || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('hasAuth', e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
editActionSecrets('user', null);
|
||||
editActionSecrets('password', null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{hasAuth ? (
|
||||
{service === AdditionalEmailServices.EXCHANGE ? (
|
||||
<ExchangeFormFields
|
||||
action={action}
|
||||
editActionConfig={editActionConfig}
|
||||
editActionSecrets={editActionSecrets}
|
||||
errors={errors}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
2,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Username and password are encrypted. Please reenter values for these fields.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailUser"
|
||||
id="emailHost"
|
||||
fullWidth
|
||||
error={errors.user}
|
||||
isInvalid={isUserInvalid}
|
||||
error={errors.host}
|
||||
isInvalid={isHostInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel',
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Username',
|
||||
defaultMessage: 'Host',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isUserInvalid}
|
||||
name="user"
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
value={user || ''}
|
||||
data-test-subj="emailUserInput"
|
||||
isInvalid={isHostInvalid}
|
||||
name="host"
|
||||
value={host || ''}
|
||||
data-test-subj="emailHostInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('user', nullableString(e.target.value));
|
||||
editActionConfig('host', e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!user) {
|
||||
editActionSecrets('user', '');
|
||||
if (!host) {
|
||||
editActionConfig('host', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPassword"
|
||||
fullWidth
|
||||
error={errors.password}
|
||||
isInvalid={isPasswordInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Password',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
readOnly={readOnly}
|
||||
isInvalid={isPasswordInvalid}
|
||||
name="password"
|
||||
value={password || ''}
|
||||
data-test-subj="emailPasswordInput"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('password', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!password) {
|
||||
editActionSecrets('password', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="emailPort"
|
||||
fullWidth
|
||||
placeholder="587"
|
||||
error={errors.port}
|
||||
isInvalid={isPortInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Port',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
prepend=":"
|
||||
isInvalid={isPortInvalid}
|
||||
fullWidth
|
||||
disabled={!emailServiceConfigurable}
|
||||
readOnly={readOnly}
|
||||
name="port"
|
||||
value={port || ''}
|
||||
data-test-subj="emailPortInput"
|
||||
onChange={(e) => {
|
||||
editActionConfig('port', parseInt(e.target.value, 10));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!port) {
|
||||
editActionConfig('port', 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.secureSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Secure',
|
||||
}
|
||||
)}
|
||||
data-test-subj="emailSecureSwitch"
|
||||
disabled={readOnly || !emailServiceConfigurable}
|
||||
checked={secure || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('secure', e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xxs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel"
|
||||
defaultMessage="Authentication"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel',
|
||||
{
|
||||
defaultMessage: 'Require authentication for this server',
|
||||
}
|
||||
)}
|
||||
disabled={readOnly}
|
||||
checked={hasAuth || false}
|
||||
onChange={(e) => {
|
||||
editActionConfig('hasAuth', e.target.checked);
|
||||
if (!e.target.checked) {
|
||||
editActionSecrets('user', null);
|
||||
editActionSecrets('password', null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{hasAuth ? authForm : null}
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// if the string == null or is empty, return null, else return string
|
||||
function nullableString(str: string | null | undefined) {
|
||||
export function nullableString(str: string | null | undefined) {
|
||||
if (str == null || str.trim() === '') return null;
|
||||
return str;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import ExchangeFormFields from './exchange_form';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
describe('ExchangeFormFields renders', () => {
|
||||
test('should display exchange form fields', () => {
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
clientSecret: 'user',
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'exchange email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
clientId: '123',
|
||||
tenantId: '1234',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ExchangeFormFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy();
|
||||
});
|
||||
|
||||
test('exchange field defaults to empty when not defined', () => {
|
||||
const actionConnector = {
|
||||
secrets: {},
|
||||
id: 'test',
|
||||
actionTypeId: '.email',
|
||||
name: 'email',
|
||||
config: {
|
||||
from: 'test@test.com',
|
||||
hasAuth: true,
|
||||
service: 'exchange_server',
|
||||
},
|
||||
} as EmailActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<ExchangeFormFields
|
||||
action={actionConnector}
|
||||
errors={{ from: [], port: [], host: [], user: [], password: [], service: [] }}
|
||||
editActionConfig={() => {}}
|
||||
editActionSecrets={() => {}}
|
||||
readOnly={false}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual('');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="emailClientId"]').prop('value')).toEqual('');
|
||||
|
||||
expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy();
|
||||
expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiFormRow,
|
||||
EuiFieldPassword,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IErrorObject } from '../../../../types';
|
||||
import { EmailActionConnector } from '../types';
|
||||
import { nullableString } from './email_connector';
|
||||
import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label';
|
||||
|
||||
interface ExchangeFormFieldsProps {
|
||||
action: EmailActionConnector;
|
||||
editActionConfig: (property: string, value: unknown) => void;
|
||||
editActionSecrets: (property: string, value: unknown) => void;
|
||||
errors: IErrorObject;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const ExchangeFormFields: React.FunctionComponent<ExchangeFormFieldsProps> = ({
|
||||
action,
|
||||
editActionConfig,
|
||||
editActionSecrets,
|
||||
errors,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { tenantId, clientId } = action.config;
|
||||
const { clientSecret } = action.secrets;
|
||||
|
||||
const isClientIdInvalid: boolean =
|
||||
clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0;
|
||||
const isTenantIdInvalid: boolean =
|
||||
tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0;
|
||||
const isClientSecretInvalid: boolean =
|
||||
clientSecret !== undefined &&
|
||||
errors.clientSecret !== undefined &&
|
||||
errors.clientSecret.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="tenantId"
|
||||
error={errors.tenantId}
|
||||
isInvalid={isTenantIdInvalid}
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Tenant ID',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isTenantIdInvalid}
|
||||
name="tenantId"
|
||||
data-test-subj="emailTenantId"
|
||||
readOnly={readOnly}
|
||||
value={tenantId || ''}
|
||||
onChange={(e) => {
|
||||
editActionConfig('tenantId', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!tenantId) {
|
||||
editActionConfig('tenantId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="clientId"
|
||||
error={errors.clientId}
|
||||
isInvalid={isClientIdInvalid}
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientIdFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client ID',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
isInvalid={isClientIdInvalid}
|
||||
name="clientId"
|
||||
data-test-subj="emailClientId"
|
||||
readOnly={readOnly}
|
||||
value={clientId || ''}
|
||||
onChange={(e) => {
|
||||
editActionConfig('clientId', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!clientId) {
|
||||
editActionConfig('clientId', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{getEncryptedFieldNotifyLabel(
|
||||
!action.id,
|
||||
1,
|
||||
action.isMissingSecrets ?? false,
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.',
|
||||
}
|
||||
)
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
id="clientSecret"
|
||||
fullWidth
|
||||
error={errors.clientSecret}
|
||||
isInvalid={isClientSecretInvalid}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientSecretTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Client Secret',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiFieldPassword
|
||||
fullWidth
|
||||
isInvalid={isClientSecretInvalid}
|
||||
name="clientSecret"
|
||||
readOnly={readOnly}
|
||||
value={clientSecret || ''}
|
||||
data-test-subj="emailClientSecret"
|
||||
onChange={(e) => {
|
||||
editActionSecrets('clientSecret', nullableString(e.target.value));
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!clientSecret) {
|
||||
editActionSecrets('clientSecret', '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ExchangeFormFields as default };
|
|
@ -21,6 +21,27 @@ export const SENDER_NOT_VALID = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CLIENT_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText',
|
||||
{
|
||||
defaultMessage: 'Client ID is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TENANT_ID_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText',
|
||||
{
|
||||
defaultMessage: 'Tenant ID is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CLIENT_SECRET_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText',
|
||||
{
|
||||
defaultMessage: 'Client Secret is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const PORT_REQUIRED = i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText',
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ import { HttpSetup } from 'kibana/public';
|
|||
import { isEmpty } from 'lodash';
|
||||
import { EmailConfig } from '../types';
|
||||
import { getServiceConfig } from './api';
|
||||
import { AdditionalEmailServices } from '../../../../../../actions/common';
|
||||
|
||||
export function useEmailConfig(
|
||||
http: HttpSetup,
|
||||
|
@ -39,9 +40,12 @@ export function useEmailConfig(
|
|||
useEffect(() => {
|
||||
(async () => {
|
||||
if (emailService) {
|
||||
editActionConfig('service', emailService);
|
||||
if (emailService === AdditionalEmailServices.EXCHANGE) {
|
||||
return;
|
||||
}
|
||||
const serviceConfig = await getEmailServiceConfig(emailService);
|
||||
|
||||
editActionConfig('service', emailService);
|
||||
editActionConfig('host', serviceConfig?.host ? serviceConfig.host : '');
|
||||
editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0);
|
||||
editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false);
|
||||
|
|
|
@ -79,11 +79,14 @@ export interface EmailConfig {
|
|||
secure?: boolean;
|
||||
hasAuth: boolean;
|
||||
service: string;
|
||||
clientId?: string;
|
||||
tenantId?: string;
|
||||
}
|
||||
|
||||
export interface EmailSecrets {
|
||||
user: string | null;
|
||||
password: string | null;
|
||||
clientSecret: string | null;
|
||||
}
|
||||
|
||||
export type EmailActionConnector = UserConfiguredActionConnector<EmailConfig, EmailSecrets>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue