[Response Ops][Connectors] New xpack.actions.webhook.ssl.pfx.enabled config (#222507)

## Summary

Closes https://github.com/elastic/kibana/issues/220416

## Release note
New `xpack.actions.webhook.ssl.pfx.enabled` Kibana setting to disable
Webhook connector PFX file support for SSL client authentication

---------

Co-authored-by: Nastasha Solomon <79124755+nastasha-solomon@users.noreply.github.com>
This commit is contained in:
Julian Gernun 2025-06-17 09:31:17 +02:00 committed by GitHub
parent dfd783e12a
commit 25b4f507e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 396 additions and 23 deletions

View file

@ -200,6 +200,7 @@ enabled:
- x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/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_aws_ses_kibana_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/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 - x-pack/test/functional/apps/advanced_settings/config.ts
- x-pack/test/functional/apps/aiops/config.ts - x-pack/test/functional/apps/aiops/config.ts
- x-pack/test/functional/apps/api_keys/config.ts - x-pack/test/functional/apps/api_keys/config.ts

View file

@ -841,6 +841,12 @@ For more examples, go to [Preconfigured connectors](/reference/connectors-kibana
Data type: `string` Data type: `string`
`xpack.actions.webhook.ssl.pfx.enabled`
: Disable PFX file support for SSL client authentication. When set to `false`, the application will not accept PFX certificate files and will require separate certificate and private key files instead. Only applies to the [Webhook connector](/reference/connectors-kibana/webhook-action-type.md).
Data type: `bool`
Default: `true`
## Alerting settings [alert-settings] ## Alerting settings [alert-settings]
`xpack.alerting.cancelAlertsOnRuleTimeout` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}") `xpack.alerting.cancelAlertsOnRuleTimeout` ![logo cloud](https://doc-icons.s3.us-east-2.amazonaws.com/logo_cloud.svg "Supported on {{ech}}")

View file

@ -2303,6 +2303,27 @@ groups:
self: all self: all
# example: | # example: |
- setting: xpack.actions.webhook.ssl.pfx.enabled
# id:
description: |
Disable PFX file support for SSL client authentication. When set to `false`, the application will not accept PFX certificate files and will require separate certificate and private key files instead. Only applies to the [Webhook connector](/reference/connectors-kibana/webhook-action-type.md).
# state: deprecated/hidden/tech-preview
# deprecation_details: ""
# note: ""
# tip: ""
# warning: ""
# important: ""
datatype: bool
default: true
# options:
# - option:
# description: ""
# type: static/dynamic
applies_to:
deployment:
self: all
# example: |
- group: Alerting settings - group: Alerting settings
id: alert-settings id: alert-settings
# description: | # description: |

View file

@ -226,6 +226,7 @@ kibana_vars=(
xpack.actions.responseTimeout xpack.actions.responseTimeout
xpack.actions.ssl.proxyVerificationMode xpack.actions.ssl.proxyVerificationMode
xpack.actions.ssl.verificationMode xpack.actions.ssl.verificationMode
xpack.actions.webhook.ssl.pfx.enabled
xpack.alerting.healthCheck.interval xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay xpack.alerting.invalidateApiKeysTask.removalDelay

View file

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

View file

@ -62,5 +62,20 @@ describe('Actions Plugin', () => {
] ]
`); `);
}); });
it('returns isWebhookSslWithPfxEnabled if set in kibana config', async () => {
const context = coreMock.createPluginInitializerContext({
webhook: {
ssl: {
pfx: {
enabled: false,
},
},
},
});
const plugin = new Plugin(context);
const pluginSetup = plugin.setup();
expect(pluginSetup.isWebhookSslWithPfxEnabled).toBe(false);
});
}); });
}); });

View file

@ -14,26 +14,37 @@ export interface ActionsPublicPluginSetup {
emails: string[], emails: string[],
options?: ValidateEmailAddressesOptions options?: ValidateEmailAddressesOptions
): ValidatedEmail[]; ): ValidatedEmail[];
isWebhookSslWithPfxEnabled?: boolean;
} }
export interface Config { export interface Config {
email: { email: {
domain_allowlist: string[]; domain_allowlist: string[];
}; };
webhook: {
ssl: {
pfx: {
enabled: boolean;
};
};
};
} }
export class Plugin implements CorePlugin<ActionsPublicPluginSetup> { export class Plugin implements CorePlugin<ActionsPublicPluginSetup> {
private readonly allowedEmailDomains: string[] | null = null; private readonly allowedEmailDomains: string[] | null = null;
private readonly webhookSslWithPfxEnabled: boolean;
constructor(ctx: PluginInitializerContext<Config>) { constructor(ctx: PluginInitializerContext<Config>) {
const config = ctx.config.get(); const config = ctx.config.get();
this.allowedEmailDomains = config.email?.domain_allowlist || null; this.allowedEmailDomains = config.email?.domain_allowlist || null;
this.webhookSslWithPfxEnabled = config.webhook?.ssl.pfx.enabled ?? true;
} }
public setup(): ActionsPublicPluginSetup { public setup(): ActionsPublicPluginSetup {
return { return {
validateEmailAddresses: (emails: string[], options: ValidateEmailAddressesOptions) => validateEmailAddresses: (emails: string[], options: ValidateEmailAddressesOptions) =>
validateEmails(this.allowedEmailDomains, emails, options), validateEmails(this.allowedEmailDomains, emails, options),
isWebhookSslWithPfxEnabled: this.webhookSslWithPfxEnabled,
}; };
} }

View file

@ -36,6 +36,13 @@ const createActionsConfigMock = () => {
getMaxAttempts: jest.fn().mockReturnValue(3), getMaxAttempts: jest.fn().mockReturnValue(3),
enableFooterInEmail: jest.fn().mockReturnValue(true), enableFooterInEmail: jest.fn().mockReturnValue(true),
getMaxQueued: jest.fn().mockReturnValue(1000), getMaxQueued: jest.fn().mockReturnValue(1000),
getWebhookSettings: jest.fn().mockReturnValue({
ssl: {
pfx: {
enabled: true,
},
},
}),
getAwsSesConfig: jest.fn().mockReturnValue(null), getAwsSesConfig: jest.fn().mockReturnValue(null),
}; };
return mocked; return mocked;

View file

@ -572,6 +572,56 @@ describe('getMaxQueued()', () => {
}); });
}); });
describe('getWebhookSettings()', () => {
test('returns the webhook settings from config', () => {
const config: ActionsConfig = {
...defaultActionsConfig,
webhook: {
ssl: {
pfx: {
enabled: true,
},
},
},
};
const webhookSettings = getActionsConfigurationUtilities(config).getWebhookSettings();
expect(webhookSettings).toEqual({
ssl: {
pfx: {
enabled: true,
},
},
});
});
test('returns the webhook settings from config when pfx is false', () => {
const config: ActionsConfig = {
...defaultActionsConfig,
webhook: {
ssl: {
pfx: {
enabled: false,
},
},
},
};
const webhookSettings = getActionsConfigurationUtilities(config).getWebhookSettings();
expect(webhookSettings).toEqual({
ssl: {
pfx: {
enabled: false,
},
},
});
});
test('returns true when no webhook settings are defined', () => {
const config: ActionsConfig = defaultActionsConfig;
const webhookSettings = getActionsConfigurationUtilities(config).getWebhookSettings();
expect(webhookSettings).toEqual({ ssl: { pfx: { enabled: true } } });
});
});
describe('getAwsSesConfig()', () => { describe('getAwsSesConfig()', () => {
test('returns null when no email config set', () => { test('returns null when no email config set', () => {
const acu = getActionsConfigurationUtilities(defaultActionsConfig); const acu = getActionsConfigurationUtilities(defaultActionsConfig);

View file

@ -55,6 +55,13 @@ export interface ActionsConfigurationUtilities {
): string | undefined; ): string | undefined;
enableFooterInEmail: () => boolean; enableFooterInEmail: () => boolean;
getMaxQueued: () => number; getMaxQueued: () => number;
getWebhookSettings(): {
ssl: {
pfx: {
enabled: boolean;
};
};
};
getAwsSesConfig: () => AwsSesConfig; getAwsSesConfig: () => AwsSesConfig;
} }
@ -226,6 +233,15 @@ export function getActionsConfigurationUtilities(
}, },
enableFooterInEmail: () => config.enableFooterInEmail, enableFooterInEmail: () => config.enableFooterInEmail,
getMaxQueued: () => config.queued?.max || DEFAULT_QUEUED_MAX, getMaxQueued: () => config.queued?.max || DEFAULT_QUEUED_MAX,
getWebhookSettings: () => {
return {
ssl: {
pfx: {
enabled: config.webhook?.ssl.pfx.enabled ?? true,
},
},
};
},
getAwsSesConfig: () => { getAwsSesConfig: () => {
if (config.email?.services?.ses.host && config.email?.services?.ses.port) { if (config.email?.services?.ses.host && config.email?.services?.ses.port) {
return { return {

View file

@ -250,6 +250,32 @@ describe('config validation', () => {
expect(result.email?.domain_allowlist).toEqual(['a.com', 'b.c.com', 'd.e.f.com']); expect(result.email?.domain_allowlist).toEqual(['a.com', 'b.c.com', 'd.e.f.com']);
}); });
test('validates xpack.actions.webhook', () => {
const config: Record<string, unknown> = {};
let result = configSchema.validate(config);
expect(result.webhook === undefined);
config.webhook = {};
result = configSchema.validate(config);
expect(result.webhook?.ssl.pfx.enabled).toEqual(true);
config.webhook = { ssl: {} };
result = configSchema.validate(config);
expect(result.webhook?.ssl.pfx.enabled).toEqual(true);
config.webhook = { ssl: { pfx: {} } };
result = configSchema.validate(config);
expect(result.webhook?.ssl.pfx.enabled).toEqual(true);
config.webhook = { ssl: { pfx: { enabled: false } } };
result = configSchema.validate(config);
expect(result.webhook?.ssl.pfx.enabled).toEqual(false);
config.webhook = { ssl: { pfx: { enabled: true } } };
result = configSchema.validate(config);
expect(result.webhook?.ssl.pfx.enabled).toEqual(true);
});
describe('email.services.ses', () => { describe('email.services.ses', () => {
const config: Record<string, unknown> = {}; const config: Record<string, unknown> = {};
test('validates no email config at all', () => { test('validates no email config at all', () => {

View file

@ -167,6 +167,15 @@ export const configSchema = schema.object({
}) })
), ),
}), }),
webhook: schema.maybe(
schema.object({
ssl: schema.object({
pfx: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
}),
})
),
}); });
export type ActionsConfig = TypeOf<typeof configSchema>; export type ActionsConfig = TypeOf<typeof configSchema>;

View file

@ -51,6 +51,7 @@ export const config: PluginConfigDescriptor<ActionsConfig> = {
schema: configSchema, schema: configSchema,
exposeToBrowser: { exposeToBrowser: {
email: { domain_allowlist: true }, email: { domain_allowlist: true },
webhook: { ssl: { pfx: { enabled: true } } },
}, },
}; };

View file

@ -39,13 +39,14 @@ import * as i18n from './translations';
interface Props { interface Props {
readOnly: boolean; readOnly: boolean;
isPfxEnabled?: boolean;
} }
const { emptyField } = fieldValidators; const { emptyField } = fieldValidators;
const VERIFICATION_MODE_DEFAULT = 'full'; const VERIFICATION_MODE_DEFAULT = 'full';
export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => { export const AuthConfig: FunctionComponent<Props> = ({ readOnly, isPfxEnabled = true }) => {
const { setFieldValue, getFieldDefaultValue } = useFormContext(); const { setFieldValue, getFieldDefaultValue } = useFormContext();
const [{ config, __internal__ }] = useFormData({ const [{ config, __internal__ }] = useFormData({
watch: [ watch: [
@ -112,6 +113,7 @@ export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => {
readOnly={readOnly} readOnly={readOnly}
certTypeDefaultValue={certTypeDefaultValue} certTypeDefaultValue={certTypeDefaultValue}
certType={certType} certType={certType}
isPfxEnabled={isPfxEnabled}
/> />
), ),
'data-test-subj': 'authSSL', 'data-test-subj': 'authSSL',
@ -180,7 +182,7 @@ export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => {
onClick={() => removeItem(item.id)} onClick={() => removeItem(item.id)}
iconType="minusInCircle" iconType="minusInCircle"
aria-label={i18n.DELETE_BUTTON} aria-label={i18n.DELETE_BUTTON}
style={{ marginTop: '28px' }} css={{ marginTop: '28px' }}
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -11,10 +11,26 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { SSLCertType } from '../../../common/auth/constants'; import { SSLCertType } from '../../../common/auth/constants';
import { AuthFormTestProvider } from '../../connector_types/lib/test_utils'; import { AuthFormTestProvider } from '../../connector_types/lib/test_utils';
import { useConnectorContext } from '@kbn/triggers-actions-ui-plugin/public';
import * as i18n from './translations';
const certTypeDefaultValue: SSLCertType = SSLCertType.CRT; const certTypeDefaultValue: SSLCertType = SSLCertType.CRT;
jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({
useConnectorContext: jest.fn(),
}));
describe('SSLCertFields', () => { describe('SSLCertFields', () => {
beforeEach(() => {
(useConnectorContext as jest.Mock).mockReturnValue({
services: { isWebhookSslWithPfxEnabled: true },
});
});
afterEach(() => {
jest.clearAllMocks();
});
const onSubmit = jest.fn(); const onSubmit = jest.fn();
it('renders all fields for certType=CRT', async () => { it('renders all fields for certType=CRT', async () => {
@ -221,3 +237,33 @@ describe('SSLCertFields', () => {
}); });
}); });
}); });
describe('validation with PFX disabled', () => {
beforeEach(() => {
(useConnectorContext as jest.Mock).mockReturnValue({
services: { isWebhookSslWithPfxEnabled: false },
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('does not render PFX tab when PFX is disabled', async () => {
render(
<AuthFormTestProvider onSubmit={jest.fn()}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={certTypeDefaultValue}
certType={SSLCertType.CRT}
isPfxEnabled={false}
/>
</AuthFormTestProvider>
);
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLPassphraseInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCertTypeTabs')).toBeInTheDocument();
expect(screen.queryByText(i18n.CERT_TYPE_PFX)).not.toBeInTheDocument();
});
});

View file

@ -22,12 +22,14 @@ interface BasicAuthProps {
readOnly: boolean; readOnly: boolean;
certTypeDefaultValue: SSLCertType; certTypeDefaultValue: SSLCertType;
certType: SSLCertType; certType: SSLCertType;
isPfxEnabled?: boolean;
} }
export const SSLCertFields: React.FC<BasicAuthProps> = ({ export const SSLCertFields: React.FC<BasicAuthProps> = ({
readOnly, readOnly,
certTypeDefaultValue, certTypeDefaultValue,
certType, certType,
isPfxEnabled = true,
}) => ( }) => (
<EuiFlexGroup justifyContent="spaceBetween" data-test-subj="sslCertFields"> <EuiFlexGroup justifyContent="spaceBetween" data-test-subj="sslCertFields">
<EuiFlexItem> <EuiFlexItem>
@ -52,21 +54,25 @@ export const SSLCertFields: React.FC<BasicAuthProps> = ({
<EuiTabs size="s" data-test-subj="webhookCertTypeTabs"> <EuiTabs size="s" data-test-subj="webhookCertTypeTabs">
<EuiTab <EuiTab
onClick={() => field.setValue(SSLCertType.CRT)} onClick={() => field.setValue(SSLCertType.CRT)}
isSelected={field.value === SSLCertType.CRT} isSelected={field.value === SSLCertType.CRT || !isPfxEnabled}
data-test-subj="webhookCertTypeCRTab"
> >
{i18n.CERT_TYPE_CRT_KEY} {i18n.CERT_TYPE_CRT_KEY}
</EuiTab> </EuiTab>
<EuiTab {isPfxEnabled && (
onClick={() => field.setValue(SSLCertType.PFX)} <EuiTab
isSelected={field.value === SSLCertType.PFX} onClick={() => field.setValue(SSLCertType.PFX)}
> isSelected={field.value === SSLCertType.PFX}
{i18n.CERT_TYPE_PFX} data-test-subj="webhookCertTypePFXTab"
</EuiTab> >
{i18n.CERT_TYPE_PFX}
</EuiTab>
)}
</EuiTabs> </EuiTabs>
)} )}
/> />
<EuiSpacer size="s" /> <EuiSpacer size="s" />
{certType === SSLCertType.CRT && ( {(!isPfxEnabled || certType === SSLCertType.CRT) && (
<EuiFlexGroup wrap> <EuiFlexGroup wrap>
<EuiFlexItem css={{ minWidth: 200 }}> <EuiFlexItem css={{ minWidth: 200 }}>
<UseField <UseField
@ -112,7 +118,7 @@ export const SSLCertFields: React.FC<BasicAuthProps> = ({
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
)} )}
{certType === SSLCertType.PFX && ( {isPfxEnabled && certType === SSLCertType.PFX && (
<UseField <UseField
path="secrets.pfx" path="secrets.pfx"
config={{ config={{

View file

@ -177,7 +177,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent<ActionConnector
<UpdateStep readOnly={readOnly} display={currentStep === 4} /> <UpdateStep readOnly={readOnly} display={currentStep === 4} />
<EuiFlexGroup alignItems="flexStart" justifyContent="flexStart" direction="rowReverse"> <EuiFlexGroup alignItems="flexStart" justifyContent="flexStart" direction="rowReverse">
{currentStep < 4 && ( {currentStep < 4 && (
<EuiFlexItem grow={false} style={{ minWidth: 160 }}> <EuiFlexItem grow={false} css={{ minWidth: 160 }}>
<EuiButton <EuiButton
data-test-subj="casesWebhookNext" data-test-subj="casesWebhookNext"
fill fill
@ -190,7 +190,7 @@ const CasesWebhookActionConnectorFields: React.FunctionComponent<ActionConnector
</EuiFlexItem> </EuiFlexItem>
)} )}
{currentStep > 1 && ( {currentStep > 1 && (
<EuiFlexItem grow={false} style={{ minWidth: 160 }}> <EuiFlexItem grow={false} css={{ minWidth: 160 }}>
<EuiButton <EuiButton
data-test-subj="casesWebhookBack" data-test-subj="casesWebhookBack"
iconSide="left" iconSide="left"

View file

@ -71,7 +71,10 @@ const FormTestProviderComponent: React.FC<FormTestProviderProps> = ({
children, children,
defaultValue, defaultValue,
onSubmit, onSubmit,
connectorServices = { validateEmailAddresses: jest.fn() }, connectorServices = {
validateEmailAddresses: jest.fn(),
isWebhookSslWithPfxEnabled: true,
},
}) => { }) => {
const { form } = useForm({ defaultValue }); const { form } = useForm({ defaultValue });
const { submit } = form; const { submit } = form;

View file

@ -11,7 +11,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { Field, SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { Field, SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public'; import {
useConnectorContext,
type ActionConnectorFieldsProps,
} from '@kbn/triggers-actions-ui-plugin/public';
import * as i18n from './translations'; import * as i18n from './translations';
import { AuthConfig } from '../../common/auth/auth_config'; import { AuthConfig } from '../../common/auth/auth_config';
@ -22,6 +25,10 @@ const { emptyField, urlField } = fieldValidators;
const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({ const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
readOnly, readOnly,
}) => { }) => {
const {
services: { isWebhookSslWithPfxEnabled: isPfxEnabled },
} = useConnectorContext();
return ( return (
<> <>
<EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexGroup justifyContent="spaceBetween">
@ -67,7 +74,7 @@ const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorField
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<AuthConfig readOnly={readOnly} /> <AuthConfig readOnly={readOnly} isPfxEnabled={isPfxEnabled} />
</> </>
); );
}; };

View file

@ -222,7 +222,7 @@ describe('config validation', () => {
expect(() => { expect(() => {
validateConfig(connectorType, config, { configurationUtilities }); validateConfig(connectorType, config, { configurationUtilities });
}).toThrowErrorMatchingInlineSnapshot( }).toThrowErrorMatchingInlineSnapshot(
'"error validating action type config: error configuring webhook action: unable to parse url: TypeError: Invalid URL: example.com/do-something"' `"error validating action type config: error validation webhook action config: unable to parse url: TypeError: Invalid URL: example.com/do-something"`
); );
}); });
@ -295,7 +295,25 @@ describe('config validation', () => {
expect(() => { expect(() => {
validateConfig(connectorType, config, { configurationUtilities: configUtils }); validateConfig(connectorType, config, { configurationUtilities: configUtils });
}).toThrowErrorMatchingInlineSnapshot( }).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: error configuring webhook action: target url is not present in allowedHosts"` `"error validating action type config: error validation webhook action config: target url is not present in allowedHosts"`
);
});
test('config validation fails when using disabled pfx certType', () => {
const config: Record<string, string | boolean> = {
url: 'https://mylisteningserver:9200/endpoint',
method: WebhookMethods.POST,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
hasAuth: true,
};
configurationUtilities.getWebhookSettings = jest.fn(() => ({
ssl: { pfx: { enabled: false } },
}));
expect(() => {
validateConfig(connectorType, config, { configurationUtilities });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: error validation webhook action config: certType \\"ssl-pfx\\" is disabled"`
); );
}); });
}); });

View file

@ -26,6 +26,7 @@ import {
import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer'; import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer';
import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib'; import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
import { SSLCertType } from '../../../common/auth/constants';
import type { import type {
WebhookConnectorType, WebhookConnectorType,
ActionParamsType, ActionParamsType,
@ -95,7 +96,7 @@ function validateConnectorTypeConfig(
} catch (err) { } catch (err) {
throw new Error( throw new Error(
i18n.translate('xpack.stackConnectors.webhook.configurationErrorNoHostname', { i18n.translate('xpack.stackConnectors.webhook.configurationErrorNoHostname', {
defaultMessage: 'error configuring webhook action: unable to parse url: {err}', defaultMessage: 'error validation webhook action config: unable to parse url: {err}',
values: { values: {
err: err.toString(), err: err.toString(),
}, },
@ -108,7 +109,7 @@ function validateConnectorTypeConfig(
} catch (allowListError) { } catch (allowListError) {
throw new Error( throw new Error(
i18n.translate('xpack.stackConnectors.webhook.configurationError', { i18n.translate('xpack.stackConnectors.webhook.configurationError', {
defaultMessage: 'error configuring webhook action: {message}', defaultMessage: 'error validation webhook action config: {message}',
values: { values: {
message: allowListError.message, message: allowListError.message,
}, },
@ -120,10 +121,25 @@ function validateConnectorTypeConfig(
throw new Error( throw new Error(
i18n.translate('xpack.stackConnectors.webhook.authConfigurationError', { i18n.translate('xpack.stackConnectors.webhook.authConfigurationError', {
defaultMessage: defaultMessage:
'error configuring webhook action: authType must be null or undefined if hasAuth is false', 'error validation webhook action config: authType must be null or undefined if hasAuth is false',
}) })
); );
} }
if (configObject.certType === SSLCertType.PFX) {
const webhookSettings = configurationUtilities.getWebhookSettings();
if (!webhookSettings.ssl.pfx.enabled) {
throw new Error(
i18n.translate('xpack.stackConnectors.webhook.pfxConfigurationError', {
defaultMessage:
'error validation webhook action config: certType "{certType}" is disabled',
values: {
certType: SSLCertType.PFX,
},
})
);
}
}
} }
// action executor // action executor

View file

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

View file

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

View file

@ -13,5 +13,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
loadTestFile(require.resolve('./opsgenie')); loadTestFile(require.resolve('./opsgenie'));
loadTestFile(require.resolve('./tines')); loadTestFile(require.resolve('./tines'));
loadTestFile(require.resolve('./slack')); loadTestFile(require.resolve('./slack'));
loadTestFile(require.resolve('./webhook'));
}); });
}; };

View file

@ -0,0 +1,34 @@
/*
* 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 find = getService('find');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
describe('webhook', () => {
beforeEach(async () => {
await pageObjects.common.navigateToApp('triggersActionsConnectors');
});
it('should render the cr and pfx tab for ssl auth', async () => {
await pageObjects.triggersActionsUI.clickCreateConnectorButton();
await testSubjects.click('.webhook-card');
await testSubjects.click('authSSL');
const certTypeTabs = await find.allByCssSelector(
'[data-test-subj="webhookCertTypeTabs"] > .euiTab'
);
expect(certTypeTabs.length).to.be(2);
expect(await certTypeTabs[0].getAttribute('data-test-subj')).to.be('webhookCertTypeCRTab');
expect(await certTypeTabs[1].getAttribute('data-test-subj')).to.be('webhookCertTypePFXTab');
});
});
};

View file

@ -0,0 +1,27 @@
/*
* 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 { 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 - Disabled Webhook SSL PFX',
},
kbnTestServer: {
...baseConfig.getAll().kbnTestServer,
serverArgs: [
...baseConfig.getAll().kbnTestServer.serverArgs,
`--xpack.actions.webhook.ssl.pfx.enabled=false`,
],
},
};
}

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Webhook - disabled ssl pfx', function () {
loadTestFile(require.resolve('./webhook'));
});
};

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 find = getService('find');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
describe('webhook', () => {
beforeEach(async () => {
await pageObjects.common.navigateToApp('triggersActionsConnectors');
});
it('should not render the pfx tab for ssl auth', async () => {
await pageObjects.triggersActionsUI.clickCreateConnectorButton();
await testSubjects.click('.webhook-card');
await testSubjects.click('authSSL');
const certTypeTabs = await find.allByCssSelector(
'[data-test-subj="webhookCertTypeTabs"] > .euiTab'
);
expect(certTypeTabs.length).to.be(1);
expect(await certTypeTabs[0].getAttribute('data-test-subj')).to.be('webhookCertTypeCRTab');
});
});
};