[Reporting] refactor routes files and helpers (#30111) (#30873)

* remove unused file

* refactor routes files and helpers

* default empty array for conflictedTypesFields

* minor prettier

* remove some unrelated diff

* some typescript conversions

* more typescripts

* more typscript

* jobtype is a string

* revert some logic change

* set payload.headers to undefined + not mutate

* fix jest import
This commit is contained in:
Tim Sullivan 2019-02-13 09:43:06 -07:00 committed by GitHub
parent 2964c33090
commit 220d22d983
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 455 additions and 462 deletions

View file

@ -1,129 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import moment from 'moment';
import sinon from 'sinon';
import { getAbsoluteTime } from '../get_absolute_time';
describe('get_absolute_time', function () {
let anchor;
let unix;
let clock;
beforeEach(() => {
anchor = '2016-07-04T14:16:32.123Z';
unix = moment(anchor).valueOf();
clock = sinon.useFakeTimers(unix);
});
afterEach(() => {
clock.restore();
});
describe('invalid input', function () {
let timeObj;
beforeEach(() => {
timeObj = {
mode: 'absolute',
from: '2016-07-04T14:01:32.123Z',
to: '2016-07-04T14:16:32.123Z',
};
});
it('should return time if missing mode', function () {
delete timeObj.mode;
expect(getAbsoluteTime(timeObj)).to.equal(timeObj);
});
it('should return time if missing from', function () {
delete timeObj.from;
expect(getAbsoluteTime(timeObj)).to.equal(timeObj);
});
it('should return time if missing to', function () {
delete timeObj.to;
expect(getAbsoluteTime(timeObj)).to.equal(timeObj);
});
});
describe('absolute time', function () {
let timeObj;
beforeEach(() => {
timeObj = {
mode: 'absolute',
from: '2016-07-04T14:01:32.123Z',
to: '2016-07-04T14:16:32.123Z',
testParam: 'some value',
};
});
it('should return time if already absolute', function () {
expect(getAbsoluteTime(timeObj)).to.equal(timeObj);
});
});
describe('relative time', function () {
let timeObj;
beforeEach(() => {
timeObj = {
mode: 'relative',
from: 'now-15m',
to: 'now',
testParam: 'some value',
};
});
it('should return the absolute time', function () {
const output = getAbsoluteTime(timeObj);
expect(output.mode).to.equal('absolute');
});
it('should map from and to values to times', function () {
const output = getAbsoluteTime(timeObj);
const check = {
from: '2016-07-04T14:01:32.123Z',
to: '2016-07-04T14:16:32.123Z',
};
expect(moment(output.from).toISOString()).to.equal(check.from);
expect(moment(output.to).toISOString()).to.equal(check.to);
});
});
describe('quick time', function () {
let timeObj;
beforeEach(() => {
timeObj = {
mode: 'quick',
from: 'now-1w/w',
to: 'now-1w/w',
testParam: 'some value',
};
});
it('should return the absolute time', function () {
const output = getAbsoluteTime(timeObj);
expect(output.mode).to.equal('absolute');
});
it('should map previous week values to times', function () {
const output = getAbsoluteTime(timeObj);
const check = {
from: /2016\-06\-2(5|6)T..\:00\:00\.000Z/,
to: /2016\-07\-0(2|3)T..\:59\:59\.999Z/,
};
expect(moment(output.from).toISOString()).to.match(check.from);
expect(moment(output.to).toISOString()).to.match(check.to);
});
});
});

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import datemath from '@elastic/datemath';
export function getAbsoluteTime(time) {
const mode = get(time, 'mode');
const timeFrom = get(time, 'from');
const timeTo = get(time, 'to');
if (!mode || !timeFrom || !timeTo) return time;
if (mode === 'absolute') {
return time;
}
const output = { mode: 'absolute' };
output.from = datemath.parse(timeFrom);
output.to = datemath.parse(timeTo, { roundUp: true });
return output;
}

View file

@ -24,11 +24,7 @@ export function createGenerateCsv(logger) {
settings
}) {
const escapeValue = createEscapeValue(settings.quoteValues);
const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields);
const formatCsvValues = createFormatCsvValues(escapeValue, settings.separator, fields, formatsMap);
const builder = new MaxSizeStringBuilder(settings.maxSizeBytes);
const header = `${fields.map(escapeValue).join(settings.separator)}\n`;
if (!builder.tryAppend(header)) {
return {
@ -40,6 +36,8 @@ export function createGenerateCsv(logger) {
const iterator = hitIterator(settings.scroll, callEndpoint, searchRequest, cancellationToken);
let maxSizeReached = false;
const flattenHit = createFlattenHit(fields, metaFields, conflictedTypesFields);
const formatCsvValues = createFormatCsvValues(escapeValue, settings.separator, fields, formatsMap);
try {
while (true) {
const { done, value: hit } = await iterator.next();

View file

@ -7,8 +7,7 @@
import { resolve } from 'path';
import { UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants';
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
import { main as mainRoutes } from './server/routes/main';
import { jobs as jobRoutes } from './server/routes/jobs';
import { registerRoutes } from './server/routes';
import { createQueueFactory } from './server/lib/create_queue';
import { config as appConfig } from './server/config/config';
@ -174,8 +173,7 @@ export const reporting = (kibana) => {
server.expose('queue', createQueueFactory(server));
// Reporting routes
mainRoutes(server);
jobRoutes(server);
registerRoutes(server);
},
deprecations: function ({ unused }) {

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { Request, ResponseToolkit } from 'hapi';
import rison from 'rison-node';
import { API_BASE_URL } from '../../common/constants';
import { KbnServer } from '../../types';
import { getRouteConfigFactoryReportingPre } from './lib/route_config_factories';
import { HandlerErrorFunction, HandlerFunction } from './types';
const BASE_GENERATE = `${API_BASE_URL}/generate`;
export function registerGenerate(
server: KbnServer,
handler: HandlerFunction,
handleError: HandlerErrorFunction
) {
const getRouteConfig = getRouteConfigFactoryReportingPre(server);
// generate report
server.route({
path: `${BASE_GENERATE}/{exportType}`,
method: 'POST',
config: getRouteConfig(request => request.params.exportType),
handler: async (request: Request, h: ResponseToolkit) => {
const { exportType } = request.params;
let response;
try {
// @ts-ignore
const jobParams = rison.decode(request.query.jobParams);
response = await handler(exportType, jobParams, request, h);
} catch (err) {
throw handleError(exportType, err);
}
return response;
},
});
// show error about GET method to user
server.route({
path: `${BASE_GENERATE}/{p*}`,
method: 'GET',
config: getRouteConfig(),
handler: () => {
const err = boom.methodNotAllowed('GET is not allowed');
err.output.headers.allow = 'POST';
return err;
},
});
}

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { Request, ResponseToolkit } from 'hapi';
import { API_BASE_URL } from '../../common/constants';
import { KbnServer } from '../../types';
// @ts-ignore
import { enqueueJobFactory } from '../lib/enqueue_job';
import { registerGenerate } from './generate';
import { registerJobs } from './jobs';
import { registerLegacy } from './legacy';
export function registerRoutes(server: KbnServer) {
const config = server.config();
const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`;
const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin');
const enqueueJob = enqueueJobFactory(server);
async function handler(exportTypeId: any, jobParams: any, request: Request, h: ResponseToolkit) {
// @ts-ignore
const user = request.pre.user;
const headers = request.headers;
const job = await enqueueJob(exportTypeId, jobParams, user, headers, request);
// return the queue's job information
const jobJson = job.toJSON();
return h
.response({
path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`,
job: jobJson,
})
.type('application/json');
}
function handleError(exportType: any, err: Error) {
if (err instanceof esErrors['401']) {
return boom.unauthorized(`Sorry, you aren't authenticated`);
}
if (err instanceof esErrors['403']) {
return boom.forbidden(`Sorry, you are not authorized to create ${exportType} reports`);
}
if (err instanceof esErrors['404']) {
return boom.boomify(err, { statusCode: 404 });
}
return err;
}
registerGenerate(server, handler, handleError);
registerLegacy(server, handler, handleError);
registerJobs(server);
}

View file

@ -1,151 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { API_BASE_URL } from '../../common/constants';
import { jobsQueryFactory } from '../lib/jobs_query';
import { reportingFeaturePreRoutingFactory } from'../lib/reporting_feature_pre_routing';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { jobResponseHandlerFactory } from '../lib/job_response_handler';
const mainEntry = `${API_BASE_URL}/jobs`;
const API_TAG = 'api';
export function jobs(server) {
const jobsQuery = jobsQueryFactory(server);
const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server);
const jobResponseHandler = jobResponseHandlerFactory(server);
const managementPreRouting = reportingFeaturePreRouting(() => 'management');
function getRouteConfig() {
return {
pre: [
{ method: authorizedUserPreRouting, assign: 'user' },
{ method: managementPreRouting, assign: 'management' },
],
};
}
// list jobs in the queue, paginated
server.route({
path: `${mainEntry}/list`,
method: 'GET',
handler: (request) => {
const page = parseInt(request.query.page) || 0;
const size = Math.min(100, parseInt(request.query.size) || 10);
const jobIds = request.query.ids ? request.query.ids.split(',') : null;
const results = jobsQuery.list(request.pre.management.jobTypes, request.pre.user, page, size, jobIds);
return results;
},
config: getRouteConfig(),
});
// return the count of all jobs in the queue
server.route({
path: `${mainEntry}/count`,
method: 'GET',
handler: (request) => {
const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user);
return results;
},
config: getRouteConfig(),
});
// return the raw output from a job
server.route({
path: `${mainEntry}/output/{docId}`,
method: 'GET',
handler: (request) => {
const { docId } = request.params;
return jobsQuery.get(request.pre.user, docId, { includeContent: true })
.then((doc) => {
if (!doc) {
return boom.notFound();
}
const { jobtype: jobType } = doc._source;
if (!request.pre.management.jobTypes.includes(jobType)) {
return boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`);
}
return doc._source.output;
});
},
config: getRouteConfig(),
});
// return some info about the job
server.route({
path: `${mainEntry}/info/{docId}`,
method: 'GET',
handler: (request) => {
const { docId } = request.params;
return jobsQuery.get(request.pre.user, docId)
.then((doc) => {
if (!doc) {
return boom.notFound();
}
const { jobtype: jobType } = doc._source;
if (!request.pre.management.jobTypes.includes(jobType)) {
return boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`);
}
const { payload } = doc._source;
payload.headers = 'not shown';
return {
...doc._source,
payload
};
});
},
config: getRouteConfig(),
});
// trigger a download of the output from a job
// NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer
// (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the
// TOC at the end of the PDF, but it's sending multiple cookies and causing our auth to fail with a 401.
// Additionally, the range-request doesn't alleviate any performance issues on the server as the entire
// download is loaded into memory.
server.route({
path: `${mainEntry}/download/{docId}`,
method: 'GET',
handler: async (request, h) => {
const { docId } = request.params;
let response = await jobResponseHandler(request.pre.management.jobTypes, request.pre.user, h, { docId });
const { statusCode } = response;
if (statusCode !== 200) {
const logLevel = statusCode === 500 ? 'error' : 'debug';
server.log(
[logLevel, "reporting", "download"],
`Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify(response.source)}]`
);
}
if (!response.isBoom) {
response = response.header('accept-ranges', 'none');
}
return response;
},
config: {
...getRouteConfig(),
tags: [API_TAG],
response: {
ranges: false
}
},
});
}

View file

@ -6,14 +6,14 @@
import Hapi from 'hapi';
import { difference, memoize } from 'lodash';
import { jobs } from './jobs';
import { registerJobs } from './jobs';
import { ExportTypesRegistry } from '../../common/export_types_registry';
jest.mock('../lib/authorized_user_pre_routing', () => {
jest.mock('./lib/authorized_user_pre_routing', () => {
return {
authorizedUserPreRoutingFactory: () => () => ({})
};
});
jest.mock('../lib/reporting_feature_pre_routing', () => {
jest.mock('./lib/reporting_feature_pre_routing', () => {
return {
reportingFeaturePreRoutingFactory: () => () => () => ({ jobTypes: ['unencodedJobType', 'base64EncodedJobType'] })
};
@ -63,7 +63,7 @@ test(`returns 404 if job not found`, async () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits()));
jobs(mockServer);
registerJobs(mockServer);
const request = {
method: 'GET',
@ -79,7 +79,7 @@ test(`returns 401 if not valid job type`, async () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' })));
jobs(mockServer);
registerJobs(mockServer);
const request = {
method: 'GET',
@ -96,7 +96,7 @@ describe(`when job is incomplete`, () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })));
jobs(mockServer);
registerJobs(mockServer);
const request = {
method: 'GET',
@ -133,7 +133,7 @@ describe(`when job is failed`, () => {
mockServer.plugins.elasticsearch.getCluster('admin')
.callWithInternalUser.mockReturnValue(Promise.resolve(hits));
jobs(mockServer);
registerJobs(mockServer);
const request = {
method: 'GET',
@ -178,7 +178,7 @@ describe(`when job is completed`, () => {
});
mockServer.plugins.elasticsearch.getCluster('admin').callWithInternalUser.mockReturnValue(Promise.resolve(hits));
jobs(mockServer);
registerJobs(mockServer);
const request = {
method: 'GET',

View file

@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { Request, ResponseToolkit } from 'hapi';
import { API_BASE_URL } from '../../common/constants';
import { JobDoc, KbnServer } from '../../types';
// @ts-ignore
import { jobsQueryFactory } from '../lib/jobs_query';
// @ts-ignore
import { jobResponseHandlerFactory } from './lib/job_response_handler';
import {
getRouteConfigFactoryDownloadPre,
getRouteConfigFactoryManagementPre,
} from './lib/route_config_factories';
const MAIN_ENTRY = `${API_BASE_URL}/jobs`;
export function registerJobs(server: KbnServer) {
const jobsQuery = jobsQueryFactory(server);
const getRouteConfig = getRouteConfigFactoryManagementPre(server);
const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server);
// list jobs in the queue, paginated
server.route({
path: `${MAIN_ENTRY}/list`,
method: 'GET',
config: getRouteConfig(),
handler: (request: Request) => {
// @ts-ignore
const page = parseInt(request.query.page, 10) || 0;
// @ts-ignore
const size = Math.min(100, parseInt(request.query.size, 10) || 10);
// @ts-ignore
const jobIds = request.query.ids ? request.query.ids.split(',') : null;
const results = jobsQuery.list(
request.pre.management.jobTypes,
request.pre.user,
page,
size,
jobIds
);
return results;
},
});
// return the count of all jobs in the queue
server.route({
path: `${MAIN_ENTRY}/count`,
method: 'GET',
config: getRouteConfig(),
handler: (request: Request) => {
const results = jobsQuery.count(request.pre.management.jobTypes, request.pre.user);
return results;
},
});
// return the raw output from a job
server.route({
path: `${MAIN_ENTRY}/output/{docId}`,
method: 'GET',
config: getRouteConfig(),
handler: (request: Request) => {
const { docId } = request.params;
return jobsQuery.get(request.pre.user, docId, { includeContent: true }).then(
(doc: any): JobDoc => {
const job = doc._source;
if (!job) {
throw boom.notFound();
}
const { jobtype: jobType } = job;
if (!request.pre.management.jobTypes.includes(jobType)) {
throw boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`);
}
return job.output;
}
);
},
});
// return some info about the job
server.route({
path: `${MAIN_ENTRY}/info/{docId}`,
method: 'GET',
config: getRouteConfig(),
handler: (request: Request) => {
const { docId } = request.params;
return jobsQuery.get(request.pre.user, docId).then(
(doc: any): JobDoc => {
const job: JobDoc = doc._source;
if (!job) {
throw boom.notFound();
}
const { jobtype: jobType, payload } = job;
if (!request.pre.management.jobTypes.includes(jobType)) {
throw boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`);
}
return {
...doc._source,
payload: {
...payload,
headers: undefined,
},
};
}
);
},
});
// trigger a download of the output from a job
const jobResponseHandler = jobResponseHandlerFactory(server);
server.route({
path: `${MAIN_ENTRY}/download/{docId}`,
method: 'GET',
config: getRouteConfigDownload(),
handler: async (request: Request, h: ResponseToolkit) => {
const { docId } = request.params;
let response = await jobResponseHandler(
request.pre.management.jobTypes,
request.pre.user,
h,
{ docId }
);
const { statusCode } = response;
if (statusCode !== 200) {
const logLevel = statusCode === 500 ? 'error' : 'debug';
server.log(
[logLevel, 'reporting', 'download'],
`Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify(
response.source
)}]`
);
}
if (!response.isBoom) {
response = response.header('accept-ranges', 'none');
}
return response;
},
});
}

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request, ResponseToolkit } from 'hapi';
import querystring from 'querystring';
import { API_BASE_URL } from '../../common/constants';
import { KbnServer } from '../../types';
import { getRouteConfigFactoryReportingPre } from './lib/route_config_factories';
import { HandlerErrorFunction, HandlerFunction } from './types';
const getStaticFeatureConfig = (getRouteConfig: any, featureId: any) =>
getRouteConfig(() => featureId);
const BASE_GENERATE = `${API_BASE_URL}/generate`;
export function registerLegacy(
server: KbnServer,
handler: HandlerFunction,
handleError: HandlerErrorFunction
) {
const getRouteConfig = getRouteConfigFactoryReportingPre(server);
function createLegacyPdfRoute({ path, objectType }: { path: string; objectType: any }) {
const exportTypeId = 'printablePdf';
server.route({
path,
method: 'POST',
config: getStaticFeatureConfig(getRouteConfig, exportTypeId),
handler: async (request: Request, h: ResponseToolkit) => {
const message = `The following URL is deprecated and will stop working in the next major version: ${
request.url.path
}`;
server.log(['warning', 'reporting', 'deprecation'], message);
try {
const savedObjectId = request.params.savedId;
const queryString = querystring.stringify(request.query);
return await handler(
exportTypeId,
{
objectType,
savedObjectId,
queryString,
},
request,
h
);
} catch (err) {
throw handleError(exportTypeId, err);
}
},
});
}
createLegacyPdfRoute({
path: `${BASE_GENERATE}/visualization/{savedId}`,
objectType: 'visualization',
});
createLegacyPdfRoute({
path: `${BASE_GENERATE}/search/{savedId}`,
objectType: 'search',
});
createLegacyPdfRoute({
path: `${BASE_GENERATE}/dashboard/{savedId}`,
objectType: 'dashboard',
});
}

View file

@ -5,8 +5,8 @@
*/
import boom from 'boom';
import { getUserFactory } from './get_user';
import { oncePerServer } from './once_per_server';
import { getUserFactory } from '../../lib/get_user';
import { oncePerServer } from '../../lib/once_per_server';
const superuserRole = 'superuser';
@ -43,4 +43,3 @@ function authorizedUserPreRoutingFn(server) {
}
export const authorizedUserPreRoutingFactory = oncePerServer(authorizedUserPreRoutingFn);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { oncePerServer } from './once_per_server';
import { oncePerServer } from '../../lib/once_per_server';
function getDocumentPayloadFn(server) {
const exportTypesRegistry = server.plugins.reporting.exportTypesRegistry;

View file

@ -5,10 +5,10 @@
*/
import boom from 'boom';
import { oncePerServer } from './once_per_server';
import { jobsQueryFactory } from './jobs_query';
import { oncePerServer } from '../../lib/once_per_server';
import { jobsQueryFactory } from '../../lib/jobs_query';
import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants';
import { getDocumentPayloadFactory } from './get_document_payload';
import { WHITELISTED_JOB_CONTENT_TYPES } from '../../common/constants';
function jobResponseHandlerFn(server) {
const jobsQuery = jobsQueryFactory(server);

View file

@ -5,7 +5,7 @@
*/
import Boom from 'boom';
import { oncePerServer } from './once_per_server';
import { oncePerServer } from '../../lib/once_per_server';
function reportingFeaturePreRoutingFn(server) {
const xpackMainPlugin = server.plugins.xpack_main;

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { KbnServer } from '../../../types';
// @ts-ignore
import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing';
// @ts-ignore
import { reportingFeaturePreRoutingFactory } from './reporting_feature_pre_routing';
const API_TAG = 'api';
interface RouteConfigFactory {
tags?: string[];
pre: any[];
response?: {
ranges: boolean;
};
}
type GetFeatureFunction = (request: Request) => any;
type PreRoutingFunction = (getFeatureId?: GetFeatureFunction) => any;
export function getRouteConfigFactoryReportingPre(server: KbnServer) {
const authorizedUserPreRouting: PreRoutingFunction = authorizedUserPreRoutingFactory(server);
const reportingFeaturePreRouting: PreRoutingFunction = reportingFeaturePreRoutingFactory(server);
return (getFeatureId?: GetFeatureFunction): RouteConfigFactory => {
const preRouting = [{ method: authorizedUserPreRouting, assign: 'user' }];
if (getFeatureId) {
preRouting.push(reportingFeaturePreRouting(getFeatureId));
}
return {
tags: [API_TAG],
pre: preRouting,
};
};
}
export function getRouteConfigFactoryManagementPre(server: KbnServer) {
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server);
const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server);
const managementPreRouting = reportingFeaturePreRouting(() => 'management');
return (): RouteConfigFactory => {
return {
pre: [
{ method: authorizedUserPreRouting, assign: 'user' },
{ method: managementPreRouting, assign: 'management' },
],
};
};
}
// NOTE: We're disabling range request for downloading the PDF. There's a bug in Firefox's PDF.js viewer
// (https://github.com/mozilla/pdf.js/issues/8958) where they're using a range request to retrieve the
// TOC at the end of the PDF, but it's sending multiple cookies and causing our auth to fail with a 401.
// Additionally, the range-request doesn't alleviate any performance issues on the server as the entire
// download is loaded into memory.
export function getRouteConfigFactoryDownloadPre(server: KbnServer) {
const getManagementRouteConfig = getRouteConfigFactoryManagementPre(server);
return (): RouteConfigFactory => ({
...getManagementRouteConfig(),
tags: [API_TAG],
response: {
ranges: false,
},
});
}

View file

@ -1,133 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import boom from 'boom';
import { API_BASE_URL } from '../../common/constants';
import { enqueueJobFactory } from '../lib/enqueue_job';
import { reportingFeaturePreRoutingFactory } from '../lib/reporting_feature_pre_routing';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import rison from 'rison-node';
import querystring from 'querystring';
const mainEntry = `${API_BASE_URL}/generate`;
const API_TAG = 'api';
export function main(server) {
const config = server.config();
const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`;
const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin');
const enqueueJob = enqueueJobFactory(server);
const reportingFeaturePreRouting = reportingFeaturePreRoutingFactory(server);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(server);
function getRouteConfig(getFeatureId) {
const preRouting = [ { method: authorizedUserPreRouting, assign: 'user' } ];
if (getFeatureId) {
preRouting.push(reportingFeaturePreRouting(getFeatureId));
}
return {
tags: [API_TAG],
pre: preRouting,
};
}
function getStaticFeatureConfig(featureId) {
return getRouteConfig(() => featureId);
}
// show error about method to user
server.route({
path: `${mainEntry}/{p*}`,
method: 'GET',
handler: () => {
const err = boom.methodNotAllowed('GET is not allowed');
err.output.headers.allow = 'POST';
return err;
},
config: getRouteConfig(),
});
function createLegacyPdfRoute({ path, objectType }) {
const exportTypeId = 'printablePdf';
server.route({
path: path,
method: 'POST',
handler: async (request, h) => {
const message = `The following URL is deprecated and will stop working in the next major version: ${request.url.path}`;
server.log(['warning', 'reporting', 'deprecation'], message);
try {
const savedObjectId = request.params.savedId;
const queryString = querystring.stringify(request.query);
return await handler(exportTypeId, {
objectType,
savedObjectId,
queryString
}, request, h);
} catch (err) {
throw handleError(exportTypeId, err);
}
},
config: getStaticFeatureConfig(exportTypeId),
});
}
createLegacyPdfRoute({
path: `${mainEntry}/visualization/{savedId}`,
objectType: 'visualization',
});
createLegacyPdfRoute({
path: `${mainEntry}/search/{savedId}`,
objectType: 'search',
});
createLegacyPdfRoute({
path: `${mainEntry}/dashboard/{savedId}`,
objectType: 'dashboard'
});
server.route({
path: `${mainEntry}/{exportType}`,
method: 'POST',
handler: async (request, h) => {
const exportType = request.params.exportType;
try {
const jobParams = rison.decode(request.query.jobParams);
return await handler(exportType, jobParams, request, h);
} catch (err) {
throw handleError(exportType, err);
}
},
config: getRouteConfig(request => request.params.exportType),
});
async function handler(exportTypeId, jobParams, request, h) {
const user = request.pre.user;
const headers = request.headers;
const job = await enqueueJob(exportTypeId, jobParams, user, headers, request);
// return the queue's job information
const jobJson = job.toJSON();
return h.response({
path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`,
job: jobJson,
})
.type('application/json');
}
function handleError(exportType, err) {
if (err instanceof esErrors['401']) return boom.unauthorized(`Sorry, you aren't authenticated`);
if (err instanceof esErrors['403']) return boom.forbidden(`Sorry, you are not authorized to create ${exportType} reports`);
if (err instanceof esErrors['404']) return boom.boomify(err, { statusCode: 404 });
return err;
}
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request, ResponseToolkit } from 'hapi';
export type HandlerFunction = (
exportType: any,
jobParams: any,
request: Request,
h: ResponseToolkit
) => any;
export type HandlerErrorFunction = (exportType: any, err: Error) => any;

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
/*

View file

@ -15,6 +15,8 @@ export interface KbnServer {
info: { protocol: string };
config: () => ConfigObject;
plugins: Record<string, any>;
route: any;
log: any;
savedObjects: {
getScopedSavedObjectsClient: (
fakeRequest: { headers: object; getBasePath: () => string }
@ -88,6 +90,7 @@ export interface ConditionalHeadersConditions {
export interface CryptoFactory {
decrypt: (headers?: Record<string, string>) => string;
}
export interface ReportingJob {
headers?: Record<string, string>;
basePath?: string;
@ -97,3 +100,9 @@ export interface ReportingJob {
timeRange?: any;
objects?: [any];
}
export interface JobDoc {
output: any;
jobtype: string;
payload: any;
}