[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:
Yuliia Naumenko 2021-09-23 19:39:26 -07:00 committed by GitHub
parent 03007d0150
commit 03cd9e8886
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 570 additions and 203 deletions

View file

@ -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',
}

View file

@ -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];

View file

@ -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}`
);

View file

@ -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';

View file

@ -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}`
);

View file

@ -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(),

View file

@ -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: [],
},
},
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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('');
});
});

View file

@ -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 };

View file

@ -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',
{

View file

@ -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);

View file

@ -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>;