diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 7aaa9c78602a9..c87c202c0ec1f 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -78,7 +78,8 @@ export class HeadlessChromiumDriverFactory { { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string }, pLogger: LevelLogger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { - return Rx.Observable.create(async (observer: InnerSubscriber) => { + // FIXME: 'create' is deprecated + return Rx.Observable.create(async (observer: InnerSubscriber) => { const logger = pLogger.clone(['browser-driver']); logger.info(`Creating browser page driver`); diff --git a/x-pack/plugins/reporting/server/config/ui_settings.test.ts b/x-pack/plugins/reporting/server/config/ui_settings.test.ts index dcd12e4c05f3f..6f40bb648ba93 100644 --- a/x-pack/plugins/reporting/server/config/ui_settings.test.ts +++ b/x-pack/plugins/reporting/server/config/ui_settings.test.ts @@ -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]" + `); }); diff --git a/x-pack/plugins/reporting/server/config/ui_settings.ts b/x-pack/plugins/reporting/server/config/ui_settings.ts index 587997bd1ecbd..bd13dcb8320ff 100644 --- a/x-pack/plugins/reporting/server/config/ui_settings.ts +++ b/x-pack/plugins/reporting/server/config/ui_settings.ts @@ -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) { core.uiSettings.register({ diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index 4e5309bcff5b1..0fc4e497e16a7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -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')}`, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index a7e492b882c20..74b013edc8cab 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -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, }); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index 8cc2d4b3037b3..9be95223a8864 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -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, }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 61d31153265f3..39163843c732f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -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), }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts index a265a24855efe..edd346c9b8928 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts @@ -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; let browser: jest.Mocked; 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); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index 77c732b3336be..9b5f234b78363 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -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 => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index e6769739ac75a..6615cbdc79c94 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -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 { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index e833a0dfcaf60..aeb3de0b04e4d 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -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, diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 1e29efd9cce0b..84566eb9c250c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -47,8 +47,8 @@ interface TaskExecutor extends Pick jobExecutor: RunTaskFn; } -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) { diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index 6c5d548e77020..6d844f9637a0b 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -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 } }); diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts similarity index 84% rename from x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts rename to x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts index efdb91d948536..52e6eb87e05cd 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts @@ -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 { + 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); } } ) diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts similarity index 81% rename from x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts rename to x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts index c519616cda5fb..cfcb7d6d2b05c 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts @@ -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); } }) ); diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts similarity index 94% rename from x-pack/plugins/reporting/server/routes/generation.test.ts rename to x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts index df5a85d71f49f..dff52f1f67464 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generation_from_jobparams.test.ts @@ -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>; diff --git a/x-pack/plugins/reporting/server/routes/generate/index.ts b/x-pack/plugins/reporting/server/routes/generate/index.ts new file mode 100644 index 0000000000000..0df9b4a725768 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/generate/index.ts @@ -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'; diff --git a/x-pack/plugins/reporting/server/routes/legacy.ts b/x-pack/plugins/reporting/server/routes/generate/legacy.ts similarity index 61% rename from x-pack/plugins/reporting/server/routes/legacy.ts rename to x-pack/plugins/reporting/server/routes/generate/legacy.ts index 79f1b7f17c2da..92f1784dc8eca 100644 --- a/x-pack/plugins/reporting/server/routes/legacy.ts +++ b/x-pack/plugins/reporting/server/routes/generate/legacy.ts @@ -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); } }) ); diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts deleted file mode 100644 index adbfbda727af2..0000000000000 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ /dev/null @@ -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); -} diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index a462da3849083..14a16e563ccbb 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -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; -} diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index c083849686ff0..89e6fcf7df21d 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -24,7 +24,7 @@ interface Payload { statusCode: number; content: string | Stream | ErrorFromPayload; contentType: string | null; - headers: Record; + headers: Record; } type TaskRunResult = Required['output']; diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 5b63b2627f931..4033b317bef62 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -60,6 +60,7 @@ export async function downloadJobResponseHandler( } catch (err) { const { logger } = reporting.getPluginSetupDeps(); logger.error(err); + throw err; } } diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index e15fa01362e97..afa83ed331672 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -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 any>( - callback: T - ): Promise> | undefined> { + async function execQuery< + T extends (client: ElasticsearchClient) => Promise> | undefined> + >(callback: T): Promise> | undefined> { try { const { asInternalUser: client } = await reportingCore.getEsClient(); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts new file mode 100644 index 0000000000000..d730da4803fe9 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -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; + let mockRequest: ReturnType; + let mockResponseFactory: ReturnType; + 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", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts new file mode 100644 index 0000000000000..8637000f41d95 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -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; + } +} diff --git a/x-pack/plugins/reporting/server/routes/management/index.ts b/x-pack/plugins/reporting/server/routes/management/index.ts new file mode 100644 index 0000000000000..0c31b2b0d6a0c --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/management/index.ts @@ -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'; diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts similarity index 98% rename from x-pack/plugins/reporting/server/routes/jobs.test.ts rename to x-pack/plugins/reporting/server/routes/management/jobs.test.ts index 883970bd45a74..c14976f616c7b 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.test.ts @@ -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>; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/management/jobs.ts similarity index 91% rename from x-pack/plugins/reporting/server/routes/jobs.ts rename to x-pack/plugins/reporting/server/routes/management/jobs.ts index 6086c1b9eb872..99c317453ca0f 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/management/jobs.ts @@ -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; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts deleted file mode 100644 index 336605e6ff9b9..0000000000000 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ /dev/null @@ -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; - }; - }; -}