[Reporting] Clean up any usage, reorganize server route files (#110740)

* refactor/reflatten server routes

* fix import

* fix any usage in server/lib

* clean up unused parameter

* remove any in server/browsers

* refactor handle request function into a class

* more cleanup
This commit is contained in:
Tim Sullivan 2021-09-02 11:35:39 -07:00 committed by GitHub
parent b0a0dc224b
commit e6faa58873
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 380 additions and 264 deletions

View file

@ -78,7 +78,8 @@ export class HeadlessChromiumDriverFactory {
{ viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string },
pLogger: LevelLogger
): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable<never> }> {
return Rx.Observable.create(async (observer: InnerSubscriber<any, any>) => {
// FIXME: 'create' is deprecated
return Rx.Observable.create(async (observer: InnerSubscriber<unknown, unknown>) => {
const logger = pLogger.clone(['browser-driver']);
logger.info(`Creating browser page driver`);

View file

@ -49,23 +49,57 @@ test('throws validation error if provided with data over max size', () => {
});
test('throws validation error if provided with non-image data', () => {
const invalidErrorMatcher = /try a different image/;
expect(() => PdfLogoSchema.validate('')).toThrowError(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(true)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(false)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate({})).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate([])).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(0)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(0x00f)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate('')).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: Sorry, that file will not work. Please try a different image file.
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate(true)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [boolean]
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate(false)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [boolean]
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate({})).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [Object]
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate([])).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [Array]
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate(0)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [number]
- [1]: expected value to equal [null]"
`);
expect(() => PdfLogoSchema.validate(0x00f)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: expected value of type [string] but got [number]
- [1]: expected value to equal [null]"
`);
const csvString =
`data:text/csv;base64,Il9pZCIsIl9pbmRleCIsIl9zY29yZSIsIl90eXBlIiwiZm9vLmJhciIsImZvby5iYXIua2V5d29yZCIKZjY1QU9IZ0J5bFZmWW04W` +
`TRvb1EsYmVlLDEsIi0iLGJheixiYXoKbks1QU9IZ0J5bFZmWW04WTdZcUcsYmVlLDEsIi0iLGJvbyxib28K`;
expect(() => PdfLogoSchema.validate(csvString)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(csvString)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: Sorry, that file will not work. Please try a different image file.
- [1]: expected value to equal [null]"
`);
const scriptString =
`data:application/octet-stream;base64,QEVDSE8gT0ZGCldFRUtPRllSLkNPTSB8IEZJTkQgIlRoaXMgaXMiID4gVEVNUC5CQV` +
`QKRUNITz5USElTLkJBVCBTRVQgV0VFSz0lJTMKQ0FMTCBURU1QLkJBVApERUwgIFRFTVAuQkFUCkRFTCAgVEhJUy5CQVQKRUNITyBXZWVrICVXRUVLJQo=`;
expect(() => PdfLogoSchema.validate(scriptString)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(scriptString)).toThrowErrorMatchingInlineSnapshot(`
"types that failed validation:
- [0]: Sorry, that file will not work. Please try a different image file.
- [1]: expected value to equal [null]"
`);
});

View file

@ -17,7 +17,7 @@ const maxLogoSizeInBase64 = kbToBase64Length(200);
const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/;
const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif'];
const isImageData = (str: any): boolean => {
const isImageData = (str: string) => {
const matches = str.match(dataurlRegex);
if (!matches) {
@ -33,7 +33,7 @@ const isImageData = (str: any): boolean => {
return true;
};
const validatePdfLogoBase64String = (str: any) => {
const validatePdfLogoBase64String = (str: string) => {
if (typeof str !== 'string' || !isImageData(str)) {
return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.badFile', {
defaultMessage: `Sorry, that file will not work. Please try a different image file.`,
@ -46,7 +46,9 @@ const validatePdfLogoBase64String = (str: any) => {
}
};
export const PdfLogoSchema = schema.nullable(schema.any({ validate: validatePdfLogoBase64String }));
export const PdfLogoSchema = schema.nullable(
schema.string({ validate: validatePdfLogoBase64String })
);
export function registerUiSettings(core: CoreSetup<object, unknown>) {
core.uiSettings.register({

View file

@ -101,7 +101,10 @@ export class PdfMaker {
this._addContents(contents);
}
addImage(image: Buffer, opts = { title: '', description: '' }) {
addImage(
image: Buffer,
opts: { title?: string; description?: string } = { title: '', description: '' }
) {
const size = this._layout.getPdfImageSize();
const img = {
image: `data:image/png;base64,${image.toString('base64')}`,

View file

@ -73,8 +73,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
tracker.startAddImage();
tracker.endAddImage();
pdfOutput.addImage(screenshot.data, {
title: screenshot.title,
description: screenshot.description,
title: screenshot.title ?? undefined,
description: screenshot.description ?? undefined,
});
});
});

View file

@ -84,8 +84,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
tracker.startAddImage();
tracker.endAddImage();
pdfOutput.addImage(screenshot.data, {
title: screenshot.title,
description: screenshot.description,
title: screenshot.title ?? undefined,
description: screenshot.description ?? undefined,
});
});
});

View file

@ -45,7 +45,7 @@ export const getElementPositionAndAttributes = async (
},
attributes: Object.keys(attributes).reduce((result: AttributesMap, key) => {
const attribute = attributes[key];
(result as any)[key] = element.getAttribute(attribute);
result[key] = element.getAttribute(attribute);
return result;
}, {} as AttributesMap),
});

View file

@ -8,13 +8,10 @@
import { HeadlessChromiumDriver } from '../../browsers';
import {
createMockBrowserDriverFactory,
createMockConfig,
createMockConfigSchema,
createMockLayoutInstance,
createMockLevelLogger,
createMockReportingCore,
} from '../../test_helpers';
import { LayoutInstance } from '../layouts';
import { getScreenshots } from './get_screenshots';
describe('getScreenshots', () => {
@ -35,17 +32,12 @@ describe('getScreenshots', () => {
},
];
let layout: LayoutInstance;
let logger: ReturnType<typeof createMockLevelLogger>;
let browser: jest.Mocked<HeadlessChromiumDriver>;
beforeEach(async () => {
const schema = createMockConfigSchema();
const config = createMockConfig(schema);
const captureConfig = config.get('capture');
const core = await createMockReportingCore(schema);
const core = await createMockReportingCore(createMockConfigSchema());
layout = createMockLayoutInstance(captureConfig);
logger = createMockLevelLogger();
await createMockBrowserDriverFactory(core, logger, {
@ -71,7 +63,7 @@ describe('getScreenshots', () => {
});
it('should return screenshots', async () => {
await expect(getScreenshots(browser, layout, elementsPositionAndAttributes, logger)).resolves
await expect(getScreenshots(browser, elementsPositionAndAttributes, logger)).resolves
.toMatchInlineSnapshot(`
Array [
Object {
@ -117,7 +109,7 @@ describe('getScreenshots', () => {
});
it('should forward elements positions', async () => {
await getScreenshots(browser, layout, elementsPositionAndAttributes, logger);
await getScreenshots(browser, elementsPositionAndAttributes, logger);
expect(browser.screenshot).toHaveBeenCalledTimes(2);
expect(browser.screenshot).toHaveBeenNthCalledWith(
@ -134,7 +126,7 @@ describe('getScreenshots', () => {
browser.screenshot.mockResolvedValue(Buffer.from(''));
await expect(
getScreenshots(browser, layout, elementsPositionAndAttributes, logger)
getScreenshots(browser, elementsPositionAndAttributes, logger)
).rejects.toBeInstanceOf(Error);
});
});

View file

@ -8,12 +8,10 @@
import { i18n } from '@kbn/i18n';
import { LevelLogger, startTrace } from '../';
import { HeadlessChromiumDriver } from '../../browsers';
import { LayoutInstance } from '../layouts';
import { ElementsPositionAndAttribute, Screenshot } from './';
export const getScreenshots = async (
browser: HeadlessChromiumDriver,
layout: LayoutInstance,
elementsPositionAndAttributes: ElementsPositionAndAttribute[],
logger: LevelLogger
): Promise<Screenshot[]> => {

View file

@ -21,7 +21,7 @@ export interface ScreenshotObservableOpts {
}
export interface AttributesMap {
[key: string]: any;
[key: string]: string | null;
}
export interface ElementPosition {
@ -45,8 +45,8 @@ export interface ElementsPositionAndAttribute {
export interface Screenshot {
data: Buffer;
title: string;
description: string;
title: string | null;
description: string | null;
}
export interface ScreenshotResults {

View file

@ -123,7 +123,7 @@ export function getScreenshots$(
const elements = data.elementsPositionAndAttributes
? data.elementsPositionAndAttributes
: getDefaultElementPosition(layout.getViewport(1));
const screenshots = await getScreenshots(driver, layout, elements, logger);
const screenshots = await getScreenshots(driver, elements, logger);
const { timeRange, error: setupError } = data;
return {
timeRange,

View file

@ -47,8 +47,8 @@ interface TaskExecutor extends Pick<ExportTypeDefinition, 'jobContentEncoding'>
jobExecutor: RunTaskFn<BasePayload>;
}
function isOutput(output: any): output is CompletedReportOutput {
return output?.size != null;
function isOutput(output: CompletedReportOutput | Error): output is CompletedReportOutput {
return (output as CompletedReportOutput).size != null;
}
function reportFromTask(task: ReportTaskParams) {

View file

@ -36,7 +36,7 @@ describe('POST /diagnose/screenshot', () => {
toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)),
}),
}));
(generatePngObservableFactory as any).mockResolvedValue(generateMock);
(generatePngObservableFactory as jest.Mock).mockResolvedValue(generateMock);
};
const config = createMockConfigSchema({ queue: { timeout: 120000 } });

View file

@ -5,16 +5,16 @@
* 2.0.
*/
import { Writable } from 'stream';
import { schema } from '@kbn/config-schema';
import { KibanaRequest } from 'src/core/server';
import { ReportingCore } from '../';
import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/execute_job';
import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types';
import { LevelLogger as Logger } from '../lib';
import { TaskRunResult } from '../lib/tasks';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { HandlerErrorFunction } from './types';
import { Writable } from 'stream';
import { ReportingCore } from '../../';
import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job';
import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types';
import { LevelLogger as Logger } from '../../lib';
import { TaskRunResult } from '../../lib/tasks';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { RequestHandler } from '../lib/request_handler';
const API_BASE_URL_V1 = '/api/reporting/v1';
const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`;
@ -32,7 +32,6 @@ export type CsvFromSavedObjectRequest = KibanaRequest<unknown, unknown, JobParam
*/
export function registerGenerateCsvFromSavedObjectImmediate(
reporting: ReportingCore,
handleError: HandlerErrorFunction,
parentLogger: Logger
) {
const setupDeps = reporting.getPluginSetupDeps();
@ -64,9 +63,10 @@ export function registerGenerateCsvFromSavedObjectImmediate(
},
authorizedUserPreRouting(
reporting,
async (_user, context, req: CsvFromSavedObjectRequest, res) => {
async (user, context, req: CsvFromSavedObjectRequest, res) => {
const logger = parentLogger.clone(['csv_searchsource_immediate']);
const runTaskFn = runTaskFnFactory(reporting, logger);
const requestHandler = new RequestHandler(reporting, user, context, req, res, logger);
try {
let buffer = Buffer.from('');
@ -107,7 +107,7 @@ export function registerGenerateCsvFromSavedObjectImmediate(
});
} catch (err) {
logger.error(err);
return handleError(res, err);
return requestHandler.handleError(err);
}
}
)

View file

@ -7,19 +7,16 @@
import { schema } from '@kbn/config-schema';
import rison from 'rison-node';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { BaseParams } from '../types';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { HandlerErrorFunction, HandlerFunction } from './types';
import { ReportingCore } from '../..';
import { API_BASE_URL } from '../../../common/constants';
import { LevelLogger } from '../../lib';
import { BaseParams } from '../../types';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { RequestHandler } from '../lib/request_handler';
const BASE_GENERATE = `${API_BASE_URL}/generate`;
export function registerGenerateFromJobParams(
reporting: ReportingCore,
handler: HandlerFunction,
handleError: HandlerErrorFunction
) {
export function registerJobGenerationRoutes(reporting: ReportingCore, logger: LevelLogger) {
const setupDeps = reporting.getPluginSetupDeps();
const { router } = setupDeps;
@ -62,7 +59,6 @@ export function registerGenerateFromJobParams(
});
}
const { exportType } = req.params;
let jobParams;
try {
@ -80,10 +76,12 @@ export function registerGenerateFromJobParams(
});
}
const requestHandler = new RequestHandler(reporting, user, context, req, res, logger);
try {
return await handler(user, exportType, jobParams, context, req, res);
return await requestHandler.handleGenerateRequest(req.params.exportType, jobParams);
} catch (err) {
return handleError(res, err);
return requestHandler.handleError(err);
}
})
);

View file

@ -12,15 +12,15 @@ import { of } from 'rxjs';
import { ElasticsearchClient } from 'kibana/server';
import { setupServer } from 'src/core/server/test_utils';
import supertest from 'supertest';
import { ReportingCore } from '..';
import { ExportTypesRegistry } from '../lib/export_types_registry';
import { createMockLevelLogger, createMockReportingCore } from '../test_helpers';
import { ReportingCore } from '../..';
import { ExportTypesRegistry } from '../../lib/export_types_registry';
import { createMockLevelLogger, createMockReportingCore } from '../../test_helpers';
import {
createMockConfigSchema,
createMockPluginSetup,
} from '../test_helpers/create_mock_reportingplugin';
import { registerJobGenerationRoutes } from './generation';
import type { ReportingRequestHandlerContext } from '../types';
} from '../../test_helpers/create_mock_reportingplugin';
import type { ReportingRequestHandlerContext } from '../../types';
import { registerJobGenerationRoutes } from './generate_from_jobparams';
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; // FIXME: should not need to register each immediate export type separately
export { registerJobGenerationRoutes } from './generate_from_jobparams';
export { registerLegacy } from './legacy';

View file

@ -6,21 +6,16 @@
*/
import { schema } from '@kbn/config-schema';
import querystring from 'querystring';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { API_BASE_URL } from '../../common/constants';
import { HandlerErrorFunction, HandlerFunction } from './types';
import { ReportingCore } from '../core';
import { LevelLogger } from '../lib';
import querystring, { ParsedUrlQueryInput } from 'querystring';
import { API_BASE_URL } from '../../../common/constants';
import { ReportingCore } from '../../core';
import { LevelLogger } from '../../lib';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { RequestHandler } from '../lib/request_handler';
const BASE_GENERATE = `${API_BASE_URL}/generate`;
export function registerLegacy(
reporting: ReportingCore,
handler: HandlerFunction,
handleError: HandlerErrorFunction,
logger: LevelLogger
) {
export function registerLegacy(reporting: ReportingCore, logger: LevelLogger) {
const { router } = reporting.getPluginSetupDeps();
function createLegacyPdfRoute({ path, objectType }: { path: string; objectType: string }) {
@ -32,12 +27,15 @@ export function registerLegacy(
validate: {
params: schema.object({
savedObjectId: schema.string({ minLength: 3 }),
title: schema.string(),
browserTimezone: schema.string(),
}),
query: schema.any(),
query: schema.maybe(schema.string()),
},
},
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
const requestHandler = new RequestHandler(reporting, user, context, req, res, logger);
const message = `The following URL is deprecated and will stop working in the next major version: ${req.url.pathname}${req.url.search}`;
logger.warn(message, ['deprecation']);
@ -46,26 +44,19 @@ export function registerLegacy(
title,
savedObjectId,
browserTimezone,
}: { title: string; savedObjectId: string; browserTimezone: string } = req.params as any;
const queryString = querystring.stringify(req.query as any);
}: { title: string; savedObjectId: string; browserTimezone: string } = req.params;
const queryString = querystring.stringify(req.query as ParsedUrlQueryInput | undefined);
return await handler(
user,
exportTypeId,
{
title,
objectType,
savedObjectId,
browserTimezone,
queryString,
version: reporting.getKibanaVersion(),
},
context,
req,
res
);
return await requestHandler.handleGenerateRequest(exportTypeId, {
title,
objectType,
savedObjectId,
browserTimezone,
queryString,
version: reporting.getKibanaVersion(),
});
} catch (err) {
throw handleError(res, err);
throw requestHandler.handleError(err);
}
})
);

View file

@ -1,92 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';
import { kibanaResponseFactory } from 'src/core/server';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { LevelLogger as Logger } from '../lib';
import { enqueueJob } from '../lib/enqueue_job';
import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate';
import { registerGenerateFromJobParams } from './generate_from_jobparams';
import { registerLegacy } from './legacy';
import { HandlerFunction } from './types';
const getDownloadBaseUrl = (reporting: ReportingCore) => {
const config = reporting.getConfig();
return config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`;
};
export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) {
/*
* Generates enqueued job details to use in responses
*/
const handler: HandlerFunction = async (user, exportTypeId, jobParams, context, req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return res.custom({ statusCode: 503, body: 'Not Available' });
}
const licenseInfo = await reporting.getLicenseInfo();
const licenseResults = licenseInfo[exportTypeId];
if (!licenseResults) {
return res.badRequest({ body: `Invalid export-type of ${exportTypeId}` });
}
if (!licenseResults.enableLinks) {
return res.forbidden({ body: licenseResults.message });
}
try {
const report = await enqueueJob(
reporting,
req,
context,
user,
exportTypeId,
jobParams,
logger
);
// return task manager's task information and the download URL
const downloadBaseUrl = getDownloadBaseUrl(reporting);
return res.ok({
headers: {
'content-type': 'application/json',
},
body: {
path: `${downloadBaseUrl}/${report._id}`,
job: report.toApiJSON(),
},
});
} catch (err) {
logger.error(err);
throw err;
}
};
/*
* Error should already have been logged by the time we get here
*/
function handleError(res: typeof kibanaResponseFactory, err: Error | Boom.Boom) {
if (err instanceof Boom.Boom) {
return res.customError({
statusCode: err.output.statusCode,
body: err.output.payload.message,
});
}
// unknown error, can't convert to 4xx
throw err;
}
registerGenerateFromJobParams(reporting, handler, handleError);
registerGenerateCsvFromSavedObjectImmediate(reporting, handleError, logger);
registerLegacy(reporting, handler, handleError, logger);
}

View file

@ -5,23 +5,22 @@
* 2.0.
*/
import { LevelLogger as Logger } from '../lib';
import { ReportingCore } from '..';
import { LevelLogger } from '../lib';
import { registerDeprecationsRoutes } from './deprecations';
import { registerDiagnosticRoutes } from './diagnostic';
import { registerJobGenerationRoutes } from './generation';
import { registerJobInfoRoutes } from './jobs';
import { ReportingCore } from '../core';
import {
registerGenerateCsvFromSavedObjectImmediate,
registerJobGenerationRoutes,
registerLegacy,
} from './generate';
import { registerJobInfoRoutes } from './management';
export function registerRoutes(reporting: ReportingCore, logger: Logger) {
export function registerRoutes(reporting: ReportingCore, logger: LevelLogger) {
registerDeprecationsRoutes(reporting, logger);
registerDiagnosticRoutes(reporting, logger);
registerGenerateCsvFromSavedObjectImmediate(reporting, logger);
registerJobGenerationRoutes(reporting, logger);
registerLegacy(reporting, logger);
registerJobInfoRoutes(reporting);
}
export interface ReportingRequestPre {
management: {
jobTypes: string[];
};
user: string;
}

View file

@ -24,7 +24,7 @@ interface Payload {
statusCode: number;
content: string | Stream | ErrorFromPayload;
contentType: string | null;
headers: Record<string, any>;
headers: Record<string, string | number>;
}
type TaskRunResult = Required<ReportApiJSON>['output'];

View file

@ -60,6 +60,7 @@ export async function downloadJobResponseHandler(
} catch (err) {
const { logger } = reporting.getPluginSetupDeps();
logger.error(err);
throw err;
}
}

View file

@ -11,9 +11,10 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { i18n } from '@kbn/i18n';
import { UnwrapPromise } from '@kbn/utility-types';
import { ElasticsearchClient } from 'src/core/server';
import { PromiseType } from 'utility-types';
import { ReportingCore } from '../../';
import { statuses } from '../../lib/statuses';
import { ReportApiJSON, ReportSource } from '../../../common/types';
import { statuses } from '../../lib/statuses';
import { Report } from '../../lib/store';
import { ReportingUser } from '../../types';
@ -58,9 +59,9 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory
return `${config.get('index')}-*`;
}
async function execQuery<T extends (client: ElasticsearchClient) => any>(
callback: T
): Promise<UnwrapPromise<ReturnType<T>> | undefined> {
async function execQuery<
T extends (client: ElasticsearchClient) => Promise<PromiseType<ReturnType<T>> | undefined>
>(callback: T): Promise<UnwrapPromise<ReturnType<T>> | undefined> {
try {
const { asInternalUser: client } = await reportingCore.getEsClient();

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaRequest, KibanaResponseFactory } from 'kibana/server';
import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { ReportingCore } from '../..';
import {
createMockConfigSchema,
createMockLevelLogger,
createMockReportingCore,
} from '../../test_helpers';
import { BaseParams, ReportingRequestHandlerContext, ReportingSetup } from '../../types';
import { RequestHandler } from './request_handler';
jest.mock('../../lib/enqueue_job', () => ({
enqueueJob: () => ({
_id: 'id-of-this-test-report',
toApiJSON: () => JSON.stringify({ id: 'id-of-this-test-report' }),
}),
}));
const getMockContext = () =>
(({
core: coreMock.createRequestHandlerContext(),
} as unknown) as ReportingRequestHandlerContext);
const getMockRequest = () =>
({
url: { port: '5601', search: '', pathname: '/foo' },
route: { path: '/foo', options: {} },
} as KibanaRequest);
const getMockResponseFactory = () =>
(({
...httpServerMock.createResponseFactory(),
forbidden: (obj: unknown) => obj,
unauthorized: (obj: unknown) => obj,
} as unknown) as KibanaResponseFactory);
const mockLogger = createMockLevelLogger();
describe('Handle request to generate', () => {
let reportingCore: ReportingCore;
let mockContext: ReturnType<typeof getMockContext>;
let mockRequest: ReturnType<typeof getMockRequest>;
let mockResponseFactory: ReturnType<typeof getMockResponseFactory>;
let requestHandler: RequestHandler;
const mockJobParams = {} as BaseParams;
beforeEach(async () => {
reportingCore = await createMockReportingCore(createMockConfigSchema({}));
mockRequest = getMockRequest();
mockResponseFactory = getMockResponseFactory();
(mockResponseFactory.ok as jest.Mock) = jest.fn((args: unknown) => args);
(mockResponseFactory.forbidden as jest.Mock) = jest.fn((args: unknown) => args);
(mockResponseFactory.badRequest as jest.Mock) = jest.fn((args: unknown) => args);
mockContext = getMockContext();
mockContext.reporting = {} as ReportingSetup;
requestHandler = new RequestHandler(
reportingCore,
{ username: 'testymcgee' },
mockContext,
mockRequest,
mockResponseFactory,
mockLogger
);
});
test('disallows invalid export type', async () => {
expect(await requestHandler.handleGenerateRequest('neanderthals', mockJobParams))
.toMatchInlineSnapshot(`
Object {
"body": "Invalid export-type of neanderthals",
}
`);
});
test('disallows unsupporting license', async () => {
(reportingCore.getLicenseInfo as jest.Mock) = jest.fn(() => ({
csv: { enableLinks: false, message: `seeing this means the license isn't supported` },
}));
expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(`
Object {
"body": "seeing this means the license isn't supported",
}
`);
});
test('generates the download path', async () => {
expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(`
Object {
"body": Object {
"job": "{\\"id\\":\\"id-of-this-test-report\\"}",
"path": "undefined/api/reporting/jobs/download/id-of-this-test-report",
},
"headers": Object {
"content-type": "application/json",
},
}
`);
});
});

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';
import { KibanaRequest, KibanaResponseFactory } from 'kibana/server';
import { ReportingCore } from '../..';
import { API_BASE_URL } from '../../../common/constants';
import { JobParamsPDFLegacy } from '../../export_types/printable_pdf/types';
import { LevelLogger } from '../../lib';
import { enqueueJob } from '../../lib/enqueue_job';
import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types';
export const handleUnavailable = (res: KibanaResponseFactory) => {
return res.custom({ statusCode: 503, body: 'Not Available' });
};
const getDownloadBaseUrl = (reporting: ReportingCore) => {
const config = reporting.getConfig();
return config.kbnConfig.get('server', 'basePath') + `${API_BASE_URL}/jobs/download`;
};
export class RequestHandler {
constructor(
private reporting: ReportingCore,
private user: ReportingUser,
private context: ReportingRequestHandlerContext,
private req: KibanaRequest,
private res: KibanaResponseFactory,
private logger: LevelLogger
) {}
public async handleGenerateRequest(
exportTypeId: string,
jobParams: BaseParams | JobParamsPDFLegacy
) {
// ensure the async dependencies are loaded
if (!this.context.reporting) {
return handleUnavailable(this.res);
}
const licenseInfo = await this.reporting.getLicenseInfo();
const licenseResults = licenseInfo[exportTypeId];
if (!licenseResults) {
return this.res.badRequest({ body: `Invalid export-type of ${exportTypeId}` });
}
if (!licenseResults.enableLinks) {
return this.res.forbidden({ body: licenseResults.message });
}
try {
const report = await enqueueJob(
this.reporting,
this.req,
this.context,
this.user,
exportTypeId,
jobParams,
this.logger
);
// return task manager's task information and the download URL
const downloadBaseUrl = getDownloadBaseUrl(this.reporting);
return this.res.ok({
headers: { 'content-type': 'application/json' },
body: {
path: `${downloadBaseUrl}/${report._id}`,
job: report.toApiJSON(),
},
});
} catch (err) {
this.logger.error(err);
throw err;
}
}
/*
* This method does not log the error, as it assumes the error has already
* been caught and logged for stack trace context, and then rethrown
*/
public handleError(err: Error | Boom.Boom) {
if (err instanceof Boom.Boom) {
return this.res.customError({
statusCode: err.output.statusCode,
body: err.output.payload.message,
});
}
// unknown error, can't convert to 4xx
throw err;
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { registerJobInfoRoutes } from './jobs';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
jest.mock('../lib/content_stream', () => ({
jest.mock('../../lib/content_stream', () => ({
getContentStream: jest.fn(),
}));
@ -16,15 +16,15 @@ import { of } from 'rxjs';
import { ElasticsearchClient } from 'kibana/server';
import { setupServer } from 'src/core/server/test_utils';
import supertest from 'supertest';
import { ReportingCore } from '..';
import { ReportingInternalSetup } from '../core';
import { ContentStream, ExportTypesRegistry, getContentStream } from '../lib';
import { ReportingCore } from '../..';
import { ReportingInternalSetup } from '../../core';
import { ContentStream, ExportTypesRegistry, getContentStream } from '../../lib';
import {
createMockConfigSchema,
createMockPluginSetup,
createMockReportingCore,
} from '../test_helpers';
import { ExportTypeDefinition, ReportingRequestHandlerContext } from '../types';
} from '../../test_helpers';
import { ExportTypeDefinition, ReportingRequestHandlerContext } from '../../types';
import { registerJobInfoRoutes } from './jobs';
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;

View file

@ -5,21 +5,18 @@
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import Boom from '@hapi/boom';
import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { jobsQueryFactory } from './lib/jobs_query';
import { deleteJobResponseHandler, downloadJobResponseHandler } from './lib/job_response_handler';
import { schema } from '@kbn/config-schema';
import { ReportingCore } from '../../';
import { ROUTE_TAG_CAN_REDIRECT } from '../../../../security/server';
import { API_BASE_URL } from '../../../common/constants';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { jobsQueryFactory } from '../lib/jobs_query';
import { deleteJobResponseHandler, downloadJobResponseHandler } from '../lib/job_response_handler';
import { handleUnavailable } from '../lib/request_handler';
const MAIN_ENTRY = `${API_BASE_URL}/jobs`;
const handleUnavailable = (res: any) => {
return res.custom({ statusCode: 503, body: 'Not Available' });
};
export function registerJobInfoRoutes(reporting: ReportingCore) {
const setupDeps = reporting.getPluginSetupDeps();
const { router } = setupDeps;

View file

@ -1,35 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaRequest, KibanaResponseFactory } from 'src/core/server';
import type {
BaseParams,
BaseParamsLegacyPDF,
BasePayload,
ReportingRequestHandlerContext,
ReportingUser,
} from '../types';
export type HandlerFunction = (
user: ReportingUser,
exportType: string,
jobParams: BaseParams | BaseParamsLegacyPDF,
context: ReportingRequestHandlerContext,
req: KibanaRequest,
res: KibanaResponseFactory
) => any;
export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any;
export interface QueuedJobPayload {
error?: boolean;
source: {
job: {
payload: BasePayload;
};
};
}