[Reporting] Remove encryption logic from export types (#111472)

* [Reporting] change CreateJobFn type to not handle KibanaRequest

* fix enqueueJob

* fix tests

* fix imports

* convert enqueue_job into a method of request_handler

* Update types.ts

* Update report.ts

* fix import

* update comment and structure

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2021-09-08 16:39:28 -07:00 committed by GitHub
parent 543a7cbd7d
commit af06aec5b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 225 additions and 332 deletions

View file

@ -76,7 +76,7 @@ export interface BasePayload extends BaseParams {
export interface ReportSource {
/*
* Required fields: populated in enqueue_job when the request comes in to
* Required fields: populated in RequestHandler.enqueueJob when the request comes in to
* generate the report
*/
jobtype: string; // refers to `ExportTypeDefinition.jobType`

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { cryptoFactory } from '../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import {
IndexPatternSavedObjectDeprecatedCSV,
@ -15,15 +14,11 @@ import {
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsDeprecatedCSV, TaskPayloadDeprecatedCSV>
> = function createJobFactoryFn(reporting, logger) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(jobParams, context, request) {
> = function createJobFactoryFn(_reporting, logger) {
return async function createJob(jobParams, context) {
logger.warn(
`The "/generate/csv" endpoint is deprecated and will be removed in Kibana 8.0. Please recreate the POST URL used to automate this CSV export.`
);
const serializedEncryptedHeaders = await crypto.encrypt(request.headers);
const savedObjectsClient = context.core.savedObjects.client;
const indexPatternSavedObject = ((await savedObjectsClient.get(
@ -33,8 +28,6 @@ export const createJobFnFactory: CreateJobFnFactory<
return {
isDeprecated: true,
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(request, logger),
indexPatternSavedObject,
...jobParams,
};

View file

@ -5,26 +5,13 @@
* 2.0.
*/
import { CSV_JOB_TYPE } from '../../../common/constants';
import { cryptoFactory } from '../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import { JobParamsCSV, TaskPayloadCSV } from './types';
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsCSV, TaskPayloadCSV>
> = function createJobFactoryFn(reporting, parentLogger) {
const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']);
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(jobParams, context, request) {
const serializedEncryptedHeaders = await crypto.encrypt(request.headers);
return {
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(request, logger),
...jobParams,
};
> = function createJobFactoryFn() {
return async function createJob(jobParams) {
return jobParams;
};
};

View file

@ -5,26 +5,18 @@
* 2.0.
*/
import { cryptoFactory } from '../../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../../types';
import { validateUrls } from '../../common';
import { JobParamsPNG, TaskPayloadPNG } from '../types';
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsPNG, TaskPayloadPNG>
> = function createJobFactoryFn(reporting, logger) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(jobParams, _context, req) {
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
> = function createJobFactoryFn() {
return async function createJob(jobParams) {
validateUrls([jobParams.relativeUrl]);
return {
...jobParams,
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
forceNow: new Date().toISOString(),
};
};

View file

@ -5,23 +5,15 @@
* 2.0.
*/
import { cryptoFactory } from '../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types';
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsPNGV2, TaskPayloadPNGV2>
> = function createJobFactoryFn(reporting, logger) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob({ locatorParams, ...jobParams }, context, req) {
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
> = function createJobFactoryFn() {
return async function createJob({ locatorParams, ...jobParams }) {
return {
...jobParams,
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
locatorParams: [locatorParams],
forceNow: new Date().toISOString(),
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { coreMock } from 'src/core/server/mocks';
import { createMockLevelLogger } from '../../../test_helpers';
import { compatibilityShim } from './compatibility_shim';
@ -15,7 +15,6 @@ const mockRequestHandlerContext = {
};
const mockLogger = createMockLevelLogger();
const mockKibanaRequest = httpServerMock.createKibanaRequest();
const createMockSavedObject = (body: any) => ({
id: 'mockSavedObjectId123',
type: 'mockSavedObjectType',
@ -36,8 +35,7 @@ test(`passes title through if provided`, async () => {
const createJobMock = jest.fn();
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title, relativeUrls: ['/something'] }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(0);
@ -58,8 +56,7 @@ test(`gets the title from the savedObject`, async () => {
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(2);
@ -83,8 +80,7 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async
const savedObjectId = 'abc';
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ objectType, savedObjectId }),
context,
mockKibanaRequest
context
);
expect(mockLogger.warn.mock.calls.length).toBe(2);
@ -107,8 +103,7 @@ test(`logs no warnings when title and relativeUrls is passed`, async () => {
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title: 'Phenomenal Dashboard', relativeUrls: ['/abc', '/def'] }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(0);
@ -119,8 +114,7 @@ test(`logs warning if title can not be provided`, async () => {
const createJobMock = jest.fn();
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ relativeUrls: ['/abc'] }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(1);
@ -140,8 +134,7 @@ test(`logs deprecations when generating the title/relativeUrl using the savedObj
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(2);
@ -159,8 +152,7 @@ test(`passes objectType through`, async () => {
const objectType = 'foo';
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title: 'test', relativeUrls: ['/something'], objectType }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(0);
@ -176,8 +168,7 @@ test(`passes the relativeUrls through`, async () => {
const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else'];
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title: 'test', relativeUrls }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(0);
@ -193,8 +184,7 @@ const testSavedObjectRelativeUrl = (objectType: string, expectedUrl: string) =>
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title: 'test', objectType, savedObjectId: 'abc' }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(1);
@ -222,8 +212,7 @@ test(`appends the queryString to the relativeUrl when generating from the savedO
savedObjectId: 'abc',
queryString: 'foo=bar',
}),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(1);
@ -248,8 +237,7 @@ test(`throw an Error if the objectType, savedObjectId and relativeUrls are provi
relativeUrls: ['/something'],
savedObjectId: 'abc',
}),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
await expect(promise).rejects.toBeDefined();
@ -260,8 +248,7 @@ test(`passes headers and request through`, async () => {
await compatibilityShim(createJobMock, mockLogger)(
createMockJobParams({ title: 'test', relativeUrls: ['/something'] }),
mockRequestHandlerContext,
mockKibanaRequest
mockRequestHandlerContext
);
expect(mockLogger.warn.mock.calls.length).toBe(0);
@ -269,5 +256,4 @@ test(`passes headers and request through`, async () => {
expect(createJobMock.mock.calls.length).toBe(1);
expect(createJobMock.mock.calls[0][1]).toBe(mockRequestHandlerContext);
expect(createJobMock.mock.calls[0][2]).toBe(mockKibanaRequest);
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
import type { SavedObjectsClientContract } from 'kibana/server';
import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/server';
import type { LevelLogger } from '../../../lib';
import type { CreateJobFn, ReportingRequestHandlerContext } from '../../../types';
@ -56,8 +56,7 @@ export function compatibilityShim(
) {
return async function (
jobParams: JobParamsPDF | JobParamsPDFLegacy,
context: ReportingRequestHandlerContext,
req: KibanaRequest
context: ReportingRequestHandlerContext
) {
let kibanaRelativeUrls = (jobParams as JobParamsPDF).relativeUrls;
let reportTitle = jobParams.title;
@ -125,6 +124,6 @@ export function compatibilityShim(
isDeprecated, // tack on this flag so it will be saved the TaskPayload
};
return await createJobFn(transformedJobParams, context, req);
return await createJobFn(transformedJobParams, context);
};
}

View file

@ -5,9 +5,7 @@
* 2.0.
*/
import { KibanaRequest } from 'src/core/server';
import { cryptoFactory } from '../../../lib';
import { CreateJobFn, CreateJobFnFactory, ReportingRequestHandlerContext } from '../../../types';
import { CreateJobFn, CreateJobFnFactory } from '../../../types';
import { validateUrls } from '../../common';
import { JobParamsPDF, JobParamsPDFLegacy, TaskPayloadPDF } from '../types';
import { compatibilityShim } from './compatibility_shim';
@ -18,24 +16,15 @@ import { compatibilityShim } from './compatibility_shim';
*/
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsPDF | JobParamsPDFLegacy, TaskPayloadPDF>
> = function createJobFactoryFn(reporting, logger) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
> = function createJobFactoryFn(_reporting, logger) {
return compatibilityShim(async function createJobFn(
{ relativeUrls, ...jobParams }: JobParamsPDF, // relativeUrls does not belong in the payload
_context: ReportingRequestHandlerContext,
req: KibanaRequest
{ relativeUrls, ...jobParams }: JobParamsPDF // relativeUrls does not belong in the payload of PDFV1
) {
validateUrls(relativeUrls);
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
// return the payload
return {
...jobParams,
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
forceNow: new Date().toISOString(),
objects: relativeUrls.map((u) => ({ relativeUrl: u })),
};

View file

@ -5,23 +5,15 @@
* 2.0.
*/
import { cryptoFactory } from '../../lib';
import { CreateJobFn, CreateJobFnFactory } from '../../types';
import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types';
export const createJobFnFactory: CreateJobFnFactory<
CreateJobFn<JobParamsPDFV2, TaskPayloadPDFV2>
> = function createJobFactoryFn(reporting, logger) {
const config = reporting.getConfig();
const crypto = cryptoFactory(config.get('encryptionKey'));
return async function createJob(jobParams, context, req) {
const serializedEncryptedHeaders = await crypto.encrypt(req.headers);
> = function createJobFactoryFn() {
return async function createJob(jobParams) {
return {
...jobParams,
headers: serializedEncryptedHeaders,
spaceId: reporting.getSpaceId(req, logger),
forceNow: new Date().toISOString(),
};
};

View file

@ -1,136 +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 } from 'src/core/server';
import { ReportingCore } from '../';
import { TaskManagerStartContract } from '../../../task_manager/server';
import { ReportingInternalStart } from '../core';
import {
createMockConfigSchema,
createMockLevelLogger,
createMockReportingCore,
} from '../test_helpers';
import { ReportingRequestHandlerContext } from '../types';
import { ExportTypesRegistry, ReportingStore } from './';
import { enqueueJob } from './enqueue_job';
import { Report } from './store';
describe('Enqueue Job', () => {
const logger = createMockLevelLogger();
let mockReporting: ReportingCore;
let mockExportTypesRegistry: ExportTypesRegistry;
const mockBaseParams = {
browserTimezone: 'UTC',
headers: 'cool_encrypted_headers',
objectType: 'cool_object_type',
title: 'cool_title',
version: 'unknown' as any,
};
beforeEach(() => {
mockBaseParams.version = '7.15.0-test';
});
beforeAll(async () => {
mockExportTypesRegistry = new ExportTypesRegistry();
mockExportTypesRegistry.register({
id: 'printablePdf',
name: 'Printable PDFble',
jobType: 'printable_pdf',
jobContentEncoding: 'base64',
jobContentExtension: 'pdf',
validLicenses: ['turquoise'],
createJobFnFactory: () => async () => mockBaseParams,
runTaskFnFactory: jest.fn(),
});
mockReporting = await createMockReportingCore(createMockConfigSchema());
mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry;
mockReporting.getStore = () =>
Promise.resolve(({
addReport: jest
.fn()
.mockImplementation(
(report) => new Report({ ...report, _index: '.reporting-foo-index-234' })
),
} as unknown) as ReportingStore);
const scheduleMock = jest.fn().mockImplementation(() => ({
id: '123-great-id',
}));
await mockReporting.pluginStart(({
taskManager: ({
ensureScheduled: jest.fn(),
schedule: scheduleMock,
} as unknown) as TaskManagerStartContract,
} as unknown) as ReportingInternalStart);
});
it('returns a Report object', async () => {
const report = await enqueueJob(
mockReporting,
({} as unknown) as KibanaRequest,
({} as unknown) as ReportingRequestHandlerContext,
false,
'printablePdf',
mockBaseParams,
logger
);
const { _id, created_at: _created_at, ...snapObj } = report;
expect(snapObj).toMatchInlineSnapshot(`
Object {
"_index": ".reporting-foo-index-234",
"_primary_term": undefined,
"_seq_no": undefined,
"attempts": 0,
"browser_type": undefined,
"completed_at": undefined,
"created_by": false,
"jobtype": "printable_pdf",
"kibana_id": undefined,
"kibana_name": undefined,
"max_attempts": undefined,
"meta": Object {
"isDeprecated": undefined,
"layout": undefined,
"objectType": "cool_object_type",
},
"migration_version": "7.14.0",
"output": null,
"payload": Object {
"browserTimezone": "UTC",
"headers": "cool_encrypted_headers",
"objectType": "cool_object_type",
"title": "cool_title",
"version": "7.15.0-test",
},
"process_expiration": undefined,
"started_at": undefined,
"status": "pending",
"timeout": undefined,
}
`);
});
it('provides a default kibana version field for older POST URLs', async () => {
mockBaseParams.version = undefined;
const report = await enqueueJob(
mockReporting,
({} as unknown) as KibanaRequest,
({} as unknown) as ReportingRequestHandlerContext,
false,
'printablePdf',
mockBaseParams,
logger
);
const { _id, created_at: _created_at, ...snapObj } = report;
expect(snapObj.payload.version).toBe('7.14.0');
});
});

View file

@ -1,66 +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 } from 'src/core/server';
import { ReportingCore } from '../';
import type { ReportingRequestHandlerContext } from '../types';
import { BaseParams, ReportingUser } from '../types';
import { checkParamsVersion, LevelLogger } from './';
import { Report } from './store';
export async function enqueueJob(
reporting: ReportingCore,
request: KibanaRequest,
context: ReportingRequestHandlerContext,
user: ReportingUser,
exportTypeId: string,
jobParams: BaseParams,
parentLogger: LevelLogger
): Promise<Report> {
const logger = parentLogger.clone(['createJob']);
const exportType = reporting.getExportTypesRegistry().getById(exportTypeId);
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!`);
}
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);
// 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: {
// telemetry fields
objectType: jobParams.objectType,
layout: jobParams.layout?.id,
isDeprecated: job.isDeprecated,
},
})
);
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}`
);
return report;
}

View file

@ -15,7 +15,7 @@ import {
ReportDocumentHead,
ReportSource,
} from '../../../common/types';
import { ReportTaskParams } from '../tasks';
import type { ReportTaskParams } from '../tasks';
export { ReportDocument };
export { ReportApiJSON, ReportSource };
@ -67,7 +67,7 @@ export class Report implements Partial<ReportSource & ReportDocumentHead> {
this.migration_version = MIGRATION_VERSION;
// see enqueue_job for all the fields that are expected to exist when adding a report
// see RequestHandler.enqueueJob for all the fields that are expected to exist when adding a report
if (opts.jobtype == null) {
throw new Error(`jobtype is expected!`);
}

View file

@ -8,18 +8,20 @@
import { KibanaRequest, KibanaResponseFactory } from 'kibana/server';
import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { ReportingCore } from '../..';
import { JobParamsPDF, TaskPayloadPDF } from '../../export_types/printable_pdf/types';
import { Report, ReportingStore } from '../../lib/store';
import { ReportApiJSON } from '../../lib/store/report';
import {
createMockConfigSchema,
createMockLevelLogger,
createMockReportingCore,
} from '../../test_helpers';
import { BaseParams, ReportingRequestHandlerContext, ReportingSetup } from '../../types';
import { 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' }),
jest.mock('../../lib/crypto', () => ({
cryptoFactory: () => ({
encrypt: () => `hello mock cypher text`,
}),
}));
@ -50,10 +52,25 @@ describe('Handle request to generate', () => {
let mockResponseFactory: ReturnType<typeof getMockResponseFactory>;
let requestHandler: RequestHandler;
const mockJobParams = {} as BaseParams;
const mockJobParams: JobParamsPDF = {
browserTimezone: 'UTC',
objectType: 'cool_object_type',
title: 'cool_title',
version: 'unknown',
layout: { id: 'preserve_layout' },
relativeUrls: [],
};
beforeEach(async () => {
reportingCore = await createMockReportingCore(createMockConfigSchema({}));
reportingCore.getStore = () =>
Promise.resolve(({
addReport: jest
.fn()
.mockImplementation(
(report) => new Report({ ...report, _index: '.reporting-foo-index-234' })
),
} as unknown) as ReportingStore);
mockRequest = getMockRequest();
mockResponseFactory = getMockResponseFactory();
@ -73,6 +90,64 @@ describe('Handle request to generate', () => {
);
});
describe('Enqueue Job', () => {
test('creates a report object to queue', async () => {
const report = await requestHandler.enqueueJob('printablePdf', mockJobParams);
const { _id, created_at: _created_at, payload, ...snapObj } = report;
expect(snapObj).toMatchInlineSnapshot(`
Object {
"_index": ".reporting-foo-index-234",
"_primary_term": undefined,
"_seq_no": undefined,
"attempts": 0,
"browser_type": undefined,
"completed_at": undefined,
"created_by": "testymcgee",
"jobtype": "printable_pdf",
"kibana_id": undefined,
"kibana_name": undefined,
"max_attempts": undefined,
"meta": Object {
"isDeprecated": false,
"layout": "preserve_layout",
"objectType": "cool_object_type",
},
"migration_version": "7.14.0",
"output": null,
"process_expiration": undefined,
"started_at": undefined,
"status": "pending",
"timeout": undefined,
}
`);
const { forceNow, ...snapPayload } = payload as TaskPayloadPDF;
expect(snapPayload).toMatchInlineSnapshot(`
Object {
"browserTimezone": "UTC",
"headers": "hello mock cypher text",
"isDeprecated": false,
"layout": Object {
"id": "preserve_layout",
},
"objectType": "cool_object_type",
"objects": Array [],
"spaceId": undefined,
"title": "cool_title",
"version": "unknown",
}
`);
});
test('provides a default kibana version field for older POST URLs', async () => {
((mockJobParams as unknown) as { version?: string }).version = undefined;
const report = await requestHandler.enqueueJob('printablePdf', mockJobParams);
const { _id, created_at: _created_at, ...snapObj } = report;
expect(snapObj.payload.version).toBe('7.14.0');
});
});
test('disallows invalid export type', async () => {
expect(await requestHandler.handleGenerateRequest('neanderthals', mockJobParams))
.toMatchInlineSnapshot(`
@ -95,15 +170,45 @@ describe('Handle request to generate', () => {
});
test('generates the download path', async () => {
expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(`
const response = ((await requestHandler.handleGenerateRequest(
'csv',
mockJobParams
)) as unknown) as { body: { job: ReportApiJSON } };
const { id, created_at: _created_at, ...snapObj } = response.body.job;
expect(snapObj).toMatchInlineSnapshot(`
Object {
"body": Object {
"job": "{\\"id\\":\\"id-of-this-test-report\\"}",
"path": "undefined/api/reporting/jobs/download/id-of-this-test-report",
"attempts": 0,
"browser_type": undefined,
"completed_at": undefined,
"created_by": "testymcgee",
"index": ".reporting-foo-index-234",
"jobtype": "csv",
"kibana_id": undefined,
"kibana_name": undefined,
"max_attempts": undefined,
"meta": Object {
"isDeprecated": true,
"layout": "preserve_layout",
"objectType": "cool_object_type",
},
"headers": Object {
"content-type": "application/json",
"migration_version": "7.14.0",
"output": Object {},
"payload": Object {
"browserTimezone": "UTC",
"indexPatternSavedObject": undefined,
"isDeprecated": true,
"layout": Object {
"id": "preserve_layout",
},
"objectType": "cool_object_type",
"relativeUrls": Array [],
"spaceId": undefined,
"title": "cool_title",
"version": "7.14.0",
},
"started_at": undefined,
"status": "pending",
"timeout": undefined,
}
`);
});

View file

@ -10,8 +10,8 @@ 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 { checkParamsVersion, cryptoFactory, LevelLogger } from '../../lib';
import { Report } from '../../lib/store';
import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types';
export const handleUnavailable = (res: KibanaResponseFactory) => {
@ -33,6 +33,75 @@ export class RequestHandler {
private logger: LevelLogger
) {}
private async encryptHeaders() {
const encryptionKey = this.reporting.getConfig().get('encryptionKey');
const crypto = cryptoFactory(encryptionKey);
return await crypto.encrypt(this.req.headers);
}
public async enqueueJob(exportTypeId: string, jobParams: BaseParams) {
const { reporting, logger, context, req: request, user } = this;
const exportType = reporting.getExportTypesRegistry().getById(exportTypeId);
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!`);
}
const [createJob, store] = await Promise.all([
exportType.createJobFnFactory(reporting, logger.clone([exportType.id])),
reporting.getStore(),
]);
if (!createJob) {
throw new Error(`Export type ${exportTypeId} is not an async job type!`);
}
// 1. ensure the incoming params have a version field
jobParams.version = checkParamsVersion(jobParams, logger);
// 2. encrypt request headers for the running report job to authenticate itself with Kibana
// 3. call the export type's createJobFn to create the job payload
const [headers, job] = await Promise.all([
this.encryptHeaders(),
createJob(jobParams, context),
]);
const payload = {
...job,
headers,
spaceId: reporting.getSpaceId(request, logger),
};
// 4. 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,
meta: {
// telemetry fields
objectType: jobParams.objectType,
layout: jobParams.layout?.id,
isDeprecated: job.isDeprecated,
},
})
);
logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`);
// 5. 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;
}
public async handleGenerateRequest(
exportTypeId: string,
jobParams: BaseParams | JobParamsPDFLegacy
@ -54,15 +123,7 @@ export class RequestHandler {
}
try {
const report = await enqueueJob(
this.reporting,
this.req,
this.context,
this.user,
exportTypeId,
jobParams,
this.logger
);
const report = await this.enqueueJob(exportTypeId, jobParams);
// return task manager's task information and the download URL
const downloadBaseUrl = getDownloadBaseUrl(this.reporting);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server';
import type { IRouter, RequestHandlerContext } from 'src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DataPluginStart } from 'src/plugins/data/server/plugin';
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server';
@ -62,9 +62,8 @@ export { BaseParams, BasePayload };
// default fn type for CreateJobFnFactory
export type CreateJobFn<JobParamsType = BaseParams, JobPayloadType = BasePayload> = (
jobParams: JobParamsType,
context: ReportingRequestHandlerContext,
request: KibanaRequest
) => Promise<JobPayloadType>;
context: ReportingRequestHandlerContext
) => Promise<Omit<JobPayloadType, 'headers' | 'spaceId'>>;
// default fn type for RunTaskFnFactory
export type RunTaskFn<TaskPayloadType = BasePayload> = (