mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
2964c33090
commit
220d22d983
20 changed files with 455 additions and 462 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
54
x-pack/plugins/reporting/server/routes/generate.ts
Normal file
54
x-pack/plugins/reporting/server/routes/generate.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
57
x-pack/plugins/reporting/server/routes/index.ts
Normal file
57
x-pack/plugins/reporting/server/routes/index.ts
Normal 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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
|
|
154
x-pack/plugins/reporting/server/routes/jobs.ts
Normal file
154
x-pack/plugins/reporting/server/routes/jobs.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
72
x-pack/plugins/reporting/server/routes/legacy.ts
Normal file
72
x-pack/plugins/reporting/server/routes/legacy.ts
Normal 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',
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
16
x-pack/plugins/reporting/server/routes/types.d.ts
vendored
Normal file
16
x-pack/plugins/reporting/server/routes/types.d.ts
vendored
Normal 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;
|
|
@ -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';
|
||||
|
||||
/*
|
||||
|
|
9
x-pack/plugins/reporting/types.d.ts
vendored
9
x-pack/plugins/reporting/types.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue