mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* Revert "Reporting cookies (#24177) (#24222)"
This reverts commit 95997528c7
.
* Take 2
* Fixing session test
This commit is contained in:
parent
1f59c294da
commit
c3656ce91f
25 changed files with 484 additions and 854 deletions
|
@ -31,7 +31,6 @@
|
|||
"@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",
|
||||
|
@ -153,7 +152,6 @@
|
|||
"chroma-js": "^1.3.6",
|
||||
"classnames": "2.2.5",
|
||||
"concat-stream": "1.5.1",
|
||||
"cookie": "^0.3.1",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"cronstrue": "^1.51.0",
|
||||
"d3": "3.5.6",
|
||||
|
@ -179,7 +177,6 @@
|
|||
"humps": "2.0.1",
|
||||
"icalendar": "0.7.1",
|
||||
"inline-style": "^2.0.0",
|
||||
"iron": "4",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"joi": "^13.5.2",
|
||||
"jquery": "^3.3.1",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"`;
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -18,16 +18,14 @@ function createJobFn(server) {
|
|||
relativeUrls,
|
||||
browserTimezone,
|
||||
layout
|
||||
}, headers, serializedSession, request) {
|
||||
}, headers, 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(),
|
||||
|
|
|
@ -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"`;
|
|
@ -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 = (job, savedObject) => {
|
||||
if (savedObject.urlHash) {
|
||||
|
@ -39,49 +27,11 @@ 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({
|
||||
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);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,328 +23,83 @@ const createMockServer = ({ security = null } = {}) => {
|
|||
return {
|
||||
get: key => config[key]
|
||||
};
|
||||
},
|
||||
plugins: {
|
||||
security
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const encrypt = async (mockServer, headers) => {
|
||||
const crypto = cryptoFactory(mockServer);
|
||||
return await crypto.encrypt(headers);
|
||||
};
|
||||
test(`it throw error if full URL is provided that is not a Kibana URL`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
describe('urls', () => {
|
||||
test(`it throw error if full URL is provided that is not a Kibana URL`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
await expect(compatibilityShim(mockCreateJob)({ query: '', objects: [ { url: 'https://localhost/app/kibana' } ] })).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
test(`it passes url through if it is a Kibana URL`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
test(`it passes url through if it is a Kibana URL`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
const url = 'http://localhost:5601/app/kibana/#visualize';
|
||||
await compatibilityShim(mockCreateJob)({ objects: [ { url } ] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].objects[0].url).toBe(url);
|
||||
});
|
||||
|
||||
const url = 'http://localhost:5601/app/kibana/#visualize';
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [ { url } ], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe(url);
|
||||
});
|
||||
test(`it generates the absolute url if a urlHash is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
test(`it generates the absolute url if a urlHash is provided`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
const urlHash = '#visualize';
|
||||
await compatibilityShim(mockCreateJob)({ objects: [ { urlHash } ] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#visualize');
|
||||
});
|
||||
|
||||
const urlHash = '#visualize';
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [ { urlHash } ], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#visualize');
|
||||
});
|
||||
test(`it generates the absolute url using server's basePath if a relativeUrl is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
test(`it generates the absolute url using server's basePath if a relativeUrl is provided`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
const relativeUrl = '/app/kibana#/visualize?';
|
||||
await compatibilityShim(mockCreateJob)({ objects: [ { relativeUrl } ] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#/visualize?');
|
||||
});
|
||||
|
||||
const relativeUrl = '/app/kibana#/visualize?';
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [ { relativeUrl } ], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].urls[0]).toBe('http://localhost:5601/app/kibana#/visualize?');
|
||||
});
|
||||
test(`it generates the absolute url using job's basePath if a relativeUrl is provided`, async () => {
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
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);
|
||||
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?');
|
||||
});
|
||||
|
||||
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?');
|
||||
});
|
||||
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());
|
||||
|
||||
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(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=()');
|
||||
});
|
||||
|
||||
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=()');
|
||||
});
|
||||
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());
|
||||
|
||||
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=()');
|
||||
});
|
||||
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 passes the provided browserTimezone through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
const mockCreateJob = jest.fn();
|
||||
const compatibilityShim = compatibilityShimFactory(createMockServer());
|
||||
|
||||
const browserTimezone = 'UTC';
|
||||
await compatibilityShim(mockExecuteJob)({ browserTimezone, objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone);
|
||||
});
|
||||
|
||||
test(`it passes the provided title through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
const title = 'thetitle';
|
||||
await compatibilityShim(mockExecuteJob)({ title, objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].title).toEqual(title);
|
||||
});
|
||||
|
||||
test(`it passes the provided layout through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
const layout = Symbol();
|
||||
await compatibilityShim(mockExecuteJob)({ layout, objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].layout).toEqual(layout);
|
||||
});
|
||||
|
||||
test(`it passes the provided basePath through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
const basePath = '/foo/bar/baz';
|
||||
await compatibilityShim(mockExecuteJob)({ basePath, objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].basePath).toEqual(basePath);
|
||||
});
|
||||
|
||||
test(`it passes the provided forceNow through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
const forceNow = 'ISO 8601 Formatted Date';
|
||||
await compatibilityShim(mockExecuteJob)({ forceNow, objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].forceNow).toEqual(forceNow);
|
||||
});
|
||||
|
||||
describe('headers', () => {
|
||||
test(`it fails if it can't decrypt the headers`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = 'imnotencryptedgrimacingface';
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders })).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test(`passes the authorization header through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const headers = {
|
||||
authorization: 'foo',
|
||||
bar: 'quz',
|
||||
};
|
||||
const mockServer = createMockServer();
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].authorizationHeader).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionCookie', () => {
|
||||
test(`it doesn't pass serializedSession through if server.plugins.security is null`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer();
|
||||
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const session = 'asession';
|
||||
const encryptedSession = await encrypt(mockServer, session);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: encryptedSession });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
|
||||
});
|
||||
|
||||
test(`it fails if it can't decrypt the session`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {}
|
||||
});
|
||||
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const session = 'asession';
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
|
||||
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session }))
|
||||
.rejects
|
||||
.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test(`it passes decrypted session through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {}
|
||||
});
|
||||
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const session = 'asession';
|
||||
const encryptedSession = await encrypt(mockServer, session);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: encryptedSession });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(session);
|
||||
});
|
||||
|
||||
test(`it passes null if encrypted headers don't have any cookies`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {}
|
||||
});
|
||||
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
|
||||
});
|
||||
|
||||
test(`it passes null if encrypted headers doesn't have session cookie`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {
|
||||
getSessionCookieOptions() {
|
||||
return {
|
||||
name: 'sid',
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'foo': 'bar',
|
||||
};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual(null);
|
||||
});
|
||||
|
||||
test(`it throws error if cookie name can't be determined`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {
|
||||
getSessionCookieOptions() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'cookie': 'foo=bar;',
|
||||
};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await expect(compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null }))
|
||||
.rejects
|
||||
.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test(`it passes value of session cookie from the headers through`, async () => {
|
||||
const mockExecuteJob = jest.fn();
|
||||
const mockServer = createMockServer({
|
||||
security: {
|
||||
getSessionCookieOptions() {
|
||||
return {
|
||||
name: 'sid'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'cookie': 'sid=foo; bar=quz;',
|
||||
};
|
||||
const encryptedHeaders = await encrypt(mockServer, headers);
|
||||
|
||||
const compatibilityShim = compatibilityShimFactory(mockServer);
|
||||
await compatibilityShim(mockExecuteJob)({ objects: [], headers: encryptedHeaders, session: null });
|
||||
expect(mockExecuteJob.mock.calls.length).toBe(1);
|
||||
expect(mockExecuteJob.mock.calls[0][0].serializedSession).toEqual('foo');
|
||||
});
|
||||
await compatibilityShim(mockCreateJob)({ browserTimezone, objects: [] });
|
||||
expect(mockCreateJob.mock.calls.length).toBe(1);
|
||||
expect(mockCreateJob.mock.calls[0][0].browserTimezone).toEqual(browserTimezone);
|
||||
});
|
||||
|
|
|
@ -6,24 +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 serverBasePath = config.get('server.basePath');
|
||||
|
||||
const getCustomLogo = async (job) => {
|
||||
const serverBasePath = server.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 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: {
|
||||
...job.authorizationHeader && { authorization: job.authorizationHeader },
|
||||
},
|
||||
headers: conditionalHeaders.headers,
|
||||
// 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
|
||||
|
@ -38,29 +75,10 @@ function executeJobFn(server) {
|
|||
|
||||
const logo = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO);
|
||||
|
||||
return { job, logo };
|
||||
return { job, conditionalHeaders, logo };
|
||||
};
|
||||
|
||||
const getSessionCookie = async ({ job, logo }) => {
|
||||
if (!job.serializedSession) {
|
||||
return { job, logo, sessionCookie: null };
|
||||
}
|
||||
|
||||
const cookieOptions = await server.plugins.security.getSessionCookieOptions();
|
||||
const { httpOnly, name, path, secure } = cookieOptions;
|
||||
|
||||
return { job, logo, sessionCookie: {
|
||||
domain: config.get('xpack.reporting.kibanaServer.hostname') || config.get('server.host'),
|
||||
httpOnly,
|
||||
name,
|
||||
path,
|
||||
sameSite: 'Strict',
|
||||
secure,
|
||||
value: job.serializedSession,
|
||||
} };
|
||||
};
|
||||
|
||||
const addForceNowQuerystring = async ({ job, logo, sessionCookie }) => {
|
||||
const addForceNowQuerystring = async ({ job, conditionalHeaders, logo }) => {
|
||||
const urls = job.urls.map(jobUrl => {
|
||||
if (!job.forceNow) {
|
||||
return jobUrl;
|
||||
|
@ -82,16 +100,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',
|
||||
|
|
|
@ -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': '/sbp'
|
||||
'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,101 +55,202 @@ beforeEach(() => {
|
|||
|
||||
afterEach(() => generatePdfObservableFactory.mockReset());
|
||||
|
||||
const encrypt = async (headers) => {
|
||||
const encryptHeaders = async (headers) => {
|
||||
const crypto = cryptoFactory(mockServer);
|
||||
return await crypto.encrypt(headers);
|
||||
};
|
||||
|
||||
describe(`sessionCookie`, () => {
|
||||
test(`if serializedSession doesn't exist it doesn't pass sessionCookie to generatePdfObservable`, async () => {
|
||||
mockServer.plugins.security = {};
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(headers);
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
describe('headers', () => {
|
||||
test(`fails if it can't decrypt headers`, async () => {
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders, session: null }, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, undefined);
|
||||
await expect(executeJob({ objects: [], timeRange: {} }, cancellationToken)).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test(`if uses xpack.reporting.kibanaServer.hostname for domain of sessionCookie passed to generatePdfObservable`, async () => {
|
||||
const sessionCookieOptions = {
|
||||
httpOnly: true,
|
||||
name: 'foo',
|
||||
path: '/bar',
|
||||
secure: false,
|
||||
test(`passes in decrypted headers to generatePdf`, async () => {
|
||||
const headers = {
|
||||
foo: 'bar',
|
||||
baz: 'quix',
|
||||
};
|
||||
mockServer.plugins.security = {
|
||||
getSessionCookieOptions() {
|
||||
return sessionCookieOptions;
|
||||
},
|
||||
};
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(headers);
|
||||
|
||||
const session = 'thisoldesession';
|
||||
const encryptedSession = await encrypt(session);
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const encryptedHeaders = await encryptHeaders(headers);
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, cancellationToken);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, {
|
||||
domain: config['xpack.reporting.kibanaServer.hostname'],
|
||||
httpOnly: sessionCookieOptions.httpOnly,
|
||||
name: sessionCookieOptions.name,
|
||||
path: sessionCookieOptions.path,
|
||||
sameSite: 'Strict',
|
||||
secure: sessionCookieOptions.secure,
|
||||
value: session
|
||||
}, undefined, undefined);
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: headers
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`if uses server.host and reporting config isn't set for domain of sessionCookie passed to generatePdfObservable`, async () => {
|
||||
config['xpack.reporting.kibanaServer.hostname'] = undefined;
|
||||
config['server.host'] = 'something.com';
|
||||
const sessionCookieOptions = {
|
||||
httpOnly: true,
|
||||
name: 'foo',
|
||||
path: '/bar',
|
||||
secure: false,
|
||||
test(`omits blacklisted headers`, async () => {
|
||||
const permittedHeaders = {
|
||||
foo: 'bar',
|
||||
baz: 'quix',
|
||||
};
|
||||
mockServer.plugins.security = {
|
||||
getSessionCookieOptions() {
|
||||
return sessionCookieOptions;
|
||||
},
|
||||
};
|
||||
const headers = {};
|
||||
const encryptedHeaders = await encrypt(headers);
|
||||
|
||||
const session = 'thisoldesession';
|
||||
const encryptedSession = await encrypt(session);
|
||||
const blacklistedHeaders = {
|
||||
'accept-encoding': '',
|
||||
'content-length': '',
|
||||
'content-type': '',
|
||||
'host': '',
|
||||
'transfer-encoding': '',
|
||||
};
|
||||
|
||||
const encryptedHeaders = await encryptHeaders({
|
||||
...permittedHeaders,
|
||||
...blacklistedHeaders
|
||||
});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders, session: encryptedSession }, cancellationToken);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, {
|
||||
domain: config['server.host'],
|
||||
httpOnly: sessionCookieOptions.httpOnly,
|
||||
name: sessionCookieOptions.name,
|
||||
path: sessionCookieOptions.path,
|
||||
sameSite: 'Strict',
|
||||
secure: sessionCookieOptions.secure,
|
||||
value: session
|
||||
}, undefined, undefined);
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: permittedHeaders
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
describe('conditions', () => {
|
||||
test(`uses hostname from reporting config if set`, async () => {
|
||||
config['xpack.reporting.kibanaServer.hostname'] = 'custom-hostname';
|
||||
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
hostname: config['xpack.reporting.kibanaServer.hostname']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses hostname from server.config if reporting config not set`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
hostname: config['server.host']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses port from reporting config if set`, async () => {
|
||||
config['xpack.reporting.kibanaServer.port'] = 443;
|
||||
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
port: config['xpack.reporting.kibanaServer.port']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses port from server if reporting config not set`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
port: config['server.port']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses basePath from server config`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
basePath: config['server.basePath']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses protocol from reporting config if set`, async () => {
|
||||
config['xpack.reporting.kibanaServer.protocol'] = 'https';
|
||||
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
protocol: config['xpack.reporting.kibanaServer.protocol']
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
|
||||
test(`uses protocol from server.info`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, expect.objectContaining({
|
||||
headers: expect.anything(),
|
||||
conditions: expect.objectContaining({
|
||||
protocol: mockServer.info.protocol
|
||||
})
|
||||
}), undefined, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('uses basePath from job when creating saved object service', async () => {
|
||||
const encryptedHeaders = await encrypt({});
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
@ -166,7 +266,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 encrypt({});
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
@ -181,11 +281,7 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav
|
|||
});
|
||||
|
||||
test(`gets logo from uiSettings`, async () => {
|
||||
const authorizationHeader = 'thisoldeheader';
|
||||
const encryptedHeaders = await encrypt({
|
||||
authorization: authorizationHeader,
|
||||
thisotherheader: 'pleasedontshowup'
|
||||
});
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
@ -196,40 +292,12 @@ test(`gets logo from uiSettings`, async () => {
|
|||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient).toBeCalledWith({
|
||||
headers: {
|
||||
authorization: authorizationHeader
|
||||
},
|
||||
getBasePath: expect.anything()
|
||||
});
|
||||
expect(mockServer.uiSettingsServiceFactory().get).toBeCalledWith('xpackReporting:customPdfLogo');
|
||||
expect(generatePdfObservable).toBeCalledWith(undefined, [], undefined, null, undefined, logo);
|
||||
});
|
||||
|
||||
test(`doesn't pass authorization header if it doesn't exist when getting logo from uiSettings`, async () => {
|
||||
const encryptedHeaders = await encrypt({
|
||||
thisotherheader: 'pleasedontshowup'
|
||||
});
|
||||
|
||||
const logo = 'custom-logo';
|
||||
mockServer.uiSettingsServiceFactory().get.mockReturnValue(logo);
|
||||
|
||||
const generatePdfObservable = generatePdfObservableFactory();
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const executeJob = executeJobFactory(mockServer);
|
||||
await executeJob({ objects: [], headers: encryptedHeaders }, cancellationToken);
|
||||
|
||||
expect(mockServer.savedObjects.getScopedSavedObjectsClient).toBeCalledWith({
|
||||
headers: {},
|
||||
getBasePath: expect.anything()
|
||||
});
|
||||
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('')));
|
||||
|
@ -239,11 +307,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('')));
|
||||
|
@ -253,11 +321,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, 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('')));
|
||||
|
@ -271,11 +339,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, 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('')));
|
||||
|
@ -284,12 +352,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, 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('')));
|
||||
|
@ -305,7 +373,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'));
|
||||
|
|
|
@ -32,9 +32,9 @@ function generatePdfObservableFn(server) {
|
|||
const screenshotsObservable = screenshotsObservableFactory(server);
|
||||
const captureConcurrency = 1;
|
||||
|
||||
const urlScreenshotsObservable = (urls, sessionCookie, layout, browserTimezone) => {
|
||||
const urlScreenshotsObservable = (urls, conditionalHeaders, layout, browserTimezone) => {
|
||||
return Rx.from(urls).pipe(
|
||||
mergeMap(url => screenshotsObservable(url, sessionCookie, layout, browserTimezone),
|
||||
mergeMap(url => screenshotsObservable(url, conditionalHeaders, layout, browserTimezone),
|
||||
(outer, inner) => inner,
|
||||
captureConcurrency
|
||||
)
|
||||
|
@ -66,11 +66,11 @@ function generatePdfObservableFn(server) {
|
|||
};
|
||||
|
||||
|
||||
return function generatePdfObservable(title, urls, browserTimezone, sessionCookie, layoutParams, logo) {
|
||||
return function generatePdfObservable(title, urls, browserTimezone, conditionalHeaders, layoutParams, logo) {
|
||||
|
||||
const layout = createLayout(server, layoutParams);
|
||||
|
||||
const screenshots$ = urlScreenshotsObservable(urls, sessionCookie, layout, browserTimezone);
|
||||
const screenshots$ = urlScreenshotsObservable(urls, conditionalHeaders, layout, browserTimezone);
|
||||
|
||||
return screenshots$.pipe(
|
||||
toArray(),
|
||||
|
|
|
@ -29,11 +29,11 @@ export function screenshotsObservableFactory(server) {
|
|||
return result;
|
||||
};
|
||||
|
||||
const openUrl = async (browser, url, sessionCookie) => {
|
||||
const openUrl = async (browser, url, conditionalHeaders) => {
|
||||
const waitForSelector = '.application';
|
||||
|
||||
await browser.open(url, {
|
||||
sessionCookie,
|
||||
conditionalHeaders,
|
||||
waitForSelector,
|
||||
});
|
||||
};
|
||||
|
@ -226,7 +226,7 @@ export function screenshotsObservableFactory(server) {
|
|||
return screenshots;
|
||||
};
|
||||
|
||||
return function screenshotsObservable(url, sessionCookie, layout, browserTimezone) {
|
||||
return function screenshotsObservable(url, conditionalHeaders, layout, browserTimezone) {
|
||||
|
||||
return Rx.defer(async () => await getPort()).pipe(
|
||||
mergeMap(bridgePort => {
|
||||
|
@ -254,7 +254,7 @@ export function screenshotsObservableFactory(server) {
|
|||
const screenshot$ = driver$.pipe(
|
||||
tap(() => logger.debug(`opening ${url}`)),
|
||||
mergeMap(
|
||||
browser => openUrl(browser, url, sessionCookie),
|
||||
browser => openUrl(browser, url, conditionalHeaders),
|
||||
browser => browser
|
||||
),
|
||||
tap(() => logger.debug('injecting custom css')),
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
*/
|
||||
|
||||
import * as Chrome from 'puppeteer';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import {
|
||||
ConditionalHeaders,
|
||||
ConditionalHeadersConditions,
|
||||
ElementPosition,
|
||||
EvalArgs,
|
||||
EvalFn,
|
||||
EvaluateOptions,
|
||||
Logger,
|
||||
SessionCookie,
|
||||
ViewZoomWidthHeight,
|
||||
} from '../../../../types';
|
||||
|
||||
|
@ -33,17 +35,26 @@ export class HeadlessChromiumDriver {
|
|||
public async open(
|
||||
url: string,
|
||||
{
|
||||
sessionCookie,
|
||||
conditionalHeaders,
|
||||
waitForSelector,
|
||||
}: {
|
||||
sessionCookie: SessionCookie;
|
||||
waitForSelector: string;
|
||||
}
|
||||
}: { conditionalHeaders: ConditionalHeaders; waitForSelector: string }
|
||||
) {
|
||||
this.logger.debug(`HeadlessChromiumDriver:opening url ${url}`);
|
||||
if (sessionCookie) {
|
||||
await this.page.setCookie(sessionCookie);
|
||||
}
|
||||
await this.page.setRequestInterception(true);
|
||||
this.page.on('request', (interceptedRequest: any) => {
|
||||
if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedRequest.url())) {
|
||||
this.logger.debug(`Using custom headers for ${interceptedRequest.url()}`);
|
||||
interceptedRequest.continue({
|
||||
headers: {
|
||||
...interceptedRequest.headers(),
|
||||
...conditionalHeaders.headers,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.logger.debug(`No custom headers for ${interceptedRequest.url()}`);
|
||||
interceptedRequest.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||
await this.page.waitFor(waitForSelector);
|
||||
|
@ -98,4 +109,41 @@ export class HeadlessChromiumDriver {
|
|||
isMobile: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _shouldUseCustomHeaders(conditions: ConditionalHeadersConditions, url: string) {
|
||||
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}/`)
|
||||
);
|
||||
}
|
||||
|
||||
private _shouldUseCustomHeadersForPort(
|
||||
conditions: ConditionalHeadersConditions,
|
||||
port: string | undefined
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,21 +11,21 @@ export const paths = {
|
|||
baseUrl: 'https://s3.amazonaws.com/headless-shell/',
|
||||
packages: [{
|
||||
platforms: ['darwin', 'freebsd', 'openbsd'],
|
||||
archiveFilename: 'chromium-4747cc2-darwin.zip',
|
||||
archiveChecksum: '3f509e2fa994da3a1399d18d03b6eef7',
|
||||
rawChecksum: 'c657bdde14f10b555b9d7ecb644ef695',
|
||||
archiveFilename: 'chromium-04c5a83-darwin.zip',
|
||||
archiveChecksum: '89a98bfa6454bec550f196232d1faeb3',
|
||||
rawChecksum: '413bbd646a4862a136bc0852ab6f41c5',
|
||||
binaryRelativePath: 'headless_shell-darwin/headless_shell',
|
||||
}, {
|
||||
platforms: ['linux'],
|
||||
archiveFilename: 'chromium-4747cc2-linux.zip',
|
||||
archiveChecksum: '8f361042d0fc8a84d60cd01777ec260f',
|
||||
rawChecksum: '8dfa6f823c663aa860ccdfa11de6713f',
|
||||
archiveFilename: 'chromium-04c5a83-linux.zip',
|
||||
archiveChecksum: '1339f6d57b6039445647dcdc949ba513',
|
||||
rawChecksum: '4824710dd8f3da9d9e2c0674a771008b',
|
||||
binaryRelativePath: 'headless_shell-linux/headless_shell'
|
||||
}, {
|
||||
platforms: ['win32'],
|
||||
archiveFilename: 'chromium-4747cc2-windows.zip',
|
||||
archiveChecksum: 'fac0967cd54bb2492a5a858fbefdf983',
|
||||
rawChecksum: 'b46de931336a341503d740ec692acbbd',
|
||||
archiveFilename: 'chromium-04c5a83-windows.zip',
|
||||
archiveChecksum: '3b3279b59ebf03db676baeb7b7ab5c24',
|
||||
rawChecksum: '724011f9acf872c9472c82c6f7981178',
|
||||
binaryRelativePath: 'headless_shell-windows\\headless_shell.exe'
|
||||
}]
|
||||
};
|
||||
|
|
|
@ -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?');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -110,12 +110,9 @@ export function main(server) {
|
|||
|
||||
async function handler(exportTypeId, jobParams, request, h) {
|
||||
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();
|
||||
|
|
18
x-pack/plugins/reporting/types.d.ts
vendored
18
x-pack/plugins/reporting/types.d.ts
vendored
|
@ -54,12 +54,14 @@ export interface HeadlessElementInfo {
|
|||
position: ElementPosition;
|
||||
}
|
||||
|
||||
export interface SessionCookie {
|
||||
name: string;
|
||||
value: string;
|
||||
domain: string;
|
||||
path: string;
|
||||
httpOnly: boolean;
|
||||
secure: boolean;
|
||||
sameSite: 'Strict' | 'Lax';
|
||||
export interface ConditionalHeaders {
|
||||
headers: Record<string, string>;
|
||||
conditions: ConditionalHeadersConditions;
|
||||
}
|
||||
|
||||
export interface ConditionalHeadersConditions {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
basePath: string;
|
||||
}
|
||||
|
|
|
@ -542,55 +542,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -189,87 +188,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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -213,25 +213,6 @@ class Authenticator {
|
|||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the request's session.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<string>}
|
||||
*/
|
||||
async serializeSession(request) {
|
||||
assertRequest(request);
|
||||
|
||||
return await this._session.serialize(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options that we're using for the session cookie
|
||||
* @returns {CookieOptions}
|
||||
*/
|
||||
getSessionCookieOptions() {
|
||||
return this._session.getCookieOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates authentication provider based on the provider key from config.
|
||||
* @param {string} providerType Provider type key.
|
||||
|
@ -300,8 +281,6 @@ 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 {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,7 +79,7 @@ export class Session {
|
|||
|
||||
request.cookieAuth.set({
|
||||
value,
|
||||
expires: this._cookieOptions.ttl && Date.now() + this._cookieOptions.ttl
|
||||
expires: this._ttl && Date.now() + this._ttl
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -106,43 +94,6 @@ export class Session {
|
|||
request.cookieAuth.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes current session.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<string>}
|
||||
*/
|
||||
async serialize(request) {
|
||||
const state = request._states[this._cookieOptions.name];
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = await new Promise((resolve, reject) => {
|
||||
iron.seal(state.value, this._cookieOptions.password, iron.defaults, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options that we're using for the session cookie
|
||||
* @returns {CookieOptions}
|
||||
*/
|
||||
getCookieOptions() {
|
||||
return {
|
||||
name: this._cookieOptions.name,
|
||||
path: this._cookieOptions.path,
|
||||
httpOnly: this._cookieOptions.httpOnly,
|
||||
secure: this._cookieOptions.secure,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and creates a session instance.
|
||||
* @param {Hapi.Server} server HapiJS Server instance.
|
||||
|
@ -160,7 +111,6 @@ export class Session {
|
|||
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', {
|
||||
cookie: name,
|
||||
|
@ -180,14 +130,7 @@ export class Session {
|
|||
});
|
||||
}
|
||||
|
||||
return new Session(server, {
|
||||
httpOnly,
|
||||
name,
|
||||
password,
|
||||
path,
|
||||
secure,
|
||||
ttl,
|
||||
});
|
||||
return new Session(server);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -187,11 +187,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e"
|
||||
integrity sha512-ECuJ+f5gGHiLeiE4RlE/xdqv/0JVDToegPV1aTb10tQStYa0Ycq2OJfQukDv3IFaw3B+CMV46jHc5bXe6QXEQg==
|
||||
|
||||
"@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"
|
||||
|
@ -2652,7 +2647,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=
|
||||
|
@ -5394,15 +5389,6 @@ 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:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
|
||||
integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg=
|
||||
dependencies:
|
||||
boom "5.x.x"
|
||||
cryptiles "3.x.x"
|
||||
hoek "4.x.x"
|
||||
|
||||
iron@5.x.x:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/iron/-/iron-5.0.4.tgz#003ed822f656f07c2b62762815f5de3947326867"
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -4193,7 +4193,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=
|
||||
|
@ -8299,15 +8299,6 @@ 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:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
|
||||
integrity sha1-TwQszri5c480a1mqc0yDqJvDFCg=
|
||||
dependencies:
|
||||
boom "5.x.x"
|
||||
cryptiles "3.x.x"
|
||||
hoek "4.x.x"
|
||||
|
||||
iron@5.x.x:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/iron/-/iron-5.0.4.tgz#003ed822f656f07c2b62762815f5de3947326867"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue