Skip to content

Commit 7bac28b

Browse files
authored
[6.4] Reporting cookies (#24177) (#24236)
* Reporting cookies (#24177) * Switching Reporting to use session cookies explicitly * Fixing bug when security is explicitly disabled * Responding to feedback * Fixing yarn.lock * Fixing yarn.lock
1 parent 86706e8 commit 7bac28b

23 files changed

Lines changed: 848 additions & 212 deletions

File tree

x-pack/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
2929
"@kbn/test": "link:../packages/kbn-test",
3030
"@types/jest": "^22.2.3",
31+
"@types/cookie": "^0.3.1",
3132
"@types/pngjs": "^3.3.1",
3233
"abab": "^1.0.4",
3334
"ansicolors": "0.3.2",
@@ -98,9 +99,10 @@
9899
"bluebird": "3.1.1",
99100
"boom": "3.1.1",
100101
"brace": "0.11.1",
101-
"chrome-remote-interface": "0.24.2",
102+
"chrome-remote-interface": "0.26.1",
102103
"classnames": "2.2.5",
103104
"concat-stream": "1.5.1",
105+
"cookie": "^0.3.1",
104106
"d3": "3.5.6",
105107
"d3-scale": "1.0.6",
106108
"dedent": "^0.7.0",
@@ -115,6 +117,7 @@
115117
"history": "4.7.2",
116118
"humps": "2.0.1",
117119
"icalendar": "0.7.1",
120+
"iron": "4",
118121
"isomorphic-fetch": "2.2.1",
119122
"joi": "6.10.1",
120123
"jquery": "^3.3.1",

x-pack/plugins/reporting/export_types/csv/server/create_job.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { cryptoFactory } from '../../../server/lib/crypto';
1010
function createJobFn(server) {
1111
const crypto = cryptoFactory(server);
1212

13-
return async function createJob(jobParams, headers, request) {
13+
return async function createJob(jobParams, headers, serializedSession, request) {
1414
const serializedEncryptedHeaders = await crypto.encrypt(headers);
1515

1616
const savedObjectsClient = request.getSavedObjectsClient();
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`throw an Error if the objectType, savedObjectId and relativeUrls are provided 1`] = `"objectType and savedObjectId should not be provided in addition to the relativeUrls"`;

x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function compatibilityShimFactory(server) {
6161
queryString,
6262
browserTimezone,
6363
layout
64-
}, headers, request) {
64+
}, headers, serializedSession, request) {
6565

6666
if (objectType && savedObjectId && relativeUrls) {
6767
throw new Error('objectType and savedObjectId should not be provided in addition to the relativeUrls');
@@ -75,7 +75,7 @@ export function compatibilityShimFactory(server) {
7575
layout
7676
};
7777

78-
return await createJob(transformedJobParams, headers, request);
78+
return await createJob(transformedJobParams, headers, serializedSession, request);
7979
};
8080
};
81-
}
81+
}

x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test(`passes title through if provided`, async () => {
2929
const title = 'test title';
3030

3131
const createJobMock = jest.fn();
32-
await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, createMockRequest());
32+
await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, null, createMockRequest());
3333

3434
expect(createJobMock.mock.calls.length).toBe(1);
3535
expect(createJobMock.mock.calls[0][0].title).toBe(title);
@@ -48,7 +48,7 @@ test(`gets the title from the savedObject`, async () => {
4848
}
4949
});
5050

51-
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest);
51+
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, null, mockRequest);
5252

5353
expect(createJobMock.mock.calls.length).toBe(1);
5454
expect(createJobMock.mock.calls[0][0].title).toBe(title);
@@ -67,7 +67,7 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async
6767

6868
const objectType = 'search';
6969
const savedObjectId = 'abc';
70-
await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, mockRequest);
70+
await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, null, mockRequest);
7171

7272
const getMock = mockRequest.getSavedObjectsClient().get.mock;
7373
expect(getMock.calls.length).toBe(1);
@@ -87,7 +87,7 @@ test(`logs deprecations when generating the title/relativeUrl using the savedObj
8787
}
8888
});
8989

90-
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest);
90+
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, null, mockRequest);
9191

9292
expect(mockServer.log.mock.calls.length).toBe(2);
9393
expect(mockServer.log.mock.calls[0][0]).toEqual(['warning', 'reporting', 'deprecation']);
@@ -101,7 +101,7 @@ test(`passes objectType through`, async () => {
101101
const mockRequest = createMockRequest();
102102

103103
const objectType = 'foo';
104-
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, mockRequest);
104+
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, null, mockRequest);
105105

106106
expect(createJobMock.mock.calls.length).toBe(1);
107107
expect(createJobMock.mock.calls[0][0].objectType).toBe(objectType);
@@ -113,7 +113,7 @@ test(`passes the relativeUrls through`, async () => {
113113
const createJobMock = jest.fn();
114114

115115
const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else'];
116-
await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null);
116+
await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null, null);
117117
expect(createJobMock.mock.calls.length).toBe(1);
118118
expect(createJobMock.mock.calls[0][0].relativeUrls).toBe(relativeUrls);
119119
});
@@ -123,7 +123,7 @@ const testSavedObjectRelativeUrl = (objectType, expectedUrl) => {
123123
const compatibilityShim = compatibilityShimFactory(createMockServer());
124124
const createJobMock = jest.fn();
125125

126-
await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null);
126+
await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null, null);
127127
expect(createJobMock.mock.calls.length).toBe(1);
128128
expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]);
129129
});
@@ -137,7 +137,10 @@ test(`appends the queryString to the relativeUrl when generating from the savedO
137137
const compatibilityShim = compatibilityShimFactory(createMockServer());
138138
const createJobMock = jest.fn();
139139

140-
await compatibilityShim(createJobMock)({ title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' }, null, null);
140+
await compatibilityShim(createJobMock)(
141+
{ title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' },
142+
null, null, null
143+
);
141144
expect(createJobMock.mock.calls.length).toBe(1);
142145
expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual(['/app/kibana#/discover/abc?foo=bar']);
143146
});
@@ -151,22 +154,24 @@ test(`throw an Error if the objectType, savedObjectId and relativeUrls are provi
151154
objectType: 'something',
152155
relativeUrls: ['/something'],
153156
savedObjectId: 'abc',
154-
}, null, null);
157+
}, null, null, null);
155158

156-
await expect(promise).rejects.toBeDefined();
159+
await expect(promise).rejects.toThrowErrorMatchingSnapshot();
157160
});
158161

159-
test(`passes headers and request through`, async () => {
162+
test(`passes headers, serializedSession and request through`, async () => {
160163
const compatibilityShim = compatibilityShimFactory(createMockServer());
161164

162165
const createJobMock = jest.fn();
163166

164167
const headers = {};
168+
const serializedSession = 'thisoldeserializedsession';
165169
const request = createMockRequest();
166170

167-
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, request);
171+
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, serializedSession, request);
168172

169173
expect(createJobMock.mock.calls.length).toBe(1);
170174
expect(createJobMock.mock.calls[0][1]).toBe(headers);
171-
expect(createJobMock.mock.calls[0][2]).toBe(request);
175+
expect(createJobMock.mock.calls[0][2]).toBe(serializedSession);
176+
expect(createJobMock.mock.calls[0][3]).toBe(request);
172177
});

x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ function createJobFn(server) {
1818
relativeUrls,
1919
browserTimezone,
2020
layout
21-
}, headers) {
21+
}, headers, serializedSession) {
2222
const serializedEncryptedHeaders = await crypto.encrypt(headers);
23+
const encryptedSerializedSession = await crypto.encrypt(serializedSession);
2324

2425
return {
2526
type: objectType,
2627
title: title,
2728
objects: relativeUrls.map(u => ({ relativeUrl: u })),
2829
headers: serializedEncryptedHeaders,
30+
session: encryptedSerializedSession,
2931
browserTimezone,
3032
layout,
3133
forceNow: new Date().toISOString(),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`headers it fails if it can't decrypt the headers 1`] = `"Failed to decrypt report job data. Please re-generate this report."`;
4+
5+
exports[`sessionCookie it fails if it can't decrypt the session 1`] = `"Failed to decrypt report job data. Please re-generate this report."`;
6+
7+
exports[`sessionCookie it throws error if cookie name can't be determined 1`] = `"Unable to determine the session cookie name"`;
8+
9+
exports[`urls it throw error if full URL is provided that is not a Kibana URL 1`] = `"Unable to generate report for url https://localhost/app/kibana, it's not a Kibana URL"`;

x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.js

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,22 @@
55
*/
66

77
import url from 'url';
8+
import cookie from 'cookie';
89
import { getAbsoluteUrlFactory } from './get_absolute_url';
10+
import { cryptoFactory } from '../../../../server/lib/crypto';
911

1012
export function compatibilityShimFactory(server) {
1113
const getAbsoluteUrl = getAbsoluteUrlFactory(server);
14+
const crypto = cryptoFactory(server);
15+
16+
const decryptJobHeaders = async (job) => {
17+
try {
18+
const decryptedHeaders = await crypto.decrypt(job.headers);
19+
return decryptedHeaders;
20+
} catch (err) {
21+
throw new Error('Failed to decrypt report job data. Please re-generate this report.');
22+
}
23+
};
1224

1325
const getSavedObjectAbsoluteUrl = (savedObj) => {
1426
if (savedObj.urlHash) {
@@ -27,11 +39,49 @@ export function compatibilityShimFactory(server) {
2739
throw new Error(`Unable to generate report for url ${savedObj.url}, it's not a Kibana URL`);
2840
};
2941

42+
const getSerializedSession = async (decryptedHeaders, jobSession) => {
43+
if (!server.plugins.security) {
44+
return null;
45+
}
46+
47+
if (jobSession) {
48+
try {
49+
return await crypto.decrypt(jobSession);
50+
} catch (err) {
51+
throw new Error('Failed to decrypt report job data. Please re-generate this report.');
52+
}
53+
}
54+
55+
const cookies = decryptedHeaders.cookie ? cookie.parse(decryptedHeaders.cookie) : null;
56+
if (cookies === null) {
57+
return null;
58+
}
59+
60+
const cookieName = server.plugins.security.getSessionCookieOptions().name;
61+
if (!cookieName) {
62+
throw new Error('Unable to determine the session cookie name');
63+
}
64+
65+
return cookies[cookieName];
66+
};
67+
3068
return function (executeJob) {
3169
return async function (job, cancellationToken) {
3270
const urls = job.objects.map(getSavedObjectAbsoluteUrl);
71+
const decryptedHeaders = await decryptJobHeaders(job);
72+
const authorizationHeader = decryptedHeaders.authorization;
73+
const serializedSession = await getSerializedSession(decryptedHeaders, job.session);
3374

34-
return await executeJob({ ...job, urls }, cancellationToken);
75+
return await executeJob({
76+
title: job.title,
77+
browserTimezone: job.browserTimezone,
78+
layout: job.layout,
79+
basePath: job.basePath,
80+
forceNow: job.forceNow,
81+
urls,
82+
authorizationHeader,
83+
serializedSession,
84+
}, cancellationToken);
3585
};
3686
};
37-
}
87+
}

0 commit comments

Comments
 (0)