Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .buildkite/ftr_platform_stateful_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,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/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/connectors/webhook_disabled_ssl_pfx/config.ts
- x-pack/test/functional/apps/advanced_settings/config.ts
- x-pack/test/functional/apps/aiops/config.ts
- x-pack/test/functional/apps/api_keys/config.ts
Expand Down
3 changes: 3 additions & 0 deletions docs/settings/alert-action-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,9 @@ Sets the number of actions that will run ephemerally. To use this, enable
ephemeral tasks in task manager first with
<<task-manager-settings,`xpack.task_manager.ephemeral_tasks.enabled`>>

`xpack.actions.webhook.ssl.pfx.enabled` {ess-icon}::
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).

`xpack.alerting.cancelAlertsOnRuleTimeout` {ess-icon}::
Specifies whether to skip writing alerts and scheduling actions if rule
processing was cancelled due to a timeout. Default: `true`. This setting can be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ kibana_vars=(
xpack.actions.responseTimeout
xpack.actions.ssl.proxyVerificationMode
xpack.actions.ssl.verificationMode
xpack.actions.webhook.ssl.pfx.enabled
xpack.alerting.healthCheck.interval
xpack.alerting.invalidateApiKeysTask.interval
xpack.alerting.invalidateApiKeysTask.removalDelay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'vis_type_xy.readOnly (boolean?|never)',
'vis_type_vega.enableExternalUrls (boolean?)',
'xpack.actions.email.domain_allowlist (array?)',
'xpack.actions.webhook.ssl.pfx.enabled (boolean?)',
'xpack.apm.serviceMapEnabled (boolean?)',
'xpack.apm.ui.enabled (boolean?)',
'xpack.apm.ui.maxTraceItems (number?)',
Expand Down
15 changes: 15 additions & 0 deletions x-pack/platform/plugins/shared/actions/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
11 changes: 11 additions & 0 deletions x-pack/platform/plugins/shared/actions/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,37 @@ export interface ActionsPublicPluginSetup {
emails: string[],
options?: ValidateEmailAddressesOptions
): ValidatedEmail[];
isWebhookSslWithPfxEnabled?: boolean;
}

export interface Config {
email: {
domain_allowlist: string[];
};
webhook: {
ssl: {
pfx: {
enabled: boolean;
};
};
};
}

export class Plugin implements CorePlugin<ActionsPublicPluginSetup> {
private readonly allowedEmailDomains: string[] | null = null;
private readonly webhookSslWithPfxEnabled: boolean;

constructor(ctx: PluginInitializerContext<Config>) {
const config = ctx.config.get();
this.allowedEmailDomains = config.email?.domain_allowlist || null;
this.webhookSslWithPfxEnabled = config.webhook?.ssl.pfx.enabled ?? true;
}

public setup(): ActionsPublicPluginSetup {
return {
validateEmailAddresses: (emails: string[], options: ValidateEmailAddressesOptions) =>
validateEmails(this.allowedEmailDomains, emails, options),
isWebhookSslWithPfxEnabled: this.webhookSslWithPfxEnabled,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ const createActionsConfigMock = () => {
getMaxAttempts: jest.fn().mockReturnValue(3),
enableFooterInEmail: jest.fn().mockReturnValue(true),
getMaxQueued: jest.fn().mockReturnValue(1000),
getWebhookSettings: jest.fn().mockReturnValue({
ssl: {
pfx: {
enabled: true,
},
},
}),
getAwsSesConfig: jest.fn().mockReturnValue(null),
};
return mocked;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,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()', () => {
test('returns null when no email config set', () => {
const acu = getActionsConfigurationUtilities(defaultActionsConfig);
Expand Down
16 changes: 16 additions & 0 deletions x-pack/platform/plugins/shared/actions/server/actions_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export interface ActionsConfigurationUtilities {
): string | undefined;
enableFooterInEmail: () => boolean;
getMaxQueued: () => number;
getWebhookSettings(): {
ssl: {
pfx: {
enabled: boolean;
};
};
};
getAwsSesConfig: () => AwsSesConfig;
}

Expand Down Expand Up @@ -230,6 +237,15 @@ export function getActionsConfigurationUtilities(
},
enableFooterInEmail: () => config.enableFooterInEmail,
getMaxQueued: () => config.queued?.max || DEFAULT_QUEUED_MAX,
getWebhookSettings: () => {
return {
ssl: {
pfx: {
enabled: config.webhook?.ssl.pfx.enabled ?? true,
},
},
};
},
getAwsSesConfig: () => {
if (config.email?.services?.ses.host && config.email?.services?.ses.port) {
return {
Expand Down
26 changes: 26 additions & 0 deletions x-pack/platform/plugins/shared/actions/server/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,32 @@ describe('config validation', () => {
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', () => {
const config: Record<string, unknown> = {};
test('validates no email config at all', () => {
Expand Down
9 changes: 9 additions & 0 deletions x-pack/platform/plugins/shared/actions/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,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>;
Expand Down
1 change: 1 addition & 0 deletions x-pack/platform/plugins/shared/actions/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const config: PluginConfigDescriptor<ActionsConfig> = {
schema: configSchema,
exposeToBrowser: {
email: { domain_allowlist: true },
webhook: { ssl: { pfx: { enabled: true } } },
},
deprecations: ({ renameFromRoot, unused }) => [
renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ import * as i18n from './translations';

interface Props {
readOnly: boolean;
isPfxEnabled?: boolean;
}

const { emptyField } = fieldValidators;

const VERIFICATION_MODE_DEFAULT = 'full';

export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => {
export const AuthConfig: FunctionComponent<Props> = ({ readOnly, isPfxEnabled = true }) => {
const { setFieldValue, getFieldDefaultValue } = useFormContext();
const [{ config, __internal__ }] = useFormData({
watch: [
Expand Down Expand Up @@ -112,6 +113,7 @@ export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => {
readOnly={readOnly}
certTypeDefaultValue={certTypeDefaultValue}
certType={certType}
isPfxEnabled={isPfxEnabled}
/>
),
'data-test-subj': 'authSSL',
Expand Down Expand Up @@ -180,7 +182,7 @@ export const AuthConfig: FunctionComponent<Props> = ({ readOnly }) => {
onClick={() => removeItem(item.id)}
iconType="minusInCircle"
aria-label={i18n.DELETE_BUTTON}
style={{ marginTop: '28px' }}
css={{ marginTop: '28px' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,26 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SSLCertType } from '../../../common/auth/constants';
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;

jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({
useConnectorContext: jest.fn(),
}));

describe('SSLCertFields', () => {
beforeEach(() => {
(useConnectorContext as jest.Mock).mockReturnValue({
services: { isWebhookSslWithPfxEnabled: true },
});
});

afterEach(() => {
jest.clearAllMocks();
});

const onSubmit = jest.fn();

it('renders all fields for certType=CRT', async () => {
Expand Down Expand Up @@ -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();
});
});
Loading