Skip to content

Commit d0e8a3b

Browse files
georgianaonoleata1904kibanamachineTinaHeiligersadcoelhocnasikas
authored
[ResponseOps][Connectors] Support for encrypted headers (frontend) (#233695)
Fixes to elastic/kibana-team#1857 - This PR introduces frontend support for sending encrypted headers alongside config headers and authentication headers both in the webhook and cases webhook connectors. Headers Refactor Summary: - `Header type selection`: - users can now choose between config headers and secret headers when adding a header - the form dynamically updates the value field depending on the selected header type -> secret headers values are not displayed in the UI for security reasons - `Validations and restrictions`: - maximum of **20 headers** per connector - headers key must be unique; duplicates are not allowed - `Secret headers handling`: - secret headers values are not returned when fetching the connector - when editing the connector, user must re-enter secret header values to successfully submit - both config headers and secret headers are stored in __internal__ for UI purposes, but serialized into config.headers and secrets.secretHeaders when saving - `Form Serialization/Deserialization`: - config headers remain in config.headers - secret headers are stored in secrets.secretHeaders during form submission - both types are merged in the form UI (__internal__.headers) for display Backend PR: #230042 ## Release notes Add support for encrypted headers in the Webhook connector --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christiane (Tina) Heiligers <christiane.heiligers@elastic.co> Co-authored-by: adcoelho <antonio.coelho@elastic.co> Co-authored-by: Christos Nasikas <xristosnasikas@gmail.com> Co-authored-by: Julian Gernun <17549662+jcger@users.noreply.github.com> Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co>
1 parent 4b008ed commit d0e8a3b

19 files changed

Lines changed: 1342 additions & 181 deletions

File tree

packages/kbn-optimizer/limits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ pageLoadAssetSize:
162162
snapshotRestore: 28452
163163
spaces: 32135
164164
stackAlerts: 31499
165-
stackConnectors: 67975
165+
stackConnectors: 68502
166166
streams: 9000
167167
streamsApp: 17000
168168
synthetics: 31571

x-pack/platform/plugins/shared/stack_connectors/common/auth/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ export enum WebhookMethods {
2222
PUT = 'put',
2323
GET = 'get',
2424
}
25+
26+
export const MAX_HEADERS: number = 20;

x-pack/platform/plugins/shared/stack_connectors/public/common/auth/auth_config.test.tsx

Lines changed: 216 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ import React from 'react';
99
import { AuthConfig } from './auth_config';
1010
import { render, screen, waitFor, within } from '@testing-library/react';
1111
import userEvent from '@testing-library/user-event';
12+
1213
import { AuthType, SSLCertType } from '../../../common/auth/constants';
1314
import { AuthFormTestProvider } from '../../connector_types/lib/test_utils';
15+
import { useSecretHeaders } from './use_secret_headers';
16+
17+
jest.mock('./use_secret_headers');
18+
19+
const useSecretHeadersMock = useSecretHeaders as jest.Mock;
1420

1521
describe('AuthConfig renders', () => {
1622
const onSubmit = jest.fn();
1723

24+
beforeEach(() => {
25+
useSecretHeadersMock.mockReturnValue({ isLoading: false, isFetching: false, data: [] });
26+
});
27+
28+
afterEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
1832
it('renders all fields for authType=None', async () => {
1933
const testFormData = {
2034
config: {
@@ -194,10 +208,125 @@ describe('AuthConfig renders', () => {
194208
expect(await screen.findByTestId('sslCertFields')).toBeInTheDocument();
195209
});
196210

211+
describe('secret headers', () => {
212+
const defaultTestFormData = {
213+
config: {
214+
hasAuth: false,
215+
},
216+
__internal__: {
217+
hasHeaders: true,
218+
hasCA: false,
219+
headers: [{ key: 'config-key', value: 'text', type: 'config' }],
220+
},
221+
};
222+
223+
beforeEach(() =>
224+
useSecretHeadersMock
225+
.mockReturnValueOnce({
226+
isLoading: true,
227+
isFetching: true,
228+
data: [],
229+
})
230+
.mockReturnValue({
231+
isLoading: false,
232+
isFetching: false,
233+
data: ['secret-key'],
234+
})
235+
);
236+
237+
it('submits secret headers merged with config headers', async () => {
238+
render(
239+
<AuthFormTestProvider defaultValue={defaultTestFormData} onSubmit={onSubmit}>
240+
<AuthConfig readOnly={false} />
241+
</AuthFormTestProvider>
242+
);
243+
244+
await userEvent.click(await screen.findByTestId('webhookHeadersSecretValueInput'));
245+
await userEvent.paste('foobar');
246+
247+
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
248+
await waitFor(() => {
249+
expect(onSubmit).toHaveBeenCalledWith({
250+
data: {
251+
config: {
252+
hasAuth: false,
253+
authType: null,
254+
},
255+
__internal__: {
256+
hasHeaders: true,
257+
hasCA: false,
258+
headers: [
259+
{ key: 'config-key', value: 'text', type: 'config' },
260+
{ key: 'secret-key', value: 'foobar', type: 'secret' },
261+
],
262+
},
263+
},
264+
isValid: true,
265+
});
266+
});
267+
});
268+
269+
it('submits properly when there are only secret headers', async () => {
270+
const testFormData = {
271+
...defaultTestFormData,
272+
__internal__: {
273+
hasHeaders: true,
274+
hasCA: false,
275+
headers: [],
276+
},
277+
};
278+
279+
render(
280+
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
281+
<AuthConfig readOnly={false} />
282+
</AuthFormTestProvider>
283+
);
284+
285+
await userEvent.click(await screen.findByTestId('webhookHeadersSecretValueInput'));
286+
await userEvent.paste('foobar');
287+
288+
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
289+
await waitFor(() => {
290+
expect(onSubmit).toHaveBeenCalledWith({
291+
data: {
292+
config: {
293+
hasAuth: false,
294+
authType: null,
295+
},
296+
__internal__: {
297+
hasHeaders: true,
298+
hasCA: false,
299+
headers: [{ key: 'secret-key', value: 'foobar', type: 'secret' }],
300+
},
301+
},
302+
isValid: true,
303+
});
304+
});
305+
});
306+
307+
it('validation fails if the secret header value is empty', async () => {
308+
render(
309+
<AuthFormTestProvider defaultValue={defaultTestFormData} onSubmit={onSubmit}>
310+
<AuthConfig readOnly={false} />
311+
</AuthFormTestProvider>
312+
);
313+
314+
// We submit without populating the secret header value field
315+
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
316+
317+
await waitFor(() => {
318+
expect(onSubmit).toHaveBeenCalledWith({
319+
data: {},
320+
isValid: false,
321+
});
322+
});
323+
});
324+
});
325+
197326
describe('Validation', () => {
198327
const defaultTestFormData = {
199328
config: {
200-
headers: [{ key: 'content-type', value: 'text' }],
329+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
201330
hasAuth: true,
202331
},
203332
secrets: {
@@ -213,13 +342,17 @@ describe('AuthConfig renders', () => {
213342
it('succeeds with hasAuth=True', async () => {
214343
const testFormData = {
215344
config: {
216-
headers: [{ key: 'content-type', value: 'text' }],
217345
hasAuth: true,
218346
},
219347
secrets: {
220348
user: 'user',
221349
password: 'pass',
222350
},
351+
__internal__: {
352+
hasHeaders: true,
353+
hasCA: false,
354+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
355+
},
223356
};
224357
render(
225358
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
@@ -233,7 +366,6 @@ describe('AuthConfig renders', () => {
233366
expect(onSubmit).toHaveBeenCalledWith({
234367
data: {
235368
config: {
236-
headers: [{ key: 'content-type', value: 'text' }],
237369
hasAuth: true,
238370
authType: AuthType.Basic,
239371
},
@@ -244,6 +376,7 @@ describe('AuthConfig renders', () => {
244376
__internal__: {
245377
hasHeaders: true,
246378
hasCA: false,
379+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
247380
},
248381
},
249382
isValid: true,
@@ -257,6 +390,11 @@ describe('AuthConfig renders', () => {
257390
...defaultTestFormData.config,
258391
hasAuth: false,
259392
},
393+
__internal__: {
394+
hasHeaders: true,
395+
hasCA: false,
396+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
397+
},
260398
};
261399
render(
262400
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
@@ -270,13 +408,13 @@ describe('AuthConfig renders', () => {
270408
expect(onSubmit).toHaveBeenCalledWith({
271409
data: {
272410
config: {
273-
headers: [{ key: 'content-type', value: 'text' }],
274411
hasAuth: false,
275412
authType: null,
276413
},
277414
__internal__: {
278415
hasHeaders: true,
279416
hasCA: false,
417+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
280418
},
281419
},
282420
isValid: true,
@@ -333,6 +471,11 @@ describe('AuthConfig renders', () => {
333471
ca: Buffer.from('some binary string').toString('base64'),
334472
verificationMode: 'full',
335473
},
474+
__internal__: {
475+
hasHeaders: true,
476+
hasCA: true,
477+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
478+
},
336479
};
337480

338481
render(
@@ -351,7 +494,6 @@ describe('AuthConfig renders', () => {
351494
authType: AuthType.Basic,
352495
ca: Buffer.from('some binary string').toString('base64'),
353496
verificationMode: 'full',
354-
headers: [{ key: 'content-type', value: 'text' }],
355497
},
356498
secrets: {
357499
user: 'user',
@@ -360,6 +502,7 @@ describe('AuthConfig renders', () => {
360502
__internal__: {
361503
hasHeaders: true,
362504
hasCA: true,
505+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
363506
},
364507
},
365508
isValid: true,
@@ -407,6 +550,11 @@ describe('AuthConfig renders', () => {
407550
crt: Buffer.from('some binary string').toString('base64'),
408551
key: Buffer.from('some binary string').toString('base64'),
409552
},
553+
__internal__: {
554+
hasHeaders: true,
555+
hasCA: false,
556+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
557+
},
410558
};
411559

412560
render(
@@ -424,7 +572,6 @@ describe('AuthConfig renders', () => {
424572
hasAuth: true,
425573
authType: AuthType.SSL,
426574
certType: SSLCertType.CRT,
427-
headers: [{ key: 'content-type', value: 'text' }],
428575
},
429576
secrets: {
430577
crt: Buffer.from('some binary string').toString('base64'),
@@ -433,6 +580,7 @@ describe('AuthConfig renders', () => {
433580
__internal__: {
434581
hasHeaders: true,
435582
hasCA: false,
583+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
436584
},
437585
},
438586
isValid: true,
@@ -450,6 +598,11 @@ describe('AuthConfig renders', () => {
450598
secrets: {
451599
pfx: Buffer.from('some binary string').toString('base64'),
452600
},
601+
__internal__: {
602+
hasHeaders: true,
603+
hasCA: false,
604+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
605+
},
453606
};
454607

455608
render(
@@ -467,20 +620,75 @@ describe('AuthConfig renders', () => {
467620
hasAuth: true,
468621
authType: AuthType.SSL,
469622
certType: SSLCertType.PFX,
470-
headers: [{ key: 'content-type', value: 'text' }],
471623
},
472624
secrets: {
473625
pfx: Buffer.from('some binary string').toString('base64'),
474626
},
475627
__internal__: {
476628
hasHeaders: true,
477629
hasCA: false,
630+
headers: [{ key: 'content-type', value: 'text', type: 'config' }],
478631
},
479632
},
480633
isValid: true,
481634
});
482635
});
483636
});
637+
638+
it('validation fails if there are 2 headers with the same key', async () => {
639+
const testFormData = {
640+
...defaultTestFormData,
641+
__internal__: {
642+
hasHeaders: true,
643+
hasCA: false,
644+
headers: [
645+
{ key: 'same-key', value: 'text 1', type: 'config' },
646+
{ key: 'same-key', value: 'text 2', type: 'config' },
647+
],
648+
},
649+
};
650+
651+
render(
652+
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
653+
<AuthConfig readOnly={false} />
654+
</AuthFormTestProvider>
655+
);
656+
657+
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
658+
659+
await waitFor(() => {
660+
expect(onSubmit).toHaveBeenCalledWith({
661+
data: {},
662+
isValid: false,
663+
});
664+
});
665+
});
666+
667+
it('validation fails if header key is empty', async () => {
668+
const testFormData = {
669+
...defaultTestFormData,
670+
__internal__: {
671+
hasHeaders: true,
672+
hasCA: false,
673+
headers: [{ key: '', value: 'text', type: 'config' }],
674+
},
675+
};
676+
677+
render(
678+
<AuthFormTestProvider defaultValue={testFormData} onSubmit={onSubmit}>
679+
<AuthConfig readOnly={false} />
680+
</AuthFormTestProvider>
681+
);
682+
683+
await userEvent.click(await screen.findByTestId('form-test-provide-submit'));
684+
685+
await waitFor(() => {
686+
expect(onSubmit).toHaveBeenCalledWith({
687+
data: {},
688+
isValid: false,
689+
});
690+
});
691+
});
484692
});
485693

486694
describe('AuthConfig with showOAuth2Option on', () => {
@@ -616,7 +824,7 @@ describe('AuthConfig renders', () => {
616824
});
617825
});
618826

619-
it('validates additionalFields input for valid/invalid JSON', async () => {
827+
it('validates additionalFields input for invalid JSON', async () => {
620828
const testFormData = {
621829
config: {
622830
hasAuth: true,
@@ -647,13 +855,6 @@ describe('AuthConfig renders', () => {
647855
await userEvent.type(additionalFieldsInput!, '{{key": "value');
648856

649857
expect(await screen.findByText('Invalid JSON')).toBeInTheDocument();
650-
651-
await userEvent.clear(additionalFieldsInput!);
652-
await userEvent.type(additionalFieldsInput!, '{{"sdf": "value"}');
653-
654-
await waitFor(() => {
655-
expect(screen.queryByText('Invalid JSON')).not.toBeInTheDocument();
656-
});
657858
});
658859

659860
it('renders OAuth2 fields as readOnly when readOnly prop is true', async () => {

0 commit comments

Comments
 (0)