[Reporting] server side code clean up (#106940)

* clean up the enqueue job function

* clean up the screenshots observable

* clean up authorized user pre routing

* clean up get_user

* fix download job response handlers

* clean up jobs query factory repetition

* clean up setup deps made available from plugin.ts

* update test for screenshots observable

* Revert "clean up setup deps made available from plugin.ts"

This reverts commit 91de680ebf.

* revert renames

* minor rename

* fix test after rename

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2021-08-10 22:34:42 -07:00 committed by GitHub
parent c0395c9ef6
commit e4e22ab928
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 375 additions and 439 deletions

View file

@ -29,7 +29,6 @@ import { ReportingConfig, ReportingSetup } from './';
import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory';
import { ReportingConfigType } from './config';
import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib';
import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots';
import { ReportingStore } from './lib/store';
import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks';
import { ReportingPluginRouter } from './types';
@ -237,12 +236,6 @@ export class ReportingCore {
.toPromise();
}
public async getScreenshotsObservable(): Promise<ScreenshotsObservableFn> {
const config = this.getConfig();
const { browserDriverFactory } = await this.getPluginStartDeps();
return screenshotsObservableFactory(config.get('capture'), browserDriverFactory);
}
public getEnableScreenshotMode() {
const { screenshotMode } = this.getPluginSetupDeps();
return screenshotMode.setScreenshotModeEnabled;

View file

@ -11,7 +11,7 @@ import { finalize, map, tap } from 'rxjs/operators';
import { ReportingCore } from '../../../';
import { LevelLogger } from '../../../lib';
import { LayoutParams, PreserveLayout } from '../../../lib/layouts';
import { ScreenshotResults } from '../../../lib/screenshots';
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
import { ConditionalHeaders } from '../../common';
function getBase64DecodedSize(value: string) {
@ -24,7 +24,9 @@ function getBase64DecodedSize(value: string) {
}
export async function generatePngObservableFactory(reporting: ReportingCore) {
const getScreenshots = await reporting.getScreenshotsObservable();
const config = reporting.getConfig();
const captureConfig = config.get('capture');
const { browserDriverFactory } = await reporting.getPluginStartDeps();
return function generatePngObservable(
logger: LevelLogger,
@ -43,7 +45,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup');
let apmBuffer: typeof apm.currentSpan;
const screenshots$ = getScreenshots({
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
logger,
urls: [url],
conditionalHeaders,

View file

@ -11,7 +11,7 @@ import { mergeMap } from 'rxjs/operators';
import { ReportingCore } from '../../../';
import { LevelLogger } from '../../../lib';
import { createLayout, LayoutParams } from '../../../lib/layouts';
import { ScreenshotResults } from '../../../lib/screenshots';
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
import { ConditionalHeaders } from '../../common';
import { PdfMaker } from './pdf';
import { getTracker } from './tracker';
@ -29,7 +29,7 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
export async function generatePdfObservableFactory(reporting: ReportingCore) {
const config = reporting.getConfig();
const captureConfig = config.get('capture');
const getScreenshots = await reporting.getScreenshotsObservable();
const { browserDriverFactory } = await reporting.getPluginStartDeps();
return function generatePdfObservable(
logger: LevelLogger,
@ -48,7 +48,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
tracker.endLayout();
tracker.startScreenshots();
const screenshots$ = getScreenshots({
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
logger,
urls,
conditionalHeaders,

View file

@ -16,7 +16,7 @@ import {
} from '../test_helpers';
import { ReportingRequestHandlerContext } from '../types';
import { ExportTypesRegistry, ReportingStore } from './';
import { enqueueJobFactory } from './enqueue_job';
import { enqueueJob } from './enqueue_job';
import { Report } from './store';
describe('Enqueue Job', () => {
@ -72,13 +72,14 @@ describe('Enqueue Job', () => {
});
it('returns a Report object', async () => {
const enqueueJob = enqueueJobFactory(mockReporting, logger);
const report = await enqueueJob(
mockReporting,
({} as unknown) as KibanaRequest,
({} as unknown) as ReportingRequestHandlerContext,
false,
'printablePdf',
mockBaseParams,
false,
({} as unknown) as ReportingRequestHandlerContext,
({} as unknown) as KibanaRequest
logger
);
const { _id, created_at: _created_at, ...snapObj } = report;
@ -117,14 +118,15 @@ describe('Enqueue Job', () => {
});
it('provides a default kibana version field for older POST URLs', async () => {
const enqueueJob = enqueueJobFactory(mockReporting, logger);
mockBaseParams.version = undefined;
const report = await enqueueJob(
mockReporting,
({} as unknown) as KibanaRequest,
({} as unknown) as ReportingRequestHandlerContext,
false,
'printablePdf',
mockBaseParams,
false,
({} as unknown) as ReportingRequestHandlerContext,
({} as unknown) as KibanaRequest
logger
);
const { _id, created_at: _created_at, ...snapObj } = report;

View file

@ -12,64 +12,53 @@ import { BaseParams, ReportingUser } from '../types';
import { checkParamsVersion, LevelLogger } from './';
import { Report } from './store';
export type EnqueueJobFn = (
export async function enqueueJob(
reporting: ReportingCore,
request: KibanaRequest,
context: ReportingRequestHandlerContext,
user: ReportingUser,
exportTypeId: string,
jobParams: BaseParams,
user: ReportingUser,
context: ReportingRequestHandlerContext,
request: KibanaRequest
) => Promise<Report>;
export function enqueueJobFactory(
reporting: ReportingCore,
parentLogger: LevelLogger
): EnqueueJobFn {
): Promise<Report> {
const logger = parentLogger.clone(['createJob']);
return async function enqueueJob(
exportTypeId: string,
jobParams: BaseParams,
user: ReportingUser,
context: ReportingRequestHandlerContext,
request: KibanaRequest
) {
const exportType = reporting.getExportTypesRegistry().getById(exportTypeId);
const exportType = reporting.getExportTypesRegistry().getById(exportTypeId);
if (exportType == null) {
throw new Error(`Export type ${exportTypeId} does not exist in the registry!`);
}
if (exportType == null) {
throw new Error(`Export type ${exportTypeId} does not exist in the registry!`);
}
if (!exportType.createJobFnFactory) {
throw new Error(`Export type ${exportTypeId} is not an async job type!`);
}
if (!exportType.createJobFnFactory) {
throw new Error(`Export type ${exportTypeId} is not an async job type!`);
}
const [createJob, store] = await Promise.all([
exportType.createJobFnFactory(reporting, logger.clone([exportType.id])),
reporting.getStore(),
]);
const [createJob, store] = await Promise.all([
exportType.createJobFnFactory(reporting, logger.clone([exportType.id])),
reporting.getStore(),
]);
jobParams.version = checkParamsVersion(jobParams, logger);
const job = await createJob!(jobParams, context, request);
jobParams.version = checkParamsVersion(jobParams, logger);
const job = await createJob!(jobParams, context, request);
// 1. Add the report to ReportingStore to show as pending
const report = await store.addReport(
new Report({
jobtype: exportType.jobType,
created_by: user ? user.username : false,
payload: job,
meta: {
objectType: jobParams.objectType,
layout: jobParams.layout?.id,
},
})
);
logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`);
// 1. Add the report to ReportingStore to show as pending
const report = await store.addReport(
new Report({
jobtype: exportType.jobType,
created_by: user ? user.username : false,
payload: job,
meta: {
objectType: jobParams.objectType,
layout: jobParams.layout?.id,
},
})
);
logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`);
// 2. Schedule the report with Task Manager
const task = await reporting.scheduleTask(report.toReportTaskJSON());
logger.info(
`Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}`
);
// 2. Schedule the report with Task Manager
const task = await reporting.scheduleTask(report.toReportTaskJSON());
logger.info(
`Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}`
);
return report;
};
return report;
}

View file

@ -5,12 +5,11 @@
* 2.0.
*/
import * as Rx from 'rxjs';
import { LevelLogger } from '../';
import { ConditionalHeaders } from '../../export_types/common';
import { LayoutInstance } from '../layouts';
export { screenshotsObservableFactory } from './observable';
export { getScreenshots$ } from './observable';
export interface ScreenshotObservableOpts {
logger: LevelLogger;
@ -55,11 +54,3 @@ export interface ScreenshotResults {
error?: Error;
elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing
}
export type ScreenshotsObservableFn = ({
logger,
urls,
conditionalHeaders,
layout,
browserTimezone,
}: ScreenshotObservableOpts) => Rx.Observable<ScreenshotResults[]>;

View file

@ -32,7 +32,7 @@ import {
} from '../../test_helpers';
import { ElementsPositionAndAttribute } from './';
import * as contexts from './constants';
import { screenshotsObservableFactory } from './observable';
import { getScreenshots$ } from './';
/*
* Mocks
@ -67,8 +67,7 @@ describe('Screenshot Observable Pipeline', () => {
});
it('pipelines a single url into screenshot and timeRange', async () => {
const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const result = await getScreenshots$({
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
logger,
urls: ['/welcome/home/start/index.htm'],
conditionalHeaders: {} as ConditionalHeaders,
@ -128,8 +127,7 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const result = await getScreenshots$({
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
logger,
urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'],
conditionalHeaders: {} as ConditionalHeaders,
@ -227,9 +225,8 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
logger,
urls: [
'/welcome/home/start/index2.htm',
@ -322,9 +319,8 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
logger,
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
conditionalHeaders: {} as ConditionalHeaders,
@ -354,50 +350,46 @@ describe('Screenshot Observable Pipeline', () => {
});
mockLayout.getViewport = () => null;
// test
const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
logger,
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
conditionalHeaders: {} as ConditionalHeaders,
layout: mockLayout,
browserTimezone: 'UTC',
}).toPromise();
};
const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
logger,
urls: ['/welcome/home/start/index.php3?page=./home.php3'],
conditionalHeaders: {} as ConditionalHeaders,
layout: mockLayout,
browserTimezone: 'UTC',
}).toPromise();
await expect(getScreenshot()).resolves.toMatchInlineSnapshot(`
Array [
Object {
"elementsPositionAndAttributes": Array [
Object {
"attributes": Object {},
"position": Object {
"boundingClientRect": Object {
"height": 1200,
"left": 0,
"top": 0,
"width": 1800,
},
"scroll": Object {
"x": 0,
"y": 0,
},
},
},
],
"error": undefined,
"screenshots": Array [
Object {
"base64EncodedData": "allyourBase64",
"description": undefined,
"title": undefined,
},
],
"timeRange": undefined,
expect(screenshots).toMatchInlineSnapshot(`
Array [
Object {
"elementsPositionAndAttributes": Array [
Object {
"attributes": Object {},
"position": Object {
"boundingClientRect": Object {
"height": 1200,
"left": 0,
"top": 0,
"width": 1800,
},
"scroll": Object {
"x": 0,
"y": 0,
},
},
]
`);
},
],
"error": undefined,
"screenshots": Array [
Object {
"base64EncodedData": "allyourBase64",
"description": undefined,
"title": undefined,
},
],
"timeRange": undefined,
},
]
`);
});
});
});

View file

@ -10,12 +10,7 @@ import * as Rx from 'rxjs';
import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators';
import { HeadlessChromiumDriverFactory } from '../../browsers';
import { CaptureConfig } from '../../types';
import {
ElementsPositionAndAttribute,
ScreenshotObservableOpts,
ScreenshotResults,
ScreenshotsObservableFn,
} from './';
import { ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults } from './';
import { checkPageIsOpen } from './check_browser_open';
import { DEFAULT_PAGELOAD_SELECTOR } from './constants';
import { getElementPositionAndAttributes } from './get_element_position_data';
@ -36,117 +31,110 @@ interface ScreenSetupData {
error?: Error;
}
export function screenshotsObservableFactory(
export function getScreenshots$(
captureConfig: CaptureConfig,
browserDriverFactory: HeadlessChromiumDriverFactory
): ScreenshotsObservableFn {
return function screenshotsObservable({
logger,
urls,
conditionalHeaders,
layout,
browserTimezone,
}: ScreenshotObservableOpts): Rx.Observable<ScreenshotResults[]> {
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
browserDriverFactory: HeadlessChromiumDriverFactory,
{ logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts
): Rx.Observable<ScreenshotResults[]> {
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
const apmCreatePage = apmTrans?.startSpan('create_page', 'wait');
const create$ = browserDriverFactory.createPage(
{ viewport: layout.getBrowserViewport(), browserTimezone },
logger
);
const apmCreatePage = apmTrans?.startSpan('create_page', 'wait');
const create$ = browserDriverFactory.createPage(
{ viewport: layout.getBrowserViewport(), browserTimezone },
logger
);
return create$.pipe(
mergeMap(({ driver, exit$ }) => {
apmCreatePage?.end();
exit$.subscribe({ error: () => apmTrans?.end() });
return create$.pipe(
mergeMap(({ driver, exit$ }) => {
apmCreatePage?.end();
exit$.subscribe({ error: () => apmTrans?.end() });
return Rx.from(urls).pipe(
concatMap((url, index) => {
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
mergeMap(() => {
// If we're moving to another page in the app, we'll want to wait for the app to tell us
// it's loaded the next page.
const page = index + 1;
const pageLoadSelector =
page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR;
return Rx.from(urls).pipe(
concatMap((url, index) => {
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
mergeMap(() => {
// If we're moving to another page in the app, we'll want to wait for the app to tell us
// it's loaded the next page.
const page = index + 1;
const pageLoadSelector =
page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR;
return openUrl(
captureConfig,
driver,
url,
pageLoadSelector,
conditionalHeaders,
logger
);
}),
mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)),
mergeMap(async (itemsCount) => {
// set the viewport to the dimentions from the job, to allow elements to flow into the expected layout
const viewport = layout.getViewport(itemsCount) || getDefaultViewPort();
await Promise.all([
driver.setViewport(viewport, logger),
waitForVisualizations(captureConfig, driver, itemsCount, layout, logger),
]);
}),
mergeMap(async () => {
// Waiting till _after_ elements have rendered before injecting our CSS
// allows for them to be displayed properly in many cases
await injectCustomCss(driver, layout, logger);
return openUrl(
captureConfig,
driver,
url,
pageLoadSelector,
conditionalHeaders,
logger
);
}),
mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)),
mergeMap(async (itemsCount) => {
// set the viewport to the dimentions from the job, to allow elements to flow into the expected layout
const viewport = layout.getViewport(itemsCount) || getDefaultViewPort();
await Promise.all([
driver.setViewport(viewport, logger),
waitForVisualizations(captureConfig, driver, itemsCount, layout, logger),
]);
}),
mergeMap(async () => {
// Waiting till _after_ elements have rendered before injecting our CSS
// allows for them to be displayed properly in many cases
await injectCustomCss(driver, layout, logger);
const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction');
if (layout.positionElements) {
// position panel elements for print layout
await layout.positionElements(driver, logger);
}
if (apmPositionElements) apmPositionElements.end();
const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction');
if (layout.positionElements) {
// position panel elements for print layout
await layout.positionElements(driver, logger);
}
if (apmPositionElements) apmPositionElements.end();
await waitForRenderComplete(captureConfig, driver, layout, logger);
}),
mergeMap(async () => {
return await Promise.all([
getTimeRange(driver, layout, logger),
getElementPositionAndAttributes(driver, layout, logger),
]).then(([timeRange, elementsPositionAndAttributes]) => ({
elementsPositionAndAttributes,
await waitForRenderComplete(captureConfig, driver, layout, logger);
}),
mergeMap(async () => {
return await Promise.all([
getTimeRange(driver, layout, logger),
getElementPositionAndAttributes(driver, layout, logger),
]).then(([timeRange, elementsPositionAndAttributes]) => ({
elementsPositionAndAttributes,
timeRange,
}));
}),
catchError((err) => {
checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it
logger.error(err);
return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err });
})
);
return setup$.pipe(
takeUntil(exit$),
mergeMap(
async (data: ScreenSetupData): Promise<ScreenshotResults> => {
checkPageIsOpen(driver); // re-check that the browser has not closed
const elements = data.elementsPositionAndAttributes
? data.elementsPositionAndAttributes
: getDefaultElementPosition(layout.getViewport(1));
const screenshots = await getScreenshots(driver, layout, elements, logger);
const { timeRange, error: setupError } = data;
return {
timeRange,
}));
}),
catchError((err) => {
checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it
logger.error(err);
return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err });
})
);
return setup$.pipe(
takeUntil(exit$),
mergeMap(
async (data: ScreenSetupData): Promise<ScreenshotResults> => {
checkPageIsOpen(driver); // re-check that the browser has not closed
const elements = data.elementsPositionAndAttributes
? data.elementsPositionAndAttributes
: getDefaultElementPosition(layout.getViewport(1));
const screenshots = await getScreenshots(driver, layout, elements, logger);
const { timeRange, error: setupError } = data;
return {
timeRange,
screenshots,
error: setupError,
elementsPositionAndAttributes: elements,
};
}
)
);
}),
take(urls.length),
toArray()
);
}),
first()
);
};
screenshots,
error: setupError,
elementsPositionAndAttributes: elements,
};
}
)
);
}),
take(urls.length),
toArray()
);
}),
first()
);
}
/*

View file

@ -13,7 +13,7 @@ import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/exe
import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types';
import { LevelLogger as Logger } from '../lib';
import { TaskRunResult } from '../lib/tasks';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { HandlerErrorFunction } from './types';
const API_BASE_URL_V1 = '/api/reporting/v1';
@ -36,7 +36,6 @@ export function registerGenerateCsvFromSavedObjectImmediate(
parentLogger: Logger
) {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
const { router } = setupDeps;
// TODO: find a way to abstract this using ExportTypeRegistry: it needs a new
@ -63,47 +62,50 @@ export function registerGenerateCsvFromSavedObjectImmediate(
tags: kibanaAccessControlTags,
},
},
userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => {
const logger = parentLogger.clone(['csv_searchsource_immediate']);
const runTaskFn = runTaskFnFactory(reporting, logger);
authorizedUserPreRouting(
reporting,
async (_user, context, req: CsvFromSavedObjectRequest, res) => {
const logger = parentLogger.clone(['csv_searchsource_immediate']);
const runTaskFn = runTaskFnFactory(reporting, logger);
try {
let buffer = Buffer.from('');
const stream = new Writable({
write(chunk, encoding, callback) {
buffer = Buffer.concat([
buffer,
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding),
]);
callback();
},
});
try {
let buffer = Buffer.from('');
const stream = new Writable({
write(chunk, encoding, callback) {
buffer = Buffer.concat([
buffer,
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding),
]);
callback();
},
});
const {
content_type: jobOutputContentType,
size: jobOutputSize,
}: TaskRunResult = await runTaskFn(null, req.body, context, stream, req);
stream.end();
const jobOutputContent = buffer.toString();
const {
content_type: jobOutputContentType,
size: jobOutputSize,
}: TaskRunResult = await runTaskFn(null, req.body, context, stream, req);
stream.end();
const jobOutputContent = buffer.toString();
logger.info(`Job output size: ${jobOutputSize} bytes`);
logger.info(`Job output size: ${jobOutputSize} bytes`);
// convert null to undefined so the value can be sent to h.response()
if (jobOutputContent === null) {
logger.warn('CSV Job Execution created empty content result');
// convert null to undefined so the value can be sent to h.response()
if (jobOutputContent === null) {
logger.warn('CSV Job Execution created empty content result');
}
return res.ok({
body: jobOutputContent || '',
headers: {
'content-type': jobOutputContentType ? jobOutputContentType : [],
'accept-ranges': 'none',
},
});
} catch (err) {
logger.error(err);
return handleError(res, err);
}
return res.ok({
body: jobOutputContent || '',
headers: {
'content-type': jobOutputContentType ? jobOutputContentType : [],
'accept-ranges': 'none',
},
});
} catch (err) {
logger.error(err);
return handleError(res, err);
}
})
)
);
}

View file

@ -10,7 +10,7 @@ import { ReportingCore } from '../..';
import { API_DIAGNOSE_URL } from '../../../common/constants';
import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs';
import { LevelLogger as Logger } from '../../lib';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
const logsToHelpMap = {
@ -47,14 +47,13 @@ const logsToHelpMap = {
export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => {
const { router } = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
router.post(
{
path: `${API_DIAGNOSE_URL}/browser`,
validate: {},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => {
try {
const logs = await browserStartLogs(reporting, logger).toPromise();
const knownIssues = Object.keys(logsToHelpMap) as Array<keyof typeof logsToHelpMap>;

View file

@ -11,7 +11,7 @@ import { defaults, get } from 'lodash';
import { ReportingCore } from '../..';
import { API_DIAGNOSE_URL } from '../../../common/constants';
import { LevelLogger as Logger } from '../../lib';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes';
@ -27,7 +27,6 @@ const numberToByteSizeValue = (value: number | ByteSizeValue) => {
export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
const { router } = setupDeps;
router.post(
@ -35,7 +34,7 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger)
path: `${API_DIAGNOSE_URL}/config`,
validate: {},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => {
const warnings = [];
const { asInternalUser: elasticsearchClient } = await reporting.getEsClient();
const config = reporting.getConfig();

View file

@ -13,12 +13,11 @@ import { omitBlockedHeaders } from '../../export_types/common';
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
import { LevelLogger as Logger } from '../../lib';
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
import { DiagnosticResponse } from './';
export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
const { router } = setupDeps;
router.post(
@ -26,7 +25,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
path: `${API_DIAGNOSE_URL}/screenshot`,
validate: {},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (_user, _context, req, res) => {
const generatePngObservable = await generatePngObservableFactory(reporting);
const config = reporting.getConfig();
const decryptedHeaders = req.headers as Record<string, string>;

View file

@ -10,7 +10,7 @@ import rison from 'rison-node';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { BaseParams } from '../types';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { HandlerErrorFunction, HandlerFunction } from './types';
const BASE_GENERATE = `${API_BASE_URL}/generate`;
@ -21,7 +21,6 @@ export function registerGenerateFromJobParams(
handleError: HandlerErrorFunction
) {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
const { router } = setupDeps;
// TODO: find a way to abstract this using ExportTypeRegistry: it needs a new
@ -41,7 +40,7 @@ export function registerGenerateFromJobParams(
},
options: { tags: kibanaAccessControlTags },
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
let jobParamsRison: null | string = null;
if (req.body) {

View file

@ -10,7 +10,7 @@ import { kibanaResponseFactory } from 'src/core/server';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { LevelLogger as Logger } from '../lib';
import { enqueueJobFactory } from '../lib/enqueue_job';
import { enqueueJob } from '../lib/enqueue_job';
import { registerGenerateFromJobParams } from './generate_from_jobparams';
import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate';
import { HandlerFunction } from './types';
@ -42,8 +42,15 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo
}
try {
const enqueueJob = enqueueJobFactory(reporting, logger);
const report = await enqueueJob(exportTypeId, jobParams, user, context, req);
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);

View file

@ -10,12 +10,9 @@ import Boom from '@hapi/boom';
import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server';
import { ReportingCore } from '../';
import { API_BASE_URL } from '../../common/constants';
import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing';
import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing';
import { jobsQueryFactory } from './lib/jobs_query';
import {
deleteJobResponseHandlerFactory,
downloadJobResponseHandlerFactory,
} from './lib/job_response_handler';
import { deleteJobResponseHandler, downloadJobResponseHandler } from './lib/job_response_handler';
const MAIN_ENTRY = `${API_BASE_URL}/jobs`;
@ -25,8 +22,8 @@ const handleUnavailable = (res: any) => {
export function registerJobInfoRoutes(reporting: ReportingCore) {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
const { router } = setupDeps;
const jobsQuery = jobsQueryFactory(reporting);
// list jobs in the queue, paginated
router.get(
@ -40,7 +37,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
}),
},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return handleUnavailable(res);
@ -53,7 +50,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
const page = parseInt(queryPage, 10) || 0;
const size = Math.min(100, parseInt(querySize, 10) || 10);
const jobIds = queryIds ? queryIds.split(',') : null;
const jobsQuery = jobsQueryFactory(reporting);
const results = await jobsQuery.list(jobTypes, user, page, size, jobIds);
return res.ok({
@ -71,7 +67,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
path: `${MAIN_ENTRY}/count`,
validate: false,
},
userHandler(async (user, context, _req, res) => {
authorizedUserPreRouting(reporting, async (user, context, _req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return handleUnavailable(res);
@ -81,7 +77,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
management: { jobTypes = [] },
} = await reporting.getLicenseInfo();
const jobsQuery = jobsQueryFactory(reporting);
const count = await jobsQuery.count(jobTypes, user);
return res.ok({
@ -103,7 +98,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
}),
},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return res.custom({ statusCode: 503 });
@ -114,7 +109,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
management: { jobTypes = [] },
} = await reporting.getLicenseInfo();
const jobsQuery = jobsQueryFactory(reporting);
const result = await jobsQuery.get(user, docId);
if (!result) {
@ -137,8 +131,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
);
// trigger a download of the output from a job
const downloadResponseHandler = downloadJobResponseHandlerFactory(reporting);
router.get(
{
path: `${MAIN_ENTRY}/download/{docId}`,
@ -149,7 +141,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
},
options: { tags: [ROUTE_TAG_CAN_REDIRECT] },
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return handleUnavailable(res);
@ -160,12 +152,11 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
management: { jobTypes = [] },
} = await reporting.getLicenseInfo();
return downloadResponseHandler(res, jobTypes, user, { docId });
return downloadJobResponseHandler(reporting, res, jobTypes, user, { docId });
})
);
// allow a report to be deleted
const deleteResponseHandler = deleteJobResponseHandlerFactory(reporting);
router.delete(
{
path: `${MAIN_ENTRY}/delete/{docId}`,
@ -175,7 +166,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
}),
},
},
userHandler(async (user, context, req, res) => {
authorizedUserPreRouting(reporting, async (user, context, req, res) => {
// ensure the async dependencies are loaded
if (!context.reporting) {
return handleUnavailable(res);
@ -186,7 +177,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) {
management: { jobTypes = [] },
} = await reporting.getLicenseInfo();
return deleteResponseHandler(res, jobTypes, user, { docId });
return deleteJobResponseHandler(reporting, res, jobTypes, user, { docId });
})
);
}

View file

@ -11,7 +11,7 @@ import { ReportingCore } from '../../';
import { ReportingInternalSetup } from '../../core';
import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers';
import type { ReportingRequestHandlerContext } from '../../types';
import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing';
import { authorizedUserPreRouting } from './authorized_user_pre_routing';
let mockCore: ReportingCore;
const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } });
@ -46,11 +46,10 @@ describe('authorized_user_pre_routing', function () {
...mockCore.pluginSetupDeps,
security: undefined, // disable security
} as unknown) as ReportingInternalSetup);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore);
const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory;
let handlerCalled = false;
authorizedUserPreRouting((user: unknown) => {
authorizedUserPreRouting(mockCore, (user: unknown) => {
expect(user).toBe(false); // verify the user is a false value
handlerCalled = true;
return Promise.resolve({ status: 200, options: {} });
@ -70,11 +69,10 @@ describe('authorized_user_pre_routing', function () {
},
}, // disable security
} as unknown) as ReportingInternalSetup);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore);
const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory;
let handlerCalled = false;
authorizedUserPreRouting((user: unknown) => {
authorizedUserPreRouting(mockCore, (user: unknown) => {
expect(user).toBe(false); // verify the user is a false value
handlerCalled = true;
return Promise.resolve({ status: 200, options: {} });
@ -93,11 +91,10 @@ describe('authorized_user_pre_routing', function () {
authc: { getCurrentUser: () => null },
},
} as unknown) as ReportingInternalSetup);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore);
const mockHandler = () => {
throw new Error('Handler callback should not be called');
};
const requestHandler = authorizedUserPreRouting(mockHandler);
const requestHandler = authorizedUserPreRouting(mockCore, mockHandler);
const mockResponseFactory = getMockResponseFactory();
expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({
@ -126,14 +123,13 @@ describe('authorized_user_pre_routing', function () {
authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) },
},
} as unknown) as ReportingInternalSetup);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore);
const mockResponseFactory = getMockResponseFactory();
const mockHandler = () => {
throw new Error('Handler callback should not be called');
};
expect(
authorizedUserPreRouting(mockHandler)(
authorizedUserPreRouting(mockCore, mockHandler)(
getMockContext(),
getMockRequest(),
mockResponseFactory
@ -153,10 +149,9 @@ describe('authorized_user_pre_routing', function () {
},
},
} as unknown) as ReportingInternalSetup);
const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore);
const mockResponseFactory = getMockResponseFactory();
authorizedUserPreRouting((user) => {
authorizedUserPreRouting(mockCore, (user) => {
expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' });
done();
return Promise.resolve({ status: 200, options: {} });

View file

@ -8,12 +8,13 @@
import { RequestHandler, RouteMethod } from 'src/core/server';
import { AuthenticatedUser } from '../../../../security/server';
import { ReportingCore } from '../../core';
import { getUserFactory } from './get_user';
import { getUser } from './get_user';
import type { ReportingRequestHandlerContext } from '../../types';
const superuserRole = 'superuser';
type ReportingRequestUser = AuthenticatedUser | false;
export type RequestHandlerUser<P, Q, B> = RequestHandler<
P,
Q,
@ -23,43 +24,40 @@ export type RequestHandlerUser<P, Q, B> = RequestHandler<
? (user: ReportingRequestUser, ...a: U) => R
: never;
export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn(
reporting: ReportingCore
) {
export const authorizedUserPreRouting = <P, Q, B>(
reporting: ReportingCore,
handler: RequestHandlerUser<P, Q, B>
): RequestHandler<P, Q, B, ReportingRequestHandlerContext, RouteMethod> => {
const { logger, security } = reporting.getPluginSetupDeps();
const getUser = getUserFactory(security);
return <P, Q, B>(
handler: RequestHandlerUser<P, Q, B>
): RequestHandler<P, Q, B, ReportingRequestHandlerContext, RouteMethod> => {
return (context, req, res) => {
try {
let user: ReportingRequestUser = false;
if (security && security.license.isEnabled()) {
// find the authenticated user, or null if security is not enabled
user = getUser(req);
if (!user) {
// security is enabled but the user is null
return res.unauthorized({ body: `Sorry, you aren't authenticated` });
}
return (context, req, res) => {
try {
let user: ReportingRequestUser = false;
if (security && security.license.isEnabled()) {
// find the authenticated user, or null if security is not enabled
user = getUser(req, security);
if (!user) {
// security is enabled but the user is null
return res.unauthorized({ body: `Sorry, you aren't authenticated` });
}
const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles();
if (user && deprecatedAllowedRoles !== false) {
// check allowance with the configured set of roleas + "superuser"
const allowedRoles = deprecatedAllowedRoles || [];
const authorizedRoles = [superuserRole, ...allowedRoles];
if (!user.roles.find((role) => authorizedRoles.includes(role))) {
// user's roles do not allow
return res.forbidden({ body: `Sorry, you don't have access to Reporting` });
}
}
return handler(user, context, req, res);
} catch (err) {
logger.error(err);
return res.custom({ statusCode: 500 });
}
};
const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles();
if (user && deprecatedAllowedRoles !== false) {
// check allowance with the configured set of roleas + "superuser"
const allowedRoles = deprecatedAllowedRoles || [];
const authorizedRoles = [superuserRole, ...allowedRoles];
if (!user.roles.find((role) => authorizedRoles.includes(role))) {
// user's roles do not allow
return res.forbidden({ body: `Sorry, you don't have access to Reporting` });
}
}
return handler(user, context, req, res);
} catch (err) {
logger.error(err);
return res.custom({ statusCode: 500 });
}
};
};

View file

@ -8,8 +8,6 @@
import { KibanaRequest } from 'kibana/server';
import { SecurityPluginSetup } from '../../../../security/server';
export function getUserFactory(security?: SecurityPluginSetup) {
return (request: KibanaRequest) => {
return security?.authc.getCurrentUser(request) ?? false;
};
export function getUser(request: KibanaRequest, security?: SecurityPluginSetup) {
return security?.authc.getCurrentUser(request) ?? false;
}

View file

@ -16,93 +16,85 @@ interface JobResponseHandlerParams {
docId: string;
}
interface JobResponseHandlerOpts {
excludeContent?: boolean;
}
export function downloadJobResponseHandlerFactory(reporting: ReportingCore) {
export async function downloadJobResponseHandler(
reporting: ReportingCore,
res: typeof kibanaResponseFactory,
validJobTypes: string[],
user: ReportingUser,
params: JobResponseHandlerParams
) {
const jobsQuery = jobsQueryFactory(reporting);
const getDocumentPayload = getDocumentPayloadFactory(reporting);
return async function jobResponseHandler(
res: typeof kibanaResponseFactory,
validJobTypes: string[],
user: ReportingUser,
params: JobResponseHandlerParams,
opts: JobResponseHandlerOpts = {}
) {
try {
const { docId } = params;
const doc = await jobsQuery.get(user, docId);
if (!doc) {
return res.notFound();
}
if (!validJobTypes.includes(doc.jobtype)) {
return res.unauthorized({
body: `Sorry, you are not authorized to download ${doc.jobtype} reports`,
});
}
const payload = await getDocumentPayload(doc);
if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) {
return res.badRequest({
body: `Unsupported content-type of ${payload.contentType} specified by job output`,
});
}
return res.custom({
body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content,
statusCode: payload.statusCode,
headers: {
...payload.headers,
'content-type': payload.contentType || '',
},
});
} catch (err) {
const { logger } = reporting.getPluginSetupDeps();
logger.error(err);
}
};
}
export function deleteJobResponseHandlerFactory(reporting: ReportingCore) {
const jobsQuery = jobsQueryFactory(reporting);
return async function deleteJobResponseHander(
res: typeof kibanaResponseFactory,
validJobTypes: string[],
user: ReportingUser,
params: JobResponseHandlerParams
) {
try {
const { docId } = params;
const doc = await jobsQuery.get(user, docId);
const doc = await jobsQuery.get(user, docId);
if (!doc) {
return res.notFound();
}
const { jobtype: jobType } = doc;
if (!validJobTypes.includes(jobType)) {
if (!validJobTypes.includes(doc.jobtype)) {
return res.unauthorized({
body: `Sorry, you are not authorized to delete ${jobType} reports`,
body: `Sorry, you are not authorized to download ${doc.jobtype} reports`,
});
}
try {
const docIndex = doc.index;
await jobsQuery.delete(docIndex, docId);
return res.ok({
body: { deleted: true },
});
} catch (error) {
return res.customError({
statusCode: error.statusCode,
body: error.message,
const payload = await getDocumentPayload(doc);
if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) {
return res.badRequest({
body: `Unsupported content-type of ${payload.contentType} specified by job output`,
});
}
};
return res.custom({
body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content,
statusCode: payload.statusCode,
headers: {
...payload.headers,
'content-type': payload.contentType || '',
},
});
} catch (err) {
const { logger } = reporting.getPluginSetupDeps();
logger.error(err);
}
}
export async function deleteJobResponseHandler(
reporting: ReportingCore,
res: typeof kibanaResponseFactory,
validJobTypes: string[],
user: ReportingUser,
params: JobResponseHandlerParams
) {
const jobsQuery = jobsQueryFactory(reporting);
const { docId } = params;
const doc = await jobsQuery.get(user, docId);
if (!doc) {
return res.notFound();
}
const { jobtype: jobType } = doc;
if (!validJobTypes.includes(jobType)) {
return res.unauthorized({
body: `Sorry, you are not authorized to delete ${jobType} reports`,
});
}
try {
const docIndex = doc.index;
await jobsQuery.delete(docIndex, docId);
return res.ok({
body: { deleted: true },
});
} catch (error) {
return res.customError({
statusCode: error.statusCode,
body: error.message,
});
}
}