From 9f4ec18000a74e269276ff943979799ccbd4d950 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Thu, 18 Oct 2018 09:30:42 -0700 Subject: [PATCH] Reporting cookies (#24177) * Switching Reporting to use session cookies explicitly * Fixing bug when security is explicitly disabled * Responding to feedback * Fixing yarn.lock --- x-pack/package.json | 3 + .../export_types/csv/server/create_job.js | 2 +- .../compatibility_shim.test.js.snap | 3 + .../server/create_job/compatibility_shim.js | 6 +- .../create_job/compatibility_shim.test.js | 31 +- .../printable_pdf/server/create_job/index.js | 4 +- .../compatibility_shim.test.js.snap | 9 + .../server/execute_job/compatibility_shim.js | 52 ++- .../execute_job/compatibility_shim.test.js | 363 +++++++++++++++--- .../printable_pdf/server/execute_job/index.js | 69 ++-- .../server/execute_job/index.test.js | 203 +++++++--- .../printable_pdf/server/lib/generate_pdf.js | 8 +- .../printable_pdf/server/lib/screenshots.js | 8 +- .../chromium/driver/chromium_driver.ts | 13 +- .../server/browsers/phantom/driver/index.js | 24 +- .../reporting/server/lib/enqueue_job.js | 4 +- .../plugins/reporting/server/routes/main.js | 7 +- x-pack/plugins/reporting/types.d.ts | 10 + .../authentication/__tests__/authenticator.js | 51 +++ .../lib/authentication/__tests__/session.js | 85 ++++ .../lib/authentication/authenticator.js | 21 + .../server/lib/authentication/session.js | 86 ++++- x-pack/yarn.lock | 9 +- yarn.lock | 4 +- 24 files changed, 863 insertions(+), 212 deletions(-) create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/__snapshots__/compatibility_shim.test.js.snap create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/__snapshots__/compatibility_shim.test.js.snap diff --git a/x-pack/package.json b/x-pack/package.json index 8336e56bd986..9860f3c265bc 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -29,6 +29,7 @@ "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", "@types/angular": "^1.6.50", + "@types/cookie": "^0.3.1", "@types/d3-array": "^1.2.1", "@types/d3-scale": "^2.0.0", "@types/d3-shape": "^1.2.2", @@ -151,6 +152,7 @@ "chroma-js": "^1.3.6", "classnames": "2.2.5", "concat-stream": "1.5.1", + "cookie": "^0.3.1", "copy-to-clipboard": "^3.0.8", "d3": "3.5.6", "d3-scale": "1.0.6", @@ -175,6 +177,7 @@ "humps": "2.0.1", "icalendar": "0.7.1", "inline-style": "^2.0.0", + "iron": "4", "isomorphic-fetch": "2.2.1", "joi": "6.10.1", "jquery": "^3.3.1", diff --git a/x-pack/plugins/reporting/export_types/csv/server/create_job.js b/x-pack/plugins/reporting/export_types/csv/server/create_job.js index f116cbb76301..e53ac03e5b1a 100644 --- a/x-pack/plugins/reporting/export_types/csv/server/create_job.js +++ b/x-pack/plugins/reporting/export_types/csv/server/create_job.js @@ -10,7 +10,7 @@ import { cryptoFactory } from '../../../server/lib/crypto'; function createJobFn(server) { const crypto = cryptoFactory(server); - return async function createJob(jobParams, headers, request) { + return async function createJob(jobParams, headers, serializedSession, request) { const serializedEncryptedHeaders = await crypto.encrypt(headers); const savedObjectsClient = request.getSavedObjectsClient(); diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/__snapshots__/compatibility_shim.test.js.snap b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/__snapshots__/compatibility_shim.test.js.snap new file mode 100644 index 000000000000..be4d57bcd315 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/__snapshots__/compatibility_shim.test.js.snap @@ -0,0 +1,3 @@ +// 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"`; diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js index 18644b99cc4e..c04be3a98cec 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js @@ -61,7 +61,7 @@ export function compatibilityShimFactory(server) { queryString, browserTimezone, layout - }, headers, request) { + }, headers, serializedSession, 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, request); + return await createJob(transformedJobParams, headers, serializedSession, request); }; }; -} \ No newline at end of file +} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js index d0344ee14542..0fd87e805d53 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js @@ -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, createMockRequest()); + await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, 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, mockRequest); + await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, 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, mockRequest); + await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, 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, mockRequest); + await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, 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, mockRequest); + await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, 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); + await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, 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); + await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null, null); expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]); }); @@ -137,7 +137,10 @@ 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); + await compatibilityShim(createJobMock)( + { title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' }, + null, null, null + ); expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual(['/app/kibana#/discover/abc?foo=bar']); }); @@ -151,22 +154,24 @@ 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.toBeDefined(); + await expect(promise).rejects.toThrowErrorMatchingSnapshot(); }); -test(`passes headers and request through`, async () => { +test(`passes headers, serializedSession 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, request); + await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, serializedSession, request); expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][1]).toBe(headers); - expect(createJobMock.mock.calls[0][2]).toBe(request); + expect(createJobMock.mock.calls[0][2]).toBe(serializedSession); + expect(createJobMock.mock.calls[0][3]).toBe(request); }); diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/index.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/index.js index bab11f100455..5178d06e5d4d 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/index.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/index.js @@ -18,14 +18,16 @@ function createJobFn(server) { relativeUrls, browserTimezone, layout - }, headers, request) { + }, headers, serializedSession, request) { 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, basePath: request.getBasePath(), diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/__snapshots__/compatibility_shim.test.js.snap b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/__snapshots__/compatibility_shim.test.js.snap new file mode 100644 index 000000000000..32b5768e6aab --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/__snapshots__/compatibility_shim.test.js.snap @@ -0,0 +1,9 @@ +// 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"`; diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.js index df55bb75d262..caf0d5789aa2 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.js @@ -5,10 +5,22 @@ */ 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 = (job, savedObject) => { if (savedObject.urlHash) { @@ -27,11 +39,49 @@ export function compatibilityShimFactory(server) { throw new Error(`Unable to generate report for url ${savedObject.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(savedObject => getSavedObjectAbsoluteUrl(job, savedObject)); + const decryptedHeaders = await decryptJobHeaders(job); + const authorizationHeader = decryptedHeaders.authorization; + const serializedSession = await getSerializedSession(decryptedHeaders, job.session); - return await executeJob({ ...job, urls }, cancellationToken); + return await executeJob({ + title: job.title, + browserTimezone: job.browserTimezone, + layout: job.layout, + basePath: job.basePath, + forceNow: job.forceNow, + urls, + authorizationHeader, + serializedSession, + }, cancellationToken); }; }; } diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.test.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.test.js index f60bc0d83e15..9bca384a563f 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.test.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/compatibility_shim.test.js @@ -5,12 +5,14 @@ */ import { compatibilityShimFactory } from './compatibility_shim'; +import { cryptoFactory } from '../../../../server/lib/crypto'; -const createMockServer = () => { +const createMockServer = ({ security = null } = {}) => { const config = { 'server.host': 'localhost', 'server.port': '5601', 'server.basePath': '', + 'xpack.reporting.encryptionKey': '1234567890qwerty' }; return { @@ -23,83 +25,328 @@ const createMockServer = () => { return { get: key => config[key] }; + }, + plugins: { + security } }; }; -test(`it throw error if full URL is provided that is not a Kibana URL`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); +const encrypt = async (mockServer, headers) => { + const crypto = cryptoFactory(mockServer); + return await crypto.encrypt(headers); +}; - await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toBeDefined(); -}); +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()); -test(`it passes url through if it is a Kibana URL`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toThrowErrorMatchingSnapshot(); + }); - 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); -}); + 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); -test(`it generates the absolute url if a urlHash is provided`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + 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); + }); - 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'); -}); + 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); -test(`it generates the absolute url using server's basePath if a relativeUrl is provided`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + 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'); + }); - 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?'); -}); + 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); -test(`it generates the absolute url using job's basePath if a relativeUrl is provided`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + 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?'); + }); - const relativeUrl = '/app/kibana#/visualize?'; - await compatibilityShim(mockCreateJob)({ basePath: '/s/marketing', objects: [ { relativeUrl } ] }); - expect(mockCreateJob.mock.calls.length).toBe(1); - expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana#/visualize?'); -}); + test(`it generates the absolute url using job'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); -test(`it generates the absolute url using server's basePath if a relativeUrl with querystring is provided`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const relativeUrl = '/app/kibana#/visualize?'; + await compatibilityShim(mockExecuteJob)({ basePath: '/s/marketing', objects: [ { relativeUrl } ], headers: encryptedHeaders }); + expect(mockExecuteJob.mock.calls.length).toBe(1); + expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana#/visualize?'); + }); - 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 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); -test(`it generates the absolute url using job's basePath if a relativeUrl with querystring is provided`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + 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)({ basePath: '/s/marketing', objects: [ { relativeUrl } ] }); - expect(mockCreateJob.mock.calls.length).toBe(1); - expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana?_t=123456789#/visualize?_g=()'); + test(`it generates the absolute url using job'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)({ basePath: '/s/marketing', objects: [ { relativeUrl } ], headers: encryptedHeaders }); + expect(mockExecuteJob.mock.calls.length).toBe(1); + expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/s/marketing/app/kibana?_t=123456789#/visualize?_g=()'); + }); }); test(`it passes the provided browserTimezone through`, async () => { - const mockCreateJob = jest.fn(); - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockExecuteJob = jest.fn(); + const headers = {}; + const mockServer = createMockServer(); + const encryptedHeaders = await encrypt(mockServer, headers); + const compatibilityShim = compatibilityShimFactory(mockServer); const browserTimezone = 'UTC'; - await compatibilityShim(mockCreateJob)({ browserTimezone, objects: [] }); - expect(mockCreateJob.mock.calls.length).toBe(1); - expect(mockCreateJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone); + 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'); + }); }); diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js index 72969083d98e..90ffaf3b3867 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.js @@ -6,46 +6,24 @@ import url from 'url'; import * as Rx from 'rxjs'; -import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; -import { omit } from 'lodash'; +import { mergeMap, map, takeUntil } from 'rxjs/operators'; 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 serverBasePath = server.config().get('server.basePath'); + const config = server.config(); + const serverBasePath = config.get('server.basePath'); - 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 getCustomLogo = async ({ job, filteredHeaders }) => { + const getCustomLogo = async (job) => { const fakeRequest = { - headers: filteredHeaders, + headers: { + ...job.authorizationHeader && { authorization: job.authorizationHeader }, + }, // This is used by the spaces SavedObjectClientWrapper to determine the existing space. // We use the basePath from the saved job, which we'll have post spaces being implemented; // or we use the server base path, which uses the default space @@ -60,10 +38,29 @@ function executeJobFn(server) { const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO); - return { job, filteredHeaders, logo }; + return { job, logo }; }; - const addForceNowQuerystring = async ({ job, filteredHeaders, 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 urls = job.urls.map(jobUrl => { if (!job.forceNow) { return jobUrl; @@ -85,18 +82,16 @@ function executeJobFn(server) { hash: transformedHash }); }); - return { job, filteredHeaders, logo, urls }; + return { job, logo, sessionCookie, 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), mergeMap(getCustomLogo), + mergeMap(getSessionCookie), mergeMap(addForceNowQuerystring), - mergeMap(({ job, filteredHeaders, logo, urls }) => { - return generatePdfObservable(job.title, urls, job.browserTimezone, filteredHeaders, job.layout, logo); + mergeMap(({ job, logo, sessionCookie, urls }) => { + return generatePdfObservable(job.title, urls, job.browserTimezone, sessionCookie, job.layout, logo); }), map(buffer => ({ content_type: 'application/pdf', diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index 10c68f508a73..5fd61c22a676 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -17,7 +17,17 @@ const cancellationToken = { }; let mockServer; +let config; 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': '/sbp' + }; + mockServer = { expose: () => { }, config: memoize(() => ({ get: jest.fn() })), @@ -28,7 +38,8 @@ beforeEach(() => { callWithRequest: jest.fn() }; }) - } + }, + security: null, }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), @@ -37,13 +48,7 @@ beforeEach(() => { }; mockServer.config().get.mockImplementation((key) => { - return { - 'xpack.reporting.encryptionKey': 'testencryptionkey', - 'xpack.reporting.kibanaServer.protocol': 'http', - 'xpack.reporting.kibanaServer.hostname': 'localhost', - 'xpack.reporting.kibanaServer.port': 5601, - 'server.basePath': '/sbp' - }[key]; + return config[key]; }); generatePdfObservableFactory.mockReturnValue(jest.fn()); @@ -51,63 +56,101 @@ beforeEach(() => { afterEach(() => generatePdfObservableFactory.mockReset()); -const encryptHeaders = async (headers) => { +const encrypt = 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); -test(`fails if it can't decrypt headers`, async () => { - const executeJob = executeJobFactory(mockServer); - await expect(executeJob({ objects: [], timeRange: {} }, cancellationToken)).rejects.toBeDefined(); -}); + const generatePdfObservable = generatePdfObservableFactory(); + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); -test(`passes in decrypted headers to generatePdf`, async () => { - const headers = { - foo: 'bar', - baz: 'quix', - }; + const executeJob = executeJobFactory(mockServer); + await executeJob({ objects: [], headers: encryptedHeaders, session: null }, cancellationToken); - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - - const encryptedHeaders = await encryptHeaders(headers); - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, headers, undefined, undefined); -}); - -test(`omits blacklisted headers`, async () => { - const permittedHeaders = { - foo: 'bar', - baz: 'quix', - }; - - const blacklistedHeaders = { - 'accept-encoding': '', - 'content-length': '', - 'content-type': '', - 'host': '', - 'transfer-encoding': '', - }; - - const encryptedHeaders = await encryptHeaders({ - ...permittedHeaders, - ...blacklistedHeaders + expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, undefined); }); - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + 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, + }; + mockServer.plugins.security = { + getSessionCookieOptions() { + return sessionCookieOptions; + }, + }; + const headers = {}; + const encryptedHeaders = await encrypt(headers); - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); + const session = 'thisoldesession'; + const encryptedSession = await encrypt(session); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, permittedHeaders, undefined, undefined); + const generatePdfObservable = generatePdfObservableFactory(); + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = executeJobFactory(mockServer); + await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, 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); + }); + + 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, + }; + 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 executeJob = executeJobFactory(mockServer); + await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, 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); + }); }); test('uses basePath from job when creating saved object service', async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const logo = 'custom-logo'; mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); @@ -123,7 +166,7 @@ test('uses basePath from job when creating saved object service', async () => { }); test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const logo = 'custom-logo'; mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); @@ -138,7 +181,11 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav }); test(`gets logo from uiSettings`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const authorizationHeader = 'thisoldeheader'; + const encryptedHeaders = await encrypt({ + authorization: authorizationHeader, + thisotherheader: 'pleasedontshowup' + }); const logo = 'custom-logo'; mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); @@ -149,12 +196,40 @@ test(`gets logo from uiSettings`, async () => { const executeJob = executeJobFactory(mockServer); await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); + expect(mockServer.savedObjects.getScopedSavedObjectsClient).toBeCalledWith({ + headers: { + authorization: authorizationHeader + }, + getBasePath: expect.anything() + }); expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, {}, undefined, logo); + 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: {}, + getBasePath: expect.anything() + }); + expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo'); + expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, logo); }); test(`passes browserTimezone to generatePdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); @@ -164,11 +239,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, {}, undefined, undefined); + expect(generatePdfObservable).toBeCalledWith(undefined, [], browserTimezone, null, undefined, undefined); }); test(`adds forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); @@ -178,11 +253,11 @@ test(`adds forceNow to hash's query, if it exists`, async () => { await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], forceNow, headers: encryptedHeaders }, cancellationToken); - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined); + expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, null, undefined, undefined); }); test(`appends forceNow to hash's query, if it exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); @@ -196,11 +271,11 @@ test(`appends forceNow to hash's query, if it exists`, async () => { headers: encryptedHeaders }, cancellationToken); - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, {}, undefined, undefined); + expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z'], undefined, null, undefined, undefined); }); test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); @@ -209,12 +284,12 @@ test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { await executeJob({ objects: [{ relativeUrl: '/app/kibana#/something' }], headers: encryptedHeaders }, cancellationToken); - expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something'], undefined, {}, undefined, undefined); + expect(generatePdfObservable).toBeCalledWith(undefined, ['http://localhost:5601/sbp/app/kibana#/something'], undefined, null, undefined, undefined); }); test(`returns content_type of application/pdf`, async () => { const executeJob = executeJobFactory(mockServer); - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); @@ -230,7 +305,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); const executeJob = executeJobFactory(mockServer); - const encryptedHeaders = await encryptHeaders({}); + const encryptedHeaders = await encrypt({}); const { content } = await executeJob({ objects: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken); expect(content).toEqual(Buffer.from(testContent).toString('base64')); diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js index ca1fa44c2591..567d53ffd598 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js @@ -32,9 +32,9 @@ function generatePdfObservableFn(server) { const screenshotsObservable = screenshotsObservableFactory(server); const captureConcurrency = 1; - const urlScreenshotsObservable = (urls, headers, layout, browserTimezone) => { + const urlScreenshotsObservable = (urls, sessionCookie, layout, browserTimezone) => { return Rx.from(urls).pipe( - mergeMap(url => screenshotsObservable(url, headers, layout, browserTimezone), + mergeMap(url => screenshotsObservable(url, sessionCookie, layout, browserTimezone), (outer, inner) => inner, captureConcurrency ) @@ -66,11 +66,11 @@ function generatePdfObservableFn(server) { }; - return function generatePdfObservable(title, urls, browserTimezone, headers, layoutParams, logo) { + return function generatePdfObservable(title, urls, browserTimezone, sessionCookie, layoutParams, logo) { const layout = createLayout(server, layoutParams); - const screenshots$ = urlScreenshotsObservable(urls, headers, layout, browserTimezone); + const screenshots$ = urlScreenshotsObservable(urls, sessionCookie, layout, browserTimezone); return screenshots$.pipe( toArray(), diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js index a97d625a46af..fa574f5414e4 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js @@ -29,11 +29,11 @@ export function screenshotsObservableFactory(server) { return result; }; - const openUrl = async (browser, url, headers) => { + const openUrl = async (browser, url, sessionCookie) => { const waitForSelector = '.application'; await browser.open(url, { - headers, + sessionCookie, waitForSelector, }); }; @@ -223,7 +223,7 @@ export function screenshotsObservableFactory(server) { return screenshots; }; - return function screenshotsObservable(url, headers, layout, browserTimezone) { + return function screenshotsObservable(url, sessionCookie, layout, browserTimezone) { return Rx.defer(async () => await getPort()).pipe( mergeMap(bridgePort => { @@ -251,7 +251,7 @@ export function screenshotsObservableFactory(server) { const screenshot$ = driver$.pipe( tap(() => logger.debug(`opening ${url}`)), mergeMap( - browser => openUrl(browser, url, headers), + browser => openUrl(browser, url, sessionCookie), browser => browser ), tap(() => logger.debug('injecting custom css')), diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index c419cae26340..6d5ef9bfbd28 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -11,6 +11,7 @@ import { EvalFn, EvaluateOptions, Logger, + SessionCookie, ViewZoomWidthHeight, } from '../../../../types'; @@ -31,11 +32,19 @@ export class HeadlessChromiumDriver { public async open( url: string, - { headers, waitForSelector }: { headers: Record; waitForSelector: string } + { + sessionCookie, + waitForSelector, + }: { + sessionCookie: SessionCookie; + waitForSelector: string; + } ) { this.logger.debug(`HeadlessChromiumDriver:opening url ${url}`); + if (sessionCookie) { + await this.page.setCookie(sessionCookie); + } - await this.page.setExtraHTTPHeaders(headers); await this.page.goto(url, { waitUntil: 'domcontentloaded' }); await this.page.waitFor(waitForSelector); } diff --git a/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js b/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js index f1b6af03261b..ddb1f704a6c0 100644 --- a/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js +++ b/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js @@ -18,14 +18,11 @@ export function PhantomDriver({ page, browser, zoom, logger }) { if (page === false || browser === false) throw new Error('Phantom instance is closed'); }; - const configurePage = (pageOptions) => { + const configurePage = () => { 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.headers) return fromCallback(cb => page.set('customHeaders', pageOptions.headers, cb)); }); }; @@ -33,9 +30,26 @@ export function PhantomDriver({ page, browser, zoom, logger }) { open(url, pageOptions) { validateInstance(); - return configurePage(pageOptions) + return configurePage() .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?'); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.js b/x-pack/plugins/reporting/server/lib/enqueue_job.js index 53dd52c8e5a9..96a52dd412a6 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.js +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.js @@ -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, request) { + return async function enqueueJob(exportTypeId, jobParams, user, headers, serializedSession, request) { const exportType = exportTypesRegistry.getById(exportTypeId); const createJob = exportType.createJobFactory(server); - const payload = await createJob(jobParams, headers, request); + const payload = await createJob(jobParams, headers, serializedSession, request); const options = { timeout: queueConfig.timeout, diff --git a/x-pack/plugins/reporting/server/routes/main.js b/x-pack/plugins/reporting/server/routes/main.js index 2c820306ae67..553d505d3157 100644 --- a/x-pack/plugins/reporting/server/routes/main.js +++ b/x-pack/plugins/reporting/server/routes/main.js @@ -110,9 +110,12 @@ export function main(server) { async function handler(exportTypeId, jobParams, request, reply) { const user = request.pre.user; - const headers = request.headers; + const headers = { + authorization: request.headers.authorization, + }; + const serializedSession = server.plugins.security ? await server.plugins.security.serializeSession(request) : null; - const job = await enqueueJob(exportTypeId, jobParams, user, headers, request); + const job = await enqueueJob(exportTypeId, jobParams, user, headers, serializedSession, request); // return the queue's job information const jobJson = job.toJSON(); diff --git a/x-pack/plugins/reporting/types.d.ts b/x-pack/plugins/reporting/types.d.ts index 11c171779fa7..7250c7872c14 100644 --- a/x-pack/plugins/reporting/types.d.ts +++ b/x-pack/plugins/reporting/types.d.ts @@ -53,3 +53,13 @@ export interface ElementPosition { export interface HeadlessElementInfo { position: ElementPosition; } + +export interface SessionCookie { + name: string; + value: string; + domain: string; + path: string; + httpOnly: boolean; + secure: boolean; + sameSite: 'Strict' | 'Lax'; +} diff --git a/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js b/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js index 7a11e694b897..557a3743e75c 100644 --- a/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js +++ b/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js @@ -542,4 +542,55 @@ 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); + }); + }); }); diff --git a/x-pack/plugins/security/server/lib/authentication/__tests__/session.js b/x-pack/plugins/security/server/lib/authentication/__tests__/session.js index 1c8612ad2f4d..1b759bf2af25 100644 --- a/x-pack/plugins/security/server/lib/authentication/__tests__/session.js +++ b/x-pack/plugins/security/server/lib/authentication/__tests__/session.js @@ -6,6 +6,7 @@ import expect from 'expect.js'; import sinon from 'sinon'; +import iron from 'iron'; import { serverFixture } from '../../__tests__/__fixtures__/server'; import { Session } from '../session'; @@ -45,6 +46,7 @@ describe('Session', () => { password: 'encryption-key', clearInvalid: true, validateFunc: sinon.match.func, + isHttpOnly: true, isSecure: 'secure-cookies', path: 'base/path/' }); @@ -197,4 +199,87 @@ 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' + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/lib/authentication/authenticator.js b/x-pack/plugins/security/server/lib/authentication/authenticator.js index e731c6724e9d..b5a6b781feda 100644 --- a/x-pack/plugins/security/server/lib/authentication/authenticator.js +++ b/x-pack/plugins/security/server/lib/authentication/authenticator.js @@ -213,6 +213,25 @@ class Authenticator { return DeauthenticationResult.notHandled(); } + /** + * Serializes the request's session. + * @param {Hapi.Request} request HapiJS request instance. + * @returns {Promise.} + */ + 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. @@ -281,6 +300,8 @@ export async function initAuthenticator(server, authorizationMode) { 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 { diff --git a/x-pack/plugins/security/server/lib/authentication/session.js b/x-pack/plugins/security/server/lib/authentication/session.js index 4b8525742046..129e92ca4c98 100644 --- a/x-pack/plugins/security/server/lib/authentication/session.js +++ b/x-pack/plugins/security/server/lib/authentication/session.js @@ -6,6 +6,8 @@ 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; @@ -16,6 +18,16 @@ 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. */ @@ -28,20 +40,20 @@ export class Session { _server = null; /** - * Session duration in ms. If `null` session will stay active until the browser is closed. - * @type {?number} + * Options for the cookie + * @type {CookieOptions} * @private */ - _ttl = null; + _cookieOptions = 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) { + constructor(server, cookieOptions) { this._server = server; - this._ttl = this._server.config().get('xpack.security.sessionTimeout'); + this._cookieOptions = cookieOptions; } /** @@ -80,7 +92,7 @@ export class Session { request.cookieAuth.set({ value, - expires: this._ttl && Date.now() + this._ttl + expires: this._cookieOptions.ttl && Date.now() + this._cookieOptions.ttl }); } @@ -95,6 +107,43 @@ export class Session { request.cookieAuth.clear(); } + /** + * Serializes current session. + * @param {Hapi.Request} request HapiJS request instance. + * @returns {Promise.} + */ + 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. @@ -113,16 +162,31 @@ 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: config.get('xpack.security.cookieName'), - password: config.get('xpack.security.encryptionKey'), + cookie: name, + password, clearInvalid: true, validateFunc: Session._validateCookie, - isSecure: config.get('xpack.security.secureCookies'), - path: `${config.get('server.basePath')}/` + isHttpOnly: httpOnly, + isSecure: secure, + path: path, }); - return new Session(server); + return new Session(server, { + httpOnly, + name, + password, + path, + secure, + ttl, + }); } /** diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 5dd1172501e9..8583a7318851 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -177,6 +177,11 @@ dependencies: "@types/babel-types" "*" +"@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/cookiejar@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" @@ -2603,7 +2608,7 @@ 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: +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= @@ -5375,7 +5380,7 @@ invert-kv@^1.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= -iron@4.x.x: +iron@4, iron@4.x.x: version "4.0.5" resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428" integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg= diff --git a/yarn.lock b/yarn.lock index b021f9629634..e208fd72b9c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,7 +3960,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= @@ -8149,7 +8149,7 @@ ip-regex@^1.0.1: resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" integrity sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0= -iron@4.x.x: +iron@4, iron@4.x.x: version "4.0.5" resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428" integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg=