[6.4] Reporting Cookies #24752 (#24794)

* Revert "[6.4] Reporting cookies (#24177) (#24236)"

This reverts commit 7bac28be6b.

* Take 2

* Fixing yarn.lock

* Fix #22579 (#22580) (#22600)
This commit is contained in:
Brandon Kobel 2018-10-30 14:45:58 -07:00 committed by GitHub
parent 0f677d87e8
commit 968768f01f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 486 additions and 827 deletions

View file

@ -28,7 +28,6 @@
"@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers",
"@kbn/test": "link:../packages/kbn-test",
"@types/jest": "^22.2.3",
"@types/cookie": "^0.3.1",
"@types/pngjs": "^3.3.1",
"abab": "^1.0.4",
"ansicolors": "0.3.2",
@ -102,7 +101,6 @@
"chrome-remote-interface": "0.26.1",
"classnames": "2.2.5",
"concat-stream": "1.5.1",
"cookie": "^0.3.1",
"d3": "3.5.6",
"d3-scale": "1.0.6",
"dedent": "^0.7.0",
@ -117,7 +115,6 @@
"history": "4.7.2",
"humps": "2.0.1",
"icalendar": "0.7.1",
"iron": "4",
"isomorphic-fetch": "2.2.1",
"joi": "6.10.1",
"jquery": "^3.3.1",

View file

@ -10,7 +10,7 @@ import { cryptoFactory } from '../../../server/lib/crypto';
function createJobFn(server) {
const crypto = cryptoFactory(server);
return async function createJob(jobParams, headers, serializedSession, request) {
return async function createJob(jobParams, headers, request) {
const serializedEncryptedHeaders = await crypto.encrypt(headers);
const savedObjectsClient = request.getSavedObjectsClient();

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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"`;

View file

@ -61,7 +61,7 @@ export function compatibilityShimFactory(server) {
queryString,
browserTimezone,
layout
}, headers, serializedSession, request) {
}, headers, request) {
if (objectType && savedObjectId && relativeUrls) {
throw new Error('objectType and savedObjectId should not be provided in addition to the relativeUrls');
@ -75,7 +75,7 @@ export function compatibilityShimFactory(server) {
layout
};
return await createJob(transformedJobParams, headers, serializedSession, request);
return await createJob(transformedJobParams, headers, request);
};
};
}
}

View file

@ -29,7 +29,7 @@ test(`passes title through if provided`, async () => {
const title = 'test title';
const createJobMock = jest.fn();
await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, null, createMockRequest());
await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, createMockRequest());
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].title).toBe(title);
@ -48,7 +48,7 @@ test(`gets the title from the savedObject`, async () => {
}
});
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, null, mockRequest);
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].title).toBe(title);
@ -67,7 +67,7 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async
const objectType = 'search';
const savedObjectId = 'abc';
await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, null, mockRequest);
await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, mockRequest);
const getMock = mockRequest.getSavedObjectsClient().get.mock;
expect(getMock.calls.length).toBe(1);
@ -87,7 +87,7 @@ test(`logs deprecations when generating the title/relativeUrl using the savedObj
}
});
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, null, mockRequest);
await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest);
expect(mockServer.log.mock.calls.length).toBe(2);
expect(mockServer.log.mock.calls[0][0]).toEqual(['warning', 'reporting', 'deprecation']);
@ -101,7 +101,7 @@ test(`passes objectType through`, async () => {
const mockRequest = createMockRequest();
const objectType = 'foo';
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, null, mockRequest);
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, mockRequest);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].objectType).toBe(objectType);
@ -113,7 +113,7 @@ test(`passes the relativeUrls through`, async () => {
const createJobMock = jest.fn();
const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else'];
await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null, null);
await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].relativeUrls).toBe(relativeUrls);
});
@ -123,7 +123,7 @@ const testSavedObjectRelativeUrl = (objectType, expectedUrl) => {
const compatibilityShim = compatibilityShimFactory(createMockServer());
const createJobMock = jest.fn();
await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null, null);
await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]);
});
@ -137,10 +137,7 @@ test(`appends the queryString to the relativeUrl when generating from the savedO
const compatibilityShim = compatibilityShimFactory(createMockServer());
const createJobMock = jest.fn();
await compatibilityShim(createJobMock)(
{ title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' },
null, null, null
);
await compatibilityShim(createJobMock)({ title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' }, null, null);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual(['/app/kibana#/discover/abc?foo=bar']);
});
@ -154,24 +151,22 @@ test(`throw an Error if the objectType, savedObjectId and relativeUrls are provi
objectType: 'something',
relativeUrls: ['/something'],
savedObjectId: 'abc',
}, null, null, null);
}, null, null);
await expect(promise).rejects.toThrowErrorMatchingSnapshot();
await expect(promise).rejects.toBeDefined();
});
test(`passes headers, serializedSession and request through`, async () => {
test(`passes headers and request through`, async () => {
const compatibilityShim = compatibilityShimFactory(createMockServer());
const createJobMock = jest.fn();
const headers = {};
const serializedSession = 'thisoldeserializedsession';
const request = createMockRequest();
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, serializedSession, request);
await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, request);
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][1]).toBe(headers);
expect(createJobMock.mock.calls[0][2]).toBe(serializedSession);
expect(createJobMock.mock.calls[0][3]).toBe(request);
expect(createJobMock.mock.calls[0][2]).toBe(request);
});

View file

@ -18,16 +18,14 @@ function createJobFn(server) {
relativeUrls,
browserTimezone,
layout
}, headers, serializedSession) {
}, headers) {
const serializedEncryptedHeaders = await crypto.encrypt(headers);
const encryptedSerializedSession = await crypto.encrypt(serializedSession);
return {
type: objectType,
title: title,
objects: relativeUrls.map(u => ({ relativeUrl: u })),
headers: serializedEncryptedHeaders,
session: encryptedSerializedSession,
browserTimezone,
layout,
forceNow: new Date().toISOString(),

View file

@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`headers it fails if it can't decrypt the headers 1`] = `"Failed to decrypt report job data. Please re-generate this report."`;
exports[`sessionCookie it fails if it can't decrypt the session 1`] = `"Failed to decrypt report job data. Please re-generate this report."`;
exports[`sessionCookie it throws error if cookie name can't be determined 1`] = `"Unable to determine the session cookie name"`;
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"`;

View file

@ -5,22 +5,10 @@
*/
import url from 'url';
import cookie from 'cookie';
import { getAbsoluteUrlFactory } from './get_absolute_url';
import { cryptoFactory } from '../../../../server/lib/crypto';
export function compatibilityShimFactory(server) {
const getAbsoluteUrl = getAbsoluteUrlFactory(server);
const crypto = cryptoFactory(server);
const decryptJobHeaders = async (job) => {
try {
const decryptedHeaders = await crypto.decrypt(job.headers);
return decryptedHeaders;
} catch (err) {
throw new Error('Failed to decrypt report job data. Please re-generate this report.');
}
};
const getSavedObjectAbsoluteUrl = (savedObj) => {
if (savedObj.urlHash) {
@ -39,49 +27,11 @@ export function compatibilityShimFactory(server) {
throw new Error(`Unable to generate report for url ${savedObj.url}, it's not a Kibana URL`);
};
const getSerializedSession = async (decryptedHeaders, jobSession) => {
if (!server.plugins.security) {
return null;
}
if (jobSession) {
try {
return await crypto.decrypt(jobSession);
} catch (err) {
throw new Error('Failed to decrypt report job data. Please re-generate this report.');
}
}
const cookies = decryptedHeaders.cookie ? cookie.parse(decryptedHeaders.cookie) : null;
if (cookies === null) {
return null;
}
const cookieName = server.plugins.security.getSessionCookieOptions().name;
if (!cookieName) {
throw new Error('Unable to determine the session cookie name');
}
return cookies[cookieName];
};
return function (executeJob) {
return async function (job, cancellationToken) {
const urls = job.objects.map(getSavedObjectAbsoluteUrl);
const decryptedHeaders = await decryptJobHeaders(job);
const authorizationHeader = decryptedHeaders.authorization;
const serializedSession = await getSerializedSession(decryptedHeaders, job.session);
return await executeJob({
title: job.title,
browserTimezone: job.browserTimezone,
layout: job.layout,
basePath: job.basePath,
forceNow: job.forceNow,
urls,
authorizationHeader,
serializedSession,
}, cancellationToken);
return await executeJob({ ...job, urls }, cancellationToken);
};
};
}
}

View file

@ -5,14 +5,12 @@
*/
import { compatibilityShimFactory } from './compatibility_shim';
import { cryptoFactory } from '../../../../server/lib/crypto';
const createMockServer = ({ security = null } = {}) => {
const createMockServer = () => {
const config = {
'server.host': 'localhost',
'server.port': '5601',
'server.basePath': '',
'xpack.reporting.encryptionKey': '1234567890qwerty'
};
return {
@ -25,302 +23,63 @@ const createMockServer = ({ security = null } = {}) => {
return {
get: key => config[key]
};
},
plugins: {
security
}
};
};
const encrypt = async (mockServer, headers) => {
const crypto = cryptoFactory(mockServer);
return await crypto.encrypt(headers);
};
test(`it throw error if full URL is provided that is not a Kibana URL`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
describe('urls', () => {
test(`it throw error if full URL is provided that is not a Kibana URL`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toBeDefined();
});
await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toThrowErrorMatchingSnapshot();
});
test(`it passes url through if it is a Kibana URL`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
test(`it passes url through if it is a Kibana URL`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const url = 'http://localhost:5601/app/kibana/#visualize';
await compatibilityShim(mockCreateJob)({ objects: [ { url } ] });
expect(mockCreateJob.mock.calls.length).toBe(1);
expect(mockCreateJob.mock.calls[0][0].objects[0].url).toBe(url);
});
const url = 'http://localhost:5601/app/kibana/#visualize';
await compatibilityShim(mockExecuteJob)({ objects: [ { url } ], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe(url);
});
test(`it generates the absolute url if a urlHash is provided`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
test(`it generates the absolute url if a urlHash is provided`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const urlHash = '#visualize';
await compatibilityShim(mockCreateJob)({ objects: [ { urlHash } ] });
expect(mockCreateJob.mock.calls.length).toBe(1);
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#visualize');
});
const urlHash = '#visualize';
await compatibilityShim(mockExecuteJob)({ objects: [ { urlHash } ], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#visualize');
});
test(`it generates the absolute url if a relativeUrl is provided`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
test(`it generates the absolute url using server's basePath if a relativeUrl is provided`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const relativeUrl = '/app/kibana#/visualize?';
await compatibilityShim(mockCreateJob)({ objects: [ { relativeUrl } ] });
expect(mockCreateJob.mock.calls.length).toBe(1);
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#/visualize?');
});
const relativeUrl = '/app/kibana#/visualize?';
await compatibilityShim(mockExecuteJob)({ objects: [ { relativeUrl } ], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#/visualize?');
});
test(`it generates the absolute url if a relativeUrl with querystring is provided`, async () => {
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
test(`it generates the absolute url using server's basePath if a relativeUrl with querystring is provided`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const relativeUrl = '/app/kibana?_t=123456789#/visualize?_g=()';
await compatibilityShim(mockExecuteJob)({ objects: [ { relativeUrl } ], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana?_t=123456789#/visualize?_g=()');
});
const relativeUrl = '/app/kibana?_t=123456789#/visualize?_g=()';
await compatibilityShim(mockCreateJob)({ objects: [ { relativeUrl } ] });
expect(mockCreateJob.mock.calls.length).toBe(1);
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana?_t=123456789#/visualize?_g=()');
});
test(`it passes the provided browserTimezone through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const mockCreateJob = jest.fn();
const compatibilityShim = compatibilityShimFactory(createMockServer());
const browserTimezone = 'UTC';
await compatibilityShim(mockExecuteJob)({ browserTimezone, objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone);
});
test(`it passes the provided title through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const title = 'thetitle';
await compatibilityShim(mockExecuteJob)({ title, objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].title).toEqual(title);
});
test(`it passes the provided layout through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const layout = Symbol();
await compatibilityShim(mockExecuteJob)({ layout, objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].layout).toEqual(layout);
});
test(`it passes the provided basePath through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const basePath = '/foo/bar/baz';
await compatibilityShim(mockExecuteJob)({ basePath, objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].basePath).toEqual(basePath);
});
test(`it passes the provided forceNow through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
const forceNow = 'ISO 8601 Formatted Date';
await compatibilityShim(mockExecuteJob)({ forceNow, objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].forceNow).toEqual(forceNow);
});
describe('headers', () => {
test(`it fails if it can't decrypt the headers`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer();
const encryptedHeaders = 'imnotencryptedgrimacingface';
const compatibilityShim = compatibilityShimFactory(mockServer);
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders })).rejects.toThrowErrorMatchingSnapshot();
});
test(`passes the authorization header through`, async () => {
const mockExecuteJob = jest.fn();
const headers = {
authorization: 'foo',
bar: 'quz',
};
const mockServer = createMockServer();
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].authorizationHeader).toEqual('foo');
});
});
describe('sessionCookie', () => {
test(`it doesn't pass serializedSession through if server.plugins.security is null`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer();
const headers = {};
const encryptedHeaders = await encrypt(mockServer, headers);
const session = 'asession';
const encryptedSession = await encrypt(mockServer, session);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: encryptedSession });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
});
test(`it fails if it can't decrypt the session`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {}
});
const headers = {};
const encryptedHeaders = await encrypt(mockServer, headers);
const session = 'asession';
const compatibilityShim = compatibilityShimFactory(mockServer);
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session }))
.rejects
.toThrowErrorMatchingSnapshot();
});
test(`it passes decrypted session through`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {}
});
const headers = {};
const encryptedHeaders = await encrypt(mockServer, headers);
const session = 'asession';
const encryptedSession = await encrypt(mockServer, session);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: encryptedSession });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(session);
});
test(`it passes null if encrypted headers don't have any cookies`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {}
});
const headers = {};
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
});
test(`it passes null if encrypted headers doesn't have session cookie`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {
getSessionCookieOptions() {
return {
name: 'sid',
};
}
}
});
const headers = {
'foo': 'bar',
};
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
});
test(`it throws error if cookie name can't be determined`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {
getSessionCookieOptions() {
return {};
}
}
});
const headers = {
'cookie': 'foo=bar;',
};
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null }))
.rejects
.toThrowErrorMatchingSnapshot();
});
test(`it passes value of session cookie from the headers through`, async () => {
const mockExecuteJob = jest.fn();
const mockServer = createMockServer({
security: {
getSessionCookieOptions() {
return {
name: 'sid'
};
}
}
});
const headers = {
'cookie': 'sid=foo; bar=quz;',
};
const encryptedHeaders = await encrypt(mockServer, headers);
const compatibilityShim = compatibilityShimFactory(mockServer);
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
expect(mockExecuteJob.mock.calls.length).toBe(1);
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual('foo');
});
await compatibilityShim(mockCreateJob)({ browserTimezone, objects: [] });
expect(mockCreateJob.mock.calls.length).toBe(1);
expect(mockCreateJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone);
});

View file

@ -6,25 +6,61 @@
import url from 'url';
import * as Rx from 'rxjs';
import { mergeMap, map, takeUntil } from 'rxjs/operators';
import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators';
import { omit } from 'lodash';
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants';
import { oncePerServer } from '../../../../server/lib/once_per_server';
import { generatePdfObservableFactory } from '../lib/generate_pdf';
import { cryptoFactory } from '../../../../server/lib/crypto';
import { compatibilityShimFactory } from './compatibility_shim';
const KBN_SCREENSHOT_HEADER_BLACKLIST = [
'accept-encoding',
'content-length',
'content-type',
'host',
'referer',
// `Transfer-Encoding` is hop-by-hop header that is meaningful
// only for a single transport-level connection, and shouldn't
// be stored by caches or forwarded by proxies.
'transfer-encoding',
];
function executeJobFn(server) {
const generatePdfObservable = generatePdfObservableFactory(server);
const crypto = cryptoFactory(server);
const compatibilityShim = compatibilityShimFactory(server);
const config = server.config();
const getCustomLogo = async (job) => {
const fakeRequest = {
headers: {
...job.authorizationHeader && { authorization: job.authorizationHeader },
const decryptJobHeaders = async (job) => {
const decryptedHeaders = await crypto.decrypt(job.headers);
return { job, decryptedHeaders };
};
const omitBlacklistedHeaders = ({ job, decryptedHeaders }) => {
const filteredHeaders = omit(decryptedHeaders, KBN_SCREENSHOT_HEADER_BLACKLIST);
return { job, filteredHeaders };
};
const getConditionalHeaders = ({ job, filteredHeaders }) => {
const conditionalHeaders = {
headers: filteredHeaders,
conditions: {
hostname: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'),
port: config.get('xpack.reporting.kibanaServer.port') || config.get('server.port'),
basePath: config.get('server.basePath'),
protocol: config.get('xpack.reporting.kibanaServer.protocol') || server.info.protocol,
}
};
return { job, conditionalHeaders };
};
const getCustomLogo = async ({ job, conditionalHeaders }) => {
const fakeRequest = {
headers: conditionalHeaders.headers,
};
const savedObjects = server.savedObjects;
const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(fakeRequest);
const uiSettings = server.uiSettingsServiceFactory({
@ -33,29 +69,10 @@ function executeJobFn(server) {
const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO);
return { job, logo };
return { job, conditionalHeaders, logo };
};
const getSessionCookie = async ({ job, logo }) => {
if (!job.serializedSession) {
return { job, logo, sessionCookie: null };
}
const cookieOptions = await server.plugins.security.getSessionCookieOptions();
const { httpOnly, name, path, secure } = cookieOptions;
return { job, logo, sessionCookie: {
domain: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'),
httpOnly,
name,
path,
sameSite: 'Strict',
secure,
value: job.serializedSession,
} };
};
const addForceNowQuerystring = async ({ job, logo, sessionCookie }) => {
const addForceNowQuerystring = async ({ job, conditionalHeaders, logo }) => {
const urls = job.urls.map(jobUrl => {
if (!job.forceNow) {
return jobUrl;
@ -77,16 +94,19 @@ function executeJobFn(server) {
hash: transformedHash
});
});
return { job, logo, sessionCookie, urls };
return { job, conditionalHeaders, logo, urls };
};
return compatibilityShim(function executeJob(jobToExecute, cancellationToken) {
const process$ = Rx.of(jobToExecute).pipe(
mergeMap(decryptJobHeaders),
catchError(() => Rx.throwError('Failed to decrypt report job data. Please re-generate this report.')),
map(omitBlacklistedHeaders),
map(getConditionalHeaders),
mergeMap(getCustomLogo),
mergeMap(getSessionCookie),
mergeMap(addForceNowQuerystring),
mergeMap(({ job, logo, sessionCookie, urls }) => {
return generatePdfObservable(job.title, urls, job.browserTimezone, sessionCookie, job.layout, logo);
mergeMap(({ job, conditionalHeaders, logo, urls }) => {
return generatePdfObservable(job.title, urls, job.browserTimezone, conditionalHeaders, job.layout, logo);
}),
map(buffer => ({
content_type: 'application/pdf',

View file

@ -16,21 +16,21 @@ const cancellationToken = {
on: jest.fn()
};
let mockServer;
let config;
let mockServer;
beforeEach(() => {
config = {
'xpack.security.cookieName': 'sid',
'xpack.reporting.encryptionKey': 'testencryptionkey',
'xpack.reporting.kibanaServer.protocol': 'http',
'xpack.reporting.kibanaServer.hostname': 'localhost',
'xpack.reporting.kibanaServer.port': 5601,
'server.basePath': ''
'server.basePath': '/sbp',
'server.host': 'localhost',
'server.port': 5601
};
mockServer = {
expose: () => { },
config: memoize(() => ({ get: jest.fn() })),
info: {
protocol: 'http',
},
plugins: {
elasticsearch: {
getCluster: memoize(() => {
@ -38,8 +38,7 @@ beforeEach(() => {
callWithRequest: jest.fn()
};
})
},
security: null,
}
},
savedObjects: {
getScopedSavedObjectsClient: jest.fn(),
@ -56,105 +55,202 @@ beforeEach(() => {
afterEach(() => generatePdfObservableFactory.mockReset());
const encrypt = async (headers) => {
const encryptHeaders = async (headers) => {
const crypto = cryptoFactory(mockServer);
return await crypto.encrypt(headers);
};
describe(`sessionCookie`, () => {
test(`if serializedSession doesn't exist it doesn't pass sessionCookie to generatePdfObservable`, async () => {
mockServer.plugins.security = {};
const headers = {};
const encryptedHeaders = await encrypt(headers);
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
describe('headers', () => {
test(`fails if it can't decrypt headers`, async () => {
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders, session: null }, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, undefined);
await expect(executeJob({ objects: [], timeRange: {} }, cancellationToken)).rejects.toBeDefined();
});
test(`if uses xpack.reporting.kibanaServer.hostname for domain of sessionCookie passed to generatePdfObservable`, async () => {
const sessionCookieOptions = {
httpOnly: true,
name: 'foo',
path: '/bar',
secure: false,
test(`passes in decrypted headers to generatePdf`, async () => {
const headers = {
foo: 'bar',
baz: 'quix',
};
mockServer.plugins.security = {
getSessionCookieOptions() {
return sessionCookieOptions;
},
};
const headers = {};
const encryptedHeaders = await encrypt(headers);
const session = 'thisoldesession';
const encryptedSession = await encrypt(session);
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const encryptedHeaders = await encryptHeaders(headers);
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, cancellationToken);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, {
domain: config['xpack.reporting.kibanaServer.hostname'],
httpOnly: sessionCookieOptions.httpOnly,
name: sessionCookieOptions.name,
path: sessionCookieOptions.path,
sameSite: 'Strict',
secure: sessionCookieOptions.secure,
value: session
}, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: headers
}), undefined, undefined);
});
test(`if uses server.host and reporting config isn't set for domain of sessionCookie passed to generatePdfObservable`, async () => {
config['xpack.reporting.kibanaServer.hostname'] = undefined;
config['server.host'] = 'something.com';
const sessionCookieOptions = {
httpOnly: true,
name: 'foo',
path: '/bar',
secure: false,
test(`omits blacklisted headers`, async () => {
const permittedHeaders = {
foo: 'bar',
baz: 'quix',
};
mockServer.plugins.security = {
getSessionCookieOptions() {
return sessionCookieOptions;
},
};
const headers = {};
const encryptedHeaders = await encrypt(headers);
const session = 'thisoldesession';
const encryptedSession = await encrypt(session);
const blacklistedHeaders = {
'accept-encoding': '',
'content-length': '',
'content-type': '',
'host': '',
'transfer-encoding': '',
};
const encryptedHeaders = await encryptHeaders({
...permittedHeaders,
...blacklistedHeaders
});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, cancellationToken);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, {
domain: config['server.host'],
httpOnly: sessionCookieOptions.httpOnly,
name: sessionCookieOptions.name,
path: sessionCookieOptions.path,
sameSite: 'Strict',
secure: sessionCookieOptions.secure,
value: session
}, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: permittedHeaders
}), undefined, undefined);
});
describe('conditions', () => {
test(`uses hostname from reporting config if set`, async () => {
config['xpack.reporting.kibanaServer.hostname'] = 'custom-hostname';
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
hostname: config['xpack.reporting.kibanaServer.hostname']
})
}), undefined, undefined);
});
test(`uses hostname from server.config if reporting config not set`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
hostname: config['server.host']
})
}), undefined, undefined);
});
test(`uses port from reporting config if set`, async () => {
config['xpack.reporting.kibanaServer.port'] = 443;
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
port: config['xpack.reporting.kibanaServer.port']
})
}), undefined, undefined);
});
test(`uses port from server if reporting config not set`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
port: config['server.port']
})
}), undefined, undefined);
});
test(`uses basePath from server config`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
basePath: config['server.basePath']
})
}), undefined, undefined);
});
test(`uses protocol from reporting config if set`, async () => {
config['xpack.reporting.kibanaServer.protocol'] = 'https';
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
protocol: config['xpack.reporting.kibanaServer.protocol']
})
}), undefined, undefined);
});
test(`uses protocol from server.info`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
headers: expect.anything(),
conditions: expect.objectContaining({
protocol: mockServer.info.protocol
})
}), undefined, undefined);
});
});
});
test(`gets logo from uiSettings`, async () => {
const authorizationHeader = 'thisoldeheader';
const encryptedHeaders = await encrypt({
authorization: authorizationHeader,
thisotherheader: 'pleasedontshowup'
});
const encryptedHeaders = await encryptHeaders({});
const logo = 'custom-logo';
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
@ -165,38 +261,12 @@ test(`gets logo from uiSettings`, async () => {
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.savedObjects.getScopedSavedObjectsClient).toBeCalledWith({
headers: {
authorization: authorizationHeader
},
});
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, logo);
});
test(`doesn't pass authorization header if it doesn't exist when getting logo from uiSettings`, async () => {
const encryptedHeaders = await encrypt({
thisotherheader: 'pleasedontshowup'
});
const logo = 'custom-logo';
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
expect(mockServer.savedObjects.getScopedSavedObjectsClient).toBeCalledWith({
headers: {},
});
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, logo);
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.anything(), undefined, logo);
});
test(`passes browserTimezone to generatePdf`, async () => {
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
@ -206,11 +276,11 @@ test(`passes browserTimezone to generatePdf`, async () => {
await executeJob({ objects: [], browserTimezone, headers: encryptedHeaders }, cancellationToken);
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
expect(generatePdfObservable).toBeCalledWith(undefined, [], browserTimezone, null, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, [], browserTimezone, expect.anything(), undefined, undefined);
});
test(`adds forceNow to hash's query, if it exists`, async () => {
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
@ -218,13 +288,13 @@ test(`adds forceNow to hash's query, if it exists`, async () => {
const executeJob = executeJobFactory(mockServer);
const forceNow = '2000-01-01T00:00:00.000Z';
await executeJob({ objects: [{ relativeUrl: 'app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken);
await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, null, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, expect.anything(), undefined, undefined);
});
test(`appends forceNow to hash's query, if it exists`, async () => {
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
@ -233,30 +303,30 @@ test(`appends forceNow to hash's query, if it exists`, async () => {
const forceNow = '2000-01-01T00:00:00.000Z';
await executeJob({
objects: [{ relativeUrl: 'app/kibana#/something?_g=something' }],
objects: [{ relativeUrl: '/app/kibana#/something?_g=something' }],
forceNow,
headers: encryptedHeaders
}, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, null, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, expect.anything(), undefined, undefined);
});
test(`doesn't append forceNow query to url, if it doesn't exists`, async () => {
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
const executeJob = executeJobFactory(mockServer);
await executeJob({ objects: [{ relativeUrl: 'app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken);
await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/app/kibana#/something'], undefined, null, undefined, undefined);
expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something'], undefined, expect.anything(), undefined, undefined);
});
test(`returns content_type of application/pdf`, async () => {
const executeJob = executeJobFactory(mockServer);
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = generatePdfObservableFactory();
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
@ -272,7 +342,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent)));
const executeJob = executeJobFactory(mockServer);
const encryptedHeaders = await encrypt({});
const encryptedHeaders = await encryptHeaders({});
const { content } = await executeJob({ objects: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken);
expect(content).toEqual(Buffer.from(testContent).toString('base64'));

View file

@ -33,9 +33,9 @@ function generatePdfObservableFn(server) {
const captureConcurrency = 1;
const getLayout = getLayoutFactory(server);
const urlScreenshotsObservable = (urls, sessionCookie, layout) => {
const urlScreenshotsObservable = (urls, conditionalHeaders, layout) => {
return Rx.from(urls).pipe(
mergeMap(url => screenshotsObservable(url, sessionCookie, layout),
mergeMap(url => screenshotsObservable(url, conditionalHeaders, layout),
(outer, inner) => inner,
captureConcurrency
)
@ -67,11 +67,9 @@ function generatePdfObservableFn(server) {
};
return function generatePdfObservable(title, urls, browserTimezone, sessionCookie, layoutParams, logo) {
return function generatePdfObservable(title, urls, browserTimezone, conditionalHeaders, layoutParams, logo) {
const layout = getLayout(layoutParams);
const screenshots$ = urlScreenshotsObservable(urls, sessionCookie, layout);
const screenshots$ = urlScreenshotsObservable(urls, conditionalHeaders, layout);
return screenshots$.pipe(
toArray(),

View file

@ -43,11 +43,11 @@ export function screenshotsObservableFactory(server) {
}
};
const openUrl = async (browser, url, sessionCookie) => {
const openUrl = async (browser, url, conditionalHeaders) => {
const waitForSelector = '.application';
await browser.open(url, {
sessionCookie,
conditionalHeaders,
waitForSelector,
});
};
@ -231,7 +231,7 @@ export function screenshotsObservableFactory(server) {
return screenshots;
};
return function screenshotsObservable(url, sessionCookie, layout) {
return function screenshotsObservable(url, conditionalHeaders, layout) {
return Rx.defer(async () => await getPort()).pipe(
mergeMap(bridgePort => {
@ -259,7 +259,7 @@ export function screenshotsObservableFactory(server) {
tap(browser => startRecording(browser)),
tap(() => logger.debug(`opening ${url}`)),
mergeMap(
browser => openUrl(browser, url, sessionCookie),
browser => openUrl(browser, url, conditionalHeaders),
browser => browser
),
tap(() => logger.debug('injecting custom css')),

View file

@ -7,6 +7,7 @@
import fs from 'fs';
import path from 'path';
import moment from 'moment';
import { parse as parseUrl } from 'url';
import { promisify, delay } from 'bluebird';
import { transformFn } from './transform_fn';
import { ignoreSSLErrorsBehavior } from './ignore_ssl_errors';
@ -30,7 +31,7 @@ export class HeadlessChromiumDriver {
return result.result.value;
}
async open(url, { sessionCookie, waitForSelector }) {
async open(url, { conditionalHeaders, waitForSelector }) {
this._logger.debug(`HeadlessChromiumDriver:opening url ${url}`);
const { Network, Page } = this._client;
await Promise.all([
@ -39,7 +40,34 @@ export class HeadlessChromiumDriver {
]);
await ignoreSSLErrorsBehavior(this._client.Security);
await Network.setCookie(sessionCookie);
Network.requestIntercepted(({ interceptionId, request, authChallenge }) => {
if (authChallenge) {
Network.continueInterceptedRequest({
interceptionId,
authChallengeResponse: {
response: 'Default'
}
});
return;
}
if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, request.url)) {
this._logger.debug(`Using custom headers for ${request.url}`);
Network.continueInterceptedRequest({
interceptionId,
headers: {
...request.headers,
...conditionalHeaders.headers
}
});
} else {
this._logger.debug(`No custom headers for ${request.url}`);
Network.continueInterceptedRequest({
interceptionId
});
}
});
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
await Page.navigate({ url });
await Page.loadEventFired();
const { frameTree } = await Page.getResourceTree();
@ -149,4 +177,41 @@ export class HeadlessChromiumDriver {
await delay(this._waitForDelayMs);
}
}
_shouldUseCustomHeaders(conditions, url) {
const { hostname, protocol, port, pathname } = parseUrl(url);
if (pathname === undefined) {
// There's a discrepancy between the NodeJS docs and the typescript types. NodeJS docs
// just say 'string' and the typescript types say 'string | undefined'. We haven't hit a
// situation where it's undefined but here's an explicit Error if we do.
throw new Error(`pathname is undefined, don't know how to proceed`);
}
return (
hostname === conditions.hostname &&
protocol === `${conditions.protocol}:` &&
this._shouldUseCustomHeadersForPort(conditions, port) &&
pathname.startsWith(`${conditions.basePath}/`)
);
}
_shouldUseCustomHeadersForPort(
conditions,
port
) {
if (conditions.protocol === 'http' && conditions.port === 80) {
return (
port === undefined || port === null || port === '' || port === conditions.port.toString()
);
}
if (conditions.protocol === 'https' && conditions.port === 443) {
return (
port === undefined || port === null || port === '' || port === conditions.port.toString()
);
}
return port === conditions.port.toString();
}
}

View file

@ -11,18 +11,21 @@ export const paths = {
baseUrl: 'https://s3.amazonaws.com/headless-shell/',
packages: [{
platforms: ['darwin', 'freebsd', 'openbsd'],
archiveFilename: 'chromium-503a3e4-darwin.zip',
archiveChecksum: 'c1b530f99374e122c0bd7ba663867a95',
archiveFilename: 'chromium-04c5a83-darwin.zip',
archiveChecksum: '89a98bfa6454bec550f196232d1faeb3',
rawChecksum: '413bbd646a4862a136bc0852ab6f41c5',
binaryRelativePath: 'headless_shell-darwin/headless_shell',
}, {
platforms: ['linux'],
archiveFilename: 'chromium-503a3e4-linux.zip',
archiveChecksum: '9486d8eff9fc4f94c899aa72f5e59520',
archiveFilename: 'chromium-04c5a83-linux.zip',
archiveChecksum: '1339f6d57b6039445647dcdc949ba513',
rawChecksum: '4824710dd8f3da9d9e2c0674a771008b',
binaryRelativePath: 'headless_shell-linux/headless_shell'
}, {
platforms: ['win32'],
archiveFilename: 'chromium-503a3e4-win32.zip',
archiveChecksum: 'a71ce5565791767492f6d0fb4fe5360d',
binaryRelativePath: 'headless_shell-win32\\headless_shell.exe'
archiveFilename: 'chromium-04c5a83-windows.zip',
archiveChecksum: '3b3279b59ebf03db676baeb7b7ab5c24',
rawChecksum: '724011f9acf872c9472c82c6f7981178',
binaryRelativePath: 'headless_shell-windows\\headless_shell.exe'
}]
};

View file

@ -6,14 +6,12 @@
import fs from 'fs';
import path from 'path';
import { promisify } from 'bluebird';
import { extract } from './extract';
import { promisify } from 'util';
import { BROWSERS_BY_TYPE } from './browsers';
import { extract } from './extract';
import { md5 } from './download/checksum';
const fsp = {
access: promisify(fs.access, fs),
chmod: promisify(fs.chmod, fs),
};
const chmod = promisify(fs.chmod);
/**
* "install" a browser by type into installs path by extracting the downloaded
@ -32,13 +30,13 @@ export async function installBrowser(logger, browserConfig, browserType, install
}
const binaryPath = path.join(installsPath, pkg.binaryRelativePath);
try {
await fsp.access(binaryPath, fs.X_OK);
} catch (accessErr) {
// error here means the binary does not exist, so install it
const rawChecksum = await md5(binaryPath).catch(() => '');
if (rawChecksum !== pkg.rawChecksum) {
logger.debug(`Extracting ${browserType} to ${binaryPath}`);
const archive = path.join(browser.paths.archivesPath, pkg.archiveFilename);
await extract(archive, installsPath);
await fsp.chmod(binaryPath, '755');
await chmod(binaryPath, '755');
}
return browser.createDriverFactory(binaryPath, logger, browserConfig);

View file

@ -18,11 +18,77 @@ export function PhantomDriver({ page, browser, zoom, logger }) {
if (page === false || browser === false) throw new Error('Phantom instance is closed');
};
const configurePage = () => {
const configurePage = (pageOptions) => {
const RESOURCE_TIMEOUT = 5000;
return fromCallback(cb => page.set('resourceTimeout', RESOURCE_TIMEOUT, cb))
.then(() => {
if (zoom) return fromCallback(cb => page.set('zoomFactor', zoom, cb));
})
.then(() => {
if (pageOptions.conditionalHeaders) {
const headers = pageOptions.conditionalHeaders.headers;
const conditions = pageOptions.conditionalHeaders.conditions;
const escape = (str) => {
return str
.replace(/'/g, `\\'`)
.replace(/\\/g, `\\\\`)
.replace(/\r?\n/g, '\\n');
};
// we're using base64 encoding for any user generated values that we need to eval
// to be sure that we're handling these properly
const btoa = (str) => {
return Buffer.from(str).toString('base64');
};
const fn = `function (requestData, networkRequest) {
var log = function (msg) {
if (!page.onConsoleMessage) {
return;
}
page.onConsoleMessage(msg);
};
var parseUrl = function (url) {
var link = document.createElement('a');
link.href = url;
return {
protocol: link.protocol,
port: link.port,
hostname: link.hostname,
pathname: link.pathname,
};
};
var shouldUseCustomHeadersForPort = function (port) {
if ('${escape(conditions.protocol)}' === 'http' && ${conditions.port} === 80) {
return port === undefined || port === null || port === '' || port === '${conditions.port}';
}
if ('${escape(conditions.protocol)}' === 'https' && ${conditions.port} === 443) {
return port === undefined || port === null || port === '' || port === '${conditions.port}';
}
return port === '${conditions.port}';
};
var url = parseUrl(requestData.url);
if (
url.hostname === '${escape(conditions.hostname)}' &&
url.protocol === '${escape(conditions.protocol)}:' &&
shouldUseCustomHeadersForPort(url.port) &&
url.pathname.indexOf('${escape(conditions.basePath)}/') === 0
) {
log('Using custom headers for ' + requestData.url);
${Object.keys(headers).map(key => `networkRequest.setHeader(atob('${btoa(key)}'), atob('${btoa(headers[key])}'));`)
.join('\n')}
} else {
log('No custom headers for ' + requestData.url);
}
}`;
return fromCallback(cb => page.setFn('onResourceRequested', fn, cb));
}
});
};
@ -30,26 +96,9 @@ export function PhantomDriver({ page, browser, zoom, logger }) {
open(url, pageOptions) {
validateInstance();
return configurePage()
return configurePage(pageOptions)
.then(() => logger.debug('Configured page'))
.then(() => fromCallback(cb => page.open(url, cb)))
.then(async (status) => {
const { sessionCookie } = pageOptions;
if (sessionCookie) {
await fromCallback(cb => page.clearCookies(cb));
// phantom doesn't support the SameSite option for the cookie, so we aren't setting it
await fromCallback(cb => page.addCookie({
name: sessionCookie.name,
value: sessionCookie.value,
path: sessionCookie.path,
httponly: sessionCookie.httpOnly,
secure: sessionCookie.secure,
}, cb));
return await fromCallback(cb => page.open(url, cb));
} else {
return status;
}
})
.then(status => {
logger.debug(`Page opened with status ${status}`);
if (status !== 'success') throw new Error('URL open failed. Is the server running?');

View file

@ -13,16 +13,19 @@ export const paths = {
platforms: ['darwin', 'freebsd', 'openbsd'],
archiveFilename: 'phantomjs-2.1.1-macosx.zip',
archiveChecksum: 'b0c038bd139b9ecaad8fd321070c1651',
rawChecksum: 'bbebe2381435309431c9d4e989aefdeb',
binaryRelativePath: 'phantomjs-2.1.1-macosx/bin/phantomjs',
}, {
platforms: ['linux'],
archiveFilename: 'phantomjs-2.1.1-linux-x86_64.tar.bz2',
archiveChecksum: '1c947d57fce2f21ce0b43fe2ed7cd361',
rawChecksum: '3f4bbbe5acd45494d8e52941936235f2',
binaryRelativePath: 'phantomjs-2.1.1-linux-x86_64/bin/phantomjs'
}, {
platforms: ['win32'],
archiveFilename: 'phantomjs-2.1.1-windows.zip',
archiveChecksum: '4104470d43ddf2a195e8869deef0aa69',
rawChecksum: '339f74c735e683502c43512a508e53d6',
binaryRelativePath: 'phantomjs-2.1.1-windows\\bin\\phantomjs.exe'
}]
};

View file

@ -13,10 +13,10 @@ function enqueueJobFn(server) {
const queueConfig = server.config().get('xpack.reporting.queue');
const exportTypesRegistry = server.plugins.reporting.exportTypesRegistry;
return async function enqueueJob(exportTypeId, jobParams, user, headers, serializedSession, request) {
return async function enqueueJob(exportTypeId, jobParams, user, headers, request) {
const exportType = exportTypesRegistry.getById(exportTypeId);
const createJob = exportType.createJobFactory(server);
const payload = await createJob(jobParams, headers, serializedSession, request);
const payload = await createJob(jobParams, headers, request);
const options = {
timeout: queueConfig.timeout,

View file

@ -110,12 +110,9 @@ export function main(server) {
async function handler(exportTypeId, jobParams, request, reply) {
const user = request.pre.user;
const headers = {
authorization: request.headers.authorization,
};
const serializedSession = server.plugins.security ? await server.plugins.security.serializeSession(request) : null;
const headers = request.headers;
const job = await enqueueJob(exportTypeId, jobParams, user, headers, serializedSession, request);
const job = await enqueueJob(exportTypeId, jobParams, user, headers, request);
// return the queue's job information
const jobJson = job.toJSON();

View file

@ -518,55 +518,4 @@ describe('Authenticator', () => {
}
});
});
describe('`serializeSession` method', () => {
let serializeSession;
beforeEach(async () => {
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
config.get.withArgs('server.basePath').returns('/base-path');
await initAuthenticator(server);
// Second argument will be a method we'd like to test.
serializeSession = server.expose.withArgs('serializeSession').firstCall.args[1];
});
it('fails if request is not provided.', async () => {
try {
await serializeSession();
expect().fail('`serializeSession` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('Request should be a valid object, was [undefined].');
}
});
it('calls session.serialize with request', async () => {
const request = {};
const expectedResult = Symbol();
session.serialize.withArgs(request).returns(Promise.resolve(expectedResult));
const actualResult = await serializeSession(request);
expect(actualResult).to.be(expectedResult);
});
});
describe('`getSessionCookieOptions` method', () => {
let getSessionCookieOptions;
beforeEach(async () => {
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
config.get.withArgs('server.basePath').returns('/base-path');
await initAuthenticator(server);
// Second argument will be a method we'd like to test.
getSessionCookieOptions = server.expose.withArgs('getSessionCookieOptions').firstCall.args[1];
});
it('calls session.getCookieOptions', async () => {
const expectedResult = Symbol();
session.getCookieOptions.returns(Promise.resolve(expectedResult));
const actualResult = await getSessionCookieOptions();
expect(actualResult).to.be(expectedResult);
});
});
});

View file

@ -6,7 +6,6 @@
import expect from 'expect.js';
import sinon from 'sinon';
import iron from 'iron';
import { serverFixture } from '../../__tests__/__fixtures__/server';
import { Session } from '../session';
@ -46,7 +45,6 @@ describe('Session', () => {
password: 'encryption-key',
clearInvalid: true,
validateFunc: sinon.match.func,
isHttpOnly: true,
isSecure: 'secure-cookies',
path: 'base/path/'
});
@ -199,87 +197,4 @@ describe('Session', () => {
sinon.assert.calledOnce(request.cookieAuth.clear);
});
});
describe('`serialize` method', () => {
let session;
beforeEach(async () => {
config.get.withArgs('xpack.security.cookieName').returns('cookie-name');
config.get.withArgs('xpack.security.encryptionKey').returns('encryption-key');
session = await Session.create(server);
});
it('returns null if state is null', async () => {
const request = {
_states: {
}
};
const returnValue = await session.serialize(request);
expect(returnValue).to.eql(null);
});
it('uses iron to encrypt the state with the set password', async () => {
const stateValue = {
foo: 'bar'
};
const request = {
_states: {
'cookie-name': {
value: stateValue,
}
}
};
sandbox.stub(iron, 'seal')
.withArgs(stateValue, 'encryption-key', iron.defaults)
.callsArgWith(3, null, 'serialized-value');
const returnValue = await session.serialize(request);
expect(returnValue).to.eql('serialized-value');
});
it(`rejects if iron can't seal the session`, async () => {
const stateValue = {
foo: 'bar'
};
const request = {
_states: {
'cookie-name': {
value: stateValue,
}
}
};
sandbox.stub(iron, 'seal')
.withArgs(stateValue, 'encryption-key', iron.defaults)
.callsArgWith(3, new Error('IDK'), null);
try {
await session.serialize(request);
expect().fail('`serialize` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('IDK');
}
});
});
describe('`getCookieOptions` method', () => {
let session;
beforeEach(async () => {
config.get.withArgs('xpack.security.cookieName').returns('cookie-name');
config.get.withArgs('xpack.security.secureCookies').returns('secure-cookies');
config.get.withArgs('server.basePath').returns('base/path');
session = await Session.create(server);
});
it('returns cookie options', () => {
expect(session.getCookieOptions()).to.eql({
name: 'cookie-name',
path: 'base/path/',
httpOnly: true,
secure: 'secure-cookies'
});
});
});
});

View file

@ -209,25 +209,6 @@ class Authenticator {
return DeauthenticationResult.notHandled();
}
/**
* Serializes the request's session.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<string>}
*/
async serializeSession(request) {
assertRequest(request);
return await this._session.serialize(request);
}
/**
* Returns the options that we're using for the session cookie
* @returns {CookieOptions}
*/
getSessionCookieOptions() {
return this._session.getCookieOptions();
}
/**
* Instantiates authentication provider based on the provider key from config.
* @param {string} providerType Provider type key.
@ -296,8 +277,6 @@ export async function initAuthenticator(server) {
server.expose('authenticate', (request) => authenticator.authenticate(request));
server.expose('deauthenticate', (request) => authenticator.deauthenticate(request));
server.expose('registerAuthScopeGetter', (scopeExtender) => authScope.registerGetter(scopeExtender));
server.expose('serializeSession', (request) => authenticator.serializeSession(request));
server.expose('getSessionCookieOptions', () => authenticator.getSessionCookieOptions());
server.expose('isAuthenticated', async (request) => {
try {

View file

@ -6,8 +6,6 @@
import hapiAuthCookie from 'hapi-auth-cookie';
import iron from 'iron';
const HAPI_STRATEGY_NAME = 'security-cookie';
// Forbid applying of Hapi authentication strategies to routes automatically.
const HAPI_STRATEGY_MODE = false;
@ -18,16 +16,6 @@ function assertRequest(request) {
}
}
/**
* CookieOptions
* @typedef {Object} CookieOptions
* @property {string} name - The name of the cookie
* @property {string} password - The password that is used to encrypt the cookie
* @property {string} path - The path that is set for the cookie
* @property {boolean} secure - Whether the cookie should only be sent over HTTPS
* @property {?number} ttl - Session duration in ms. If `null` session will stay active until the browser is closed.
*/
/**
* Manages Kibana user session.
*/
@ -40,20 +28,20 @@ export class Session {
_server = null;
/**
* Options for the cookie
* @type {CookieOptions}
* Session duration in ms. If `null` session will stay active until the browser is closed.
* @type {?number}
* @private
*/
_cookieOptions = null;
_ttl = null;
/**
* Instantiates Session. Constructor is not supposed to be used directly. To make sure that all
* `Session` dependencies/plugins are properly initialized one should use static `Session.create` instead.
* @param {Hapi.Server} server HapiJS Server instance.
*/
constructor(server, cookieOptions) {
constructor(server) {
this._server = server;
this._cookieOptions = cookieOptions;
this._ttl = this._server.config().get('xpack.security.sessionTimeout');
}
/**
@ -92,7 +80,7 @@ export class Session {
request.cookieAuth.set({
value,
expires: this._cookieOptions.ttl && Date.now() + this._cookieOptions.ttl
expires: this._ttl && Date.now() + this._ttl
});
}
@ -107,43 +95,6 @@ export class Session {
request.cookieAuth.clear();
}
/**
* Serializes current session.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<string>}
*/
async serialize(request) {
const state = request._states[this._cookieOptions.name];
if (!state) {
return null;
}
const value = await new Promise((resolve, reject) => {
iron.seal(state.value, this._cookieOptions.password, iron.defaults, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
return value;
}
/**
* Returns the options that we're using for the session cookie
* @returns {CookieOptions}
*/
getCookieOptions() {
return {
name: this._cookieOptions.name,
path: this._cookieOptions.path,
httpOnly: this._cookieOptions.httpOnly,
secure: this._cookieOptions.secure,
};
}
/**
* Prepares and creates a session instance.
* @param {Hapi.Server} server HapiJS Server instance.
@ -162,31 +113,16 @@ export class Session {
});
const config = server.config();
const httpOnly = true;
const name = config.get('xpack.security.cookieName');
const password = config.get('xpack.security.encryptionKey');
const path = `${config.get('server.basePath')}/`;
const secure = config.get('xpack.security.secureCookies');
const ttl = config.get(`xpack.security.sessionTimeout`);
server.auth.strategy(HAPI_STRATEGY_NAME, 'cookie', HAPI_STRATEGY_MODE, {
cookie: name,
password,
cookie: config.get('xpack.security.cookieName'),
password: config.get('xpack.security.encryptionKey'),
clearInvalid: true,
validateFunc: Session._validateCookie,
isHttpOnly: httpOnly,
isSecure: secure,
path: path,
isSecure: config.get('xpack.security.secureCookies'),
path: `${config.get('server.basePath')}/`
});
return new Session(server, {
httpOnly,
name,
password,
path,
secure,
ttl,
});
return new Session(server);
}
/**

View file

@ -129,11 +129,6 @@
url-join "^4.0.0"
ws "^4.1.0"
"@types/cookie@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.1.tgz#720a756ea8e760a258708b52441bd341f1ef4296"
integrity sha512-64Uv+8bTRVZHlbB8eXQgMP9HguxPgnOOIYrQpwHWrtLDrtcG/lILKhUl7bV65NSOIJ9dXGYD7skQFXzhL8tk1A==
"@types/delay@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901"
@ -1960,11 +1955,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
integrity sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=
cookie@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookiejar@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a"
@ -4461,7 +4451,7 @@ ip@1.1.5:
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
iron@4, iron@4.x.x:
iron@4.x.x:
version "4.0.5"
resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg=

View file

@ -3787,7 +3787,7 @@ convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, co
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
integrity sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=
cookie@0.3.1, cookie@^0.3.1:
cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
@ -7966,7 +7966,7 @@ ip@1.1.5:
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
iron@4, iron@4.x.x:
iron@4.x.x:
version "4.0.5"
resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg=