diff --git a/x-pack/package.json b/x-pack/package.json index c437be0d5d22f9..47f15d9781e52a 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -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", 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 9e9ef021dac614..02d78a97ef9bee 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, serializedSession, request) { + return async function createJob(jobParams, headers, 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 deleted file mode 100644 index be4d57bcd3155f..00000000000000 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/create_job/__snapshots__/compatibility_shim.test.js.snap +++ /dev/null @@ -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"`; 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 c04be3a98cec94..18644b99cc4e96 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, 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); }; }; -} +} \ 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 0fd87e805d53f6..d0344ee145427b 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, 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); }); 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 479a44d5f8ba7e..bfd3cb6eaa9d56 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,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(), 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 deleted file mode 100644 index 32b5768e6aabf5..00000000000000 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/execute_job/__snapshots__/compatibility_shim.test.js.snap +++ /dev/null @@ -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"`; 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 e1d85c25941359..f2d5430f9e1fcf 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,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); }; }; -} +} \ No newline at end of file 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 7aaf65575a35c4..7552ebc665cba1 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,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); -}; - -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.toThrowErrorMatchingSnapshot(); - }); - - 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(mockExecuteJob)({ objects: [ { url } ], headers: encryptedHeaders }); - expect(mockExecuteJob.mock.calls.length).toBe(1); - expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe(url); - }); +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 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(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 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(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 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=()'); - }); + await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toBeDefined(); }); -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); +test(`it passes url through if it is a Kibana URL`, async () => { + 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); + 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 the provided layout through`, 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 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); + 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 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); +test(`it generates the absolute url if a relativeUrl is provided`, async () => { + const mockCreateJob = jest.fn(); + const compatibilityShim = compatibilityShimFactory(createMockServer()); - 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); + 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 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); +test(`it generates the absolute url if a relativeUrl with querystring is provided`, async () => { + const mockCreateJob = jest.fn(); + const compatibilityShim = compatibilityShimFactory(createMockServer()); - 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); + 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=()'); }); -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); +test(`it passes the provided browserTimezone through`, async () => { + const mockCreateJob = jest.fn(); + const compatibilityShim = compatibilityShimFactory(createMockServer()); - 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'); - }); + const browserTimezone = 'UTC'; + await compatibilityShim(mockCreateJob)({ browserTimezone, objects: [] }); + expect(mockCreateJob.mock.calls.length).toBe(1); + expect(mockCreateJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone); }); 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 70984e7b91809a..b15ae6d285f2c4 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,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 }; - }; - - 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, - } }; + return { job, conditionalHeaders, logo }; }; - 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', 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 848c68b8ba94d2..fa54ce8c8ce928 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 @@ -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,128 +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); +describe('headers', () => { + test(`fails if it can't decrypt headers`, async () => { + const executeJob = executeJobFactory(mockServer); + await expect(executeJob({ objects: [], timeRange: {} }, cancellationToken)).rejects.toBeDefined(); + }); + + test(`passes in decrypted headers to generatePdf`, async () => { + const headers = { + foo: 'bar', + baz: 'quix', + }; const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + const encryptedHeaders = await encryptHeaders(headers); const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders, session: null }, cancellationToken); + await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); - expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, undefined); + expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ + headers: headers + }), undefined, undefined); }); - 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(`omits blacklisted headers`, async () => { + const permittedHeaders = { + foo: 'bar', + baz: 'quix', }; - mockServer.plugins.security = { - getSessionCookieOptions() { - return sessionCookieOptions; - }, + + const blacklistedHeaders = { + 'accept-encoding': '', + 'content-length': '', + 'content-type': '', + 'host': '', + 'transfer-encoding': '', }; - const headers = {}; - const encryptedHeaders = await encrypt(headers); - const session = 'thisoldesession'; - const encryptedSession = await encrypt(session); + 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); - - 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); + await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); + + expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({ + headers: permittedHeaders + }), 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); + describe('conditions', () => { + test(`uses hostname from reporting config if set`, async () => { + config['xpack.reporting.kibanaServer.hostname'] = 'custom-hostname'; - const session = 'thisoldesession'; - const encryptedSession = await encrypt(session); + const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = generatePdfObservableFactory(); - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + 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); - }); -}); + const executeJob = executeJobFactory(mockServer); + await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); -test(`gets logo from uiSettings`, async () => { - const authorizationHeader = 'thisoldeheader'; - const encryptedHeaders = await encrypt({ - authorization: authorizationHeader, - thisotherheader: 'pleasedontshowup' - }); + 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); + }); - const logo = 'custom-logo'; - mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); + 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 generatePdfObservable = generatePdfObservableFactory(); + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer); - await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken); + 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, 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); + }); }); - 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' - }); +test(`gets logo from uiSettings`, async () => { + const encryptedHeaders = await encryptHeaders({}); const logo = 'custom-logo'; mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo); @@ -188,15 +261,12 @@ test(`doesn't pass authorization header if it doesn't exist when getting logo fr 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')); 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 44ce3c7d0e9cd4..fd16c4977b1a6e 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 @@ -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(), 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 ce47934d750aa3..2e45ba16596372 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 @@ -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')), diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/index.js b/x-pack/plugins/reporting/server/browsers/chromium/driver/index.js index 83d65ca77ce573..4957f937810273 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/index.js +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/index.js @@ -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(); + } } diff --git a/x-pack/plugins/reporting/server/browsers/chromium/paths.js b/x-pack/plugins/reporting/server/browsers/chromium/paths.js index 136b87df4b4658..53df19ebb4c7b0 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/paths.js +++ b/x-pack/plugins/reporting/server/browsers/chromium/paths.js @@ -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' }] }; diff --git a/x-pack/plugins/reporting/server/browsers/install.js b/x-pack/plugins/reporting/server/browsers/install.js index 8619206aa96602..421de08aa61c8a 100644 --- a/x-pack/plugins/reporting/server/browsers/install.js +++ b/x-pack/plugins/reporting/server/browsers/install.js @@ -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); 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 ddb1f704a6c027..af2036c5382e10 100644 --- a/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js +++ b/x-pack/plugins/reporting/server/browsers/phantom/driver/index.js @@ -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?'); diff --git a/x-pack/plugins/reporting/server/browsers/phantom/paths.js b/x-pack/plugins/reporting/server/browsers/phantom/paths.js index e24114ab96ab81..9af8f0c4f9f2dc 100644 --- a/x-pack/plugins/reporting/server/browsers/phantom/paths.js +++ b/x-pack/plugins/reporting/server/browsers/phantom/paths.js @@ -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' }] }; diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.js b/x-pack/plugins/reporting/server/lib/enqueue_job.js index 96a52dd412a6f8..53dd52c8e5a90b 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, 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, diff --git a/x-pack/plugins/reporting/server/routes/main.js b/x-pack/plugins/reporting/server/routes/main.js index 553d505d315713..2c820306ae670e 100644 --- a/x-pack/plugins/reporting/server/routes/main.js +++ b/x-pack/plugins/reporting/server/routes/main.js @@ -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(); 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 634f99f234657b..ab32f30c2e3156 100644 --- a/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js +++ b/x-pack/plugins/security/server/lib/authentication/__tests__/authenticator.js @@ -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); - }); - }); }); 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 1b759bf2af2504..1c8612ad2f4d76 100644 --- a/x-pack/plugins/security/server/lib/authentication/__tests__/session.js +++ b/x-pack/plugins/security/server/lib/authentication/__tests__/session.js @@ -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' - }); - }); - }); }); diff --git a/x-pack/plugins/security/server/lib/authentication/authenticator.js b/x-pack/plugins/security/server/lib/authentication/authenticator.js index 5197433e559a3a..3f11f478921054 100644 --- a/x-pack/plugins/security/server/lib/authentication/authenticator.js +++ b/x-pack/plugins/security/server/lib/authentication/authenticator.js @@ -209,25 +209,6 @@ 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. @@ -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 { diff --git a/x-pack/plugins/security/server/lib/authentication/session.js b/x-pack/plugins/security/server/lib/authentication/session.js index 129e92ca4c98d1..4b852574204639 100644 --- a/x-pack/plugins/security/server/lib/authentication/session.js +++ b/x-pack/plugins/security/server/lib/authentication/session.js @@ -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.} - */ - 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); } /** diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index d2835cbe1addde..1dad72cb91cd26 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -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= diff --git a/yarn.lock b/yarn.lock index 227937689c034b..e7b57b2a79729b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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=