[ResponseOps][Connectors] SSL for Cases Webhook (#185925)

Fixes #180255

## Summary

Adds API support and UI for CA and client-side SSL certificate
authentication to the Cases webhook connector.

<img width="977" alt="aux"
src="03495377-edfb-4f02-9fd1-3e0ca1d2b0fb">

This PR is to merge a feature branch into `main`.

This feature branch is composed of the following PRs:
- https://github.com/elastic/kibana/pull/183711
- https://github.com/elastic/kibana/pull/183919
- https://github.com/elastic/kibana/pull/184313

### How to test

@cnasikas kindly provided a node server that can be setup locally to
test the certificates against.

Ping me offline and i will send you the rar. You will need to configure
a connector of type `Cases Webhook connector`.

#### Configuring `Authentication`:

The project folder has two sets of keys, one for Alice and one for Bob.
The Alice keys should work and the Bob keys are expected to be
unauthorized.

- For `CRT and KEY File` use:
  - `alice_cert.pem` and `alice_key.pem` respectively (or `bob_*`)
- For `PFX File` use:
  - `alice.p12` or `bob.p12`.
- Toggle `Add certificate authority`.
- Select `Verification mode` to be `none`.

#### Configuring `Create case`:

Only the URL is relevant. It should be
`https://localhost:9999/authenticate`.

Everything else can have whatever values you want.

### Release Notes

The Cases webhook connector now supports SSL certificate authentication.
This commit is contained in:
Antonio 2024-06-18 11:45:41 +02:00 committed by GitHub
parent cf67fede6e
commit 3a2e1621f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3333 additions and 1473 deletions

View file

@ -1139,6 +1139,117 @@ Object {
"presence": "optional",
},
"keys": Object {
"authType": Object {
"flags": Object {
"default": [Function],
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"allow": Array [
"webhook-authentication-basic",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
Object {
"schema": Object {
"allow": Array [
"webhook-authentication-ssl",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"metas": Array [
Object {
"x-oas-optional": true,
},
],
"type": "alternatives",
},
"ca": Object {
"flags": Object {
"default": [Function],
"error": [Function],
"presence": "optional",
},
"metas": Array [
Object {
"x-oas-optional": true,
},
],
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
"certType": Object {
"flags": Object {
"default": [Function],
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"allow": Array [
"ssl-crt-key",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
Object {
"schema": Object {
"allow": Array [
"ssl-pfx",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"metas": Array [
Object {
"x-oas-optional": true,
},
],
"type": "alternatives",
},
"createCommentJson": Object {
"flags": Object {
"default": null,
@ -1399,7 +1510,7 @@ Object {
},
"headers": Object {
"flags": Object {
"default": [Function],
"default": null,
"error": [Function],
"presence": "optional",
},
@ -1541,6 +1652,57 @@ Object {
],
"type": "string",
},
"verificationMode": Object {
"flags": Object {
"default": [Function],
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"allow": Array [
"none",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
Object {
"schema": Object {
"allow": Array [
"certificate",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
Object {
"schema": Object {
"allow": Array [
"full",
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"metas": Array [
Object {
"x-oas-optional": true,
},
],
"type": "alternatives",
},
"viewIncidentUrl": Object {
"flags": Object {
"error": [Function],
@ -1575,6 +1737,82 @@ Object {
"presence": "optional",
},
"keys": Object {
"crt": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"key": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"password": Object {
"flags": Object {
"default": null,
@ -1613,6 +1851,44 @@ Object {
],
"type": "alternatives",
},
"pfx": Object {
"flags": Object {
"default": null,
"error": [Function],
"presence": "optional",
},
"matches": Array [
Object {
"schema": Object {
"flags": Object {
"error": [Function],
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "string",
},
},
Object {
"schema": Object {
"allow": Array [
null,
],
"flags": Object {
"error": [Function],
"only": true,
},
"type": "any",
},
},
],
"type": "alternatives",
},
"user": Object {
"flags": Object {
"default": null,
@ -1657,6 +1933,14 @@ Object {
"objects": false,
},
},
"rules": Array [
Object {
"args": Object {
"method": [Function],
},
"name": "custom",
},
],
"type": "object",
}
`;
@ -32515,7 +32799,7 @@ Object {
},
"headers": Object {
"flags": Object {
"default": [Function],
"default": null,
"error": [Function],
"presence": "optional",
},

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export enum WebhookAuthType {
export enum AuthType {
Basic = 'webhook-authentication-basic',
SSL = 'webhook-authentication-ssl',
}
@ -14,3 +14,9 @@ export enum SSLCertType {
CRT = 'ssl-crt-key',
PFX = 'ssl-pfx',
}
export enum WebhookMethods {
PATCH = 'patch',
POST = 'post',
PUT = 'put',
}

View file

@ -0,0 +1,61 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { AuthType, SSLCertType } from './constants';
export const authTypeSchema = schema.maybe(
schema.oneOf(
[schema.literal(AuthType.Basic), schema.literal(AuthType.SSL), schema.literal(null)],
{
defaultValue: AuthType.Basic,
}
)
);
export const hasAuthSchema = schema.boolean({ defaultValue: true });
export const AuthConfiguration = {
hasAuth: hasAuthSchema,
authType: authTypeSchema,
certType: schema.maybe(
schema.oneOf([schema.literal(SSLCertType.CRT), schema.literal(SSLCertType.PFX)])
),
ca: schema.maybe(schema.string()),
verificationMode: schema.maybe(
schema.oneOf([schema.literal('none'), schema.literal('certificate'), schema.literal('full')])
),
};
export const SecretConfiguration = {
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
crt: schema.nullable(schema.string()),
key: schema.nullable(schema.string()),
pfx: schema.nullable(schema.string()),
};
export const SecretConfigurationSchemaValidation = {
validate: (secrets: any) => {
// user and password must be set together (or not at all)
if (!secrets.password && !secrets.user && !secrets.crt && !secrets.key && !secrets.pfx) return;
if (secrets.password && secrets.user && !secrets.crt && !secrets.key && !secrets.pfx) return;
if (secrets.crt && secrets.key && !secrets.user && !secrets.pfx) return;
if (!secrets.crt && !secrets.key && !secrets.user && secrets.pfx) return;
return i18n.translate('xpack.stackConnectors.webhook.invalidSecrets', {
defaultMessage:
'must specify one of the following schemas: user and password; crt and key (with optional password); or pfx (with optional password)',
});
},
};
export const SecretConfigurationSchema = schema.object(
SecretConfiguration,
SecretConfigurationSchemaValidation
);

View file

@ -0,0 +1,20 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import {
AuthConfiguration,
authTypeSchema,
hasAuthSchema,
SecretConfigurationSchema,
} from './schema';
export type HasAuth = TypeOf<typeof hasAuthSchema>;
export type AuthTypeName = TypeOf<typeof authTypeSchema>;
export type SecretsConfigurationType = TypeOf<typeof SecretConfigurationSchema>;
export type CAType = TypeOf<typeof AuthConfiguration.ca>;
export type VerificationModeType = TypeOf<typeof AuthConfiguration.verificationMode>;

View file

@ -0,0 +1,209 @@
/*
* 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 { AuthType } from './constants';
import { buildConnectorAuth, isBasicAuth, validateConnectorAuthConfiguration } from './utils';
describe('utils', () => {
describe('isBasicAuth', () => {
it('returns false when hasAuth is false and authType is undefined', () => {
expect(
isBasicAuth({
hasAuth: false,
authType: undefined,
})
).toBe(false);
});
it('returns false when hasAuth is false and authType is basic', () => {
expect(
isBasicAuth({
hasAuth: false,
authType: AuthType.Basic,
})
).toBe(false);
});
it('returns false when hasAuth is true and authType is ssl', () => {
expect(
isBasicAuth({
hasAuth: true,
authType: AuthType.SSL,
})
).toBe(false);
});
it('returns true when hasAuth is true and authType is undefined', () => {
expect(
isBasicAuth({
hasAuth: true,
authType: undefined,
})
).toBe(true);
});
it('returns true when hasAuth is true and authType is basic', () => {
expect(
isBasicAuth({
hasAuth: true,
authType: AuthType.Basic,
})
).toBe(true);
});
});
describe('validateConnectorAuthConfiguration', () => {
it('does not throw with correct authType=basic params', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: AuthType.Basic,
basicAuth: { auth: { username: 'foo', password: 'bar' } },
sslOverrides: {},
connectorName: 'foobar',
})
).not.toThrow();
});
it('does not throw with correct authType=undefined params', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: undefined,
basicAuth: { auth: { username: 'foo', password: 'bar' } },
sslOverrides: {},
connectorName: 'foobar',
})
).not.toThrow();
});
it('throws when type is basic and the username is missing', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: undefined,
// @ts-ignore: that's what we are testing
basicAuth: { auth: { password: 'bar' } },
sslOverrides: {},
connectorName: 'Foobar',
})
).toThrow('[Action]Foobar: Wrong configuration.');
});
it('throws when type is basic and the password is missing', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: undefined,
// @ts-ignore: that's what we are testing
basicAuth: { auth: { username: 'foo' } },
sslOverrides: {},
connectorName: 'Foobar',
})
).toThrow('[Action]Foobar: Wrong configuration.');
});
it('does not throw with correct authType=ssl params', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: AuthType.SSL,
basicAuth: {},
sslOverrides: { verificationMode: 'none', passphrase: 'passphrase' },
connectorName: 'foobar',
})
).not.toThrow();
});
it('throws when type is SSL and the sslOverrides are missing', () => {
expect(() =>
validateConnectorAuthConfiguration({
hasAuth: true,
authType: AuthType.SSL,
basicAuth: {},
sslOverrides: {},
connectorName: 'Foobar',
})
).toThrow('[Action]Foobar: Wrong configuration.');
});
});
describe('buildConnectorAuth', () => {
it('returns empty objects when hasAuth=false', () => {
expect(
buildConnectorAuth({
hasAuth: false,
authType: AuthType.SSL,
secrets: { user: 'foo', password: 'bar', crt: null, key: null, pfx: null },
verificationMode: undefined,
ca: undefined,
})
).toEqual({ basicAuth: {}, sslOverrides: {} });
});
it('builds basicAuth correctly with authType=basic', () => {
expect(
buildConnectorAuth({
hasAuth: true,
authType: AuthType.Basic,
secrets: { user: 'foo', password: 'bar', crt: null, key: null, pfx: null },
verificationMode: undefined,
ca: undefined,
})
).toEqual({ basicAuth: { auth: { username: 'foo', password: 'bar' } }, sslOverrides: {} });
});
it('builds basicAuth correctly with hasAuth=true and authType=undefined', () => {
expect(
buildConnectorAuth({
hasAuth: true,
authType: undefined,
secrets: { user: 'foo', password: 'bar', crt: null, key: null, pfx: null },
verificationMode: undefined,
ca: undefined,
})
).toEqual({ basicAuth: { auth: { username: 'foo', password: 'bar' } }, sslOverrides: {} });
});
it('builds sslOverrides correctly with authType=ssl', () => {
expect(
buildConnectorAuth({
hasAuth: true,
authType: AuthType.SSL,
secrets: { user: 'foo', password: 'bar', crt: 'null', key: 'null', pfx: 'null' },
verificationMode: 'certificate',
ca: 'foobar?',
})
).toMatchInlineSnapshot(`
Object {
"basicAuth": Object {},
"sslOverrides": Object {
"ca": Object {
"data": Array [
126,
138,
27,
106,
],
"type": "Buffer",
},
"passphrase": "bar",
"pfx": Object {
"data": Array [
158,
233,
101,
],
"type": "Buffer",
},
"verificationMode": "certificate",
},
}
`);
});
});
});

View file

@ -0,0 +1,101 @@
/*
* 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 { isString, isEmpty } from 'lodash';
import type { SSLSettings } from '@kbn/actions-plugin/server/types';
import type {
AuthTypeName,
CAType,
HasAuth,
SecretsConfigurationType,
VerificationModeType,
} from './types';
import { AuthType } from './constants';
// For backwards compatibility with connectors created before authType was added, interpret a
// hasAuth: true and undefined authType as basic auth
export const isBasicAuth = ({
hasAuth,
authType,
}: {
hasAuth: HasAuth;
authType: AuthTypeName;
}): boolean => hasAuth && (authType === AuthType.Basic || !authType);
interface BasicAuthResponse {
auth?: { username: string; password: string };
}
export const buildConnectorAuth = ({
hasAuth,
authType,
secrets,
verificationMode,
ca,
}: {
hasAuth: HasAuth;
authType: AuthTypeName;
secrets: SecretsConfigurationType;
verificationMode: VerificationModeType;
ca: CAType;
}): { basicAuth: BasicAuthResponse; sslOverrides: SSLSettings } => {
let basicAuth: BasicAuthResponse = {};
let sslOverrides: SSLSettings = {};
let sslCertificate = {};
if (isBasicAuth({ hasAuth, authType })) {
basicAuth =
isString(secrets.user) && isString(secrets.password)
? { auth: { username: secrets.user, password: secrets.password } }
: {};
} else if (hasAuth && authType === AuthType.SSL) {
sslCertificate =
(isString(secrets.crt) && isString(secrets.key)) || isString(secrets.pfx)
? isString(secrets.pfx)
? {
pfx: Buffer.from(secrets.pfx, 'base64'),
...(isString(secrets.password) ? { passphrase: secrets.password } : {}),
}
: {
cert: Buffer.from(secrets.crt!, 'base64'),
key: Buffer.from(secrets.key!, 'base64'),
...(isString(secrets.password) ? { passphrase: secrets.password } : {}),
}
: {};
}
sslOverrides = {
...sslCertificate,
...(verificationMode ? { verificationMode } : {}),
...(ca ? { ca: Buffer.from(ca, 'base64') } : {}),
};
return { basicAuth, sslOverrides };
};
export const validateConnectorAuthConfiguration = ({
hasAuth,
authType,
basicAuth,
sslOverrides,
connectorName,
}: {
hasAuth: HasAuth;
authType: AuthTypeName;
basicAuth: BasicAuthResponse;
sslOverrides: SSLSettings;
connectorName: string;
}) => {
if (
(isBasicAuth({ hasAuth, authType }) &&
(!basicAuth.auth?.password || !basicAuth.auth?.username)) ||
(authType === AuthType.SSL && isEmpty(sslOverrides))
) {
throw Error(`[Action]${connectorName}: Wrong configuration.`);
}
};

View file

@ -0,0 +1,483 @@
/*
* 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 { AuthConfig } from './auth_config';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AuthType, SSLCertType } from '../../../common/auth/constants';
import { AuthFormTestProvider } from '../../connector_types/lib/test_utils';
describe('AuthConfig renders', () => {
const onSubmit = jest.fn();
it('renders all fields for authType=None', async () => {
const testFormData = {
config: {
hasAuth: false,
},
__internal__: {
hasCA: true,
hasHeaders: true,
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeaderText')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersKeyInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersValueInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookAddHeaderButton')).toBeInTheDocument();
expect(await screen.findByTestId('webhookViewCASwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCAInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookVerificationModeSelect')).toBeInTheDocument();
expect(await screen.findByTestId('authNone')).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
expect(screen.queryByTestId('basicAuthFields')).not.toBeInTheDocument();
expect(await screen.findByTestId('authSSL')).toBeInTheDocument();
expect(screen.queryByTestId('sslCertFields')).not.toBeInTheDocument();
});
it('toggles headers as expected', async () => {
const testFormData = {
config: {
hasAuth: false,
},
__internal__: {
hasCA: false,
hasHeaders: false,
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
const headersToggle = await screen.findByTestId('webhookViewHeadersSwitch');
expect(headersToggle).toBeInTheDocument();
userEvent.click(headersToggle);
expect(await screen.findByTestId('webhookHeaderText')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersKeyInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersValueInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookAddHeaderButton')).toBeInTheDocument();
});
it('toggles CA as expected', async () => {
const testFormData = {
config: {
hasAuth: false,
},
__internal__: {
hasCA: false,
hasHeaders: false,
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
const caToggle = await screen.findByTestId('webhookViewCASwitch');
expect(caToggle).toBeInTheDocument();
userEvent.click(caToggle);
expect(await screen.findByTestId('webhookViewCASwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCAInput')).toBeInTheDocument();
const verificationModeSelect = await screen.findByTestId('webhookVerificationModeSelect');
expect(verificationModeSelect).toBeInTheDocument();
['None', 'Certificate', 'Full'].forEach((optionName) => {
const select = within(verificationModeSelect);
expect(select.getByRole('option', { name: optionName }));
});
});
it('renders all fields for authType=Basic', async () => {
const testFormData = {
config: {
hasAuth: true,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
password: 'pass',
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookViewCASwitch')).toBeInTheDocument();
expect(await screen.findByTestId('authNone')).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
expect(await screen.findByTestId('basicAuthFields')).toBeInTheDocument();
expect(await screen.findByTestId('authSSL')).toBeInTheDocument();
expect(screen.queryByTestId('sslCertFields')).not.toBeInTheDocument();
});
it('renders all fields for authType=SSL', async () => {
const testFormData = {
config: {
hasAuth: true,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookViewCASwitch')).toBeInTheDocument();
expect(await screen.findByTestId('authNone')).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
expect(screen.queryByTestId('basicAuthFields')).not.toBeInTheDocument();
expect(await screen.findByTestId('authSSL')).toBeInTheDocument();
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
});
it('renders all fields for authType=SSL and certType=PFX', async () => {
const testFormData = {
config: {
hasAuth: true,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toBeInTheDocument();
expect(await screen.findByTestId('webhookViewCASwitch')).toBeInTheDocument();
expect(await screen.findByTestId('authNone')).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
expect(screen.queryByTestId('basicAuthFields')).not.toBeInTheDocument();
expect(await screen.findByTestId('authSSL')).toBeInTheDocument();
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
});
describe('Validation', () => {
const defaultTestFormData = {
config: {
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: true,
},
secrets: {
user: 'user',
password: 'pass',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('succeeds with hasAuth=True', async () => {
const testFormData = {
config: {
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: true,
},
secrets: {
user: 'user',
password: 'pass',
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: true,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
password: 'pass',
},
__internal__: {
hasHeaders: true,
hasCA: false,
},
},
isValid: true,
});
});
});
it('succeeds with hasAuth=false', async () => {
const testFormData = {
config: {
...defaultTestFormData.config,
hasAuth: false,
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: false,
authType: null,
},
__internal__: {
hasHeaders: true,
hasCA: false,
},
},
isValid: true,
});
});
});
it('succeeds without headers', async () => {
const testConfig = {
config: {
hasAuth: true,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
password: 'pass',
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
hasAuth: true,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
password: 'pass',
},
__internal__: {
hasHeaders: false,
hasCA: false,
},
},
isValid: true,
});
});
});
it('succeeds with CA and verificationMode', async () => {
const testConfig = {
...defaultTestFormData,
config: {
...defaultTestFormData.config,
ca: Buffer.from('some binary string').toString('base64'),
verificationMode: 'full',
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
hasAuth: true,
authType: AuthType.Basic,
ca: Buffer.from('some binary string').toString('base64'),
verificationMode: 'full',
headers: [{ key: 'content-type', value: 'text' }],
},
secrets: {
user: 'user',
password: 'pass',
},
__internal__: {
hasHeaders: true,
hasCA: true,
},
},
isValid: true,
});
});
});
it('fails with hasCa=true and a missing CA', async () => {
const testConfig = {
...defaultTestFormData,
config: {
...defaultTestFormData.config,
verificationMode: 'full',
},
__internal__: {
hasHeaders: true,
hasCA: true,
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {},
isValid: false,
});
});
});
it('succeeds with authType=SSL and a CRT and KEY', async () => {
const testConfig = {
config: {
...defaultTestFormData.config,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
hasAuth: true,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
headers: [{ key: 'content-type', value: 'text' }],
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
__internal__: {
hasHeaders: true,
hasCA: false,
},
},
isValid: true,
});
});
});
it('succeeds with authType=SSL and a PFX', async () => {
const testConfig = {
config: {
...defaultTestFormData.config,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
},
secrets: {
pfx: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<AuthConfig readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
hasAuth: true,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
headers: [{ key: 'content-type', value: 'text' }],
},
secrets: {
pfx: Buffer.from('some binary string').toString('base64'),
},
__internal__: {
hasHeaders: true,
hasCA: false,
},
},
isValid: true,
});
});
});
});
});

View file

@ -0,0 +1,292 @@
/*
* 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, { FunctionComponent, useEffect } from 'react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import {
UseArray,
UseField,
useFormContext,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import {
ToggleField,
TextField,
CardRadioGroupField,
HiddenField,
FilePickerField,
SelectField,
} from '@kbn/es-ui-shared-plugin/static/forms/components';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { AuthType, SSLCertType } from '../../../common/auth/constants';
import { SSLCertFields } from './ssl_cert_fields';
import { BasicAuthFields } from './basic_auth_fields';
import * as i18n from './translations';
interface Props {
readOnly: boolean;
hideSSL?: boolean;
}
const { emptyField } = fieldValidators;
const VERIFICATION_MODE_DEFAULT = 'full';
export const AuthConfig: FunctionComponent<Props> = ({ readOnly, hideSSL }) => {
const { setFieldValue, getFieldDefaultValue } = useFormContext();
const [{ config, __internal__ }] = useFormData({
watch: [
'config.hasAuth',
'config.authType',
'config.certType',
'config.verificationMode',
'__internal__.hasHeaders',
'__internal__.hasCA',
],
});
const authType = config == null ? AuthType.Basic : config.authType;
const certType = config == null ? SSLCertType.CRT : config.certType;
const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false;
const hasCA = __internal__ != null ? __internal__.hasCA : false;
const hasInitialCA = !!getFieldDefaultValue<boolean | undefined>('config.ca');
const hasHeadersDefaultValue = !!getFieldDefaultValue<boolean | undefined>('config.headers');
const authTypeDefaultValue =
getFieldDefaultValue('config.hasAuth') === false
? null
: getFieldDefaultValue('config.authType') ?? AuthType.Basic;
const certTypeDefaultValue: SSLCertType =
getFieldDefaultValue('config.certType') ?? SSLCertType.CRT;
const hasCADefaultValue =
!!getFieldDefaultValue<boolean | undefined>('config.ca') ||
getFieldDefaultValue('config.verificationMode') === 'none';
useEffect(() => setFieldValue('config.hasAuth', Boolean(authType)), [authType, setFieldValue]);
const hideSSLFields = hideSSL && authType !== AuthType.SSL;
const authOptions = [
{
value: null,
label: i18n.AUTHENTICATION_NONE,
'data-test-subj': 'authNone',
},
{
value: AuthType.Basic,
label: i18n.AUTHENTICATION_BASIC,
children: authType === AuthType.Basic && <BasicAuthFields readOnly={readOnly} />,
'data-test-subj': 'authBasic',
},
];
if (!hideSSLFields) {
authOptions.push({
value: AuthType.SSL,
label: i18n.AUTHENTICATION_SSL,
children: authType === AuthType.SSL && (
<SSLCertFields
readOnly={readOnly}
certTypeDefaultValue={certTypeDefaultValue}
certType={certType}
/>
),
'data-test-subj': 'authSSL',
});
}
return (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xxs">
<h4>{i18n.AUTHENTICATION_TITLE}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<UseField path="config.hasAuth" component={HiddenField} />
<UseField
path="config.authType"
defaultValue={authTypeDefaultValue}
component={CardRadioGroupField}
componentProps={{
options: authOptions,
}}
/>
<EuiSpacer size="m" />
<UseField
path="__internal__.hasHeaders"
component={ToggleField}
config={{
defaultValue: hasHeadersDefaultValue,
label: i18n.HEADERS_SWITCH,
}}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'webhookViewHeadersSwitch',
},
}}
/>
{hasHeaders && (
<UseArray path="config.headers" initialNumberOfItems={1}>
{({ items, addItem, removeItem }) => {
return (
<>
<EuiTitle size="xxs" data-test-subj="webhookHeaderText">
<h5>{i18n.HEADERS_TITLE}</h5>
</EuiTitle>
<EuiSpacer size="s" />
{items.map((item) => (
<EuiFlexGroup key={item.id}>
<EuiFlexItem>
<UseField
path={`${item.path}.key`}
config={{
label: i18n.KEY_LABEL,
}}
component={TextField}
// This is needed because when you delete
// a row and add a new one, the stale values will appear
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: { readOnly, ['data-test-subj']: 'webhookHeadersKeyInput' },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path={`${item.path}.value`}
config={{ label: i18n.VALUE_LABEL }}
component={TextField}
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: {
readOnly,
['data-test-subj']: 'webhookHeadersValueInput',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
onClick={() => removeItem(item.id)}
iconType="minusInCircle"
aria-label={i18n.DELETE_BUTTON}
style={{ marginTop: '28px' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiSpacer size="m" />
<EuiButtonEmpty
iconType="plusInCircle"
onClick={addItem}
data-test-subj="webhookAddHeaderButton"
>
{i18n.ADD_BUTTON}
</EuiButtonEmpty>
<EuiSpacer />
</>
);
}}
</UseArray>
)}
<EuiSpacer size="m" />
{!hideSSLFields && (
<>
<UseField
path="__internal__.hasCA"
component={ToggleField}
config={{ defaultValue: hasCADefaultValue, label: i18n.ADD_CA_LABEL }}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'webhookViewCASwitch',
},
}}
/>
{hasCA && (
<>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<UseField
path="config.ca"
config={{
label: 'CA file',
validations: [
{
validator:
config?.verificationMode !== 'none'
? emptyField(i18n.CA_REQUIRED)
: () => {},
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
display: 'default',
'data-test-subj': 'webhookCAInput',
accept: '.ca,.pem',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="config.verificationMode"
component={SelectField}
config={{
label: i18n.VERIFICATION_MODE_LABEL,
defaultValue: VERIFICATION_MODE_DEFAULT,
validations: [
{
validator: emptyField(i18n.VERIFICATION_MODE_LABEL),
},
],
}}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookVerificationModeSelect',
options: [
{ text: 'None', value: 'none' },
{ text: 'Certificate', value: 'certificate' },
{ text: 'Full', value: 'full' },
],
fullWidth: true,
readOnly,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{hasInitialCA && (
<>
<EuiSpacer size="s" />
<EuiCallOut size="s" iconType="document" title={i18n.EDIT_CA_CALLOUT} />
</>
)}
</>
)}
</>
)}
</>
);
};

View file

@ -0,0 +1,112 @@
/*
* 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 { BasicAuthFields } from './basic_auth_fields';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AuthFormTestProvider } from '../../connector_types/lib/test_utils';
describe('BasicAuthFields', () => {
const onSubmit = jest.fn();
it('renders all fields', async () => {
const testFormData = {
secrets: {
user: 'user',
password: 'pass',
},
};
render(
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
<BasicAuthFields readOnly={false} />
</AuthFormTestProvider>
);
expect(await screen.findByTestId('basicAuthFields')).toBeInTheDocument();
expect(await screen.findByTestId('webhookUserInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookPasswordInput')).toBeInTheDocument();
});
describe('Validation', () => {
const defaultTestFormData = {
secrets: {
user: 'user',
password: 'pass',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('validation succeeds with correct fields', async () => {
render(
<AuthFormTestProvider defaultValue={defaultTestFormData} onSubmit={onSubmit}>
<BasicAuthFields readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
secrets: {
user: 'user',
password: 'pass',
},
},
isValid: true,
});
});
});
it('validates correctly missing user', async () => {
const testConfig = {
secrets: {
user: '',
password: 'password',
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<BasicAuthFields readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
});
});
it('validates correctly missing password', async () => {
const testConfig = {
secrets: {
user: 'user',
password: '',
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<BasicAuthFields readOnly={false} />
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
});
});
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { Field, PasswordField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import * as i18n from './translations';
const { emptyField } = fieldValidators;
interface BasicAuthProps {
readOnly: boolean;
}
export const BasicAuthFields: React.FC<BasicAuthProps> = ({ readOnly }) => (
<EuiFlexGroup justifyContent="spaceBetween" data-test-subj="basicAuthFields">
<EuiFlexItem>
<UseField
path="secrets.user"
config={{
label: i18n.USERNAME,
validations: [
{
validator: emptyField(i18n.USERNAME_REQUIRED),
},
],
}}
component={Field}
componentProps={{
euiFieldProps: { readOnly, 'data-test-subj': 'webhookUserInput', fullWidth: true },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="secrets.password"
config={{
label: i18n.PASSWORD,
validations: [
{
validator: emptyField(i18n.PASSWORD_REQUIRED),
},
],
}}
component={PasswordField}
componentProps={{
euiFieldProps: { readOnly, 'data-test-subj': 'webhookPasswordInput' },
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,223 @@
/*
* 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 { SSLCertFields } from './ssl_cert_fields';
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';
const certTypeDefaultValue: SSLCertType = SSLCertType.CRT;
describe('SSLCertFields', () => {
const onSubmit = jest.fn();
it('renders all fields for certType=CRT', async () => {
render(
<AuthFormTestProvider onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={certTypeDefaultValue}
certType={SSLCertType.CRT}
/>
</AuthFormTestProvider>
);
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLPassphraseInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCertTypeTabs')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLCRTInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLKEYInput')).toBeInTheDocument();
});
it('renders all fields for certType=PFX', async () => {
render(
<AuthFormTestProvider onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={certTypeDefaultValue}
certType={SSLCertType.PFX}
/>
</AuthFormTestProvider>
);
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLPassphraseInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCertTypeTabs')).toBeInTheDocument();
expect(await screen.findByTestId('webhookSSLPFXInput')).toBeInTheDocument();
});
describe('Validation', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('validates correctly with a PFX', async () => {
const testConfig = {
config: {
certType: SSLCertType.PFX,
},
secrets: {
pfx: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={SSLCertType.PFX}
certType={SSLCertType.PFX}
/>
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
certType: SSLCertType.PFX,
},
secrets: {
pfx: Buffer.from('some binary string').toString('base64'),
},
},
isValid: true,
});
});
});
it('validates correctly a missing PFX', async () => {
const testConfig = {
config: {
certType: SSLCertType.PFX,
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={SSLCertType.PFX}
certType={SSLCertType.PFX}
/>
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {},
isValid: false,
});
});
});
it('validates correctly with a CRT and KEY', async () => {
const testConfig = {
config: {
certType: SSLCertType.CRT,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={SSLCertType.CRT}
certType={SSLCertType.CRT}
/>
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {
config: {
certType: SSLCertType.CRT,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
key: Buffer.from('some binary string').toString('base64'),
},
},
isValid: true,
});
});
});
it('validates correctly with a CRT but a missing KEY', async () => {
const testConfig = {
config: {
certType: SSLCertType.CRT,
},
secrets: {
crt: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={SSLCertType.CRT}
certType={SSLCertType.CRT}
/>
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {},
isValid: false,
});
});
});
it('validates correctly with a KEY but a missing CRT', async () => {
const testConfig = {
config: {
certType: SSLCertType.CRT,
},
secrets: {
key: Buffer.from('some binary string').toString('base64'),
},
};
render(
<AuthFormTestProvider defaultValue={testConfig} onSubmit={onSubmit}>
<SSLCertFields
readOnly={false}
certTypeDefaultValue={SSLCertType.CRT}
certType={SSLCertType.CRT}
/>
</AuthFormTestProvider>
);
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
data: {},
isValid: false,
});
});
});
});
});

View file

@ -0,0 +1,138 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { PasswordField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { FilePickerField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { SSLCertType } from '../../../common/auth/constants';
import * as i18n from './translations';
const { emptyField } = fieldValidators;
interface BasicAuthProps {
readOnly: boolean;
certTypeDefaultValue: SSLCertType;
certType: SSLCertType;
}
export const SSLCertFields: React.FC<BasicAuthProps> = ({
readOnly,
certTypeDefaultValue,
certType,
}) => (
<EuiFlexGroup justifyContent="spaceBetween" data-test-subj="sslCertFields">
<EuiFlexItem>
<UseField
path="secrets.password"
config={{
label: i18n.PASSWORD,
}}
component={PasswordField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLPassphraseInput',
readOnly,
},
}}
/>
<EuiSpacer size="s" />
<UseField
path="config.certType"
defaultValue={certTypeDefaultValue}
component={({ field }) => (
<EuiTabs size="s" data-test-subj="webhookCertTypeTabs">
<EuiTab
onClick={() => field.setValue(SSLCertType.CRT)}
isSelected={field.value === SSLCertType.CRT}
>
{i18n.CERT_TYPE_CRT_KEY}
</EuiTab>
<EuiTab
onClick={() => field.setValue(SSLCertType.PFX)}
isSelected={field.value === SSLCertType.PFX}
>
{i18n.CERT_TYPE_PFX}
</EuiTab>
</EuiTabs>
)}
/>
<EuiSpacer size="s" />
{certType === SSLCertType.CRT && (
<EuiFlexGroup wrap>
<EuiFlexItem css={{ minWidth: 200 }}>
<UseField
path="secrets.crt"
config={{
label: 'CRT file',
validations: [
{
validator: emptyField(i18n.CRT_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLCRTInput',
display: 'default',
accept: '.crt,.cert,.cer,.pem',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem css={{ minWidth: 200 }}>
<UseField
path="secrets.key"
config={{
label: 'KEY file',
validations: [
{
validator: emptyField(i18n.KEY_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLKEYInput',
display: 'default',
accept: '.key,.pem',
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
{certType === SSLCertType.PFX && (
<UseField
path="secrets.pfx"
config={{
label: 'PFX file',
validations: [
{
validator: emptyField(i18n.PFX_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLPFXInput',
display: 'default',
accept: '.pfx,.p12',
},
}}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,159 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const AUTHENTICATION_TITLE = i18n.translate(
'xpack.stackConnectors.components.auth.authenticationTitle',
{
defaultMessage: 'Authentication',
}
);
export const AUTHENTICATION_NONE = i18n.translate(
'xpack.stackConnectors.components.auth.authenticationMethodNoneLabel',
{
defaultMessage: 'None',
}
);
export const AUTHENTICATION_BASIC = i18n.translate(
'xpack.stackConnectors.components.auth.authenticationMethodBasicLabel',
{
defaultMessage: 'Basic authentication',
}
);
export const AUTHENTICATION_SSL = i18n.translate(
'xpack.stackConnectors.components.auth.authenticationMethodSSLLabel',
{
defaultMessage: 'SSL authentication',
}
);
export const USERNAME = i18n.translate('xpack.stackConnectors.components.auth.userTextFieldLabel', {
defaultMessage: 'Username',
});
export const PASSWORD = i18n.translate(
'xpack.stackConnectors.components.auth.passwordTextFieldLabel',
{
defaultMessage: 'Password',
}
);
export const USERNAME_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredAuthUserNameText',
{
defaultMessage: 'Username is required.',
}
);
export const PASSWORD_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredAuthPasswordText',
{
defaultMessage: 'Password is required.',
}
);
export const CERT_TYPE_CRT_KEY = i18n.translate(
'xpack.stackConnectors.components.auth.certTypeCrtKeyLabel',
{
defaultMessage: 'CRT and KEY file',
}
);
export const CERT_TYPE_PFX = i18n.translate(
'xpack.stackConnectors.components.auth.certTypePfxLabel',
{
defaultMessage: 'PFX file',
}
);
export const CRT_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredCRTText',
{
defaultMessage: 'CRT file is required.',
}
);
export const KEY_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredKEYText',
{
defaultMessage: 'KEY file is required.',
}
);
export const PFX_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredPFXText',
{
defaultMessage: 'PFX file is required.',
}
);
export const HEADERS_SWITCH = i18n.translate(
'xpack.stackConnectors.components.auth.viewHeadersSwitch',
{
defaultMessage: 'Add HTTP header',
}
);
export const HEADERS_TITLE = i18n.translate(
'xpack.stackConnectors.components.auth.httpHeadersTitle',
{
defaultMessage: 'Headers in use',
}
);
export const KEY_LABEL = i18n.translate('xpack.stackConnectors.components.auth.keyTextFieldLabel', {
defaultMessage: 'Key',
});
export const VALUE_LABEL = i18n.translate(
'xpack.stackConnectors.components.auth.valueTextFieldLabel',
{
defaultMessage: 'Value',
}
);
export const ADD_BUTTON = i18n.translate('xpack.stackConnectors.components.auth.addHeaderButton', {
defaultMessage: 'Add',
});
export const DELETE_BUTTON = i18n.translate(
'xpack.stackConnectors.components.auth.deleteHeaderButton',
{
defaultMessage: 'Delete',
description: 'Delete HTTP header',
}
);
export const CA_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.auth.error.requiredCAText',
{
defaultMessage: 'CA file is required.',
}
);
export const ADD_CA_LABEL = i18n.translate(
'xpack.stackConnectors.components.auth.viewCertificateAuthoritySwitch',
{
defaultMessage: 'Add certificate authority',
}
);
export const VERIFICATION_MODE_LABEL = i18n.translate(
'xpack.stackConnectors.components.auth.verificationModeFieldLabel',
{ defaultMessage: 'Verification mode' }
);
export const EDIT_CA_CALLOUT = i18n.translate(
'xpack.stackConnectors.components.auth.editCACallout',
{
defaultMessage:
'This connector has an existing certificate authority file. Upload a new one to replace it.',
}
);

View file

@ -6,25 +6,10 @@
*/
import React, { FunctionComponent } from 'react';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import {
FIELD_TYPES,
UseArray,
UseField,
useFormContext,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { Field, TextField, PasswordField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import * as i18n from '../translations';
const { emptyField } = fieldValidators;
import { EuiSpacer } from '@elastic/eui';
import { AuthConfig } from '../../../common/auth/auth_config';
interface Props {
display: boolean;
@ -32,158 +17,10 @@ interface Props {
}
export const AuthStep: FunctionComponent<Props> = ({ display, readOnly }) => {
const { getFieldDefaultValue } = useFormContext();
const [{ config, __internal__ }] = useFormData({
watch: ['config.hasAuth', '__internal__.hasHeaders'],
});
const hasHeadersDefaultValue = !!getFieldDefaultValue<boolean | undefined>('config.headers');
const hasAuth = config == null ? true : config.hasAuth;
const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false;
return (
<span data-test-subj="authStep" style={{ display: display ? 'block' : 'none' }}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xxs">
<h4>{i18n.AUTH_TITLE}</h4>
</EuiTitle>
<EuiSpacer size="m" />
<UseField
path="config.hasAuth"
component={Field}
config={{ defaultValue: true, type: FIELD_TYPES.TOGGLE }}
componentProps={{
euiFieldProps: {
label: i18n.HAS_AUTH,
disabled: readOnly,
'data-test-subj': 'hasAuthToggle',
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{hasAuth ? (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<UseField
path="secrets.user"
config={{
label: i18n.USERNAME,
validations: [
{
validator: emptyField(i18n.USERNAME_REQUIRED),
},
],
}}
component={Field}
componentProps={{
euiFieldProps: { readOnly, 'data-test-subj': 'webhookUserInput', fullWidth: true },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="secrets.password"
config={{
label: i18n.PASSWORD,
validations: [
{
validator: emptyField(i18n.PASSWORD_REQUIRED),
},
],
}}
component={PasswordField}
componentProps={{
euiFieldProps: { readOnly, 'data-test-subj': 'webhookPasswordInput' },
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<EuiSpacer size="m" />
<UseField
path="__internal__.hasHeaders"
component={Field}
config={{
defaultValue: hasHeadersDefaultValue,
label: i18n.HEADERS_SWITCH,
type: FIELD_TYPES.TOGGLE,
}}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'webhookViewHeadersSwitch',
},
}}
/>
<EuiSpacer size="m" />
{hasHeaders ? (
<UseArray path="config.headers" initialNumberOfItems={1}>
{({ items, addItem, removeItem }) => {
return (
<>
<EuiTitle size="xxs" data-test-subj="webhookHeaderText">
<h5>{i18n.HEADERS_TITLE}</h5>
</EuiTitle>
<EuiSpacer size="s" />
{items.map((item) => (
<EuiFlexGroup key={item.id}>
<EuiFlexItem>
<UseField
path={`${item.path}.key`}
config={{
label: i18n.KEY_LABEL,
}}
component={TextField}
// This is needed because when you delete
// a row and add a new one, the stale values will appear
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: { readOnly, ['data-test-subj']: 'webhookHeadersKeyInput' },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path={`${item.path}.value`}
config={{ label: i18n.VALUE_LABEL }}
component={TextField}
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: {
readOnly,
['data-test-subj']: 'webhookHeadersValueInput',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
onClick={() => removeItem(item.id)}
iconType="minusInCircle"
aria-label={i18n.DELETE_BUTTON}
style={{ marginTop: '28px' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiSpacer size="m" />
<EuiButtonEmpty
iconType="plusInCircle"
onClick={addItem}
data-test-subj="webhookAddHeaderButton"
>
{i18n.ADD_BUTTON}
</EuiButtonEmpty>
<EuiSpacer />
</>
);
}}
</UseArray>
) : null}
<AuthConfig readOnly={readOnly} hideSSL />
<EuiSpacer size="s" />
</span>
);
};

View file

@ -112,20 +112,6 @@ export const MISSING_VARIABLES = (variables: string[]) =>
values: { variableCount: variables.length, variables: variables.join(', ') },
});
export const USERNAME_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.error.requiredAuthUserNameText',
{
defaultMessage: 'Username is required.',
}
);
export const PASSWORD_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.error.requiredAuthPasswordText',
{
defaultMessage: 'Password is required.',
}
);
export const SUMMARY_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.error.requiredWebhookSummaryText',
{
@ -133,35 +119,6 @@ export const SUMMARY_REQUIRED = i18n.translate(
}
);
export const KEY_LABEL = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.keyTextFieldLabel',
{
defaultMessage: 'Key',
}
);
export const VALUE_LABEL = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.valueTextFieldLabel',
{
defaultMessage: 'Value',
}
);
export const ADD_BUTTON = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.addHeaderButton',
{
defaultMessage: 'Add',
}
);
export const DELETE_BUTTON = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.deleteHeaderButton',
{
defaultMessage: 'Delete',
description: 'Delete HTTP header',
}
);
export const CREATE_INCIDENT_METHOD = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.createIncidentMethodTextFieldLabel',
{
@ -338,41 +295,6 @@ export const HAS_AUTH = i18n.translate(
}
);
export const USERNAME = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.userTextFieldLabel',
{
defaultMessage: 'Username',
}
);
export const PASSWORD = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.passwordTextFieldLabel',
{
defaultMessage: 'Password',
}
);
export const HEADERS_SWITCH = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.viewHeadersSwitch',
{
defaultMessage: 'Add HTTP header',
}
);
export const HEADERS_TITLE = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.httpHeadersTitle',
{
defaultMessage: 'Headers in use',
}
);
export const AUTH_TITLE = i18n.translate(
'xpack.stackConnectors.components.casesWebhook.authenticationLabel',
{
defaultMessage: 'Authentication',
}
);
export const STEP_1 = i18n.translate('xpack.stackConnectors.components.casesWebhook.step1', {
defaultMessage: 'Set up connector',
});

View file

@ -7,8 +7,9 @@
import React from 'react';
import CasesWebhookActionConnectorFields from './webhook_connectors';
import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils';
import { act, render } from '@testing-library/react';
import { ConnectorFormTestProvider } from '../lib/test_utils';
import { render, screen, waitFor } from '@testing-library/react';
import { AuthType } from '../../../common/auth/constants';
import userEvent from '@testing-library/user-event';
import * as i18n from './translations';
@ -27,6 +28,7 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public', () => {
const invalidJsonTitle = `{"fields":{"summary":"wrong","description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`;
const invalidJsonBoth = `{"fields":{"summary":"wrong","description":"wrong","project":{"key":"ROC"},"issuetype":{"id":"10024"}}}`;
const config = {
authType: AuthType.Basic,
createCommentJson: '{"body":{{{case.comment}}}}',
createCommentMethod: 'post',
createCommentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}/comment',
@ -59,8 +61,8 @@ const actionConnector = {
};
describe('CasesWebhookActionConnectorFields renders', () => {
test('All inputs are properly rendered', async () => {
const { getByTestId } = render(
it('All inputs are properly rendered', async () => {
render(
<ConnectorFormTestProvider connector={actionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -69,55 +71,70 @@ describe('CasesWebhookActionConnectorFields renders', () => {
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('webhookUserInput')).toBeInTheDocument();
expect(getByTestId('webhookPasswordInput')).toBeInTheDocument();
expect(getByTestId('webhookHeadersKeyInput')).toBeInTheDocument();
expect(getByTestId('webhookHeadersValueInput')).toBeInTheDocument();
expect(getByTestId('webhookCreateMethodSelect')).toBeInTheDocument();
expect(getByTestId('webhookCreateUrlText')).toBeInTheDocument();
expect(getByTestId('webhookCreateIncidentJson')).toBeInTheDocument();
expect(getByTestId('createIncidentResponseKeyText')).toBeInTheDocument();
expect(getByTestId('getIncidentUrlInput')).toBeInTheDocument();
expect(getByTestId('getIncidentResponseExternalTitleKeyText')).toBeInTheDocument();
expect(getByTestId('viewIncidentUrlInput')).toBeInTheDocument();
expect(getByTestId('webhookUpdateMethodSelect')).toBeInTheDocument();
expect(getByTestId('updateIncidentUrlInput')).toBeInTheDocument();
expect(getByTestId('webhookUpdateIncidentJson')).toBeInTheDocument();
expect(getByTestId('webhookCreateCommentMethodSelect')).toBeInTheDocument();
expect(getByTestId('createCommentUrlInput')).toBeInTheDocument();
expect(getByTestId('webhookCreateCommentJson')).toBeInTheDocument();
});
test('Toggles work properly', async () => {
const { getByTestId, queryByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'true');
await act(async () => {
userEvent.click(getByTestId('hasAuthToggle'));
});
expect(getByTestId('hasAuthToggle')).toHaveAttribute('aria-checked', 'false');
expect(queryByTestId('webhookUserInput')).not.toBeInTheDocument();
expect(queryByTestId('webhookPasswordInput')).not.toBeInTheDocument();
expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'true');
await act(async () => {
userEvent.click(getByTestId('webhookViewHeadersSwitch'));
});
expect(getByTestId('webhookViewHeadersSwitch')).toHaveAttribute('aria-checked', 'false');
expect(queryByTestId('webhookHeadersKeyInput')).not.toBeInTheDocument();
expect(queryByTestId('webhookHeadersValueInput')).not.toBeInTheDocument();
expect(await screen.findByTestId('authNone')).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
// expect(await screen.findByTestId('authSSL')).toBeInTheDocument();
expect(await screen.findByTestId('webhookUserInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookPasswordInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersKeyInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookHeadersValueInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCreateMethodSelect')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCreateUrlText')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCreateIncidentJson')).toBeInTheDocument();
expect(await screen.findByTestId('createIncidentResponseKeyText')).toBeInTheDocument();
expect(await screen.findByTestId('getIncidentUrlInput')).toBeInTheDocument();
expect(
await screen.findByTestId('getIncidentResponseExternalTitleKeyText')
).toBeInTheDocument();
expect(await screen.findByTestId('viewIncidentUrlInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookUpdateMethodSelect')).toBeInTheDocument();
expect(await screen.findByTestId('updateIncidentUrlInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookUpdateIncidentJson')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCreateCommentMethodSelect')).toBeInTheDocument();
expect(await screen.findByTestId('createCommentUrlInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookCreateCommentJson')).toBeInTheDocument();
});
it('connector auth toggles work as expected', async () => {
render(
<ConnectorFormTestProvider connector={actionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
const authNoneToggle = await screen.findByTestId('authNone');
expect(authNoneToggle).toBeInTheDocument();
expect(await screen.findByTestId('authBasic')).toBeInTheDocument();
expect(await screen.findByTestId('webhookUserInput')).toBeInTheDocument();
expect(await screen.findByTestId('webhookPasswordInput')).toBeInTheDocument();
userEvent.click(authNoneToggle);
expect(screen.queryByTestId('webhookUserInput')).not.toBeInTheDocument();
expect(screen.queryByTestId('webhookPasswordInput')).not.toBeInTheDocument();
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toHaveAttribute(
'aria-checked',
'true'
);
userEvent.click(await screen.findByTestId('webhookViewHeadersSwitch'));
expect(await screen.findByTestId('webhookViewHeadersSwitch')).toHaveAttribute(
'aria-checked',
'false'
);
expect(screen.queryByTestId('webhookHeadersKeyInput')).not.toBeInTheDocument();
expect(screen.queryByTestId('webhookHeadersValueInput')).not.toBeInTheDocument();
});
describe('Step Validation', () => {
test('Steps work correctly when all fields valid', async () => {
const { queryByTestId, getByTestId } = render(
it('Steps work correctly when all fields valid', async () => {
render(
<ConnectorFormTestProvider connector={actionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -126,52 +143,52 @@ describe('CasesWebhookActionConnectorFields renders', () => {
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('horizontalStep1-current')).toBeInTheDocument();
expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(getByTestId('authStep')).toHaveAttribute('style', 'display: block;');
expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
expect(queryByTestId('casesWebhookBack')).not.toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep2-current')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('createStep')).toHaveAttribute('style', 'display: block;');
expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-current')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('getStep')).toHaveAttribute('style', 'display: block;');
expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-current')).toBeInTheDocument();
expect(getByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(getByTestId('updateStep')).toHaveAttribute('style', 'display: block;');
expect(queryByTestId('casesWebhookNext')).not.toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep1-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('authStep')).toHaveAttribute('style', 'display: block;');
expect(await screen.findByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
expect(screen.queryByTestId('casesWebhookBack')).not.toBeInTheDocument();
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('createStep')).toHaveAttribute('style', 'display: block;');
expect(await screen.findByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('getStep')).toHaveAttribute('style', 'display: block;');
expect(await screen.findByTestId('updateStep')).toHaveAttribute('style', 'display: none;');
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-current')).toBeInTheDocument();
expect(await screen.findByTestId('authStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('createStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('getStep')).toHaveAttribute('style', 'display: none;');
expect(await screen.findByTestId('updateStep')).toHaveAttribute('style', 'display: block;');
expect(screen.queryByTestId('casesWebhookNext')).not.toBeInTheDocument();
});
test('Step 1 is properly validated', async () => {
it('Step 1 is properly validated', async () => {
const incompleteActionConnector = {
...actionConnector,
secrets: {
@ -179,7 +196,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
password: '',
},
};
const { getByTestId } = render(
render(
<ConnectorFormTestProvider connector={incompleteActionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -188,29 +205,22 @@ describe('CasesWebhookActionConnectorFields renders', () => {
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('horizontalStep1-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep1-current')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
await waitForComponentToUpdate();
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(getByTestId('horizontalStep1-danger')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep1-danger')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('hasAuthToggle'));
userEvent.click(getByTestId('webhookViewHeadersSwitch'));
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
userEvent.click(await screen.findByTestId('authNone'));
userEvent.click(await screen.findByTestId('webhookViewHeadersSwitch'));
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(getByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep2-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep1-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-current')).toBeInTheDocument();
});
test('Step 2 is properly validated', async () => {
it('Step 2 is properly validated', async () => {
const incompleteActionConnector = {
...actionConnector,
config: {
@ -218,7 +228,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
createIncidentUrl: undefined,
},
};
const { getByText, getByTestId } = render(
render(
<ConnectorFormTestProvider connector={incompleteActionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -227,37 +237,31 @@ describe('CasesWebhookActionConnectorFields renders', () => {
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
getByText(i18n.CREATE_URL_REQUIRED);
expect(getByTestId('horizontalStep2-danger')).toBeInTheDocument();
await act(async () => {
await userEvent.type(
getByTestId('webhookCreateUrlText'),
`{selectall}{backspace}${config.createIncidentUrl}`,
{
delay: 10,
}
);
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
expect(getByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-current')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('horizontalStep2-complete'));
});
expect(getByTestId('horizontalStep2-current')).toBeInTheDocument();
expect(getByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
userEvent.click(await screen.findByTestId('casesWebhookNext'));
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByText(i18n.CREATE_URL_REQUIRED)).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-danger')).toBeInTheDocument();
await userEvent.type(
await screen.findByTestId('webhookCreateUrlText'),
`{selectall}{backspace}${config.createIncidentUrl}`,
{
delay: 10,
}
);
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByTestId('horizontalStep2-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-current')).toBeInTheDocument();
userEvent.click(await screen.findByTestId('horizontalStep2-complete'));
expect(await screen.findByTestId('horizontalStep2-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-incomplete')).toBeInTheDocument();
});
test('Step 3 is properly validated', async () => {
it('Step 3 is properly validated', async () => {
const incompleteActionConnector = {
...actionConnector,
config: {
@ -265,7 +269,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
getIncidentResponseExternalTitleKey: undefined,
},
};
const { getByText, getByTestId } = render(
render(
<ConnectorFormTestProvider connector={incompleteActionConnector}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -274,38 +278,34 @@ describe('CasesWebhookActionConnectorFields renders', () => {
/>
</ConnectorFormTestProvider>
);
await waitForComponentToUpdate();
expect(getByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
getByText(i18n.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED);
expect(getByTestId('horizontalStep3-danger')).toBeInTheDocument();
await act(async () => {
await userEvent.type(
getByTestId('getIncidentResponseExternalTitleKeyText'),
`{selectall}{backspace}${config.getIncidentResponseExternalTitleKey}`,
{
delay: 10,
}
);
});
await act(async () => {
userEvent.click(getByTestId('casesWebhookNext'));
});
expect(getByTestId('horizontalStep3-complete')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-current')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestId('horizontalStep3-complete'));
});
expect(getByTestId('horizontalStep3-current')).toBeInTheDocument();
expect(getByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep2-incomplete')).toBeInTheDocument();
userEvent.click(await screen.findByTestId('casesWebhookNext'));
userEvent.click(await screen.findByTestId('casesWebhookNext'));
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(
await screen.findByText(i18n.GET_RESPONSE_EXTERNAL_TITLE_KEY_REQUIRED)
).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep3-danger')).toBeInTheDocument();
await userEvent.type(
await screen.findByTestId('getIncidentResponseExternalTitleKeyText'),
`{selectall}{backspace}${config.getIncidentResponseExternalTitleKey}`,
{
delay: 10,
}
);
userEvent.click(await screen.findByTestId('casesWebhookNext'));
expect(await screen.findByTestId('horizontalStep3-complete')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-current')).toBeInTheDocument();
userEvent.click(await screen.findByTestId('horizontalStep3-complete'));
expect(await screen.findByTestId('horizontalStep3-current')).toBeInTheDocument();
expect(await screen.findByTestId('horizontalStep4-incomplete')).toBeInTheDocument();
});
// step 4 is not validated like the others since it is the last step
@ -346,7 +346,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
];
it('connector validation succeeds when connector config is valid', async () => {
const { getByTestId } = render(
render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -356,20 +356,20 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(getByTestId('form-test-provide-submit'));
});
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
const { isPreconfigured, ...rest } = actionConnector;
expect(onSubmit).toBeCalledWith({
data: {
...rest,
__internal__: {
hasHeaders: true,
await waitFor(() =>
expect(onSubmit).toBeCalledWith({
data: {
...rest,
__internal__: {
// hasCA: false,
hasHeaders: true,
},
},
},
isValid: true,
});
isValid: true,
})
);
});
it('connector validation succeeds when auth=false', async () => {
@ -381,7 +381,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
},
};
const { getByTestId } = render(
render(
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -391,24 +391,26 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(getByTestId('form-test-provide-submit'));
});
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
const { isPreconfigured, secrets, ...rest } = actionConnector;
expect(onSubmit).toBeCalledWith({
data: {
...rest,
config: {
...actionConnector.config,
hasAuth: false,
await waitFor(() =>
expect(onSubmit).toBeCalledWith({
data: {
...rest,
config: {
...actionConnector.config,
hasAuth: false,
authType: null,
},
__internal__: {
// hasCA: false,
hasHeaders: true,
},
},
__internal__: {
hasHeaders: true,
},
},
isValid: true,
});
isValid: true,
})
);
});
it('connector validation succeeds without headers', async () => {
@ -420,7 +422,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
},
};
const { getByTestId } = render(
render(
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -430,22 +432,23 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(getByTestId('form-test-provide-submit'));
});
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
const { isPreconfigured, ...rest } = actionConnector;
const { headers, ...rest2 } = actionConnector.config;
expect(onSubmit).toBeCalledWith({
data: {
...rest,
config: rest2,
__internal__: {
hasHeaders: false,
await waitFor(() =>
expect(onSubmit).toBeCalledWith({
data: {
...rest,
config: rest2,
__internal__: {
// hasCA: false,
hasHeaders: false,
},
},
},
isValid: true,
});
isValid: true,
})
);
});
it('validates correctly if the method is empty', async () => {
@ -457,7 +460,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
},
};
const res = render(
render(
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -467,11 +470,8 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(res.getByTestId('form-test-provide-submit'));
});
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }));
});
it.each(tests)('validates correctly %p', async (field, value) => {
@ -483,7 +483,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
},
};
const res = render(
render(
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -493,17 +493,13 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
delay: 10,
});
await userEvent.type(await screen.findByTestId(field), `{selectall}{backspace}${value}`, {
delay: 10,
});
await act(async () => {
userEvent.click(res.getByTestId('form-test-provide-submit'));
});
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }));
});
it.each(mustacheTests)(
@ -518,7 +514,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
},
};
const res = render(
render(
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
<CasesWebhookActionConnectorFields
readOnly={false}
@ -528,12 +524,11 @@ describe('CasesWebhookActionConnectorFields renders', () => {
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(res.getByTestId('form-test-provide-submit'));
});
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
expect(res.getByText(i18n.MISSING_VARIABLES(missingVariables))).toBeInTheDocument();
userEvent.click(await screen.findByTestId('form-test-provide-submit'));
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }));
expect(
await screen.findByText(i18n.MISSING_VARIABLES(missingVariables))
).toBeInTheDocument();
}
);
});

View file

@ -52,6 +52,21 @@ const ConnectorFormTestProviderComponent: React.FC<ConnectorFormTestProviderProp
ConnectorFormTestProviderComponent.displayName = 'ConnectorFormTestProvider';
export const ConnectorFormTestProvider = React.memo(ConnectorFormTestProviderComponent);
const AuthFormTestProviderComponent: React.FC<FormTestProviderProps> = ({
children,
defaultValue,
onSubmit,
}) => {
return (
<FormTestProviderComponent onSubmit={onSubmit} defaultValue={defaultValue}>
{children}
</FormTestProviderComponent>
);
};
AuthFormTestProviderComponent.displayName = 'AuthFormTestProvider';
export const AuthFormTestProvider = React.memo(AuthFormTestProviderComponent);
const FormTestProviderComponent: React.FC<FormTestProviderProps> = ({
children,
defaultValue,

View file

@ -14,13 +14,6 @@ export const METHOD_LABEL = i18n.translate(
}
);
export const HAS_AUTH_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.hasAuthSwitchLabel',
{
defaultMessage: 'Require authentication for this webhook',
}
);
export const URL_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.urlTextFieldLabel',
{
@ -28,62 +21,6 @@ export const URL_LABEL = i18n.translate(
}
);
export const USERNAME_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.userTextFieldLabel',
{
defaultMessage: 'Username',
}
);
export const PASSWORD_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.passwordTextFieldLabel',
{
defaultMessage: 'Password',
}
);
export const PASSPHRASE_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.passphraseTextFieldLabel',
{
defaultMessage: 'Passphrase',
}
);
export const ADD_HEADERS_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.viewHeadersSwitch',
{
defaultMessage: 'Add HTTP header',
}
);
export const HEADER_KEY_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.headerKeyTextFieldLabel',
{
defaultMessage: 'Key',
}
);
export const REMOVE_ITEM_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.removeHeaderIconLabel',
{
defaultMessage: 'Key',
}
);
export const ADD_HEADER_BTN = i18n.translate(
'xpack.stackConnectors.components.webhook.addHeaderButtonLabel',
{
defaultMessage: 'Add header',
}
);
export const HEADER_VALUE_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.headerValueTextFieldLabel',
{
defaultMessage: 'Value',
}
);
export const URL_INVALID = i18n.translate(
'xpack.stackConnectors.components.webhook.error.invalidUrlTextField',
{
@ -98,105 +35,9 @@ export const METHOD_REQUIRED = i18n.translate(
}
);
export const USERNAME_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredAuthUserNameText',
{
defaultMessage: 'Username is required.',
}
);
export const BODY_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookBodyText',
{
defaultMessage: 'Body is required.',
}
);
export const PASSWORD_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookPasswordText',
{
defaultMessage: 'Password is required.',
}
);
export const AUTHENTICATION_NONE = i18n.translate(
'xpack.stackConnectors.components.webhook.authenticationMethodNoneLabel',
{
defaultMessage: 'None',
}
);
export const AUTHENTICATION_BASIC = i18n.translate(
'xpack.stackConnectors.components.webhook.authenticationMethodBasicLabel',
{
defaultMessage: 'Basic authentication',
}
);
export const AUTHENTICATION_SSL = i18n.translate(
'xpack.stackConnectors.components.webhook.authenticationMethodSSLLabel',
{
defaultMessage: 'SSL authentication',
}
);
export const CERT_TYPE_CRT_KEY = i18n.translate(
'xpack.stackConnectors.components.webhook.certTypeCrtKeyLabel',
{
defaultMessage: 'CRT and KEY file',
}
);
export const CERT_TYPE_PFX = i18n.translate(
'xpack.stackConnectors.components.webhook.certTypePfxLabel',
{
defaultMessage: 'PFX file',
}
);
export const CRT_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookCRTText',
{
defaultMessage: 'CRT file is required.',
}
);
export const KEY_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookKEYText',
{
defaultMessage: 'KEY file is required.',
}
);
export const PFX_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookPFXText',
{
defaultMessage: 'PFX file is required.',
}
);
export const CA_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.webhook.error.requiredWebhookCAText',
{
defaultMessage: 'CA file is required.',
}
);
export const ADD_CA_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.viewCertificateAuthoritySwitch',
{
defaultMessage: 'Add certificate authority',
}
);
export const VERIFICATION_MODE_LABEL = i18n.translate(
'xpack.stackConnectors.components.webhook.verificationModeFieldLabel',
{ defaultMessage: 'Verification mode' }
);
export const EDIT_CA_CALLOUT = i18n.translate(
'xpack.stackConnectors.components.webhook.editCACallout',
{
defaultMessage:
'This webhook has an existing certificate authority file. Upload a new one to replace it.',
}
);

View file

@ -11,10 +11,10 @@ import WebhookActionConnectorFields from './webhook_connectors';
import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils';
import { act, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { WebhookAuthType, SSLCertType } from '../../../common/webhook/constants';
import { AuthType, SSLCertType } from '../../../common/auth/constants';
describe('WebhookActionConnectorFields renders', () => {
test('all connector fields is rendered', async () => {
it('renders all connector fields', async () => {
const actionConnector = {
actionTypeId: '.webhook',
name: 'webhook',
@ -23,7 +23,7 @@ describe('WebhookActionConnectorFields renders', () => {
url: 'https://test.com',
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: true,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
@ -104,7 +104,7 @@ describe('WebhookActionConnectorFields renders', () => {
url: 'https://test.com',
headers: [{ key: 'content-type', value: 'text' }],
hasAuth: true,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
@ -171,7 +171,7 @@ describe('WebhookActionConnectorFields renders', () => {
method: 'PUT',
url: 'https://test.com',
hasAuth: true,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
},
};
@ -197,7 +197,7 @@ describe('WebhookActionConnectorFields renders', () => {
method: 'PUT',
url: 'https://test.com',
hasAuth: true,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
},
secrets: {
user: 'user',
@ -303,7 +303,7 @@ describe('WebhookActionConnectorFields renders', () => {
method: 'PUT',
url: 'https://test.com',
hasAuth: true,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
ca: Buffer.from('some binary string').toString('base64'),
verificationMode: 'full',
headers: [{ key: 'content-type', value: 'text' }],
@ -327,7 +327,7 @@ describe('WebhookActionConnectorFields renders', () => {
...actionConnector,
config: {
...actionConnector.config,
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
},
secrets: {
@ -358,7 +358,7 @@ describe('WebhookActionConnectorFields renders', () => {
method: 'PUT',
url: 'https://test.com',
hasAuth: true,
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
headers: [{ key: 'content-type', value: 'text' }],
},
@ -381,7 +381,7 @@ describe('WebhookActionConnectorFields renders', () => {
...actionConnector,
config: {
...actionConnector.config,
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
},
secrets: {
@ -411,7 +411,7 @@ describe('WebhookActionConnectorFields renders', () => {
method: 'PUT',
url: 'https://test.com',
hasAuth: true,
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.PFX,
headers: [{ key: 'content-type', value: 'text' }],
},
@ -433,7 +433,7 @@ describe('WebhookActionConnectorFields renders', () => {
...actionConnector,
config: {
...actionConnector.config,
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
},
secrets: {

View file

@ -5,237 +5,25 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiButtonIcon,
EuiTitle,
EuiButtonEmpty,
EuiCallOut,
EuiTabs,
EuiTab,
} from '@elastic/eui';
import {
UseArray,
UseField,
useFormContext,
useFormData,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import {
Field,
SelectField,
TextField,
ToggleField,
PasswordField,
FilePickerField,
CardRadioGroupField,
HiddenField,
} from '@kbn/es-ui-shared-plugin/static/forms/components';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
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 { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import type { ActionConnectorFieldsProps } from '@kbn/triggers-actions-ui-plugin/public';
import { WebhookAuthType, SSLCertType } from '../../../common/webhook/constants';
import * as i18n from './translations';
import { AuthConfig } from '../../common/auth/auth_config';
const HTTP_VERBS = ['post', 'put'];
const { emptyField, urlField } = fieldValidators;
const VERIFICATION_MODE_DEFAULT = 'full';
const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps> = ({
readOnly,
}) => {
const { setFieldValue, getFieldDefaultValue } = useFormContext();
const [{ config, __internal__ }] = useFormData({
watch: [
'config.hasAuth',
'config.authType',
'config.certType',
'config.verificationMode',
'__internal__.hasHeaders',
'__internal__.hasCA',
],
});
const hasHeadersDefaultValue = !!getFieldDefaultValue<boolean | undefined>('config.headers');
const authTypeDefaultValue =
getFieldDefaultValue('config.hasAuth') === false
? null
: getFieldDefaultValue('config.authType') ?? WebhookAuthType.Basic;
const certTypeDefaultValue = getFieldDefaultValue('config.certType') ?? SSLCertType.CRT;
const hasCADefaultValue =
!!getFieldDefaultValue<boolean | undefined>('config.ca') ||
getFieldDefaultValue('config.verificationMode') === 'none';
const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false;
const hasCA = __internal__ != null ? __internal__.hasCA : false;
const authType = config == null ? WebhookAuthType.Basic : config.authType;
const certType = config == null ? SSLCertType.CRT : config.certType;
const hasInitialCA = !!getFieldDefaultValue<boolean | undefined>('config.ca');
useEffect(() => setFieldValue('config.hasAuth', Boolean(authType)), [authType, setFieldValue]);
const basicAuthFields = (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<UseField
path="secrets.user"
config={{
label: i18n.USERNAME_LABEL,
validations: [
{
validator: emptyField(i18n.USERNAME_REQUIRED),
},
],
}}
component={Field}
componentProps={{
euiFieldProps: {
readOnly,
'data-test-subj': 'webhookUserInput',
fullWidth: true,
},
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="secrets.password"
config={{
label: i18n.PASSWORD_LABEL,
validations: [
{
validator: emptyField(i18n.PASSWORD_REQUIRED),
},
],
}}
component={PasswordField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookPasswordInput',
readOnly,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
const sslCertAuthFields = (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<UseField
path="secrets.password"
config={{
label: i18n.PASSPHRASE_LABEL,
}}
component={PasswordField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLPassphraseInput',
readOnly,
},
}}
/>
<EuiSpacer size="s" />
<UseField
path="config.certType"
defaultValue={certTypeDefaultValue}
component={({ field }) => (
<EuiTabs size="s">
<EuiTab
onClick={() => field.setValue(SSLCertType.CRT)}
isSelected={field.value === SSLCertType.CRT}
>
{i18n.CERT_TYPE_CRT_KEY}
</EuiTab>
<EuiTab
onClick={() => field.setValue(SSLCertType.PFX)}
isSelected={field.value === SSLCertType.PFX}
>
{i18n.CERT_TYPE_PFX}
</EuiTab>
</EuiTabs>
)}
/>
<EuiSpacer size="s" />
{certType === SSLCertType.CRT && (
<EuiFlexGroup>
<EuiFlexItem>
<UseField
path="secrets.crt"
config={{
label: 'CRT file',
validations: [
{
validator: emptyField(i18n.CRT_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLCRTInput',
display: 'default',
accept: '.crt,.cert,.cer,.pem',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="secrets.key"
config={{
label: 'KEY file',
validations: [
{
validator: emptyField(i18n.KEY_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLKEYInput',
display: 'default',
accept: '.key,.pem',
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
{certType === SSLCertType.PFX && (
<UseField
path="secrets.pfx"
config={{
label: 'PFX file',
validations: [
{
validator: emptyField(i18n.PFX_REQUIRED),
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookSSLPFXInput',
display: 'default',
accept: '.pfx,.p12',
},
}}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<>
<UseField path="config.hasAuth" component={HiddenField} />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<UseField
@ -278,189 +66,8 @@ const WebhookActionConnectorFields: React.FunctionComponent<ActionConnectorField
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSpacer size="m" />
<EuiTitle size="xxs">
<h4>
<FormattedMessage
id="xpack.stackConnectors.components.webhook.authenticationLabel"
defaultMessage="Authentication"
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<UseField
path="config.authType"
defaultValue={authTypeDefaultValue}
component={CardRadioGroupField}
componentProps={{
options: [
{
value: null,
label: i18n.AUTHENTICATION_NONE,
},
{
value: WebhookAuthType.Basic,
label: i18n.AUTHENTICATION_BASIC,
children: authType === WebhookAuthType.Basic && basicAuthFields,
},
{
value: WebhookAuthType.SSL,
label: i18n.AUTHENTICATION_SSL,
children: authType === WebhookAuthType.SSL && sslCertAuthFields,
},
],
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<UseField
path="__internal__.hasHeaders"
component={ToggleField}
config={{ defaultValue: hasHeadersDefaultValue, label: i18n.ADD_HEADERS_LABEL }}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'webhookViewHeadersSwitch',
},
}}
/>
{hasHeaders ? (
<>
<EuiSpacer size="m" />
<UseArray path="config.headers" initialNumberOfItems={1}>
{({ items, addItem, removeItem }) => {
return (
<>
{items.map((item) => (
<EuiFlexGroup key={item.id}>
<EuiFlexItem>
<UseField
path={`${item.path}.key`}
config={{
label: i18n.HEADER_KEY_LABEL,
}}
component={TextField}
// This is needed because when you delete
// a row and add a new one, the stale values will appear
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: { readOnly },
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path={`${item.path}.value`}
config={{ label: i18n.HEADER_VALUE_LABEL }}
component={TextField}
readDefaultValueOnForm={!item.isNew}
componentProps={{
euiFieldProps: { readOnly },
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
onClick={() => removeItem(item.id)}
iconType="minusInCircle"
aria-label={i18n.REMOVE_ITEM_LABEL}
style={{ marginTop: '28px' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
<EuiSpacer size="m" />
<EuiButtonEmpty iconType="plusInCircle" onClick={addItem}>
{i18n.ADD_HEADER_BTN}
</EuiButtonEmpty>
<EuiSpacer />
</>
);
}}
</UseArray>
</>
) : null}
<EuiSpacer size="m" />
<UseField
path="__internal__.hasCA"
component={ToggleField}
config={{ defaultValue: hasCADefaultValue, label: i18n.ADD_CA_LABEL }}
componentProps={{
euiFieldProps: {
disabled: readOnly,
'data-test-subj': 'webhookViewCASwitch',
},
}}
/>
<EuiSpacer size="m" />
{hasCA && (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<UseField
path="config.ca"
config={{
label: 'CA file',
validations: [
{
validator:
config?.verificationMode !== 'none'
? emptyField(i18n.CA_REQUIRED)
: () => {},
},
],
}}
component={FilePickerField}
componentProps={{
euiFieldProps: {
display: 'default',
'data-test-subj': 'webhookCAInput',
accept: '.ca,.pem',
},
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="config.verificationMode"
component={SelectField}
config={{
label: i18n.VERIFICATION_MODE_LABEL,
defaultValue: VERIFICATION_MODE_DEFAULT,
validations: [
{
validator: emptyField(i18n.VERIFICATION_MODE_LABEL),
},
],
}}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookVerificationModeSelect',
options: [
{ text: 'None', value: 'none' },
{ text: 'Certificate', value: 'certificate' },
{ text: 'Full', value: 'full' },
],
fullWidth: true,
readOnly,
},
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{hasInitialCA && (
<>
<EuiSpacer size="s" />
<EuiCallOut size="s" iconType="document" title={i18n.EDIT_CA_CALLOUT} />
</>
)}
</>
)}
<AuthConfig readOnly={readOnly} />
</>
);
};

View file

@ -26,12 +26,13 @@ import {
ExternalIncidentServiceSecretConfigurationSchema,
} from './schema';
import { api } from './api';
import { validate } from './validators';
import { validateCasesWebhookConfig, validateConnector } from './validators';
import * as i18n from './translations';
const supportedSubActions: string[] = ['pushToService'];
export type ActionParamsType = CasesWebhookActionParamsType;
export const ConnectorTypeId = '.cases-webhook';
// connector type definition
export function getConnectorType(): ConnectorType<
CasesWebhookPublicConfigurationType,
@ -46,16 +47,15 @@ export function getConnectorType(): ConnectorType<
validate: {
config: {
schema: ExternalIncidentServiceConfigurationSchema,
customValidator: validate.config,
customValidator: validateCasesWebhookConfig,
},
secrets: {
schema: ExternalIncidentServiceSecretConfigurationSchema,
customValidator: validate.secrets,
},
params: {
schema: ExecutorParamsSchema,
},
connector: validate.connector,
connector: validateConnector,
},
executor,
supportedFeatureIds: [CasesConnectorFeatureId],

View file

@ -6,17 +6,17 @@
*/
import { schema } from '@kbn/config-schema';
import { CasesWebhookMethods } from './types';
import { nullableType } from '../lib/nullable';
import { WebhookMethods } from '../../../common/auth/constants';
import { AuthConfiguration, SecretConfigurationSchema } from '../../../common/auth/schema';
const HeadersSchema = schema.recordOf(schema.string(), schema.string());
export const ExternalIncidentServiceConfiguration = {
createIncidentUrl: schema.string(),
createIncidentMethod: schema.oneOf(
[schema.literal(CasesWebhookMethods.POST), schema.literal(CasesWebhookMethods.PUT)],
[schema.literal(WebhookMethods.POST), schema.literal(WebhookMethods.PUT)],
{
defaultValue: CasesWebhookMethods.POST,
defaultValue: WebhookMethods.POST,
}
),
createIncidentJson: schema.string(), // stringified object
@ -27,12 +27,12 @@ export const ExternalIncidentServiceConfiguration = {
updateIncidentUrl: schema.string(),
updateIncidentMethod: schema.oneOf(
[
schema.literal(CasesWebhookMethods.POST),
schema.literal(CasesWebhookMethods.PATCH),
schema.literal(CasesWebhookMethods.PUT),
schema.literal(WebhookMethods.POST),
schema.literal(WebhookMethods.PATCH),
schema.literal(WebhookMethods.PUT),
],
{
defaultValue: CasesWebhookMethods.PUT,
defaultValue: WebhookMethods.PUT,
}
),
updateIncidentJson: schema.string(),
@ -40,33 +40,28 @@ export const ExternalIncidentServiceConfiguration = {
createCommentMethod: schema.nullable(
schema.oneOf(
[
schema.literal(CasesWebhookMethods.POST),
schema.literal(CasesWebhookMethods.PUT),
schema.literal(CasesWebhookMethods.PATCH),
schema.literal(WebhookMethods.POST),
schema.literal(WebhookMethods.PUT),
schema.literal(WebhookMethods.PATCH),
],
{
defaultValue: CasesWebhookMethods.PUT,
defaultValue: WebhookMethods.PUT,
}
)
),
createCommentJson: schema.nullable(schema.string()),
headers: nullableType(HeadersSchema),
hasAuth: schema.boolean({ defaultValue: true }),
headers: schema.nullable(HeadersSchema),
hasAuth: AuthConfiguration.hasAuth,
authType: AuthConfiguration.authType,
certType: AuthConfiguration.certType,
ca: AuthConfiguration.ca,
verificationMode: AuthConfiguration.verificationMode,
};
export const ExternalIncidentServiceConfigurationSchema = schema.object(
ExternalIncidentServiceConfiguration
);
export const ExternalIncidentServiceSecretConfiguration = {
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
};
export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
ExternalIncidentServiceSecretConfiguration
);
export const ExecutorSubActionPushParamsSchema = schema.object({
incident: schema.object({
title: schema.string(),
@ -93,3 +88,5 @@ export const ExecutorParamsSchema = schema.oneOf([
subActionParams: ExecutorSubActionPushParamsSchema,
}),
]);
export const ExternalIncidentServiceSecretConfigurationSchema = SecretConfigurationSchema;

View file

@ -12,6 +12,7 @@ import { renderMustacheStringNoEscape } from '@kbn/actions-plugin/server/lib/mus
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
import { buildConnectorAuth, validateConnectorAuthConfiguration } from '../../../common/auth/utils';
import { validateAndNormalizeUrl, validateJson } from './validators';
import {
createServiceError,
@ -20,12 +21,11 @@ import {
removeSlash,
throwDescriptiveErrorIfResponseIsNotValid,
} from './utils';
import {
import type {
CreateIncidentParams,
ExternalServiceCredentials,
ExternalService,
CasesWebhookPublicConfigurationType,
CasesWebhookSecretConfigurationType,
ExternalServiceIncidentResponse,
GetIncidentResponse,
UpdateIncidentParams,
@ -51,31 +51,41 @@ export const createExternalService = (
getIncidentResponseExternalTitleKey,
getIncidentUrl,
hasAuth,
authType,
headers,
viewIncidentUrl,
updateIncidentJson,
updateIncidentMethod,
updateIncidentUrl,
verificationMode,
ca,
} = config as CasesWebhookPublicConfigurationType;
const { password, user } = secrets as CasesWebhookSecretConfigurationType;
if (
!getIncidentUrl ||
!createIncidentUrlConfig ||
!viewIncidentUrl ||
!updateIncidentUrl ||
(hasAuth && (!password || !user))
) {
const { basicAuth, sslOverrides } = buildConnectorAuth({
hasAuth,
authType,
secrets,
verificationMode,
ca,
});
validateConnectorAuthConfiguration({
hasAuth,
authType,
basicAuth,
sslOverrides,
connectorName: i18n.NAME,
});
if (!getIncidentUrl || !createIncidentUrlConfig || !viewIncidentUrl || !updateIncidentUrl) {
throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
}
const createIncidentUrl = removeSlash(createIncidentUrlConfig);
const headersWithBasicAuth = hasAuth
? combineHeadersWithBasicAuthHeader({
username: user ?? undefined,
password: password ?? undefined,
headers,
})
: {};
const headersWithBasicAuth = combineHeadersWithBasicAuthHeader({
username: basicAuth.auth?.username,
password: basicAuth.auth?.password,
headers,
});
const axiosInstance = axios.create({
headers: {
@ -84,6 +94,8 @@ export const createExternalService = (
},
});
const createIncidentUrl = removeSlash(createIncidentUrlConfig);
const getIncident = async (id: string): Promise<GetIncidentResponse> => {
try {
const getUrl = renderMustacheStringNoEscape(getIncidentUrl, {
@ -104,6 +116,7 @@ export const createExternalService = (
url: normalizedUrl,
logger,
configurationUtilities,
sslOverrides,
});
throwDescriptiveErrorIfResponseIsNotValid({
@ -148,6 +161,7 @@ export const createExternalService = (
method: createIncidentMethod,
data: json,
configurationUtilities,
sslOverrides,
});
const { status, statusText, data } = res;
@ -231,6 +245,7 @@ export const createExternalService = (
logger,
data: json,
configurationUtilities,
sslOverrides,
});
throwDescriptiveErrorIfResponseIsNotValid({
@ -303,6 +318,7 @@ export const createExternalService = (
logger,
data: json,
configurationUtilities,
sslOverrides,
});
throwDescriptiveErrorIfResponseIsNotValid({

View file

@ -28,12 +28,9 @@ export const CONFIG_ERR = (err: string) =>
},
});
export const INVALID_USER_PW = i18n.translate(
'xpack.stackConnectors.casesWebhook.invalidUsernamePassword',
{
defaultMessage: 'both user and password must be specified',
}
);
export const INVALID_AUTH = i18n.translate('xpack.stackConnectors.casesWebhook.invalidSecrets', {
defaultMessage: 'must specify a secrets configuration',
});
export const ALLOWED_HOSTS_ERROR = (message: string) =>
i18n.translate('xpack.stackConnectors.casesWebhook.configuration.apiAllowedHostsError', {

View file

@ -7,7 +7,6 @@
import { TypeOf } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
import {
ExecutorParamsSchema,
ExecutorSubActionPushParamsSchema,
@ -15,13 +14,6 @@ import {
ExternalIncidentServiceSecretConfigurationSchema,
} from './schema';
// config definition
export enum CasesWebhookMethods {
PATCH = 'patch',
POST = 'post',
PUT = 'put',
}
// config
export type CasesWebhookPublicConfigurationType = TypeOf<
typeof ExternalIncidentServiceConfigurationSchema
@ -38,21 +30,6 @@ export interface ExternalServiceCredentials {
secrets: CasesWebhookSecretConfigurationType;
}
export interface ExternalServiceValidation {
config: (
configObject: CasesWebhookPublicConfigurationType,
validatorServices: ValidatorServices
) => void;
secrets: (
secrets: CasesWebhookSecretConfigurationType,
validatorServices: ValidatorServices
) => void;
connector: (
configObject: CasesWebhookPublicConfigurationType,
secrets: CasesWebhookSecretConfigurationType
) => string | null;
}
export interface ExternalServiceIncidentResponse {
id: string;
title: string;

View file

@ -7,14 +7,11 @@
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
import { isEmpty } from 'lodash';
import * as i18n from './translations';
import {
CasesWebhookPublicConfigurationType,
CasesWebhookSecretConfigurationType,
ExternalServiceValidation,
} from './types';
import { CasesWebhookPublicConfigurationType, CasesWebhookSecretConfigurationType } from './types';
const validateConfig = (
export const validateCasesWebhookConfig = (
configObject: CasesWebhookPublicConfigurationType,
validatorServices: ValidatorServices
) => {
@ -55,26 +52,8 @@ export const validateConnector = (
configObject: CasesWebhookPublicConfigurationType,
secrets: CasesWebhookSecretConfigurationType
): string | null => {
// user and password must be set together (or not at all)
if (!configObject.hasAuth) return null;
if (secrets.password && secrets.user) return null;
return i18n.INVALID_USER_PW;
};
export const validateSecrets = (
secrets: CasesWebhookSecretConfigurationType,
validatorServices: ValidatorServices
) => {
// user and password must be set together (or not at all)
if (!secrets.password && !secrets.user) return;
if (secrets.password && secrets.user) return;
throw new Error(i18n.INVALID_USER_PW);
};
export const validate: ExternalServiceValidation = {
config: validateConfig,
secrets: validateSecrets,
connector: validateConnector,
if (configObject.hasAuth && isEmpty(secrets)) return i18n.INVALID_AUTH;
return null;
};
const validProtocols: string[] = ['http:', 'https:'];

View file

@ -58,7 +58,7 @@ export type { SlackApiActionParams as SlackApiActionParams } from '../../common/
export { ConnectorTypeId as TeamsConnectorTypeId } from './teams';
export type { ActionParamsType as TeamsActionParams } from './teams';
export { ConnectorTypeId as WebhookConnectorTypeId } from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook/types';
export { ConnectorTypeId as XmattersConnectorTypeId } from './xmatters';
export type { ActionParamsType as XmattersActionParams } from './xmatters';

View file

@ -1,14 +0,0 @@
/*
* 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 { schema, Type } from '@kbn/config-schema';
// TODO: remove once this is merged: https://github.com/elastic/kibana/pull/41728
export function nullableType<V>(type: Type<V>) {
return schema.oneOf([type, schema.literal(null)], { defaultValue: () => null });
}

View file

@ -12,18 +12,14 @@ import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/action
import { Logger } from '@kbn/core/server';
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import axios from 'axios';
import {
ConnectorTypeConfigType,
ConnectorTypeSecretsType,
getConnectorType,
WebhookConnectorType,
WebhookMethods,
} from '.';
import { ConnectorTypeConfigType, ConnectorTypeSecretsType, WebhookConnectorType } from './types';
import { getConnectorType } from '.';
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';
import { loggerMock } from '@kbn/logging-mocks';
import { SSLCertType, WebhookAuthType } from '../../../common/webhook/constants';
import { PFX_FILE, CRT_FILE, KEY_FILE } from './mocks';
import { AuthType, SSLCertType, WebhookMethods } from '../../../common/auth/constants';
import { PFX_FILE, CRT_FILE, KEY_FILE } from '../../../common/auth/mocks';
jest.mock('axios');
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
@ -157,7 +153,7 @@ describe('config validation', () => {
test('config validation passes when only required fields are provided', () => {
const config: Record<string, string | boolean> = {
url: 'http://mylisteningserver:9200/endpoint',
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
expect(validateConfig(connectorType, config, { configurationUtilities })).toEqual({
@ -171,7 +167,7 @@ describe('config validation', () => {
const config: Record<string, string | boolean> = {
url: 'http://mylisteningserver:9200/endpoint',
method,
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
expect(validateConfig(connectorType, config, { configurationUtilities })).toEqual({
@ -198,7 +194,7 @@ describe('config validation', () => {
test('config validation passes when a url is specified', () => {
const config: Record<string, string | boolean> = {
url: 'http://mylisteningserver:9200/endpoint',
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
expect(validateConfig(connectorType, config, { configurationUtilities })).toEqual({
@ -226,7 +222,7 @@ describe('config validation', () => {
headers: {
'Content-Type': 'application/json',
},
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
expect(validateConfig(connectorType, config, { configurationUtilities })).toEqual({
@ -257,7 +253,7 @@ describe('config validation', () => {
headers: {
'Content-Type': 'application/json',
},
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
@ -332,7 +328,7 @@ describe('execute()', () => {
headers: {
aheader: 'a value',
},
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
await connectorType.executor({
@ -392,7 +388,7 @@ describe('execute()', () => {
headers: {
aheader: 'a value',
},
authType: WebhookAuthType.SSL,
authType: AuthType.SSL,
certType: SSLCertType.CRT,
hasAuth: true,
};
@ -575,7 +571,7 @@ describe('execute()', () => {
headers: {
aheader: 'a value',
},
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
};
requestMock.mockReset();

View file

@ -6,18 +6,16 @@
*/
import { i18n } from '@kbn/i18n';
import { isString } from 'lodash';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from '@kbn/core/server';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, getOrElse } from 'fp-ts/lib/Option';
import type {
ActionType as ConnectorType,
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
ActionTypeExecutorResult as ConnectorTypeExecutorResult,
ValidatorServices,
} from '@kbn/actions-plugin/server/types';
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
import {
AlertingConnectorFeatureId,
@ -26,90 +24,23 @@ import {
} from '@kbn/actions-plugin/common/types';
import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer';
import { combineHeadersWithBasicAuthHeader } from '@kbn/actions-plugin/server/lib';
import { SSLCertType, WebhookAuthType } from '../../../common/webhook/constants';
import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header';
import { nullableType } from '../lib/nullable';
import { isOk, promiseResult, Result } from '../lib/result_type';
// config definition
export enum WebhookMethods {
POST = 'post',
PUT = 'put',
}
export type WebhookConnectorType = ConnectorType<
ConnectorTypeConfigType,
ConnectorTypeSecretsType,
import type {
WebhookConnectorType,
ActionParamsType,
unknown
>;
export type WebhookConnectorTypeExecutorOptions = ConnectorTypeExecutorOptions<
ConnectorTypeConfigType,
WebhookConnectorTypeExecutorOptions,
ConnectorTypeSecretsType,
ActionParamsType
>;
} from './types';
const HeadersSchema = schema.recordOf(schema.string(), schema.string());
const configSchemaProps = {
url: schema.string(),
method: schema.oneOf([schema.literal(WebhookMethods.POST), schema.literal(WebhookMethods.PUT)], {
defaultValue: WebhookMethods.POST,
}),
headers: nullableType(HeadersSchema),
hasAuth: schema.boolean({ defaultValue: true }),
authType: schema.maybe(
schema.oneOf(
[
schema.literal(WebhookAuthType.Basic),
schema.literal(WebhookAuthType.SSL),
schema.literal(null),
],
{
defaultValue: WebhookAuthType.Basic,
}
)
),
certType: schema.maybe(
schema.oneOf([schema.literal(SSLCertType.CRT), schema.literal(SSLCertType.PFX)])
),
ca: schema.maybe(schema.string()),
verificationMode: schema.maybe(
schema.oneOf([schema.literal('none'), schema.literal('certificate'), schema.literal('full')])
),
};
const ConfigSchema = schema.object(configSchemaProps);
export type ConnectorTypeConfigType = TypeOf<typeof ConfigSchema>;
// secrets definition
export type ConnectorTypeSecretsType = TypeOf<typeof SecretsSchema>;
const secretSchemaProps = {
user: schema.nullable(schema.string()),
password: schema.nullable(schema.string()),
crt: schema.nullable(schema.string()),
key: schema.nullable(schema.string()),
pfx: schema.nullable(schema.string()),
};
const SecretsSchema = schema.object(secretSchemaProps, {
validate: (secrets) => {
// user and password must be set together (or not at all)
if (!secrets.password && !secrets.user && !secrets.crt && !secrets.key && !secrets.pfx) return;
if (secrets.password && secrets.user && !secrets.crt && !secrets.key && !secrets.pfx) return;
if (secrets.crt && secrets.key && !secrets.user && !secrets.pfx) return;
if (!secrets.crt && !secrets.key && !secrets.user && secrets.pfx) return;
return i18n.translate('xpack.stackConnectors.webhook.invalidUsernamePassword', {
defaultMessage:
'must specify one of the following schemas: user and password; crt and key (with optional password); or pfx (with optional password)',
});
},
});
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;
export const ParamsSchema = schema.object({
body: schema.maybe(schema.string()),
});
import { getRetryAfterIntervalFromHeaders } from '../lib/http_response_retry_header';
import { isOk, promiseResult, Result } from '../lib/result_type';
import { ConfigSchema, ParamsSchema } from './schema';
import { buildConnectorAuth } from '../../../common/auth/utils';
import { SecretConfigurationSchema } from '../../../common/auth/schema';
export const ConnectorTypeId = '.webhook';
// connector type definition
export function getConnectorType(): WebhookConnectorType {
return {
@ -129,7 +60,7 @@ export function getConnectorType(): WebhookConnectorType {
customValidator: validateConnectorTypeConfig,
},
secrets: {
schema: SecretsSchema,
schema: SecretConfigurationSchema,
},
params: {
schema: ParamsSchema,
@ -202,39 +133,16 @@ export async function executor(
const { body: data } = params;
const secrets: ConnectorTypeSecretsType = execOptions.secrets;
// For backwards compatibility with connectors created before authType was added, interpret a
// hasAuth: true and undefined authType as basic auth
const basicAuth =
hasAuth &&
(authType === WebhookAuthType.Basic || !authType) &&
isString(secrets.user) &&
isString(secrets.password)
? { auth: { username: secrets.user, password: secrets.password } }
: {};
const sslCertificate =
authType === WebhookAuthType.SSL &&
((isString(secrets.crt) && isString(secrets.key)) || isString(secrets.pfx))
? isString(secrets.pfx)
? {
pfx: Buffer.from(secrets.pfx, 'base64'),
...(isString(secrets.password) ? { passphrase: secrets.password } : {}),
}
: {
cert: Buffer.from(secrets.crt!, 'base64'),
key: Buffer.from(secrets.key!, 'base64'),
...(isString(secrets.password) ? { passphrase: secrets.password } : {}),
}
: {};
const { basicAuth, sslOverrides } = buildConnectorAuth({
hasAuth,
authType,
secrets,
verificationMode,
ca,
});
const axiosInstance = axios.create();
const sslOverrides = {
...sslCertificate,
...(verificationMode ? { verificationMode } : {}),
...(ca ? { ca: Buffer.from(ca, 'base64') } : {}),
};
const headersWithBasicAuth = combineHeadersWithBasicAuthHeader({
username: basicAuth.auth?.username,
password: basicAuth.auth?.password,

View file

@ -0,0 +1,32 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { AuthConfiguration } from '../../../common/auth/schema';
import { WebhookMethods } from '../../../common/auth/constants';
export const HeadersSchema = schema.recordOf(schema.string(), schema.string());
const configSchemaProps = {
url: schema.string(),
method: schema.oneOf([schema.literal(WebhookMethods.POST), schema.literal(WebhookMethods.PUT)], {
defaultValue: WebhookMethods.POST,
}),
headers: schema.nullable(HeadersSchema),
hasAuth: AuthConfiguration.hasAuth,
authType: AuthConfiguration.authType,
certType: AuthConfiguration.certType,
ca: AuthConfiguration.ca,
verificationMode: AuthConfiguration.verificationMode,
};
export const ConfigSchema = schema.object(configSchemaProps);
// params definition
export const ParamsSchema = schema.object({
body: schema.maybe(schema.string()),
});

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 { TypeOf } from '@kbn/config-schema';
import type {
ActionType as ConnectorType,
ActionTypeExecutorOptions as ConnectorTypeExecutorOptions,
} from '@kbn/actions-plugin/server/types';
import { ParamsSchema, ConfigSchema } from './schema';
import { SecretConfigurationSchema } from '../../../common/auth/schema';
export type WebhookConnectorType = ConnectorType<
ConnectorTypeConfigType,
ConnectorTypeSecretsType,
ActionParamsType,
unknown
>;
export type WebhookConnectorTypeExecutorOptions = ConnectorTypeExecutorOptions<
ConnectorTypeConfigType,
ConnectorTypeSecretsType,
ActionParamsType
>;
export type ConnectorTypeConfigType = TypeOf<typeof ConfigSchema>;
// secrets definition
export type ConnectorTypeSecretsType = TypeOf<typeof SecretConfigurationSchema>;
// params definition
export type ActionParamsType = TypeOf<typeof ParamsSchema>;

View file

@ -10,7 +10,7 @@ import { SlackApiParamsSchema } from '../common/slack_api/schema';
export { ParamsSchema as SlackParamsSchema } from './connector_types/slack';
export { ParamsSchema as EmailParamsSchema } from './connector_types/email';
export { ParamsSchema as WebhookParamsSchema } from './connector_types/webhook';
export { ParamsSchema as WebhookParamsSchema } from './connector_types/webhook/schema';
export { ExecutorParamsSchema as JiraParamsSchema } from './connector_types/jira/schema';
export { ParamsSchema as PagerdutyParamsSchema } from './connector_types/pagerduty';

View file

@ -39312,7 +39312,6 @@
"xpack.stackConnectors.xmatters.invalidUrlError": "secretsUrl non valide : {err}",
"xpack.stackConnectors.xmatters.postingRetryErrorMessage": "Erreur lors du déclenchement du flux xMatters : statut http {status}, réessayer plus tard",
"xpack.stackConnectors.xmatters.unexpectedStatusErrorMessage": "Erreur de déclenchement du flux xMatters : statut inattendu {status}",
"xpack.stackConnectors.casesWebhook.invalidUsernamePassword": "l'utilisateur et le mot de passe doivent être spécifiés",
"xpack.stackConnectors.casesWebhook.title": "Webhook - Gestion des cas",
"xpack.stackConnectors.components.bedrock.accessKeySecret": "Clé d'accès",
"xpack.stackConnectors.components.bedrock.apiUrlTextFieldLabel": "URL",
@ -39327,9 +39326,7 @@
"xpack.stackConnectors.components.bedrock.selectMessageText": "Envoyer une requête à Amazon Bedrock.",
"xpack.stackConnectors.components.bedrock.title": "Amazon Bedrock",
"xpack.stackConnectors.components.bedrock.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.casesWebhook.addHeaderButton": "Ajouter",
"xpack.stackConnectors.components.casesWebhook.addVariable": "Ajouter une variable",
"xpack.stackConnectors.components.casesWebhook.authenticationLabel": "Authentification",
"xpack.stackConnectors.components.casesWebhook.caseCommentDesc": "Commentaire de cas Kibana",
"xpack.stackConnectors.components.casesWebhook.caseDescriptionDesc": "Description de cas Kibana",
"xpack.stackConnectors.components.casesWebhook.caseIdDesc": "ID de cas Kibana",
@ -39352,11 +39349,8 @@
"xpack.stackConnectors.components.casesWebhook.createIncidentResponseKeyTextFieldLabel": "Clé externe dans la réponse de création d'un cas",
"xpack.stackConnectors.components.casesWebhook.createIncidentUrlTextFieldLabel": "URL de création d'un cas",
"xpack.stackConnectors.components.casesWebhook.criticalLabel": "Critique",
"xpack.stackConnectors.components.casesWebhook.deleteHeaderButton": "Supprimer",
"xpack.stackConnectors.components.casesWebhook.descriptionTextAreaFieldLabel": "Description",
"xpack.stackConnectors.components.casesWebhook.docLink": "Configuration de Webhook - Connecteur de gestion des cas.",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthPasswordText": "Le mot de passe est requis.",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentIncidentText": "L'objet de création de commentaire doit être un JSON valide.",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentMethodText": "La méthode de création de commentaire est requise.",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentUrlText": "L'URL de création de commentaire doit être au format URL.",
@ -39381,15 +39375,12 @@
"xpack.stackConnectors.components.casesWebhook.getIncidentUrlTextFieldLabel": "URL d'obtention de cas",
"xpack.stackConnectors.components.casesWebhook.hasAuthSwitchLabel": "Demander une authentification pour ce webhook",
"xpack.stackConnectors.components.casesWebhook.highLabel": "Élevé",
"xpack.stackConnectors.components.casesWebhook.httpHeadersTitle": "En-têtes utilisés",
"xpack.stackConnectors.components.casesWebhook.idFieldLabel": "ID de cas",
"xpack.stackConnectors.components.casesWebhook.jsonCodeEditorAriaLabel": "Éditeur de code",
"xpack.stackConnectors.components.casesWebhook.jsonFieldLabel": "JSON",
"xpack.stackConnectors.components.casesWebhook.keyTextFieldLabel": "Clé",
"xpack.stackConnectors.components.casesWebhook.lowLabel": "Bas",
"xpack.stackConnectors.components.casesWebhook.mediumLabel": "Moyenne",
"xpack.stackConnectors.components.casesWebhook.next": "Suivant",
"xpack.stackConnectors.components.casesWebhook.passwordTextFieldLabel": "Mot de passe",
"xpack.stackConnectors.components.casesWebhook.previous": "Précédent",
"xpack.stackConnectors.components.casesWebhook.selectMessageText": "Envoyer une requête à un service web de gestion de cas.",
"xpack.stackConnectors.components.casesWebhook.severityFieldLabel": "Sévérité",
@ -39414,9 +39405,6 @@
"xpack.stackConnectors.components.casesWebhook.updateIncidentMethodTextFieldLabel": "Méthode de mise à jour de cas",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlHelp": "URL d'API pour mettre à jour un cas.",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlTextFieldLabel": "URL de mise à jour du cas",
"xpack.stackConnectors.components.casesWebhook.userTextFieldLabel": "Nom d'utilisateur",
"xpack.stackConnectors.components.casesWebhook.valueTextFieldLabel": "Valeur",
"xpack.stackConnectors.components.casesWebhook.viewHeadersSwitch": "Ajouter un en-tête HTTP",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlHelp": "URL pour afficher un cas dans le système externe. Utilisez le sélecteur de variable pour ajouter à l'URL l'ID ou le titre du système externe.",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlTextFieldLabel": "URL de visualisation de cas externe",
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - Données de gestion des cas",
@ -39766,39 +39754,15 @@
"xpack.stackConnectors.components.teams.messageTextAreaFieldLabel": "Message",
"xpack.stackConnectors.components.teams.selectMessageText": "Envoyer un message à un canal Microsoft Teams.",
"xpack.stackConnectors.components.teams.webhookUrlHelpLabel": "Créer une URL de webhook Microsoft Teams",
"xpack.stackConnectors.components.webhook.addHeaderButtonLabel": "Ajouter un en-tête",
"xpack.stackConnectors.components.webhook.authenticationLabel": "Authentification",
"xpack.stackConnectors.components.webhook.authenticationMethodBasicLabel": "Authentification de base",
"xpack.stackConnectors.components.webhook.authenticationMethodNoneLabel": "Aucun",
"xpack.stackConnectors.components.webhook.authenticationMethodSSLLabel": "Authentification SSL",
"xpack.stackConnectors.components.webhook.bodyCodeEditorAriaLabel": "Éditeur de code",
"xpack.stackConnectors.components.webhook.bodyFieldLabel": "Corps",
"xpack.stackConnectors.components.webhook.certTypeCrtKeyLabel": "Fichier CRT et KEY",
"xpack.stackConnectors.components.webhook.certTypePfxLabel": "Fichier PFX",
"xpack.stackConnectors.components.webhook.connectorTypeTitle": "Données de webhook",
"xpack.stackConnectors.components.webhook.editCACallout": "Ce webhook comporte déjà un fichier d'autorité de certificat. Charger un nouveau pour le remplacer.",
"xpack.stackConnectors.components.webhook.error.invalidUrlTextField": "L'URL n'est pas valide.",
"xpack.stackConnectors.components.webhook.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.",
"xpack.stackConnectors.components.webhook.error.requiredMethodText": "La méthode est requise.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookBodyText": "Le corps est requis.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCAText": "Le fichier CA est requis.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCRTText": "Le fichier CRT est requis.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookKEYText": "Le fichier KEY est requis.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPasswordText": "Le mot de passe est requis.",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPFXText": "Le fichier PFX est requis.",
"xpack.stackConnectors.components.webhook.hasAuthSwitchLabel": "Demander une authentification pour ce webhook",
"xpack.stackConnectors.components.webhook.headerKeyTextFieldLabel": "Clé",
"xpack.stackConnectors.components.webhook.headerValueTextFieldLabel": "Valeur",
"xpack.stackConnectors.components.webhook.methodTextFieldLabel": "Méthode",
"xpack.stackConnectors.components.webhook.passphraseTextFieldLabel": "Phrase secrète",
"xpack.stackConnectors.components.webhook.passwordTextFieldLabel": "Mot de passe",
"xpack.stackConnectors.components.webhook.removeHeaderIconLabel": "Clé",
"xpack.stackConnectors.components.webhook.selectMessageText": "Envoyer une requête à un service Web.",
"xpack.stackConnectors.components.webhook.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.webhook.userTextFieldLabel": "Nom d'utilisateur",
"xpack.stackConnectors.components.webhook.verificationModeFieldLabel": "Mode de vérification",
"xpack.stackConnectors.components.webhook.viewCertificateAuthoritySwitch": "Ajouter une autorité de certificat",
"xpack.stackConnectors.components.webhook.viewHeadersSwitch": "Ajouter un en-tête HTTP",
"xpack.stackConnectors.components.xmatters.authenticationLabel": "Authentification",
"xpack.stackConnectors.components.xmatters.basicAuthButtonGroupLegend": "Authentification de base",
"xpack.stackConnectors.components.xmatters.basicAuthLabel": "Authentification de base",
@ -39975,7 +39939,6 @@
"xpack.stackConnectors.webhook.authConfigurationError": "erreur lors de la configuration d'action webhook : authType doit être null ou undefined si hasAuth est faux",
"xpack.stackConnectors.webhook.invalidResponseErrorMessage": "erreur lors de l'appel de webhook, réponse non valide",
"xpack.stackConnectors.webhook.invalidResponseRetryLaterErrorMessage": "erreur lors de l'appel de webhook, réessayer ultérieurement",
"xpack.stackConnectors.webhook.invalidUsernamePassword": "doit préciser l'un des schémas suivants : utilisateur et mot de passe, crt et key (avec mot de passe facultatif) ou pfx (avec mot de passe facultatif)",
"xpack.stackConnectors.webhook.requestFailedErrorMessage": "erreur lors de l'appel de webhook, requête échouée",
"xpack.stackConnectors.webhook.title": "Webhook",
"xpack.stackConnectors.webhook.unexpectedNullResponseErrorMessage": "réponse nulle inattendue de webhook",

View file

@ -39284,7 +39284,6 @@
"xpack.stackConnectors.xmatters.invalidUrlError": "無効なsecretsUrl{err}",
"xpack.stackConnectors.xmatters.postingRetryErrorMessage": "xMattersフローのトリガーエラーHTTPステータス{status}。しばらくたってから再試行してください",
"xpack.stackConnectors.xmatters.unexpectedStatusErrorMessage": "xMattersフローのトリガーエラー予期しないステータス{status}",
"xpack.stackConnectors.casesWebhook.invalidUsernamePassword": "ユーザーとパスワードの両方を指定する必要があります",
"xpack.stackConnectors.casesWebhook.title": "Webフック - ケース管理",
"xpack.stackConnectors.components.bedrock.accessKeySecret": "アクセスキー",
"xpack.stackConnectors.components.bedrock.apiUrlTextFieldLabel": "URL",
@ -39299,9 +39298,7 @@
"xpack.stackConnectors.components.bedrock.selectMessageText": "Amazon Bedrockにリクエストを送信します。",
"xpack.stackConnectors.components.bedrock.title": "Amazon Bedrock",
"xpack.stackConnectors.components.bedrock.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.casesWebhook.addHeaderButton": "追加",
"xpack.stackConnectors.components.casesWebhook.addVariable": "変数を追加",
"xpack.stackConnectors.components.casesWebhook.authenticationLabel": "認証",
"xpack.stackConnectors.components.casesWebhook.caseCommentDesc": "Kibanaケースコメント",
"xpack.stackConnectors.components.casesWebhook.caseDescriptionDesc": "Kibanaケース説明",
"xpack.stackConnectors.components.casesWebhook.caseIdDesc": "KibanaケースID",
@ -39324,11 +39321,8 @@
"xpack.stackConnectors.components.casesWebhook.createIncidentResponseKeyTextFieldLabel": "ケース対応の作成の外部キー",
"xpack.stackConnectors.components.casesWebhook.createIncidentUrlTextFieldLabel": "ケースURLを作成",
"xpack.stackConnectors.components.casesWebhook.criticalLabel": "重大",
"xpack.stackConnectors.components.casesWebhook.deleteHeaderButton": "削除",
"xpack.stackConnectors.components.casesWebhook.descriptionTextAreaFieldLabel": "説明",
"xpack.stackConnectors.components.casesWebhook.docLink": "Webフックの構成 - ケース管理コネクター。",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthPasswordText": "パスワードが必要です。",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthUserNameText": "ユーザー名が必要です。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentIncidentText": "コメントオブジェクトの作成は有効なJSONでなければなりません。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentMethodText": "コメントメソッドを作成は必須です。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentUrlText": "コメントURLの作成はURL形式でなければなりません。",
@ -39353,15 +39347,12 @@
"xpack.stackConnectors.components.casesWebhook.getIncidentUrlTextFieldLabel": "ケースURLを取得",
"xpack.stackConnectors.components.casesWebhook.hasAuthSwitchLabel": "この Web フックの認証が必要です",
"xpack.stackConnectors.components.casesWebhook.highLabel": "高",
"xpack.stackConnectors.components.casesWebhook.httpHeadersTitle": "使用中のヘッダー",
"xpack.stackConnectors.components.casesWebhook.idFieldLabel": "ケースID",
"xpack.stackConnectors.components.casesWebhook.jsonCodeEditorAriaLabel": "コードエディター",
"xpack.stackConnectors.components.casesWebhook.jsonFieldLabel": "JSON",
"xpack.stackConnectors.components.casesWebhook.keyTextFieldLabel": "キー",
"xpack.stackConnectors.components.casesWebhook.lowLabel": "低",
"xpack.stackConnectors.components.casesWebhook.mediumLabel": "中",
"xpack.stackConnectors.components.casesWebhook.next": "次へ",
"xpack.stackConnectors.components.casesWebhook.passwordTextFieldLabel": "パスワード",
"xpack.stackConnectors.components.casesWebhook.previous": "前へ",
"xpack.stackConnectors.components.casesWebhook.selectMessageText": "ケース管理Webサービスにリクエストを送信します。",
"xpack.stackConnectors.components.casesWebhook.severityFieldLabel": "深刻度",
@ -39386,9 +39377,6 @@
"xpack.stackConnectors.components.casesWebhook.updateIncidentMethodTextFieldLabel": "ケースメソッドを更新",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlHelp": "ケースを更新するAPI URL。",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlTextFieldLabel": "ケースURLを更新",
"xpack.stackConnectors.components.casesWebhook.userTextFieldLabel": "ユーザー名",
"xpack.stackConnectors.components.casesWebhook.valueTextFieldLabel": "値",
"xpack.stackConnectors.components.casesWebhook.viewHeadersSwitch": "HTTP ヘッダーを追加",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlHelp": "外部システムでケースを表示するURL。変数セレクターを使用して、外部システムIDまたは外部システムタイトルをURLに追加します。",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlTextFieldLabel": "外部ケース表示URL",
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webフック - ケース管理データ",
@ -39738,39 +39726,15 @@
"xpack.stackConnectors.components.teams.messageTextAreaFieldLabel": "メッセージ",
"xpack.stackConnectors.components.teams.selectMessageText": "メッセージを Microsoft Teams チャネルに送信します。",
"xpack.stackConnectors.components.teams.webhookUrlHelpLabel": "Microsoft Teams Web フック URL を作成",
"xpack.stackConnectors.components.webhook.addHeaderButtonLabel": "ヘッダーを追加",
"xpack.stackConnectors.components.webhook.authenticationLabel": "認証",
"xpack.stackConnectors.components.webhook.authenticationMethodBasicLabel": "基本認証",
"xpack.stackConnectors.components.webhook.authenticationMethodNoneLabel": "なし",
"xpack.stackConnectors.components.webhook.authenticationMethodSSLLabel": "SSL認証",
"xpack.stackConnectors.components.webhook.bodyCodeEditorAriaLabel": "コードエディター",
"xpack.stackConnectors.components.webhook.bodyFieldLabel": "本文",
"xpack.stackConnectors.components.webhook.certTypeCrtKeyLabel": "CRTおよびKEYファイル",
"xpack.stackConnectors.components.webhook.certTypePfxLabel": "PFXファイル",
"xpack.stackConnectors.components.webhook.connectorTypeTitle": "Web フックデータ",
"xpack.stackConnectors.components.webhook.editCACallout": "このWebフックには既存の認証局ファイルがあります。新しいファイルをアップロードして置き換えてください。",
"xpack.stackConnectors.components.webhook.error.invalidUrlTextField": "URL が無効です。",
"xpack.stackConnectors.components.webhook.error.requiredAuthUserNameText": "ユーザー名が必要です。",
"xpack.stackConnectors.components.webhook.error.requiredMethodText": "メソッドが必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookBodyText": "本文が必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCAText": "CAファイルが必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCRTText": "CRTファイルが必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookKEYText": "KEYファイルが必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPasswordText": "パスワードが必要です。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPFXText": "PFXファイルが必要です。",
"xpack.stackConnectors.components.webhook.hasAuthSwitchLabel": "この Web フックの認証が必要です",
"xpack.stackConnectors.components.webhook.headerKeyTextFieldLabel": "キー",
"xpack.stackConnectors.components.webhook.headerValueTextFieldLabel": "値",
"xpack.stackConnectors.components.webhook.methodTextFieldLabel": "メソド",
"xpack.stackConnectors.components.webhook.passphraseTextFieldLabel": "パスフレーズ",
"xpack.stackConnectors.components.webhook.passwordTextFieldLabel": "パスワード",
"xpack.stackConnectors.components.webhook.removeHeaderIconLabel": "キー",
"xpack.stackConnectors.components.webhook.selectMessageText": "Web サービスにリクエストを送信してください。",
"xpack.stackConnectors.components.webhook.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.webhook.userTextFieldLabel": "ユーザー名",
"xpack.stackConnectors.components.webhook.verificationModeFieldLabel": "認証モード",
"xpack.stackConnectors.components.webhook.viewCertificateAuthoritySwitch": "認証局を追加",
"xpack.stackConnectors.components.webhook.viewHeadersSwitch": "HTTP ヘッダーを追加",
"xpack.stackConnectors.components.xmatters.authenticationLabel": "認証",
"xpack.stackConnectors.components.xmatters.basicAuthButtonGroupLegend": "基本認証",
"xpack.stackConnectors.components.xmatters.basicAuthLabel": "基本認証",
@ -39947,7 +39911,6 @@
"xpack.stackConnectors.webhook.authConfigurationError": "webフックアクションの構成エラーhasAuthがfalseの場合、authTypeはnullまたはundefinedでなければなりません",
"xpack.stackConnectors.webhook.invalidResponseErrorMessage": "Webフックの呼び出しエラー、無効な応答",
"xpack.stackConnectors.webhook.invalidResponseRetryLaterErrorMessage": "Webフックの呼び出しエラー、後ほど再試行",
"xpack.stackConnectors.webhook.invalidUsernamePassword": "ユーザーとパスワード、crtと鍵任意のパスワード付き、またはpfx任意のパスワード付きのいずれかのスキーマを指定する必要があります",
"xpack.stackConnectors.webhook.requestFailedErrorMessage": "Webフックの呼び出しエラー。要求が失敗しました",
"xpack.stackConnectors.webhook.title": "Web フック",
"xpack.stackConnectors.webhook.unexpectedNullResponseErrorMessage": "Webフックからの予期しないnull応答",

View file

@ -39330,7 +39330,6 @@
"xpack.stackConnectors.xmatters.invalidUrlError": "secretsUrl 无效:{err}",
"xpack.stackConnectors.xmatters.postingRetryErrorMessage": "触发 xMatters 流时出错http 状态为 {status},请稍后重试",
"xpack.stackConnectors.xmatters.unexpectedStatusErrorMessage": "触发 xMatters 流时出错:非预期状态 {status}",
"xpack.stackConnectors.casesWebhook.invalidUsernamePassword": "必须指定用户及密码",
"xpack.stackConnectors.casesWebhook.title": "Webhook - 案例管理",
"xpack.stackConnectors.components.bedrock.accessKeySecret": "访问密钥",
"xpack.stackConnectors.components.bedrock.apiUrlTextFieldLabel": "URL",
@ -39345,9 +39344,7 @@
"xpack.stackConnectors.components.bedrock.selectMessageText": "向 Amazon Bedrock 发送请求。",
"xpack.stackConnectors.components.bedrock.title": "Amazon Bedrock",
"xpack.stackConnectors.components.bedrock.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.casesWebhook.addHeaderButton": "添加",
"xpack.stackConnectors.components.casesWebhook.addVariable": "添加变量",
"xpack.stackConnectors.components.casesWebhook.authenticationLabel": "身份验证",
"xpack.stackConnectors.components.casesWebhook.caseCommentDesc": "Kibana 案例注释",
"xpack.stackConnectors.components.casesWebhook.caseDescriptionDesc": "Kibana 案例描述",
"xpack.stackConnectors.components.casesWebhook.caseIdDesc": "Kibana 案例 ID",
@ -39370,11 +39367,8 @@
"xpack.stackConnectors.components.casesWebhook.createIncidentResponseKeyTextFieldLabel": "创建案例响应外部键",
"xpack.stackConnectors.components.casesWebhook.createIncidentUrlTextFieldLabel": "创建案例 URL",
"xpack.stackConnectors.components.casesWebhook.criticalLabel": "紧急",
"xpack.stackConnectors.components.casesWebhook.deleteHeaderButton": "删除",
"xpack.stackConnectors.components.casesWebhook.descriptionTextAreaFieldLabel": "描述",
"xpack.stackConnectors.components.casesWebhook.docLink": "正在配置 Webhook - 案例管理连接器。",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthPasswordText": "“密码”必填。",
"xpack.stackConnectors.components.casesWebhook.error.requiredAuthUserNameText": "“用户名”必填。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentIncidentText": "创建注释对象必须为有效 JSON。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentMethodText": "“创建注释方法”必填。",
"xpack.stackConnectors.components.casesWebhook.error.requiredCreateCommentUrlText": "创建注释 URL 必须为 URL 格式。",
@ -39399,15 +39393,12 @@
"xpack.stackConnectors.components.casesWebhook.getIncidentUrlTextFieldLabel": "获取案例 URL",
"xpack.stackConnectors.components.casesWebhook.hasAuthSwitchLabel": "此 Webhook 需要身份验证",
"xpack.stackConnectors.components.casesWebhook.highLabel": "高",
"xpack.stackConnectors.components.casesWebhook.httpHeadersTitle": "在用的标头",
"xpack.stackConnectors.components.casesWebhook.idFieldLabel": "案例 ID",
"xpack.stackConnectors.components.casesWebhook.jsonCodeEditorAriaLabel": "代码编辑器",
"xpack.stackConnectors.components.casesWebhook.jsonFieldLabel": "JSON",
"xpack.stackConnectors.components.casesWebhook.keyTextFieldLabel": "钥匙",
"xpack.stackConnectors.components.casesWebhook.lowLabel": "低",
"xpack.stackConnectors.components.casesWebhook.mediumLabel": "中",
"xpack.stackConnectors.components.casesWebhook.next": "下一步",
"xpack.stackConnectors.components.casesWebhook.passwordTextFieldLabel": "密码",
"xpack.stackConnectors.components.casesWebhook.previous": "上一步",
"xpack.stackConnectors.components.casesWebhook.selectMessageText": "发送请求到案例管理 Web 服务。",
"xpack.stackConnectors.components.casesWebhook.severityFieldLabel": "严重性",
@ -39432,9 +39423,6 @@
"xpack.stackConnectors.components.casesWebhook.updateIncidentMethodTextFieldLabel": "更新案例方法",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlHelp": "用于更新案例的 API URL。",
"xpack.stackConnectors.components.casesWebhook.updateIncidentUrlTextFieldLabel": "更新案例 URL",
"xpack.stackConnectors.components.casesWebhook.userTextFieldLabel": "用户名",
"xpack.stackConnectors.components.casesWebhook.valueTextFieldLabel": "值",
"xpack.stackConnectors.components.casesWebhook.viewHeadersSwitch": "添加 HTTP 标头",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlHelp": "用于查看外部系统中的案例的 URL。使用变量选择器添加外部系统 ID 或外部系统标题到 URL。",
"xpack.stackConnectors.components.casesWebhook.viewIncidentUrlTextFieldLabel": "外部案例查看 URL",
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - 案例管理数据",
@ -39784,39 +39772,15 @@
"xpack.stackConnectors.components.teams.messageTextAreaFieldLabel": "消息",
"xpack.stackConnectors.components.teams.selectMessageText": "向 Microsoft Teams 频道发送消息。",
"xpack.stackConnectors.components.teams.webhookUrlHelpLabel": "创建 Microsoft Teams Webhook URL",
"xpack.stackConnectors.components.webhook.addHeaderButtonLabel": "添加标头",
"xpack.stackConnectors.components.webhook.authenticationLabel": "身份验证",
"xpack.stackConnectors.components.webhook.authenticationMethodBasicLabel": "基本身份验证",
"xpack.stackConnectors.components.webhook.authenticationMethodNoneLabel": "无",
"xpack.stackConnectors.components.webhook.authenticationMethodSSLLabel": "SSL 身份验证",
"xpack.stackConnectors.components.webhook.bodyCodeEditorAriaLabel": "代码编辑器",
"xpack.stackConnectors.components.webhook.bodyFieldLabel": "正文",
"xpack.stackConnectors.components.webhook.certTypeCrtKeyLabel": "CRT 和 KEY 文件",
"xpack.stackConnectors.components.webhook.certTypePfxLabel": "PFX 文件",
"xpack.stackConnectors.components.webhook.connectorTypeTitle": "Webhook 数据",
"xpack.stackConnectors.components.webhook.editCACallout": "此 Webhook 具有现有证书颁发机构文件。上传新文件将其替换。",
"xpack.stackConnectors.components.webhook.error.invalidUrlTextField": "URL 无效。",
"xpack.stackConnectors.components.webhook.error.requiredAuthUserNameText": "“用户名”必填。",
"xpack.stackConnectors.components.webhook.error.requiredMethodText": "“方法”必填",
"xpack.stackConnectors.components.webhook.error.requiredWebhookBodyText": "“正文”必填。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCAText": "CA 文件必填。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookCRTText": "CRT 文件必填。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookKEYText": "KEY 文件必填。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPasswordText": "“密码”必填。",
"xpack.stackConnectors.components.webhook.error.requiredWebhookPFXText": "PFX 文件必填。",
"xpack.stackConnectors.components.webhook.hasAuthSwitchLabel": "此 Webhook 需要身份验证",
"xpack.stackConnectors.components.webhook.headerKeyTextFieldLabel": "钥匙",
"xpack.stackConnectors.components.webhook.headerValueTextFieldLabel": "值",
"xpack.stackConnectors.components.webhook.methodTextFieldLabel": "方法",
"xpack.stackConnectors.components.webhook.passphraseTextFieldLabel": "密码",
"xpack.stackConnectors.components.webhook.passwordTextFieldLabel": "密码",
"xpack.stackConnectors.components.webhook.removeHeaderIconLabel": "钥匙",
"xpack.stackConnectors.components.webhook.selectMessageText": "将请求发送到 Web 服务。",
"xpack.stackConnectors.components.webhook.urlTextFieldLabel": "URL",
"xpack.stackConnectors.components.webhook.userTextFieldLabel": "用户名",
"xpack.stackConnectors.components.webhook.verificationModeFieldLabel": "验证模式",
"xpack.stackConnectors.components.webhook.viewCertificateAuthoritySwitch": "添加证书颁发机构",
"xpack.stackConnectors.components.webhook.viewHeadersSwitch": "添加 HTTP 标头",
"xpack.stackConnectors.components.xmatters.authenticationLabel": "身份验证",
"xpack.stackConnectors.components.xmatters.basicAuthButtonGroupLegend": "基本身份验证",
"xpack.stackConnectors.components.xmatters.basicAuthLabel": "基本身份验证",
@ -39993,7 +39957,6 @@
"xpack.stackConnectors.webhook.authConfigurationError": "配置 Webhook 操作时出错:如果 hasAuth 为 falseauthType 必须为 null 或未定义",
"xpack.stackConnectors.webhook.invalidResponseErrorMessage": "调用 webhook 时出错,响应无效",
"xpack.stackConnectors.webhook.invalidResponseRetryLaterErrorMessage": "调用 webhook 时出错,请稍后重试",
"xpack.stackConnectors.webhook.invalidUsernamePassword": "必须指定以下方案之一用户和密码crt 和密钥(密码可选);或 pfx密码可选",
"xpack.stackConnectors.webhook.requestFailedErrorMessage": "调用 webhook 时出错,请求失败",
"xpack.stackConnectors.webhook.title": "Webhook",
"xpack.stackConnectors.webhook.unexpectedNullResponseErrorMessage": "来自 Webhook 的异常空响应",

View file

@ -202,7 +202,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
statusCode: 400,
error: 'Bad Request',
message:
'error validating action type connector: both user and password must be specified',
'error validating action type connector: must specify a secrets configuration',
});
});
});

View file

@ -18,7 +18,7 @@ import {
import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock';
import { WebhookAuthType } from '@kbn/stack-connectors-plugin/common/webhook/constants';
import { AuthType } from '@kbn/stack-connectors-plugin/common/auth/constants';
import { BaseDefaultableFields } from '@kbn/security-solution-plugin/common/api/detection_engine';
import {
binaryToString,
@ -190,7 +190,7 @@ export default ({ getService }: FtrProviderContext): void => {
attributes: {
actionTypeId: '.webhook',
config: {
authType: WebhookAuthType.Basic,
authType: AuthType.Basic,
hasAuth: true,
method: 'post',
url: 'http://localhost',