From 4e3d32fc0883b916026616fe821da56757ab6e0b Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 1 Aug 2023 08:19:45 -0700 Subject: [PATCH] [Reporting] Separate internal and public API endpoints in Reporting (#162288) ## Summary Closes https://github.com/elastic/kibana/issues/160827 Closes https://github.com/elastic/kibana/issues/134517 Closes https://github.com/elastic/kibana/issues/149942 In this PR, the following API endpoints are moved into an internal namespace: | New endpoint path | Previous | |---|---| | `/internal/reporting/diagnose/browser` | `/api/reporting/diagnose/browser` | | `/internal/reporting/diagnose/screenshot` | `/api/reporting/diagnose/screenshot` | | `/internal/reporting/generate/immediate/csv_searchsource` | `/api/reporting/v1/generate/immediate/csv_searchsource` | | `/internal/reporting/generate/{exportTypeId}` | `/api/reporting/generate/{exportTypeId}` | | `/internal/reporting/jobs/count` | `/api/reporting/jobs/count` | | `/internal/reporting/jobs/delete/{jobId}` | `/api/reporting/jobs/delete/{jobId}` | | `/internal/reporting/jobs/info/{jobId}` | `/api/reporting/jobs/info/{jobId}` | | `/internal/reporting/jobs/list` | `/api/reporting/jobs/list` | Support for the public APIs continues: | Public endpoint path | |---| | `/api/reporting/generate/{exportTypeId}` | | `/api/reporting/jobs/delete/{jobId}` | | `/api/reporting/jobs/download/{jobId}` | ## Other changes 1. Set access options on the routes 2. Removed API Counter functional tests, which were skipped to begin with. 3. Replaced functional tests with Jest integration tests. 4. Consolidated code in the generation routes by creating the `getJobParams` method of the `RequestHandler` class. 5. Added a new test for `getJobParams` 6. Consolidated code in the job management routes 7. Added new code for shared helpers in job management routes 8. Reorganized libs used for route handlers: ``` routes/lib/request_handler.ts => routes/common/generate/request_handler.ts routes/lib/job_management_pre_routing.ts => routes/common/jobs/job_management_pre_routing.ts routes/lib/jobs_query.ts => routes/common/jobs/jobs_query.ts routes/lib/get_document_payload.ts => routes/common/jobs/get_document_payload.ts routes/lib/get_counter.ts => routes/common/get_counter.ts routes/lib/authorized_user_pre_routing.ts => routes/common/authorized_user_pre_routing.ts routes/lib/get_user.ts => routes/common/get_user.ts ``` ## Release Note Updated API endpoint paths for Reporting to clarify which routes are public and which are not. Make sure that any custom script or application that uses Reporting endpoints only uses the public endpoints. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../reporting/common/constants/index.ts | 22 +- .../reporting/common/constants/routes.ts | 48 +++ .../public/lib/reporting_api_client/hooks.ts | 9 +- .../reporting_api_client.test.ts | 26 +- .../reporting_api_client.ts | 72 +++-- .../reporting_panel_content.tsx | 4 +- ...igrate_existing_indices_ilm_policy.test.ts | 2 +- .../migrate_existing_indices_ilm_policy.ts | 4 +- .../authorized_user_pre_routing.test.ts | 0 .../authorized_user_pre_routing.ts | 0 .../{management => common/generate}/index.ts | 2 +- .../generate}/request_handler.test.ts | 148 +++++---- .../generate}/request_handler.ts | 101 ++++-- .../routes/{lib => common}/get_counter.ts | 0 .../server/routes/{lib => common}/get_user.ts | 0 .../routes/{generate => common}/index.ts | 4 +- .../jobs}/get_document_payload.test.ts | 12 +- .../jobs}/get_document_payload.ts | 10 +- .../routes/common/jobs/get_job_routes.ts | 101 ++++++ .../routes/{lib => common/jobs}/index.ts | 7 +- .../jobs}/job_management_pre_routing.test.ts | 8 +- .../jobs}/job_management_pre_routing.ts | 11 +- .../{lib => common/jobs}/jobs_query.test.ts | 7 +- .../routes/{lib => common/jobs}/jobs_query.ts | 14 +- .../generate/generate_from_jobparams.ts | 111 ------- .../plugins/reporting/server/routes/index.ts | 20 +- .../deprecations/deprecations.ts | 55 ++-- .../integration_tests/deprecations.test.ts | 38 ++- .../{ => internal}/diagnostic/browser.ts | 15 +- .../routes/{ => internal}/diagnostic/index.ts | 2 +- .../integration_tests/browser.test.ts | 48 ++- .../integration_tests/screenshot.test.ts | 26 +- .../{ => internal}/diagnostic/screenshot.ts | 18 +- .../generate/csv_searchsource_immediate.ts | 23 +- .../generate/generate_from_jobparams.ts | 56 ++++ .../generation_from_jobparams.test.ts | 262 ++++++++++++++++ .../management/integration_tests/jobs.test.ts | 182 ++++++++--- .../routes/{ => internal}/management/jobs.ts | 131 +++----- .../routes/public/generate_from_jobparams.ts | 71 +++++ .../generation_from_jobparams.test.ts | 80 +++-- .../public/integration_tests/jobs.test.ts | 292 ++++++++++++++++++ .../reporting/server/routes/public/jobs.ts | 53 ++++ x-pack/plugins/reporting/tsconfig.json | 1 + .../download_csv_dashboard.ts | 2 +- .../reporting_and_security/error_codes.ts | 3 +- .../ilm_migration_apis.ts | 5 +- .../usage/api_counters.ts | 263 ---------------- .../reporting_and_security/usage/index.ts | 1 - .../job_apis_csv.ts | 11 +- .../services/scenarios.ts | 19 +- .../services/usage.ts | 10 +- 51 files changed, 1553 insertions(+), 857 deletions(-) create mode 100644 x-pack/plugins/reporting/common/constants/routes.ts rename x-pack/plugins/reporting/server/routes/{lib => common}/authorized_user_pre_routing.test.ts (100%) rename x-pack/plugins/reporting/server/routes/{lib => common}/authorized_user_pre_routing.ts (100%) rename x-pack/plugins/reporting/server/routes/{management => common/generate}/index.ts (78%) rename x-pack/plugins/reporting/server/routes/{lib => common/generate}/request_handler.test.ts (66%) rename x-pack/plugins/reporting/server/routes/{lib => common/generate}/request_handler.ts (67%) rename x-pack/plugins/reporting/server/routes/{lib => common}/get_counter.ts (100%) rename x-pack/plugins/reporting/server/routes/{lib => common}/get_user.ts (100%) rename x-pack/plugins/reporting/server/routes/{generate => common}/index.ts (51%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/get_document_payload.test.ts (96%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/get_document_payload.ts (93%) create mode 100644 x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/index.ts (61%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/job_management_pre_routing.test.ts (97%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/job_management_pre_routing.ts (88%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/jobs_query.test.ts (99%) rename x-pack/plugins/reporting/server/routes/{lib => common/jobs}/jobs_query.ts (93%) delete mode 100644 x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts rename x-pack/plugins/reporting/server/routes/{ => internal}/deprecations/deprecations.ts (79%) rename x-pack/plugins/reporting/server/routes/{ => internal}/deprecations/integration_tests/deprecations.test.ts (67%) rename x-pack/plugins/reporting/server/routes/{ => internal}/diagnostic/browser.ts (90%) rename x-pack/plugins/reporting/server/routes/{ => internal}/diagnostic/index.ts (93%) rename x-pack/plugins/reporting/server/routes/{ => internal}/diagnostic/integration_tests/browser.test.ts (76%) rename x-pack/plugins/reporting/server/routes/{ => internal}/diagnostic/integration_tests/screenshot.test.ts (84%) rename x-pack/plugins/reporting/server/routes/{ => internal}/diagnostic/screenshot.ts (86%) rename x-pack/plugins/reporting/server/routes/{ => internal}/generate/csv_searchsource_immediate.ts (83%) create mode 100644 x-pack/plugins/reporting/server/routes/internal/generate/generate_from_jobparams.ts create mode 100644 x-pack/plugins/reporting/server/routes/internal/generate/integration_tests/generation_from_jobparams.test.ts rename x-pack/plugins/reporting/server/routes/{ => internal}/management/integration_tests/jobs.test.ts (65%) rename x-pack/plugins/reporting/server/routes/{ => internal}/management/jobs.ts (53%) create mode 100644 x-pack/plugins/reporting/server/routes/public/generate_from_jobparams.ts rename x-pack/plugins/reporting/server/routes/{generate => public}/integration_tests/generation_from_jobparams.test.ts (73%) create mode 100644 x-pack/plugins/reporting/server/routes/public/integration_tests/jobs.test.ts create mode 100644 x-pack/plugins/reporting/server/routes/public/jobs.ts delete mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts diff --git a/x-pack/plugins/reporting/common/constants/index.ts b/x-pack/plugins/reporting/common/constants/index.ts index 85ce65eb4691a..f02ea766da66b 100644 --- a/x-pack/plugins/reporting/common/constants/index.ts +++ b/x-pack/plugins/reporting/common/constants/index.ts @@ -6,8 +6,8 @@ */ import { CONTENT_TYPE_CSV } from '@kbn/generate-csv/src/constants'; -import * as reportTypes from './report_types'; import * as jobTypes from './job_types'; +import * as reportTypes from './report_types'; const { PDF_JOB_TYPE, PDF_JOB_TYPE_V2, PNG_JOB_TYPE, PNG_JOB_TYPE_V2 } = jobTypes; @@ -28,10 +28,6 @@ export const ALLOWED_JOB_CONTENT_TYPES = [ 'text/plain', ]; -// Re-export type definitions here for convenience. -export * from './report_types'; -export * from './job_types'; - type ReportTypeDeclaration = typeof reportTypes; export type ReportTypes = ReportTypeDeclaration[keyof ReportTypeDeclaration]; @@ -62,16 +58,6 @@ export const LICENSE_TYPE_GOLD = 'gold' as const; export const LICENSE_TYPE_PLATINUM = 'platinum' as const; export const LICENSE_TYPE_ENTERPRISE = 'enterprise' as const; -// Routes -export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu -export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_LIST_URL = `${API_BASE_URL}/jobs`; -export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; - -export const API_GET_ILM_POLICY_STATUS = `${API_BASE_URL}/ilm_policy_status`; -export const API_MIGRATE_ILM_POLICY_URL = `${API_BASE_URL}/deprecations/migrate_ilm_policy`; -export const API_BASE_URL_V1 = '/api/reporting/v1'; // - export const ILM_POLICY_NAME = 'kibana-reporting'; // Usage counter types @@ -111,6 +97,6 @@ export const REPORT_TABLE_ROW_ID = 'reportJobRow'; // intended version is 7.14.0 export const UNVERSIONED_VERSION = '7.14.0'; -// hacky endpoint: download CSV without queueing a report -// FIXME: find a way to make these endpoints "generic" instead of hardcoded, as are the queued report export types -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; +export * from './job_types'; +export * from './report_types'; +export * from './routes'; diff --git a/x-pack/plugins/reporting/common/constants/routes.ts b/x-pack/plugins/reporting/common/constants/routes.ts new file mode 100644 index 0000000000000..864b9c0653e13 --- /dev/null +++ b/x-pack/plugins/reporting/common/constants/routes.ts @@ -0,0 +1,48 @@ +/* + * 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. + */ + +const prefixInternalPath = '/internal/reporting'; +export const INTERNAL_ROUTES = { + MIGRATE: { + MIGRATE_ILM_POLICY: prefixInternalPath + '/deprecations/migrate_ilm_policy', + GET_ILM_POLICY_STATUS: prefixInternalPath + '/ilm_policy_status', + }, + DIAGNOSE: { + BROWSER: prefixInternalPath + '/diagnose/browser', + SCREENSHOT: prefixInternalPath + '/diagnose/screenshot', + }, + JOBS: { + COUNT: prefixInternalPath + '/jobs/count', + LIST: prefixInternalPath + '/jobs/list', + INFO_PREFIX: prefixInternalPath + '/jobs/info', // docId is added to the final path + DELETE_PREFIX: prefixInternalPath + '/jobs/delete', // docId is added to the final path + DOWNLOAD_PREFIX: prefixInternalPath + '/jobs/download', // docId is added to the final path + }, + DOWNLOAD_CSV: prefixInternalPath + '/generate/immediate/csv_searchsource', + GENERATE_PREFIX: prefixInternalPath + '/generate', // exportTypeId is added to the final path +}; + +const prefixPublicPath = '/api/reporting'; +export const PUBLIC_ROUTES = { + /** + * Public endpoint for POST URL strings and automated report generation + * exportTypeId is added to the final path + */ + GENERATE_PREFIX: prefixPublicPath + `/generate`, + JOBS: { + /** + * Public endpoint used by Watcher and automated report downloads + * jobId is added to the final path + */ + DOWNLOAD_PREFIX: prefixPublicPath + `/jobs/download`, + /** + * Public endpoint potentially used to delete a report after download in automation + * jobId is added to the final path + */ + DELETE_PREFIX: prefixPublicPath + `/jobs/delete`, + }, +}; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts index 0b697b333dddd..8410ec8f82019 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/hooks.ts @@ -5,16 +5,13 @@ * 2.0. */ -import { useRequest, UseRequestResponse } from '../../shared_imports'; +import { INTERNAL_ROUTES } from '../../../common/constants'; import { IlmPolicyStatusResponse } from '../../../common/types'; - -import { API_GET_ILM_POLICY_STATUS } from '../../../common/constants'; - -import { useKibana } from '../../shared_imports'; +import { useKibana, useRequest, UseRequestResponse } from '../../shared_imports'; export const useCheckIlmPolicyStatus = (): UseRequestResponse => { const { services: { http }, } = useKibana(); - return useRequest(http, { path: API_GET_ILM_POLICY_STATUS, method: 'get' }); + return useRequest(http, { path: INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS, method: 'get' }); }; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts index 3a0086e642f56..38e95e9d75323 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.test.ts @@ -25,9 +25,9 @@ describe('ReportingAPIClient', () => { }); describe('getReportURL', () => { - it('should generate report URL', () => { + it('should generate the internal report download URL', () => { expect(apiClient.getReportURL('123')).toMatchInlineSnapshot( - `"/base/path/api/reporting/jobs/download/123"` + `"/base/path/internal/reporting/jobs/download/123"` ); }); }); @@ -52,10 +52,7 @@ describe('ReportingAPIClient', () => { it('should send a delete request', async () => { await apiClient.deleteReport('123'); - expect(httpClient.delete).toHaveBeenCalledWith( - expect.stringContaining('/delete/123'), - expect.any(Object) - ); + expect(httpClient.delete).toHaveBeenCalledWith(expect.stringContaining('/delete/123')); }); }); @@ -112,10 +109,7 @@ describe('ReportingAPIClient', () => { it('should send a get request', async () => { await apiClient.getInfo('123'); - expect(httpClient.get).toHaveBeenCalledWith( - expect.stringContaining('/info/123'), - expect.any(Object) - ); + expect(httpClient.get).toHaveBeenCalledWith(expect.stringContaining('/info/123')); }); it('should return a job instance', async () => { @@ -174,7 +168,7 @@ describe('ReportingAPIClient', () => { describe('getReportingJobPath', () => { it('should generate a job path', () => { expect( - apiClient.getReportingJobPath('pdf', { + apiClient.getReportingPublicJobPath('pdf', { browserTimezone: 'UTC', objectType: 'something', title: 'some title', @@ -293,10 +287,7 @@ describe('ReportingAPIClient', () => { it('should send a post request', async () => { await apiClient.verifyBrowser(); - expect(httpClient.post).toHaveBeenCalledWith( - expect.stringContaining('/diagnose/browser'), - expect.any(Object) - ); + expect(httpClient.post).toHaveBeenCalledWith(expect.stringContaining('/diagnose/browser')); }); }); @@ -304,10 +295,7 @@ describe('ReportingAPIClient', () => { it('should send a post request', async () => { await apiClient.verifyScreenCapture(); - expect(httpClient.post).toHaveBeenCalledWith( - expect.stringContaining('/diagnose/screenshot'), - expect.any(Object) - ); + expect(httpClient.post).toHaveBeenCalledWith(expect.stringContaining('/diagnose/screenshot')); }); }); diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 91abac38f4d66..2681789745751 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -4,20 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { HttpFetchQuery } from '@kbn/core/public'; +import { HttpSetup, IUiSettingsClient } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import rison from '@kbn/rison'; import moment from 'moment'; import { stringify } from 'query-string'; -import rison from '@kbn/rison'; -import type { HttpFetchQuery } from '@kbn/core/public'; -import { HttpSetup, IUiSettingsClient } from '@kbn/core/public'; import { buildKibanaPath } from '../../../common/build_kibana_path'; import { - API_BASE_GENERATE, - API_BASE_URL, - API_GENERATE_IMMEDIATE, - API_LIST_URL, - API_MIGRATE_ILM_POLICY_URL, getRedirectAppPath, + INTERNAL_ROUTES, + PUBLIC_ROUTES, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; import { @@ -46,7 +43,7 @@ export interface DiagnoseResponse { interface IReportingAPI { // Helpers getReportURL(jobId: string): string; - getReportingJobPath(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL + getReportingPublicJobPath(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL createReportingJob(exportType: string, jobParams: BaseParams & T): Promise; // Sends a request to queue a job, with the job params in the POST body getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params @@ -96,9 +93,13 @@ export class ReportingAPIClient implements IReportingAPI { return href; } + /** + * Get the internal URL + */ public getReportURL(jobId: string) { - const apiBaseUrl = this.http.basePath.prepend(API_LIST_URL); - const downloadLink = `${apiBaseUrl}/download/${jobId}`; + const downloadLink = this.http.basePath.prepend( + `${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/${jobId}` + ); return downloadLink; } @@ -110,9 +111,7 @@ export class ReportingAPIClient implements IReportingAPI { } public async deleteReport(jobId: string) { - return await this.http.delete(`${API_LIST_URL}/delete/${jobId}`, { - asSystemRequest: true, - }); + return await this.http.delete(`${INTERNAL_ROUTES.JOBS.DELETE_PREFIX}/${jobId}`); } public async list(page = 0, jobIds: string[] = []) { @@ -122,7 +121,7 @@ export class ReportingAPIClient implements IReportingAPI { query.ids = jobIds.slice(0, 10).join(','); } - const jobQueueEntries: ReportApiJSON[] = await this.http.get(`${API_LIST_URL}/list`, { + const jobQueueEntries: ReportApiJSON[] = await this.http.get(INTERNAL_ROUTES.JOBS.LIST, { query, asSystemRequest: true, }); @@ -131,7 +130,7 @@ export class ReportingAPIClient implements IReportingAPI { } public async total() { - return await this.http.get(`${API_LIST_URL}/count`, { + return await this.http.get(INTERNAL_ROUTES.JOBS.COUNT, { asSystemRequest: true, }); } @@ -151,47 +150,50 @@ export class ReportingAPIClient implements IReportingAPI { } public async getInfo(jobId: string) { - const report: ReportApiJSON = await this.http.get(`${API_LIST_URL}/info/${jobId}`, { - asSystemRequest: true, - }); + const report: ReportApiJSON = await this.http.get( + `${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/${jobId}` + ); return new Job(report); } public async findForJobIds(jobIds: JobId[]) { - const reports: ReportApiJSON[] = await this.http.fetch(`${API_LIST_URL}/list`, { + const reports: ReportApiJSON[] = await this.http.fetch(INTERNAL_ROUTES.JOBS.LIST, { query: { page: 0, ids: jobIds.join(',') }, method: 'GET', }); return reports.map((report) => new Job(report)); } - public getReportingJobPath(exportType: string, jobParams: BaseParams) { + /** + * Returns a string for the public API endpoint used to automate the generation of reports + * This string must be shown when the user selects the option to view/copy the POST URL + */ + public getReportingPublicJobPath(exportType: string, jobParams: BaseParams) { const params = stringify({ jobParams: rison.encode(jobParams), }); - return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`; + return `${this.http.basePath.prepend(PUBLIC_ROUTES.GENERATE_PREFIX)}/${exportType}?${params}`; } + /** + * Calls the internal API to generate a report job on-demand + */ public async createReportingJob(exportType: string, jobParams: BaseParams) { const jobParamsRison = rison.encode(jobParams); const resp: { job: ReportApiJSON } = await this.http.post( - `${API_BASE_GENERATE}/${exportType}`, + `${INTERNAL_ROUTES.GENERATE_PREFIX}/${exportType}`, { method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), + body: JSON.stringify({ jobParams: jobParamsRison }), } ); - add(resp.job.id); - return new Job(resp.job); } public async createImmediateReport(baseParams: BaseParams) { const { objectType: _objectType, ...params } = baseParams; // objectType is not needed for immediate download api - return this.http.post(`${API_GENERATE_IMMEDIATE}`, { + return this.http.post(INTERNAL_ROUTES.DOWNLOAD_CSV, { asResponse: true, body: JSON.stringify(params), }); @@ -217,23 +219,19 @@ export class ReportingAPIClient implements IReportingAPI { this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); public getDownloadLink: DownloadReportFn = (jobId: JobId) => - this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`); + this.http.basePath.prepend(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/${jobId}`); public getServerBasePath = () => this.http.basePath.serverBasePath; public verifyBrowser() { - return this.http.post(`${API_BASE_URL}/diagnose/browser`, { - asSystemRequest: true, - }); + return this.http.post(INTERNAL_ROUTES.DIAGNOSE.BROWSER); } public verifyScreenCapture() { - return this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { - asSystemRequest: true, - }); + return this.http.post(INTERNAL_ROUTES.DIAGNOSE.SCREENSHOT); } public migrateReportingIndicesIlmPolicy() { - return this.http.put(`${API_MIGRATE_ILM_POLICY_URL}`); + return this.http.put(INTERNAL_ROUTES.MIGRATE.MIGRATE_ILM_POLICY); } } diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx index 6ac0e374b3919..2252694fcaa97 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content/reporting_panel_content.tsx @@ -84,11 +84,11 @@ class ReportingPanelContentUi extends Component { } private getAbsoluteReportGenerationUrl = (props: Props) => { - const relativePath = this.props.apiClient.getReportingJobPath( + const relativePath = this.props.apiClient.getReportingPublicJobPath( props.reportType, this.props.apiClient.getDecoratedJobParams(this.props.getJobParams(true)) ); - return url.resolve(window.location.href, relativePath); // FIXME: '(from: string, to: string): string' is deprecated + return url.resolve(window.location.href, relativePath); }; public componentDidUpdate(_prevProps: Props, prevState: State) { diff --git a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts index 551bf76465642..5dbbb9e9570c1 100644 --- a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts +++ b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.test.ts @@ -52,7 +52,7 @@ describe("Migrate existing indices' ILM policy deprecations", () => { "correctiveActions": Object { "api": Object { "method": "PUT", - "path": "/api/reporting/deprecations/migrate_ilm_policy", + "path": "/internal/reporting/deprecations/migrate_ilm_policy", }, "manualSteps": Array [ "Update all reporting indices to use the \\"kibana-reporting\\" policy using the index settings API.", diff --git a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts index 7ce888b57727f..6f0ddba9ce296 100644 --- a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts +++ b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { DeprecationsDetails, GetDeprecationsContext } from '@kbn/core/server'; -import { API_MIGRATE_ILM_POLICY_URL, ILM_POLICY_NAME } from '../../common/constants'; +import { INTERNAL_ROUTES, ILM_POLICY_NAME } from '../../common/constants'; import { ReportingCore } from '../core'; import { deprecations } from '../lib/deprecations'; @@ -54,7 +54,7 @@ export const getDeprecationsInfo = async ( ], api: { method: 'PUT', - path: API_MIGRATE_ILM_POLICY_URL, + path: INTERNAL_ROUTES.MIGRATE.MIGRATE_ILM_POLICY, }, }, }, diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/common/authorized_user_pre_routing.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts rename to x-pack/plugins/reporting/server/routes/common/authorized_user_pre_routing.test.ts diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/common/authorized_user_pre_routing.ts similarity index 100% rename from x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts rename to x-pack/plugins/reporting/server/routes/common/authorized_user_pre_routing.ts diff --git a/x-pack/plugins/reporting/server/routes/management/index.ts b/x-pack/plugins/reporting/server/routes/common/generate/index.ts similarity index 78% rename from x-pack/plugins/reporting/server/routes/management/index.ts rename to x-pack/plugins/reporting/server/routes/common/generate/index.ts index 0c31b2b0d6a0c..a16ddf1204b8f 100644 --- a/x-pack/plugins/reporting/server/routes/management/index.ts +++ b/x-pack/plugins/reporting/server/routes/common/generate/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { registerJobInfoRoutes } from './jobs'; +export { handleUnavailable, RequestHandler } from './request_handler'; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/common/generate/request_handler.test.ts similarity index 66% rename from x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts rename to x-pack/plugins/reporting/server/routes/common/generate/request_handler.test.ts index eeb0aa1ada393..5d6ba3f20a004 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/common/generate/request_handler.test.ts @@ -6,17 +6,18 @@ */ import { KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; +import rison from '@kbn/rison'; import { coreMock, httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { TaskPayloadPDFV2 } from '../../../common/types/export_types/printable_pdf_v2'; -import { ReportingCore } from '../..'; -import { JobParamsPDFDeprecated } from '../../export_types/printable_pdf/types'; -import { Report, ReportingStore } from '../../lib/store'; -import { ReportApiJSON } from '../../lib/store/report'; -import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; -import { ReportingRequestHandlerContext, ReportingSetup } from '../../types'; +import { ReportingCore } from '../../..'; +import { TaskPayloadPDFV2 } from '../../../../common/types/export_types/printable_pdf_v2'; +import { JobParamsPDFDeprecated } from '../../../export_types/printable_pdf/types'; +import { Report, ReportingStore } from '../../../lib/store'; +import { ReportApiJSON } from '../../../lib/store/report'; +import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; +import { ReportingRequestHandlerContext, ReportingSetup } from '../../../types'; import { RequestHandler } from './request_handler'; -jest.mock('../../lib/crypto', () => ({ +jest.mock('../../../lib/crypto', () => ({ cryptoFactory: () => ({ encrypt: () => `hello mock cypher text`, }), @@ -31,13 +32,18 @@ const getMockRequest = () => ({ url: { port: '5601', search: '', pathname: '/foo' }, route: { path: '/foo', options: {} }, - } as KibanaRequest); + } as KibanaRequest< + { exportType: string }, + { jobParams: string } | null, + { jobParams: string } | null + >); const getMockResponseFactory = () => ({ ...httpServerMock.createResponseFactory(), forbidden: (obj: unknown) => obj, unauthorized: (obj: unknown) => obj, + customError: (err: unknown) => err, } as unknown as KibanaResponseFactory); const mockLogger = loggingSystemMock.createLogger(); @@ -50,11 +56,6 @@ const mockJobParams: JobParamsPDFDeprecated = { relativeUrls: [], }; -const mockCounters = { - usageCounter: jest.fn(), - errorCounter: jest.fn(), -}; - describe('Handle request to generate', () => { let reportingCore: ReportingCore; let mockContext: ReturnType; @@ -87,6 +88,7 @@ describe('Handle request to generate', () => { reportingCore, { username: 'testymcgee' }, mockContext, + '/api/reporting/test/generate/pdf', mockRequest, mockResponseFactory, mockLogger @@ -155,64 +157,101 @@ describe('Handle request to generate', () => { }); }); - test('disallows invalid export type', async () => { - expect(await requestHandler.handleGenerateRequest('neanderthals', mockJobParams, mockCounters)) - .toMatchInlineSnapshot(` + describe('getJobParams', () => { + test('parse jobParams from query string', () => { + // @ts-ignore query is a read-only property + mockRequest.query = { jobParams: rison.encode(mockJobParams) }; + expect(requestHandler.getJobParams()).toEqual(mockJobParams); + }); + + test('parse jobParams from body', () => { + // @ts-ignore body is a read-only property + mockRequest.body = { jobParams: rison.encode(mockJobParams) }; + expect(requestHandler.getJobParams()).toEqual(mockJobParams); + }); + + test('handles missing job params', () => { + try { + requestHandler.getJobParams(); + } catch (err) { + expect(err.statusCode).toBe(400); + } + }); + + test('handles null job params', () => { + try { + // @ts-ignore body is a read-only property + mockRequest.body = { jobParams: rison.encode(null) }; + requestHandler.getJobParams(); + } catch (err) { + expect(err.statusCode).toBe(400); + } + }); + + test('handles invalid rison', () => { + // @ts-ignore body is a read-only property + mockRequest.body = { jobParams: mockJobParams }; + try { + requestHandler.getJobParams(); + } catch (err) { + expect(err.statusCode).toBe(400); + } + }); + }); + + describe('handleGenerateRequest', () => { + 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_searchsource: { + enableLinks: false, + message: `seeing this means the license isn't supported`, + }, + })); - test('disallows unsupporting license', async () => { - (reportingCore.getLicenseInfo as jest.Mock) = jest.fn(() => ({ - csv_searchsource: { - enableLinks: false, - message: `seeing this means the license isn't supported`, - }, - })); - - expect( - await requestHandler.handleGenerateRequest('csv_searchsource', mockJobParams, mockCounters) - ).toMatchInlineSnapshot(` + expect(await requestHandler.handleGenerateRequest('csv_searchsource', mockJobParams)) + .toMatchInlineSnapshot(` Object { "body": "seeing this means the license isn't supported", } `); - }); + }); - test('disallows invalid browser timezone', async () => { - (reportingCore.getLicenseInfo as jest.Mock) = jest.fn(() => ({ - csv_searchsource: { - enableLinks: false, - message: `seeing this means the license isn't supported`, - }, - })); + test('disallows invalid browser timezone', async () => { + (reportingCore.getLicenseInfo as jest.Mock) = jest.fn(() => ({ + csv_searchsource: { + enableLinks: false, + message: `seeing this means the license isn't supported`, + }, + })); - expect( - await requestHandler.handleGenerateRequest( - 'csv_searchsource', - { + expect( + await requestHandler.handleGenerateRequest('csv_searchsource', { ...mockJobParams, browserTimezone: 'America/Amsterdam', - }, - mockCounters - ) - ).toMatchInlineSnapshot(` + }) + ).toMatchInlineSnapshot(` Object { "body": "seeing this means the license isn't supported", } `); - }); + }); - test('generates the download path', async () => { - const response = (await requestHandler.handleGenerateRequest( - 'csv_searchsource', - mockJobParams, - mockCounters - )) as unknown as { body: { job: ReportApiJSON } }; - const { id, created_at: _created_at, ...snapObj } = response.body.job; - expect(snapObj).toMatchInlineSnapshot(` + test('generates the download path', async () => { + const response = (await requestHandler.handleGenerateRequest( + 'csv_searchsource', + mockJobParams + )) as unknown as { body: { job: ReportApiJSON } }; + const { id, created_at: _created_at, ...snapObj } = response.body.job; + expect(snapObj).toMatchInlineSnapshot(` Object { "attempts": 0, "completed_at": undefined, @@ -248,5 +287,6 @@ describe('Handle request to generate', () => { "timeout": undefined, } `); + }); }); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/common/generate/request_handler.ts similarity index 67% rename from x-pack/plugins/reporting/server/routes/lib/request_handler.ts rename to x-pack/plugins/reporting/server/routes/common/generate/request_handler.ts index 0811eae3be7ea..25b7feff925d7 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/common/generate/request_handler.ts @@ -5,24 +5,27 @@ * 2.0. */ -import moment from 'moment'; import Boom from '@hapi/boom'; -import { i18n } from '@kbn/i18n'; +import { schema, TypeOf } from '@kbn/config-schema'; import type { KibanaRequest, KibanaResponseFactory, Logger } from '@kbn/core/server'; -import type { ReportingCore } from '../..'; -import { API_BASE_URL } from '../../../common/constants'; -import { checkParamsVersion, cryptoFactory } from '../../lib'; -import { Report } from '../../lib/store'; -import type { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; -import { Counters } from './get_counter'; +import { i18n } from '@kbn/i18n'; +import rison from '@kbn/rison'; +import moment from 'moment'; +import { Counters, getCounters } from '..'; +import type { ReportingCore } from '../../..'; +import { PUBLIC_ROUTES } from '../../../../common/constants'; +import { checkParamsVersion, cryptoFactory } from '../../../lib'; +import { Report } from '../../../lib/store'; +import type { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../../types'; export const handleUnavailable = (res: KibanaResponseFactory) => { return res.custom({ statusCode: 503, body: 'Not Available' }); }; -const getDownloadBaseUrl = (reporting: ReportingCore) => { - const { basePath } = reporting.getServerInfo(); - return basePath + `${API_BASE_URL}/jobs/download`; +const validation = { + params: schema.object({ exportType: schema.string({ minLength: 2 }) }), + body: schema.nullable(schema.object({ jobParams: schema.maybe(schema.string()) })), + query: schema.nullable(schema.object({ jobParams: schema.string({ defaultValue: '' }) })), }; /** @@ -33,7 +36,12 @@ export class RequestHandler { private reporting: ReportingCore, private user: ReportingUser, private context: ReportingRequestHandlerContext, - private req: KibanaRequest, + private path: string, + private req: KibanaRequest< + TypeOf, + TypeOf, + TypeOf + >, private res: KibanaResponseFactory, private logger: Logger ) {} @@ -106,11 +114,64 @@ export class RequestHandler { return report; } - public async handleGenerateRequest( - exportTypeId: string, - jobParams: BaseParams, - counters: Counters - ) { + public getJobParams(): BaseParams { + let jobParamsRison: null | string = null; + const req = this.req; + const res = this.res; + + if (req.body) { + const { jobParams: jobParamsPayload } = req.body; + jobParamsRison = jobParamsPayload ? jobParamsPayload : null; + } else if (req.query?.jobParams) { + const { jobParams: queryJobParams } = req.query; + if (queryJobParams) { + jobParamsRison = queryJobParams; + } else { + jobParamsRison = null; + } + } + + if (!jobParamsRison) { + throw res.customError({ + statusCode: 400, + body: 'A jobParams RISON string is required in the querystring or POST body', + }); + } + + let jobParams; + + try { + jobParams = rison.decode(jobParamsRison) as BaseParams | null; + if (!jobParams) { + throw res.customError({ + statusCode: 400, + body: 'Missing jobParams!', + }); + } + } catch (err) { + throw res.customError({ + statusCode: 400, + body: `invalid rison: ${jobParamsRison}`, + }); + } + + return jobParams; + } + + public static getValidation() { + return validation; + } + + public async handleGenerateRequest(exportTypeId: string, jobParams: BaseParams) { + const req = this.req; + const reporting = this.reporting; + + const counters = getCounters( + req.route.method, + this.path.replace(/{exportType}/, exportTypeId), + reporting.getUsageCounter() + ); + // ensure the async dependencies are loaded if (!this.context.reporting) { return handleUnavailable(this.res); @@ -136,14 +197,16 @@ export class RequestHandler { let report: Report | undefined; try { report = await this.enqueueJob(exportTypeId, jobParams); + const { basePath } = this.reporting.getServerInfo(); + const publicDownloadPath = basePath + PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX; + // return task manager's task information and the download URL - const downloadBaseUrl = getDownloadBaseUrl(this.reporting); counters.usageCounter(); return this.res.ok({ headers: { 'content-type': 'application/json' }, body: { - path: `${downloadBaseUrl}/${report._id}`, + path: `${publicDownloadPath}/${report._id}`, job: report.toApiJSON(), }, }); diff --git a/x-pack/plugins/reporting/server/routes/lib/get_counter.ts b/x-pack/plugins/reporting/server/routes/common/get_counter.ts similarity index 100% rename from x-pack/plugins/reporting/server/routes/lib/get_counter.ts rename to x-pack/plugins/reporting/server/routes/common/get_counter.ts diff --git a/x-pack/plugins/reporting/server/routes/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/common/get_user.ts similarity index 100% rename from x-pack/plugins/reporting/server/routes/lib/get_user.ts rename to x-pack/plugins/reporting/server/routes/common/get_user.ts diff --git a/x-pack/plugins/reporting/server/routes/generate/index.ts b/x-pack/plugins/reporting/server/routes/common/index.ts similarity index 51% rename from x-pack/plugins/reporting/server/routes/generate/index.ts rename to x-pack/plugins/reporting/server/routes/common/index.ts index 210abee3e6606..cd5341a3ae143 100644 --- a/x-pack/plugins/reporting/server/routes/generate/index.ts +++ b/x-pack/plugins/reporting/server/routes/common/index.ts @@ -5,5 +5,5 @@ * 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 { authorizedUserPreRouting } from './authorized_user_pre_routing'; +export { type Counters, getCounters } from './get_counter'; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts b/x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.test.ts similarity index 96% rename from x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.test.ts index b8091e09027a2..8c308de4544a1 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.test.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.test.ts @@ -6,14 +6,14 @@ */ import { Readable } from 'stream'; -import { CSV_JOB_TYPE, PDF_JOB_TYPE, PDF_JOB_TYPE_V2 } from '../../../common/constants'; -import { ReportApiJSON } from '../../../common/types'; -import { ContentStream, getContentStream, statuses } from '../../lib'; -import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; -import { jobsQueryFactory } from './jobs_query'; +import { CSV_JOB_TYPE, PDF_JOB_TYPE, PDF_JOB_TYPE_V2 } from '../../../../common/constants'; +import { ReportApiJSON } from '../../../../common/types'; +import { ContentStream, getContentStream, statuses } from '../../../lib'; +import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { getDocumentPayloadFactory } from './get_document_payload'; +import { jobsQueryFactory } from './jobs_query'; -jest.mock('../../lib/content_stream'); +jest.mock('../../../lib/content_stream'); jest.mock('./jobs_query'); describe('getDocumentPayload', () => { diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.ts similarity index 93% rename from x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.ts index 158d42b6e94e3..92fac37bb26f2 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/get_document_payload.ts @@ -7,11 +7,11 @@ import { ResponseHeaders } from '@kbn/core-http-server'; import { Stream } from 'stream'; -import { ReportingCore } from '../..'; -import { CSV_JOB_TYPE, CSV_JOB_TYPE_DEPRECATED } from '../../../common/constants'; -import { ReportApiJSON } from '../../../common/types'; -import { ExportType } from '../../export_types/common'; -import { getContentStream, statuses } from '../../lib'; +import { ReportingCore } from '../../..'; +import { CSV_JOB_TYPE, CSV_JOB_TYPE_DEPRECATED } from '../../../../common/constants'; +import { ReportApiJSON } from '../../../../common/types'; +import { ExportType } from '../../../export_types/common'; +import { getContentStream, statuses } from '../../../lib'; import { jobsQueryFactory } from './jobs_query'; export interface ErrorFromPayload { diff --git a/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts b/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts new file mode 100644 index 0000000000000..c0d29849e94f3 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/common/jobs/get_job_routes.ts @@ -0,0 +1,101 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server'; +import { promisify } from 'util'; +import { getCounters } from '..'; +import { ReportingCore } from '../../..'; +import { ALLOWED_JOB_CONTENT_TYPES } from '../../../../common/constants'; +import { getContentStream } from '../../../lib'; +import { ReportingRequestHandlerContext, ReportingUser } from '../../../types'; +import { handleUnavailable } from '../generate'; +import { jobsQueryFactory } from './jobs_query'; +import { jobManagementPreRouting } from './job_management_pre_routing'; + +const validate = { + params: schema.object({ + docId: schema.string({ minLength: 3 }), + }), +}; + +interface HandlerOpts { + path: string; + user: ReportingUser; + context: ReportingRequestHandlerContext; + req: KibanaRequest>; + res: KibanaResponseFactory; +} + +export const commonJobsRouteHandlerFactory = (reporting: ReportingCore) => { + const jobsQuery = jobsQueryFactory(reporting); + + const handleDownloadReport = ({ path, user, context, req, res }: HandlerOpts) => { + const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); + + // ensure the async dependencies are loaded + if (!context.reporting) { + return handleUnavailable(res); + } + + const { docId } = req.params; + + return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { + const payload = await jobsQuery.getDocumentPayload(doc); + const { contentType, content, filename, statusCode } = payload; + + if (!contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${contentType} specified by job output`, + }); + } + + const body = typeof content === 'string' ? Buffer.from(content) : content; + + const headers = { + ...payload.headers, + 'content-type': contentType, + }; + + if (filename) { + return res.file({ body, headers, filename }); + } + + return res.custom({ body, headers, statusCode }); + }); + }; + + const handleDeleteReport = ({ path, user, context, req, res }: HandlerOpts) => { + const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); + + // ensure the async dependencies are loaded + if (!context.reporting) { + return handleUnavailable(res); + } + + const { docId } = req.params; + + return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { + const docIndex = doc.index; + const stream = await getContentStream(reporting, { id: docId, index: docIndex }); + + /** @note Overwriting existing content with an empty buffer to remove all the chunks. */ + await promisify(stream.end.bind(stream, '', 'utf8'))(); + await jobsQuery.delete(docIndex, docId); + + return res.ok({ + body: { deleted: true }, + }); + }); + }; + + return { + validate, + handleDownloadReport, + handleDeleteReport, + }; +}; diff --git a/x-pack/plugins/reporting/server/routes/lib/index.ts b/x-pack/plugins/reporting/server/routes/common/jobs/index.ts similarity index 61% rename from x-pack/plugins/reporting/server/routes/lib/index.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/index.ts index 50f5653f894ff..9cc52f8fb35b6 100644 --- a/x-pack/plugins/reporting/server/routes/lib/index.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/index.ts @@ -5,11 +5,6 @@ * 2.0. */ -export { authorizedUserPreRouting } from './authorized_user_pre_routing'; +export { commonJobsRouteHandlerFactory } from './get_job_routes'; export { jobManagementPreRouting } from './job_management_pre_routing'; - -export { getCounters } from './get_counter'; -export type { Counters } from './get_counter'; - -export { handleUnavailable, RequestHandler } from './request_handler'; export { jobsQueryFactory } from './jobs_query'; diff --git a/x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.test.ts similarity index 97% rename from x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.test.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.test.ts index 3e3bebbc614b1..3d4a182d0d743 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.test.ts @@ -6,18 +6,18 @@ */ import { httpServerMock } from '@kbn/core/server/mocks'; -import { ReportingCore } from '../..'; -import { ReportingInternalSetup, ReportingInternalStart } from '../../core'; +import { ReportingCore } from '../../..'; +import { ReportingInternalSetup, ReportingInternalStart } from '../../../core'; import { createMockConfigSchema, createMockPluginSetup, createMockPluginStart, createMockReportingCore, -} from '../../test_helpers'; +} from '../../../test_helpers'; import { jobsQueryFactory } from './jobs_query'; import { jobManagementPreRouting } from './job_management_pre_routing'; -jest.mock('../../lib/content_stream'); +jest.mock('../../../lib/content_stream'); jest.mock('./jobs_query'); const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); diff --git a/x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.ts b/x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.ts similarity index 88% rename from x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.ts index a443b6fc05bb3..79221c0c49d34 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_management_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/job_management_pre_routing.ts @@ -8,11 +8,11 @@ import Boom from '@hapi/boom'; import { IKibanaResponse, kibanaResponseFactory } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; -import { jobsQueryFactory } from '.'; -import { ReportingCore } from '../..'; -import { ReportApiJSON } from '../../lib/store/report'; -import { ReportingUser } from '../../types'; -import type { Counters } from './get_counter'; +import { Counters } from '..'; +import { ReportingCore } from '../../..'; +import { ReportApiJSON } from '../../../lib/store/report'; +import { ReportingUser } from '../../../types'; +import { jobsQueryFactory } from './jobs_query'; /** * The body of a route handler to call via callback @@ -52,7 +52,6 @@ export const jobManagementPreRouting = async ( }); } - // Count usage once allowing the request counters.usageCounter(jobtype); try { diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.test.ts b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.test.ts similarity index 99% rename from x-pack/plugins/reporting/server/routes/lib/jobs_query.test.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.test.ts index 202fa55049b3d..f0c656da2050a 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.test.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.test.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { set } from '@kbn/safer-lodash-set'; import { ElasticsearchClient } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; -import { statuses } from '../../lib'; -import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; - +import { set } from '@kbn/safer-lodash-set'; +import { statuses } from '../../../lib'; +import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { jobsQueryFactory } from './jobs_query'; describe('jobsQuery', () => { diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts similarity index 93% rename from x-pack/plugins/reporting/server/routes/lib/jobs_query.ts rename to x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts index ff01c0e635e9e..87bf38648bd2f 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/common/jobs/jobs_query.ts @@ -8,13 +8,13 @@ import { estypes, errors, TransportResult } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; -import type { ReportingCore } from '../..'; -import { REPORTING_SYSTEM_INDEX } from '../../../common/constants'; -import type { ReportApiJSON, ReportSource } from '../../../common/types'; -import { statuses } from '../../lib/statuses'; -import { Report } from '../../lib/store'; -import { runtimeFieldKeys, runtimeFields } from '../../lib/store/runtime_fields'; -import type { ReportingUser } from '../../types'; +import type { ReportingCore } from '../../..'; +import { REPORTING_SYSTEM_INDEX } from '../../../../common/constants'; +import type { ReportApiJSON, ReportSource } from '../../../../common/types'; +import { statuses } from '../../../lib/statuses'; +import { Report } from '../../../lib/store'; +import { runtimeFieldKeys, runtimeFields } from '../../../lib/store/runtime_fields'; +import type { ReportingUser } from '../../../types'; import type { Payload } from './get_document_payload'; import { getDocumentPayloadFactory } from './get_document_payload'; diff --git a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts deleted file mode 100644 index 373e72efb8bd0..0000000000000 --- a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts +++ /dev/null @@ -1,111 +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 { schema } from '@kbn/config-schema'; -import type { Logger } from '@kbn/core/server'; -import rison from '@kbn/rison'; -import type { ReportingCore } from '../..'; -import { API_BASE_URL } from '../../../common/constants'; -import type { BaseParams } from '../../types'; -import { authorizedUserPreRouting, getCounters, RequestHandler } from '../lib'; - -const BASE_GENERATE = `${API_BASE_URL}/generate`; - -export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { - const setupDeps = reporting.getPluginSetupDeps(); - const { router } = setupDeps; - - // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new - // public method to return this array - // const registry = reporting.getExportTypesRegistry(); - // const kibanaAccessControlTags = registry.getAllAccessControlTags(); - const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if Reporting's deprecated access control feature is disabled - const kibanaAccessControlTags = useKibanaAccessControl ? ['access:generateReport'] : []; - - const registerPostGenerationEndpoint = () => { - const path = `${BASE_GENERATE}/{exportType}`; - router.post( - { - path, - validate: { - params: schema.object({ exportType: schema.string({ minLength: 2 }) }), - body: schema.nullable(schema.object({ jobParams: schema.maybe(schema.string()) })), - query: schema.nullable(schema.object({ jobParams: schema.string({ defaultValue: '' }) })), - }, - options: { tags: kibanaAccessControlTags }, - }, - authorizedUserPreRouting(reporting, async (user, context, req, res) => { - const counters = getCounters( - req.route.method, - path.replace(/{exportType}/, req.params.exportType), - reporting.getUsageCounter() - ); - - let jobParamsRison: null | string = null; - - if (req.body) { - const { jobParams: jobParamsPayload } = req.body; - jobParamsRison = jobParamsPayload ? jobParamsPayload : null; - } else if (req.query?.jobParams) { - const { jobParams: queryJobParams } = req.query; - if (queryJobParams) { - jobParamsRison = queryJobParams; - } else { - jobParamsRison = null; - } - } - - if (!jobParamsRison) { - return res.customError({ - statusCode: 400, - body: 'A jobParams RISON string is required in the querystring or POST body', - }); - } - - let jobParams; - - try { - jobParams = rison.decode(jobParamsRison) as BaseParams | null; - if (!jobParams) { - return res.customError({ - statusCode: 400, - body: 'Missing jobParams!', - }); - } - } catch (err) { - return res.customError({ - statusCode: 400, - body: `invalid rison: ${jobParamsRison}`, - }); - } - - const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); - return await requestHandler.handleGenerateRequest( - req.params.exportType, - jobParams, - counters - ); - }) - ); - }; - - const registerGetGenerationEndpoint = () => { - // Get route to generation endpoint: show error about GET method to user - router.get( - { - path: `${BASE_GENERATE}/{p*}`, - validate: false, - }, - (_context, _req, res) => { - return res.customError({ statusCode: 405, body: 'GET is not allowed' }); - } - ); - }; - - registerPostGenerationEndpoint(); - registerGetGenerationEndpoint(); -} diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 570d74f3e02ab..f0ce70e00d6bd 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -7,18 +7,20 @@ import type { Logger } from '@kbn/core/server'; import { ReportingCore } from '..'; -import { registerDeprecationsRoutes } from './deprecations/deprecations'; -import { registerDiagnosticRoutes } from './diagnostic'; -import { - registerGenerateCsvFromSavedObjectImmediate, - registerJobGenerationRoutes, -} from './generate'; -import { registerJobInfoRoutes } from './management'; +import { registerDeprecationsRoutes } from './internal/deprecations/deprecations'; +import { registerDiagnosticRoutes } from './internal/diagnostic'; +import { registerGenerateCsvFromSavedObjectImmediate } from './internal/generate/csv_searchsource_immediate'; +import { registerGenerationRoutesInternal } from './internal/generate/generate_from_jobparams'; +import { registerJobInfoRoutesInternal } from './internal/management/jobs'; +import { registerGenerationRoutesPublic } from './public/generate_from_jobparams'; +import { registerJobInfoRoutesPublic } from './public/jobs'; export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerDeprecationsRoutes(reporting, logger); registerDiagnosticRoutes(reporting, logger); registerGenerateCsvFromSavedObjectImmediate(reporting, logger); - registerJobGenerationRoutes(reporting, logger); - registerJobInfoRoutes(reporting); + registerGenerationRoutesInternal(reporting, logger); + registerJobInfoRoutesInternal(reporting); + registerGenerationRoutesPublic(reporting, logger); + registerJobInfoRoutesPublic(reporting); } diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/internal/deprecations/deprecations.ts similarity index 79% rename from x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts rename to x-pack/plugins/reporting/server/routes/internal/deprecations/deprecations.ts index ac9cf8be2074b..04ff2474674d1 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/internal/deprecations/deprecations.ts @@ -6,21 +6,16 @@ */ import { errors } from '@elastic/elasticsearch'; import type { Logger, RequestHandler } from '@kbn/core/server'; -import { - API_GET_ILM_POLICY_STATUS, - API_MIGRATE_ILM_POLICY_URL, - ILM_POLICY_NAME, -} from '../../../common/constants'; -import type { IlmPolicyStatusResponse } from '../../../common/types'; -import type { ReportingCore } from '../../core'; -import { IlmPolicyManager } from '../../lib'; -import { deprecations } from '../../lib/deprecations'; -import { getCounters } from '../lib'; - -export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { - const { router } = reporting.getPluginSetupDeps(); - - const authzWrapper = (handler: RequestHandler): RequestHandler => { +import { ILM_POLICY_NAME, INTERNAL_ROUTES } from '../../../../common/constants'; +import type { IlmPolicyStatusResponse } from '../../../../common/types'; +import type { ReportingCore } from '../../../core'; +import { IlmPolicyManager } from '../../../lib'; +import { deprecations } from '../../../lib/deprecations'; +import { getCounters } from '../../common'; + +const getAuthzWrapper = + (reporting: ReportingCore, logger: Logger) => + (handler: RequestHandler): RequestHandler => { return async (ctx, req, res) => { const { security } = reporting.getPluginSetupDeps(); if (!security?.license.isEnabled()) { @@ -56,14 +51,19 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log }; }; +export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { + const { router } = reporting.getPluginSetupDeps(); + const authzWrapper = getAuthzWrapper(reporting, logger); + + const getStatusPath = INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS; router.get( - { path: API_GET_ILM_POLICY_STATUS, validate: false }, + { + path: getStatusPath, + validate: false, + options: { access: 'internal' }, + }, authzWrapper(async ({ core }, req, res) => { - const counters = getCounters( - req.route.method, - API_GET_ILM_POLICY_STATUS, - reporting.getUsageCounter() - ); + const counters = getCounters(req.route.method, getStatusPath, reporting.getUsageCounter()); const { elasticsearch: { client: scopedClient }, @@ -96,14 +96,15 @@ export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Log }) ); + const migrateApiPath = INTERNAL_ROUTES.MIGRATE.MIGRATE_ILM_POLICY; router.put( - { path: API_MIGRATE_ILM_POLICY_URL, validate: false }, + { + path: migrateApiPath, + validate: false, + options: { access: 'internal' }, + }, authzWrapper(async ({ core }, req, res) => { - const counters = getCounters( - req.route.method, - API_GET_ILM_POLICY_STATUS, - reporting.getUsageCounter() - ); + const counters = getCounters(req.route.method, migrateApiPath, reporting.getUsageCounter()); const store = await reporting.getStore(); const { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/internal/deprecations/integration_tests/deprecations.test.ts similarity index 67% rename from x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts rename to x-pack/plugins/reporting/server/routes/internal/deprecations/integration_tests/deprecations.test.ts index 02aa8de4959d7..8b82e2bef151f 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/internal/deprecations/integration_tests/deprecations.test.ts @@ -5,23 +5,23 @@ * 2.0. */ -import { loggingSystemMock } from '@kbn/core/server/mocks'; import { setupServer } from '@kbn/core-test-helpers-test-utils'; -import supertest from 'supertest'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import { securityMock } from '@kbn/security-plugin/server/mocks'; -import { API_GET_ILM_POLICY_STATUS } from '../../../../common/constants'; +import supertest from 'supertest'; +import { INTERNAL_ROUTES } from '../../../../../common/constants'; import { createMockConfigSchema, createMockPluginSetup, createMockPluginStart, createMockReportingCore, -} from '../../../test_helpers'; +} from '../../../../test_helpers'; import { registerDeprecationsRoutes } from '../deprecations'; type SetupServerReturn = Awaited>; -describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { +describe(`GET ${INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS}`, () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; @@ -57,7 +57,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { await server.start(); await supertest(httpSetup.server.listener) - .get(API_GET_ILM_POLICY_STATUS) + .get(INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS) .expect(200) .then(/* Ignore result */); }); @@ -71,8 +71,32 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { await server.start(); await supertest(httpSetup.server.listener) - .get(API_GET_ILM_POLICY_STATUS) + .get(INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS) .expect(200) .then(/* Ignore result */); }); + + describe('usage counter', () => { + it('increments the download api counter', async () => { + const core = await createReportingCore({}); + const usageCounter = { + incrementCounter: jest.fn(), + }; + core.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); + await server.start(); + + await supertest(httpSetup.server.listener) + .get(INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS) + .expect(200) + .then(/* Ignore result */); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `get ${INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS}`, + counterType: 'reportingApi', + }); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/internal/diagnostic/browser.ts similarity index 90% rename from x-pack/plugins/reporting/server/routes/diagnostic/browser.ts rename to x-pack/plugins/reporting/server/routes/internal/diagnostic/browser.ts index 93a5ab72aff28..34c7583f9ef06 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/internal/diagnostic/browser.ts @@ -9,9 +9,9 @@ import type { DocLinksServiceSetup, Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { lastValueFrom } from 'rxjs'; import type { DiagnosticResponse } from '.'; -import type { ReportingCore } from '../..'; -import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { authorizedUserPreRouting, getCounters } from '../lib'; +import type { ReportingCore } from '../../..'; +import { INTERNAL_ROUTES } from '../../../../common/constants'; +import { authorizedUserPreRouting, getCounters } from '../../common'; const logsToHelpMapFactory = (docLinks: DocLinksServiceSetup) => ({ 'error while loading shared libraries': i18n.translate( @@ -36,13 +36,16 @@ const logsToHelpMapFactory = (docLinks: DocLinksServiceSetup) => ({ }), }); -const path = `${API_DIAGNOSE_URL}/browser`; - +const path = INTERNAL_ROUTES.DIAGNOSE.BROWSER; export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => { const { router } = reporting.getPluginSetupDeps(); router.post( - { path: `${path}`, validate: {} }, + { + path, + validate: {}, + options: { access: 'internal' }, + }, authorizedUserPreRouting(reporting, async (_user, _context, req, res) => { const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/internal/diagnostic/index.ts similarity index 93% rename from x-pack/plugins/reporting/server/routes/diagnostic/index.ts rename to x-pack/plugins/reporting/server/routes/internal/diagnostic/index.ts index 952163a260806..d6bf4781d8fea 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts +++ b/x-pack/plugins/reporting/server/routes/internal/diagnostic/index.ts @@ -6,7 +6,7 @@ */ import type { Logger } from '@kbn/core/server'; -import type { ReportingCore } from '../../core'; +import type { ReportingCore } from '../../../core'; import { registerDiagnoseBrowser } from './browser'; import { registerDiagnoseScreenshot } from './screenshot'; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts b/x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/browser.test.ts similarity index 76% rename from x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts rename to x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/browser.test.ts index 77034c15495fc..be23298a0f0e1 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/browser.test.ts @@ -5,32 +5,35 @@ * 2.0. */ -import * as Rx from 'rxjs'; -import { docLinksServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { setupServer } from '@kbn/core-test-helpers-test-utils'; -import supertest from 'supertest'; -import { ReportingCore } from '../../..'; +import { docLinksServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; +import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; +import * as Rx from 'rxjs'; +import supertest from 'supertest'; +import { ReportingCore } from '../../../..'; +import { INTERNAL_ROUTES } from '../../../../../common/constants'; +import { reportingMock } from '../../../../mocks'; import { createMockConfigSchema, createMockPluginSetup, createMockReportingCore, -} from '../../../test_helpers'; -import type { ReportingRequestHandlerContext } from '../../../types'; +} from '../../../../test_helpers'; +import type { ReportingRequestHandlerContext } from '../../../../types'; import { registerDiagnoseBrowser } from '../browser'; -import { reportingMock } from '../../../mocks'; type SetupServerReturn = Awaited>; const devtoolMessage = 'DevTools listening on (ws://localhost:4000)'; const fontNotFoundMessage = 'Could not find the default font'; -describe('POST /diagnose/browser', () => { +describe(`POST ${INTERNAL_ROUTES.DIAGNOSE.BROWSER}`, () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); const mockLogger = loggingSystemMock.createLogger(); let server: SetupServerReturn['server']; + let usageCounter: IUsageCounter; let httpSetup: SetupServerReturn['httpSetup']; let core: ReportingCore; let screenshotting: jest.Mocked; @@ -67,6 +70,11 @@ describe('POST /diagnose/browser', () => { }) ); + usageCounter = { + incrementCounter: jest.fn(), + }; + core.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + screenshotting = (await core.getPluginStartDeps()).screenshotting as typeof screenshotting; }); @@ -82,7 +90,7 @@ describe('POST /diagnose/browser', () => { screenshotting.diagnose.mockReturnValue(Rx.of(devtoolMessage)); return supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/browser') + .post(INTERNAL_ROUTES.DIAGNOSE.BROWSER) .expect(200) .then(({ body }) => { expect(body.success).toEqual(true); @@ -98,7 +106,7 @@ describe('POST /diagnose/browser', () => { screenshotting.diagnose.mockReturnValue(Rx.of(logs)); return supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/browser') + .post(INTERNAL_ROUTES.DIAGNOSE.BROWSER) .expect(200) .then(({ body }) => { expect(body).toMatchInlineSnapshot(` @@ -120,7 +128,7 @@ describe('POST /diagnose/browser', () => { screenshotting.diagnose.mockReturnValue(Rx.of(`${devtoolMessage}\n${fontNotFoundMessage}`)); return supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/browser') + .post(INTERNAL_ROUTES.DIAGNOSE.BROWSER) .expect(200) .then(({ body }) => { expect(body).toMatchInlineSnapshot(` @@ -135,4 +143,22 @@ describe('POST /diagnose/browser', () => { `); }); }); + + describe('usage counter', () => { + it('increments the counter', async () => { + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + + screenshotting.diagnose.mockReturnValue(Rx.of(devtoolMessage)); + + await supertest(httpSetup.server.listener).post(INTERNAL_ROUTES.DIAGNOSE.BROWSER).expect(200); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `post ${INTERNAL_ROUTES.DIAGNOSE.BROWSER}:success`, + counterType: 'reportingApi', + }); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/screenshot.test.ts similarity index 84% rename from x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts rename to x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/screenshot.test.ts index 812f3d64eb2d5..74b88599d2628 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/internal/diagnostic/integration_tests/screenshot.test.ts @@ -5,25 +5,29 @@ * 2.0. */ -import { loggingSystemMock } from '@kbn/core/server/mocks'; import { setupServer } from '@kbn/core-test-helpers-test-utils'; +import { loggingSystemMock } from '@kbn/core/server/mocks'; +import { defer } from 'rxjs'; import supertest from 'supertest'; -import { ReportingCore } from '../../..'; -import { generatePngObservable } from '../../../export_types/common'; +import { ReportingCore } from '../../../..'; +import { INTERNAL_ROUTES } from '../../../../../common/constants'; +import { generatePngObservable } from '../../../../export_types/common'; +import { reportingMock } from '../../../../mocks'; import { createMockConfigSchema, createMockPluginSetup, createMockReportingCore, -} from '../../../test_helpers'; -import type { ReportingRequestHandlerContext } from '../../../types'; +} from '../../../../test_helpers'; +import type { ReportingRequestHandlerContext } from '../../../../types'; import { registerDiagnoseScreenshot } from '../screenshot'; -import { defer } from 'rxjs'; -import { reportingMock } from '../../../mocks'; -jest.mock('../../../export_types/common/generate_png'); +jest.mock('../../../../export_types/common/generate_png'); type SetupServerReturn = Awaited>; +/** + * Tests internal diagnostic API endpoints + */ describe('POST /diagnose/screenshot', () => { const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; @@ -68,7 +72,7 @@ describe('POST /diagnose/screenshot', () => { await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/screenshot') + .post(INTERNAL_ROUTES.DIAGNOSE.SCREENSHOT) .expect(200) .then(({ body }) => { expect(body).toMatchInlineSnapshot(` @@ -87,7 +91,7 @@ describe('POST /diagnose/screenshot', () => { await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/screenshot') + .post(INTERNAL_ROUTES.DIAGNOSE.SCREENSHOT) .expect(200) .then(({ body }) => { expect(body).toMatchInlineSnapshot(` @@ -108,7 +112,7 @@ describe('POST /diagnose/screenshot', () => { await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/diagnose/screenshot') + .post(INTERNAL_ROUTES.DIAGNOSE.SCREENSHOT) .expect(200) .then(({ body }) => { expect(body.help).toContain(`We couldn't screenshot your Kibana install.`); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/internal/diagnostic/screenshot.ts similarity index 86% rename from x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts rename to x-pack/plugins/reporting/server/routes/internal/diagnostic/screenshot.ts index 1afb438981b97..10bccd6dd5941 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/internal/diagnostic/screenshot.ts @@ -9,20 +9,24 @@ import type { Logger } from '@kbn/core/server'; import { APP_WRAPPER_CLASS } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { lastValueFrom } from 'rxjs'; -import type { ReportingCore } from '../..'; -import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { generatePngObservable } from '../../export_types/common'; -import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; -import { authorizedUserPreRouting, getCounters } from '../lib'; +import type { ReportingCore } from '../../..'; +import { INTERNAL_ROUTES } from '../../../../common/constants'; +import { generatePngObservable } from '../../../export_types/common'; +import { getAbsoluteUrlFactory } from '../../../export_types/common/get_absolute_url'; +import { authorizedUserPreRouting, getCounters } from '../../common'; -const path = `${API_DIAGNOSE_URL}/screenshot`; +const path = INTERNAL_ROUTES.DIAGNOSE.SCREENSHOT; export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; router.post( - { path, validate: {} }, + { + path, + validate: {}, + options: { access: 'internal' }, + }, authorizedUserPreRouting(reporting, async (_user, _context, req, res) => { const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); diff --git a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/internal/generate/csv_searchsource_immediate.ts similarity index 83% rename from x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts rename to x-pack/plugins/reporting/server/routes/internal/generate/csv_searchsource_immediate.ts index ed8cad2fbb2d0..90fc2d01dba66 100644 --- a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/internal/generate/csv_searchsource_immediate.ts @@ -9,19 +9,16 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import type { KibanaRequest, Logger } from '@kbn/core/server'; import moment from 'moment'; -import type { ReportingCore } from '../..'; -import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; -import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; -import { PassThroughStream } from '../../lib'; -import { authorizedUserPreRouting, getCounters } from '../lib'; +import type { ReportingCore } from '../../..'; +import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE, INTERNAL_ROUTES } from '../../../../common/constants'; +import type { JobParamsDownloadCSV } from '../../../export_types/csv_searchsource_immediate/types'; +import { PassThroughStream } from '../../../lib'; +import { authorizedUserPreRouting, getCounters } from '../../common'; -const API_BASE_URL_V1 = '/api/reporting/v1'; -const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; +const path = INTERNAL_ROUTES.DOWNLOAD_CSV; export type CsvFromSavedObjectRequest = KibanaRequest; -const path = `${API_BASE_GENERATE_V1}/immediate/csv_searchsource`; - /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: * - saved object type and ID @@ -38,10 +35,6 @@ export function registerGenerateCsvFromSavedObjectImmediate( const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; - // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new - // public method to return this array - // const registry = reporting.getExportTypesRegistry(); - // const kibanaAccessControlTags = registry.getAllAccessControlTags(); const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if deprecated config is turned off const kibanaAccessControlTags = useKibanaAccessControl ? ['access:downloadCsv'] : []; @@ -62,9 +55,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( version: schema.maybe(schema.string()), }), }, - options: { - tags: kibanaAccessControlTags, - }, + options: { tags: kibanaAccessControlTags, access: 'internal' }, }, authorizedUserPreRouting( reporting, diff --git a/x-pack/plugins/reporting/server/routes/internal/generate/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/internal/generate/generate_from_jobparams.ts new file mode 100644 index 0000000000000..4dbd187069e58 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/internal/generate/generate_from_jobparams.ts @@ -0,0 +1,56 @@ +/* + * 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 { KibanaResponse } from '@kbn/core-http-router-server-internal'; +import type { Logger } from '@kbn/core/server'; +import type { ReportingCore } from '../../..'; +import { INTERNAL_ROUTES } from '../../../../common/constants'; +import { authorizedUserPreRouting } from '../../common'; +import { RequestHandler } from '../../common/generate'; + +const { GENERATE_PREFIX } = INTERNAL_ROUTES; + +export function registerGenerationRoutesInternal(reporting: ReportingCore, logger: Logger) { + const setupDeps = reporting.getPluginSetupDeps(); + const { router } = setupDeps; + + const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if Reporting's deprecated access control feature is disabled + const kibanaAccessControlTags = useKibanaAccessControl ? ['access:generateReport'] : []; + + const registerInternalPostGenerationEndpoint = () => { + const path = `${GENERATE_PREFIX}/{exportType}`; + router.post( + { + path, + validate: RequestHandler.getValidation(), + options: { tags: kibanaAccessControlTags, access: 'internal' }, + }, + authorizedUserPreRouting(reporting, async (user, context, req, res) => { + try { + const requestHandler = new RequestHandler( + reporting, + user, + context, + path, + req, + res, + logger + ); + const jobParams = requestHandler.getJobParams(); + return await requestHandler.handleGenerateRequest(req.params.exportType, jobParams); + } catch (err) { + if (err instanceof KibanaResponse) { + return err; + } + throw err; + } + }) + ); + }; + + registerInternalPostGenerationEndpoint(); +} diff --git a/x-pack/plugins/reporting/server/routes/internal/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/internal/generate/integration_tests/generation_from_jobparams.test.ts new file mode 100644 index 0000000000000..c36385ab5174f --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/internal/generate/integration_tests/generation_from_jobparams.test.ts @@ -0,0 +1,262 @@ +/* + * 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 { setupServer } from '@kbn/core-test-helpers-test-utils'; +import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import rison from '@kbn/rison'; +import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; +import { BehaviorSubject } from 'rxjs'; +import supertest from 'supertest'; +import { ReportingCore } from '../../../..'; +import { INTERNAL_ROUTES } from '../../../../../common/constants'; +import { PdfExportType } from '../../../../export_types/printable_pdf_v2'; +import { ReportingStore } from '../../../../lib'; +import { ExportTypesRegistry } from '../../../../lib/export_types_registry'; +import { Report } from '../../../../lib/store'; +import { reportingMock } from '../../../../mocks'; +import { + createMockConfigSchema, + createMockPluginSetup, + createMockPluginStart, + createMockReportingCore, +} from '../../../../test_helpers'; +import type { ReportingRequestHandlerContext } from '../../../../types'; +import { registerGenerationRoutesInternal } from '../generate_from_jobparams'; + +type SetupServerReturn = Awaited>; + +describe(`POST ${INTERNAL_ROUTES.GENERATE_PREFIX}`, () => { + const reportingSymbol = Symbol('reporting'); + let server: SetupServerReturn['server']; + let usageCounter: IUsageCounter; + let httpSetup: SetupServerReturn['httpSetup']; + let mockExportTypesRegistry: ExportTypesRegistry; + let mockReportingCore: ReportingCore; + let store: ReportingStore; + + const mockConfigSchema = createMockConfigSchema({ + queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, + }); + + const mockLogger = loggingSystemMock.createLogger(); + const mockCoreSetup = coreMock.createSetup(); + + const mockPdfExportType = new PdfExportType( + mockCoreSetup, + mockConfigSchema, + mockLogger, + coreMock.createPluginInitializerContext(mockConfigSchema) + ); + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(reportingSymbol)); + httpSetup.registerRouteHandlerContext( + reportingSymbol, + 'reporting', + () => reportingMock.createStart() + ); + + const mockSetupDeps = createMockPluginSetup({ + security: { license: { isEnabled: () => true } }, + router: httpSetup.createRouter(''), + }); + + const mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), + }, + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), + }, + }, + }, + mockConfigSchema + ); + + mockReportingCore = await createMockReportingCore( + mockConfigSchema, + mockSetupDeps, + mockStartDeps + ); + + usageCounter = { + incrementCounter: jest.fn(), + }; + mockReportingCore.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + + mockExportTypesRegistry = new ExportTypesRegistry(); + mockExportTypesRegistry.register(mockPdfExportType); + + store = await mockReportingCore.getStore(); + store.addReport = jest.fn().mockImplementation(async (opts) => { + return new Report({ + ...opts, + _id: 'foo', + _index: 'foo-index', + }); + }); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('returns 400 if there are no job params', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"A jobParams RISON string is required in the querystring or POST body"' + ) + ); + }); + + it('returns 400 if job params query is invalid', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf?jobParams=foo:`) + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 if job params body is invalid', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ jobParams: `foo:` }) + .expect(400) + .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); + }); + + it('returns 400 export type is invalid', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/TonyHawksProSkater2`) + .send({ jobParams: rison.encode({ title: `abc` }) }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot('"Invalid export-type of TonyHawksProSkater2"') + ); + }); + + it('returns 400 on invalid browser timezone', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ jobParams: rison.encode({ browserTimezone: 'America/Amsterdam', title: `abc` }) }) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Invalid timezone \\"America/Amsterdam\\"."`) + ); + }); + + it('returns 500 if job handler throws an error', async () => { + store.addReport = jest.fn().mockRejectedValue('silly'); + + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ jobParams: rison.encode({ title: `abc` }) }) + .expect(500); + }); + + it(`returns 200 if job handler doesn't error`, async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ + jobParams: rison.encode({ + title: `abc`, + relativeUrls: ['test'], + layout: { id: 'test' }, + objectType: 'canvas workpad', + }), + }) + .expect(200) + .then(({ body }) => { + expect(body).toMatchObject({ + job: { + attempts: 0, + created_by: 'Tom Riddle', + id: 'foo', + index: 'foo-index', + jobtype: 'printable_pdf', + payload: { + forceNow: expect.any(String), + isDeprecated: true, + layout: { + id: 'test', + }, + objectType: 'canvas workpad', + objects: [ + { + relativeUrl: 'test', + }, + ], + title: 'abc', + version: '7.14.0', + }, + status: 'pending', + }, + path: '/mock-server-basepath/api/reporting/jobs/download/foo', + }); + }); + }); + + describe('usage counters', () => { + it('increments generation api counter', async () => { + registerGenerationRoutesInternal(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${INTERNAL_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ + jobParams: rison.encode({ + title: `abc`, + relativeUrls: ['test'], + layout: { id: 'test' }, + objectType: 'canvas workpad', + }), + }) + .expect(200); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `post /internal/reporting/generate/printablePdf`, + counterType: 'reportingApi', + }); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/internal/management/integration_tests/jobs.test.ts similarity index 65% rename from x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts rename to x-pack/plugins/reporting/server/routes/internal/management/integration_tests/jobs.test.ts index a8a1bf36a02b7..d77a3840cfac3 100644 --- a/x-pack/plugins/reporting/server/routes/management/integration_tests/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/internal/management/integration_tests/jobs.test.ts @@ -5,35 +5,38 @@ * 2.0. */ -jest.mock('../../../lib/content_stream', () => ({ +jest.mock('../../../../lib/content_stream', () => ({ getContentStream: jest.fn(), })); import { estypes } from '@elastic/elasticsearch'; import { setupServer } from '@kbn/core-test-helpers-test-utils'; import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; import { BehaviorSubject } from 'rxjs'; import { Readable } from 'stream'; import supertest from 'supertest'; -import { ReportingCore } from '../../..'; -import { ReportingInternalSetup, ReportingInternalStart } from '../../../core'; -import { ContentStream, ExportTypesRegistry, getContentStream } from '../../../lib'; +import { ReportingCore } from '../../../..'; +import { INTERNAL_ROUTES } from '../../../../../common/constants'; +import { ReportingInternalSetup, ReportingInternalStart } from '../../../../core'; +import { ExportType } from '../../../../export_types/common'; +import { ContentStream, ExportTypesRegistry, getContentStream } from '../../../../lib'; +import { reportingMock } from '../../../../mocks'; import { createMockConfigSchema, createMockPluginSetup, createMockPluginStart, createMockReportingCore, -} from '../../../test_helpers'; -import { ReportingRequestHandlerContext } from '../../../types'; -import { registerJobInfoRoutes } from '../jobs'; -import { ExportType } from '../../../export_types/common'; -import { reportingMock } from '../../../mocks'; +} from '../../../../test_helpers'; +import { ReportingRequestHandlerContext } from '../../../../types'; +import { registerJobInfoRoutesInternal as registerJobInfoRoutes } from '../jobs'; type SetupServerReturn = Awaited>; -describe('GET /api/reporting/jobs/download', () => { +describe(`GET ${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}`, () => { const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; + let usageCounter: IUsageCounter; let httpSetup: SetupServerReturn['httpSetup']; let exportTypesRegistry: ExportTypesRegistry; let core: ReportingCore; @@ -50,6 +53,21 @@ describe('GET /api/reporting/jobs/download', () => { } as estypes.SearchResponseBody; }; + const mockJobTypeUnencoded = 'unencodedJobType'; + const mockJobTypeBase64Encoded = 'base64EncodedJobType'; + const getCompleteHits = ({ + jobType = mockJobTypeUnencoded, + outputContentType = 'text/plain', + title = '', + } = {}) => { + return getHits({ + jobtype: jobType, + status: 'completed', + output: { content_type: outputContentType }, + payload: { title }, + }) as estypes.SearchResponseBody; + }; + const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); beforeEach(async () => { @@ -84,16 +102,21 @@ describe('GET /api/reporting/jobs/download', () => { core = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps); + usageCounter = { + incrementCounter: jest.fn(), + }; + core.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ id: 'unencoded', - jobType: 'unencodedJobType', + jobType: mockJobTypeUnencoded, jobContentExtension: 'csv', validLicenses: ['basic', 'gold'], } as ExportType); exportTypesRegistry.register({ id: 'base64Encoded', - jobType: 'base64EncodedJobType', + jobType: mockJobTypeBase64Encoded, jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], @@ -107,6 +130,9 @@ describe('GET /api/reporting/jobs/download', () => { this.push(null); }, }) as typeof stream; + stream.end = jest.fn().mockImplementation((_name, _encoding, callback) => { + callback(); + }); (getContentStream as jest.MockedFunction).mockResolvedValue(stream); }); @@ -122,7 +148,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/1') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/1`) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot( @@ -148,7 +174,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dope') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dope`) .expect(401) .then(({ body }) => expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`) @@ -161,7 +187,9 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); - await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(404); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/poo`) + .expect(404); }); it('returns a 403 if not a valid job type', async () => { @@ -175,13 +203,15 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); - await supertest(httpSetup.server.listener).get('/api/reporting/jobs/download/poo').expect(403); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/poo`) + .expect(403); }); it(`returns job's info`, async () => { mockEsClient.search.mockResponseOnce( getHits({ - jobtype: 'base64EncodedJobType', + jobtype: mockJobTypeBase64Encoded, payload: {}, // payload is irrelevant }) ); @@ -190,7 +220,9 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); - await supertest(httpSetup.server.listener).get('/api/reporting/jobs/info/test').expect(200); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/test`) + .expect(200); }); it(`returns 403 if a user cannot view a job's info`, async () => { @@ -205,13 +237,15 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); - await supertest(httpSetup.server.listener).get('/api/reporting/jobs/info/test').expect(403); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/test`) + .expect(403); }); it('when a job is incomplete', async () => { mockEsClient.search.mockResponseOnce( getHits({ - jobtype: 'unencodedJobType', + jobtype: mockJobTypeUnencoded, status: 'pending', payload: { title: 'incomplete!' }, }) @@ -220,7 +254,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dank') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) .expect(503) .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Retry-After', '30') @@ -230,7 +264,7 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job fails', async () => { mockEsClient.search.mockResponse( getHits({ - jobtype: 'unencodedJobType', + jobtype: mockJobTypeUnencoded, status: 'failed', output: { content: 'job failure message' }, payload: { title: 'failing job!' }, @@ -240,7 +274,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dank') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) .expect(500) .expect('Content-Type', 'application/json; charset=utf-8') .then(({ body }) => @@ -249,26 +283,13 @@ describe('GET /api/reporting/jobs/download', () => { }); describe('successful downloads', () => { - const getCompleteHits = ({ - jobType = 'unencodedJobType', - outputContentType = 'text/plain', - title = '', - } = {}) => { - return getHits({ - jobtype: jobType, - status: 'completed', - output: { content_type: outputContentType }, - payload: { title }, - }) as estypes.SearchResponseBody; - }; - it('when a known job-type is complete', async () => { mockEsClient.search.mockResponseOnce(getCompleteHits()); registerJobInfoRoutes(core); await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dank') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) .expect(200) .expect('Content-Type', 'text/csv; charset=utf-8') .expect('content-disposition', 'attachment; filename=report.csv'); @@ -285,7 +306,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dope') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dope`) .expect(200) .expect('Content-Type', 'text/csv; charset=utf-8') .expect('content-disposition', 'attachment; filename=report.csv'); @@ -294,14 +315,14 @@ describe('GET /api/reporting/jobs/download', () => { it('forwards job content stream', async () => { mockEsClient.search.mockResponseOnce( getCompleteHits({ - jobType: 'unencodedJobType', + jobType: mockJobTypeUnencoded, }) ); registerJobInfoRoutes(core); await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dank') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) .expect(200) .expect('Content-Type', 'text/csv; charset=utf-8') .then(({ text }) => expect(text).toEqual('test')); @@ -310,7 +331,7 @@ describe('GET /api/reporting/jobs/download', () => { it('refuses to return unknown content-types', async () => { mockEsClient.search.mockResponseOnce( getCompleteHits({ - jobType: 'unencodedJobType', + jobType: mockJobTypeUnencoded, outputContentType: 'application/html', }) ); @@ -318,7 +339,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dank') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) .expect(400) .then(({ body }) => { expect(body).toEqual({ @@ -332,7 +353,7 @@ describe('GET /api/reporting/jobs/download', () => { it('allows multi-byte characters in file names', async () => { mockEsClient.search.mockResponseOnce( getCompleteHits({ - jobType: 'base64EncodedJobType', + jobType: mockJobTypeBase64Encoded, title: '日本語ダッシュボード', }) ); @@ -340,7 +361,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/japanese-dashboard') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/japanese-dashboard`) .expect(200) .expect('Content-Type', 'application/pdf') .expect( @@ -378,7 +399,7 @@ describe('GET /api/reporting/jobs/download', () => { await server.start(); await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dope') + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dope`) .expect(403) .then(({ body }) => expect(body.message).toMatchInlineSnapshot(` @@ -388,4 +409,75 @@ describe('GET /api/reporting/jobs/download', () => { ); }); }); + + describe('usage counters', () => { + it('increments the info api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/dank`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `get ${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/{docId}:${mockJobTypeUnencoded}`, + counterType: 'reportingApi', + }); + }); + + it('increments the download api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) + .expect(200) + .expect('Content-Type', 'text/csv; charset=utf-8') + .expect('content-disposition', 'attachment; filename=report.csv'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `get ${INTERNAL_ROUTES.JOBS.DOWNLOAD_PREFIX}/{docId}:${mockJobTypeUnencoded}`, + counterType: 'reportingApi', + }); + }); + + it('increments the delete api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .delete(`${INTERNAL_ROUTES.JOBS.DELETE_PREFIX}/dank`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `delete ${INTERNAL_ROUTES.JOBS.DELETE_PREFIX}/{docId}:${mockJobTypeUnencoded}`, + counterType: 'reportingApi', + }); + }); + + it('increments the count api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutes(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(INTERNAL_ROUTES.JOBS.COUNT) + .expect(200) + .expect('Content-Type', 'text/plain; charset=utf-8'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `get ${INTERNAL_ROUTES.JOBS.COUNT}`, + counterType: 'reportingApi', + }); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/routes/management/jobs.ts b/x-pack/plugins/reporting/server/routes/internal/management/jobs.ts similarity index 53% rename from x-pack/plugins/reporting/server/routes/management/jobs.ts rename to x-pack/plugins/reporting/server/routes/internal/management/jobs.ts index 9b2a63deca2ba..717ac8db9b104 100644 --- a/x-pack/plugins/reporting/server/routes/management/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/internal/management/jobs.ts @@ -7,29 +7,26 @@ import { schema } from '@kbn/config-schema'; import { ROUTE_TAG_CAN_REDIRECT } from '@kbn/security-plugin/server'; -import { promisify } from 'util'; -import { ReportingCore } from '../..'; -import { ALLOWED_JOB_CONTENT_TYPES, API_BASE_URL } from '../../../common/constants'; -import { getContentStream } from '../../lib'; +import { ReportingCore } from '../../..'; +import { INTERNAL_ROUTES } from '../../../../common/constants'; +import { authorizedUserPreRouting, getCounters } from '../../common'; +import { handleUnavailable } from '../../common/generate'; import { - authorizedUserPreRouting, - getCounters, - handleUnavailable, + commonJobsRouteHandlerFactory, jobManagementPreRouting, jobsQueryFactory, -} from '../lib'; +} from '../../common/jobs'; -const MAIN_ENTRY = `${API_BASE_URL}/jobs`; +const { JOBS } = INTERNAL_ROUTES; -export function registerJobInfoRoutes(reporting: ReportingCore) { +export function registerJobInfoRoutesInternal(reporting: ReportingCore) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; const jobsQuery = jobsQueryFactory(reporting); - const registerGetList = () => { + const registerInternalGetList = () => { // list jobs in the queue, paginated - const path = `${MAIN_ENTRY}/list`; - + const path = JOBS.LIST; router.get( { path, @@ -40,6 +37,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ids: schema.maybe(schema.string()), }), }, + options: { access: 'internal' }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); @@ -70,14 +68,15 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ); }; - const registerGetCount = () => { + const registerInternalGetCount = () => { // return the count of all jobs in the queue - const path = `${MAIN_ENTRY}/count`; + const path = JOBS.COUNT; router.get( { path, validate: false, + options: { access: 'internal' }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); @@ -105,18 +104,18 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ); }; - const registerGetInfo = () => { + // use common route handlers that are shared for public and internal routes + const jobHandlers = commonJobsRouteHandlerFactory(reporting); + + const registerInternalGetInfo = () => { // return some info about the job - const path = `${MAIN_ENTRY}/info/{docId}`; + const path = `${JOBS.INFO_PREFIX}/{docId}`; router.get( { path, - validate: { - params: schema.object({ - docId: schema.string({ minLength: 2 }), - }), - }, + validate: jobHandlers.validate, + options: { access: 'internal' }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); @@ -139,99 +138,41 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ); }; - const registerDownloadReport = () => { + const registerInternalDownloadReport = () => { // trigger a download of the output from a job - const path = `${MAIN_ENTRY}/download/{docId}`; + const path = `${JOBS.DOWNLOAD_PREFIX}/{docId}`; router.get( { path, - validate: { - params: schema.object({ - docId: schema.string({ minLength: 3 }), - }), - }, - options: { tags: [ROUTE_TAG_CAN_REDIRECT] }, + validate: jobHandlers.validate, + options: { tags: [ROUTE_TAG_CAN_REDIRECT], access: 'internal' }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { - const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); - - // ensure the async dependencies are loaded - if (!context.reporting) { - return handleUnavailable(res); - } - - const { docId } = req.params; - - return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { - const payload = await jobsQuery.getDocumentPayload(doc); - const { contentType, content, filename, statusCode } = payload; - - if (!contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(contentType)) { - return res.badRequest({ - body: `Unsupported content-type of ${contentType} specified by job output`, - }); - } - - const body = typeof content === 'string' ? Buffer.from(content) : content; - - const headers = { - ...payload.headers, - 'content-type': contentType, - }; - - if (filename) { - return res.file({ body, headers, filename }); - } - - return res.custom({ body, headers, statusCode }); - }); + return jobHandlers.handleDownloadReport({ path, user, context, req, res }); }) ); }; - const registerDeleteReport = () => { + const registerInternalDeleteReport = () => { // allow a report to be deleted - const path = `${MAIN_ENTRY}/delete/{docId}`; + const path = `${JOBS.DELETE_PREFIX}/{docId}`; router.delete( { path, - validate: { - params: schema.object({ - docId: schema.string({ minLength: 3 }), - }), - }, + validate: jobHandlers.validate, + options: { access: 'internal' }, }, authorizedUserPreRouting(reporting, async (user, context, req, res) => { - const counters = getCounters(req.route.method, path, reporting.getUsageCounter()); - - // ensure the async dependencies are loaded - if (!context.reporting) { - return handleUnavailable(res); - } - - const { docId } = req.params; - - return jobManagementPreRouting(reporting, res, docId, user, counters, async (doc) => { - const docIndex = doc.index; - const stream = await getContentStream(reporting, { id: docId, index: docIndex }); - - /** @note Overwriting existing content with an empty buffer to remove all the chunks. */ - await promisify(stream.end.bind(stream, '', 'utf8'))(); - await jobsQuery.delete(docIndex, docId); - - return res.ok({ - body: { deleted: true }, - }); - }); + return jobHandlers.handleDeleteReport({ path, user, context, req, res }); }) ); }; - registerGetList(); - registerGetCount(); - registerGetInfo(); - registerDownloadReport(); - registerDeleteReport(); + registerInternalGetList(); + registerInternalGetCount(); + registerInternalGetInfo(); + registerInternalDownloadReport(); + registerInternalDeleteReport(); } diff --git a/x-pack/plugins/reporting/server/routes/public/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/public/generate_from_jobparams.ts new file mode 100644 index 0000000000000..512b39935de5e --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/public/generate_from_jobparams.ts @@ -0,0 +1,71 @@ +/* + * 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 { KibanaResponse } from '@kbn/core-http-router-server-internal'; +import type { Logger } from '@kbn/core/server'; +import type { ReportingCore } from '../..'; +import { PUBLIC_ROUTES } from '../../../common/constants'; +import { authorizedUserPreRouting } from '../common'; +import { RequestHandler } from '../common/generate'; + +export function registerGenerationRoutesPublic(reporting: ReportingCore, logger: Logger) { + const setupDeps = reporting.getPluginSetupDeps(); + const { router } = setupDeps; + + const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if Reporting's deprecated access control feature is disabled + const kibanaAccessControlTags = useKibanaAccessControl ? ['access:generateReport'] : []; + + const registerPublicPostGenerationEndpoint = () => { + const path = `${PUBLIC_ROUTES.GENERATE_PREFIX}/{exportType}`; + router.post( + { + path, + validate: RequestHandler.getValidation(), + options: { tags: kibanaAccessControlTags, access: 'public' }, + }, + authorizedUserPreRouting(reporting, async (user, context, req, res) => { + try { + const requestHandler = new RequestHandler( + reporting, + user, + context, + path, + req, + res, + logger + ); + return await requestHandler.handleGenerateRequest( + req.params.exportType, + requestHandler.getJobParams() + ); + } catch (err) { + if (err instanceof KibanaResponse) { + return err; + } + throw err; + } + }) + ); + }; + + const registerPublicGetGenerationEndpoint = () => { + // Get route to generation endpoint: show error about GET method to user + router.get( + { + path: `${PUBLIC_ROUTES.GENERATE_PREFIX}/{p*}`, + validate: false, + options: { access: 'public' }, + }, + (_context, _req, res) => { + return res.customError({ statusCode: 405, body: 'GET is not allowed' }); + } + ); + }; + + registerPublicPostGenerationEndpoint(); + registerPublicGetGenerationEndpoint(); +} diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/public/integration_tests/generation_from_jobparams.test.ts similarity index 73% rename from x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts rename to x-pack/plugins/reporting/server/routes/public/integration_tests/generation_from_jobparams.test.ts index 039a0054a1b45..18883564030d1 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/public/integration_tests/generation_from_jobparams.test.ts @@ -5,16 +5,20 @@ * 2.0. */ +import { setupServer } from '@kbn/core-test-helpers-test-utils'; +import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; import rison from '@kbn/rison'; +import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; import { BehaviorSubject } from 'rxjs'; -import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { setupServer } from '@kbn/core-test-helpers-test-utils'; import supertest from 'supertest'; import { ReportingCore } from '../../..'; -import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { PUBLIC_ROUTES } from '../../../../common/constants'; +import { PdfExportType } from '../../../export_types/printable_pdf_v2'; import { ReportingStore } from '../../../lib'; import { ExportTypesRegistry } from '../../../lib/export_types_registry'; import { Report } from '../../../lib/store'; +import { reportingMock } from '../../../mocks'; import { createMockConfigSchema, createMockPluginSetup, @@ -22,15 +26,14 @@ import { createMockReportingCore, } from '../../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../../types'; -import { registerJobGenerationRoutes } from '../generate_from_jobparams'; -import { PdfExportType } from '../../../export_types/printable_pdf_v2'; -import { reportingMock } from '../../../mocks'; +import { registerGenerationRoutesPublic } from '../generate_from_jobparams'; type SetupServerReturn = Awaited>; -describe('POST /api/reporting/generate', () => { +describe(`POST ${PUBLIC_ROUTES.GENERATE_PREFIX}`, () => { const reportingSymbol = Symbol('reporting'); let server: SetupServerReturn['server']; + let usageCounter: IUsageCounter; let httpSetup: SetupServerReturn['httpSetup']; let mockExportTypesRegistry: ExportTypesRegistry; let mockReportingCore: ReportingCore; @@ -59,9 +62,7 @@ describe('POST /api/reporting/generate', () => { ); const mockSetupDeps = createMockPluginSetup({ - security: { - license: { isEnabled: () => true }, - }, + security: { license: { isEnabled: () => true } }, router: httpSetup.createRouter(''), }); @@ -86,6 +87,11 @@ describe('POST /api/reporting/generate', () => { mockStartDeps ); + usageCounter = { + incrementCounter: jest.fn(), + }; + mockReportingCore.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + mockExportTypesRegistry = new ExportTypesRegistry(); mockExportTypesRegistry.register(mockPdfExportType); @@ -104,12 +110,12 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 if there are no job params', async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot( @@ -119,35 +125,35 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 if job params query is invalid', async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf?jobParams=foo:') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf?jobParams=foo:`) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); }); it('returns 400 if job params body is invalid', async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) .send({ jobParams: `foo:` }) .expect(400) .then(({ body }) => expect(body.message).toMatchInlineSnapshot('"invalid rison: foo:"')); }); it('returns 400 export type is invalid', async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/TonyHawksProSkater2') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/TonyHawksProSkater2`) .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(400) .then(({ body }) => @@ -156,12 +162,12 @@ describe('POST /api/reporting/generate', () => { }); it('returns 400 on invalid browser timezone', async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) .send({ jobParams: rison.encode({ browserTimezone: 'America/Amsterdam', title: `abc` }) }) .expect(400) .then(({ body }) => @@ -172,23 +178,23 @@ describe('POST /api/reporting/generate', () => { it('returns 500 if job handler throws an error', async () => { store.addReport = jest.fn().mockRejectedValue('silly'); - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) .send({ jobParams: rison.encode({ title: `abc` }) }) .expect(500); }); it(`returns 200 if job handler doesn't error`, async () => { - registerJobGenerationRoutes(mockReportingCore, mockLogger); + registerGenerationRoutesPublic(mockReportingCore, mockLogger); await server.start(); await supertest(httpSetup.server.listener) - .post('/api/reporting/generate/printablePdf') + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) .send({ jobParams: rison.encode({ title: `abc`, @@ -227,4 +233,30 @@ describe('POST /api/reporting/generate', () => { }); }); }); + + describe('usage counters', () => { + it('increments generation api counter', async () => { + registerGenerationRoutesPublic(mockReportingCore, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post(`${PUBLIC_ROUTES.GENERATE_PREFIX}/printablePdf`) + .send({ + jobParams: rison.encode({ + title: `abc`, + relativeUrls: ['test'], + layout: { id: 'test' }, + objectType: 'canvas workpad', + }), + }) + .expect(200); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `post /api/reporting/generate/printablePdf`, + counterType: 'reportingApi', + }); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/routes/public/integration_tests/jobs.test.ts b/x-pack/plugins/reporting/server/routes/public/integration_tests/jobs.test.ts new file mode 100644 index 0000000000000..b268d8c089d57 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/public/integration_tests/jobs.test.ts @@ -0,0 +1,292 @@ +/* + * 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. + */ + +jest.mock('../../../lib/content_stream', () => ({ + getContentStream: jest.fn(), +})); +import { estypes } from '@elastic/elasticsearch'; +import { setupServer } from '@kbn/core-test-helpers-test-utils'; +import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; +import { IUsageCounter } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counter'; +import { BehaviorSubject } from 'rxjs'; +import { Readable } from 'stream'; +import supertest from 'supertest'; +import { ReportingCore } from '../../..'; +import { PUBLIC_ROUTES } from '../../../../common/constants'; +import { ReportingInternalSetup, ReportingInternalStart } from '../../../core'; +import { ExportType } from '../../../export_types/common'; +import { ContentStream, ExportTypesRegistry, getContentStream } from '../../../lib'; +import { reportingMock } from '../../../mocks'; +import { + createMockConfigSchema, + createMockPluginSetup, + createMockPluginStart, + createMockReportingCore, +} from '../../../test_helpers'; +import { ReportingRequestHandlerContext } from '../../../types'; +import { registerJobInfoRoutesPublic } from '../jobs'; + +type SetupServerReturn = Awaited>; + +describe(`GET ${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}`, () => { + const reportingSymbol = Symbol('reporting'); + let server: SetupServerReturn['server']; + let usageCounter: IUsageCounter; + let httpSetup: SetupServerReturn['httpSetup']; + let exportTypesRegistry: ExportTypesRegistry; + let core: ReportingCore; + let mockSetupDeps: ReportingInternalSetup; + let mockStartDeps: ReportingInternalStart; + let mockEsClient: ElasticsearchClientMock; + let stream: jest.Mocked; + + const getHits = (...sources: any) => { + return { + hits: { + hits: sources.map((source: object) => ({ _source: source })), + }, + } as estypes.SearchResponseBody; + }; + + const getCompleteHits = ({ + jobType = 'unencodedJobType', + outputContentType = 'text/plain', + title = '', + } = {}) => { + return getHits({ + jobtype: jobType, + status: 'completed', + output: { content_type: outputContentType }, + payload: { title }, + }) as estypes.SearchResponseBody; + }; + + const mockConfigSchema = createMockConfigSchema({ roles: { enabled: false } }); + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(reportingSymbol)); + httpSetup.registerRouteHandlerContext( + reportingSymbol, + 'reporting', + () => reportingMock.createStart() + ); + + mockSetupDeps = createMockPluginSetup({ + security: { + license: { isEnabled: () => true }, + }, + router: httpSetup.createRouter(''), + }); + + mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), + }, + security: { + authc: { + getCurrentUser: () => ({ id: '123', roles: ['superuser'], username: 'Tom Riddle' }), + }, + }, + }, + mockConfigSchema + ); + + core = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps); + + usageCounter = { + incrementCounter: jest.fn(), + }; + core.getUsageCounter = jest.fn().mockReturnValue(usageCounter); + + exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry.register({ + id: 'unencoded', + jobType: 'unencodedJobType', + jobContentExtension: 'csv', + validLicenses: ['basic', 'gold'], + } as ExportType); + core.getExportTypesRegistry = () => exportTypesRegistry; + + mockEsClient = (await core.getEsClient()).asInternalUser as typeof mockEsClient; + stream = new Readable({ + read() { + this.push('test'); + this.push(null); + }, + }) as typeof stream; + stream.end = jest.fn().mockImplementation((_name, _encoding, callback) => { + callback(); + }); + + (getContentStream as jest.MockedFunction).mockResolvedValue(stream); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('fails on malformed download IDs', async () => { + mockEsClient.search.mockResponseOnce(getHits()); + registerJobInfoRoutesPublic(core); + + await server.start(); + + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/1`) + .expect(400) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot( + '"[request params.docId]: value has length [1] but it must have a minimum length of [3]."' + ) + ); + }); + + it('fails on unauthenticated users', async () => { + mockStartDeps = await createMockPluginStart( + { + licensing: { + ...licensingMock.createStart(), + license$: new BehaviorSubject({ isActive: true, isAvailable: true, type: 'gold' }), + }, + security: { authc: { getCurrentUser: () => undefined } }, + }, + mockConfigSchema + ); + core = await createMockReportingCore(mockConfigSchema, mockSetupDeps, mockStartDeps); + registerJobInfoRoutesPublic(core); + + await server.start(); + + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/dope`) + .expect(401) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you aren't authenticated"`) + ); + }); + + it('returns 404 if job not found', async () => { + mockEsClient.search.mockResponseOnce(getHits()); + registerJobInfoRoutesPublic(core); + + await server.start(); + + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/poo`) + .expect(404); + }); + + it('returns a 403 if not a valid job type', async () => { + mockEsClient.search.mockResponseOnce( + getHits({ + jobtype: 'invalidJobType', + payload: { title: 'invalid!' }, + }) + ); + registerJobInfoRoutesPublic(core); + + await server.start(); + + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/poo`) + .expect(403); + }); + + it('when a job is incomplete', async () => { + mockEsClient.search.mockResponseOnce( + getHits({ + jobtype: 'unencodedJobType', + status: 'pending', + payload: { title: 'incomplete!' }, + }) + ); + registerJobInfoRoutesPublic(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) + .expect(503) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Retry-After', '30') + .then(({ text }) => expect(text).toEqual('pending')); + }); + + it('when a job fails', async () => { + mockEsClient.search.mockResponse( + getHits({ + jobtype: 'unencodedJobType', + status: 'failed', + output: { content: 'job failure message' }, + payload: { title: 'failing job!' }, + }) + ); + registerJobInfoRoutesPublic(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) + .expect(500) + .expect('Content-Type', 'application/json; charset=utf-8') + .then(({ body }) => + expect(body.message).toEqual('Reporting generation failed: job failure message') + ); + }); + + describe('successful downloads', () => { + it('when a known job-type is complete', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutesPublic(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) + .expect(200) + .expect('Content-Type', 'text/csv; charset=utf-8') + .expect('content-disposition', 'attachment; filename=report.csv'); + }); + }); + + describe('usage counters', () => { + it('increments the download api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutesPublic(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .get(`${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/dank`) + .expect(200) + .expect('Content-Type', 'text/csv; charset=utf-8') + .expect('content-disposition', 'attachment; filename=report.csv'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: `get ${PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX}/{docId}:unencodedJobType`, + counterType: 'reportingApi', + }); + }); + + it('increments the delete api counter', async () => { + mockEsClient.search.mockResponseOnce(getCompleteHits()); + registerJobInfoRoutesPublic(core); + + await server.start(); + await supertest(httpSetup.server.listener) + .delete(`/api/reporting/jobs/delete/dank`) + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8'); + + expect(usageCounter.incrementCounter).toHaveBeenCalledTimes(1); + expect(usageCounter.incrementCounter).toHaveBeenCalledWith({ + counterName: 'delete /api/reporting/jobs/delete/{docId}:unencodedJobType', + counterType: 'reportingApi', + }); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/public/jobs.ts b/x-pack/plugins/reporting/server/routes/public/jobs.ts new file mode 100644 index 0000000000000..7b56e4393a31a --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/public/jobs.ts @@ -0,0 +1,53 @@ +/* + * 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 { ROUTE_TAG_CAN_REDIRECT } from '@kbn/security-plugin/server'; +import { ReportingCore } from '../..'; +import { PUBLIC_ROUTES } from '../../../common/constants'; +import { authorizedUserPreRouting } from '../common'; +import { commonJobsRouteHandlerFactory } from '../common/jobs'; + +export function registerJobInfoRoutesPublic(reporting: ReportingCore) { + const setupDeps = reporting.getPluginSetupDeps(); + const { router } = setupDeps; + + // use common route handlers that are shared for public and internal routes + const jobHandlers = commonJobsRouteHandlerFactory(reporting); + + const registerPublicDownloadReport = () => { + // trigger a download of the output from a job + const path = PUBLIC_ROUTES.JOBS.DOWNLOAD_PREFIX + '/{docId}'; + router.get( + { + path, + validate: jobHandlers.validate, + options: { tags: [ROUTE_TAG_CAN_REDIRECT], access: 'public' }, + }, + authorizedUserPreRouting(reporting, async (user, context, req, res) => { + return jobHandlers.handleDownloadReport({ path, user, context, req, res }); + }) + ); + }; + + const registerPublicDeleteReport = () => { + // allow a report to be deleted + const path = PUBLIC_ROUTES.JOBS.DELETE_PREFIX + '/{docId}'; + router.delete( + { + path, + validate: jobHandlers.validate, + options: { access: 'public' }, + }, + authorizedUserPreRouting(reporting, async (user, context, req, res) => { + return jobHandlers.handleDeleteReport({ path, user, context, req, res }); + }) + ); + }; + + registerPublicDownloadReport(); + registerPublicDeleteReport(); +} diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index e6b3e0b81e9d4..05e62e6251e19 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -39,6 +39,7 @@ "@kbn/generate-csv", "@kbn/reporting-common", "@kbn/saved-search-plugin", + "@kbn/core-http-router-server-internal", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts index 3941037733c70..f71efb61aeb2c 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/download_csv_dashboard.ts @@ -27,7 +27,7 @@ export default function ({ getService }: FtrProviderContext) { const generateAPI = { getCSVFromSearchSource: async (job: JobParamsDownloadCSV) => { return await supertestSvc - .post(`/api/reporting/v1/generate/immediate/csv_searchsource`) + .post(`/internal/reporting/generate/immediate/csv_searchsource`) .set('kbn-xsrf', 'xxx') .send(job); }, diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts b/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts index 9f0d6ebb15c3e..0d83da710ba23 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/error_codes.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { INTERNAL_ROUTES } from '@kbn/reporting-plugin/common/constants/routes'; import { ReportApiJSON } from '@kbn/reporting-plugin/common/types'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -48,7 +49,7 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); const jobInfo = await supertest - .get('/api/reporting/jobs/info/kraz4j94154g0763b583rc37') + .get(INTERNAL_ROUTES.JOBS.INFO_PREFIX + '/kraz4j94154g0763b583rc37') .auth('test_user', 'changeme'); expect(jobInfo.body.output.warnings).to.eql([ diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts index db131130f40ea..6f71974364942 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { ILM_POLICY_NAME } from '@kbn/reporting-plugin/common/constants'; +import { INTERNAL_ROUTES } from '@kbn/reporting-plugin/common/constants/routes'; import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -194,13 +195,13 @@ export default function ({ getService }: FtrProviderContext) { try { await supertestWithoutAuth - .put(reportingAPI.routes.API_MIGRATE_ILM_POLICY_URL) + .put(INTERNAL_ROUTES.MIGRATE.MIGRATE_ILM_POLICY) .auth(UNAUTHZD_TEST_USERNAME, UNAUTHZD_TEST_USER_PASSWORD) .set('kbn-xsrf', 'xxx') .expect(404); await supertestWithoutAuth - .get(reportingAPI.routes.API_GET_ILM_POLICY_STATUS) + .get(INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS) .auth(UNAUTHZD_TEST_USERNAME, UNAUTHZD_TEST_USER_PASSWORD) .set('kbn-xsrf', 'xxx') .expect(404); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts deleted file mode 100644 index a79799a4ee850..0000000000000 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/api_counters.ts +++ /dev/null @@ -1,263 +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 expect from '@kbn/expect'; -import { createPdfV2Params, createPngV2Params } from '..'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { UsageStatsPayloadTestFriendly } from '../../../api_integration/services/usage_api'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const log = getService('log'); - const supertest = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - const usageAPI = getService('usageAPI'); - const reportingAPI = getService('reportingAPI'); - - // Failing: See https://github.com/elastic/kibana/issues/134517 - // Failing: See https://github.com/elastic/kibana/issues/149942 - describe.skip(`Usage Counters`, () => { - before(async () => { - await esArchiver.emptyKibanaIndex(); - await reportingAPI.initEcommerce(); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); - }); - - after(async () => { - await reportingAPI.deleteAllReports(); - await reportingAPI.teardownEcommerce(); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/archived_reports'); - }); - - describe('server', function () { - this.tags('skipCloud'); - it('configuration settings of the tests_server', async () => { - const [{ stats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - const usage = stats.stack_stats.kibana.plugins; - expect(usage.kibana_config_usage['xpack.reporting.capture.maxAttempts']).to.be(1); - expect(usage.kibana_config_usage['xpack.reporting.csv.maxSizeBytes']).to.be(6000); - expect(usage.kibana_config_usage['xpack.reporting.roles.enabled']).to.be(false); - }); - }); - - describe('API counters: management', () => { - enum paths { - LIST = '/api/reporting/jobs/list', - COUNT = '/api/reporting/jobs/count', - INFO = '/api/reporting/jobs/info/kraz0qle154g0763b569zz83', - ILM = '/api/reporting/ilm_policy_status', - DIAG_BROWSER = '/api/reporting/diagnose/browser', - DIAG_SCREENSHOT = '/api/reporting/diagnose/screenshot', - } - - let initialStats: UsageStatsPayloadTestFriendly; - let stats: UsageStatsPayloadTestFriendly; - const CALL_COUNT = 3; - - before('call APIs', async () => { - [{ stats: initialStats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - - await Promise.all( - Object.keys(paths).map(async (key) => { - await Promise.all( - [...Array(CALL_COUNT)].map(() => - supertest.get(paths[key as keyof typeof paths]).auth('test_user', 'changeme') - ) - ); - }) - ); - - // wait for events to aggregate into the usage stats - await waitOnAggregation(); - - // determine the result usage count - [{ stats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - }); - - it('job listing', async () => { - const initialCount = getUsageCount(initialStats, `get ${paths.LIST}`); - expect(getUsageCount(stats, `get ${paths.LIST}`)).to.be(CALL_COUNT + initialCount); - }); - - it('job count', async () => { - const initialCount = getUsageCount(initialStats, `get ${paths.COUNT}`); - expect(getUsageCount(stats, `get ${paths.COUNT}`)).to.be(CALL_COUNT + initialCount); - }); - - it('job info', async () => { - const initialCount = getUsageCount( - initialStats, - `get /api/reporting/jobs/info/{docId}:printable_pdf` - ); - expect(getUsageCount(stats, `get /api/reporting/jobs/info/{docId}:printable_pdf`)).to.be( - CALL_COUNT + initialCount - ); - }); - }); - - describe('downloading and deleting', () => { - let initialStats: UsageStatsPayloadTestFriendly; - before('gather initial stats', async () => { - [{ stats: initialStats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - }); - - it('downloading', async () => { - try { - await Promise.all([ - supertest - .get('/api/reporting/jobs/download/kraz0qle154g0763b569zz83') - .auth('test_user', 'changeme'), - supertest - .get('/api/reporting/jobs/download/kraz0vj4154g0763b5curq51') - .auth('test_user', 'changeme'), - supertest - .get('/api/reporting/jobs/download/k9a9rq1i0gpe1457b17s7yc6') - .auth('test_user', 'changeme'), - ]); - } catch (error) { - log.error(error); - } - - log.info(`waiting on internal stats aggregation...`); - await waitOnAggregation(); - log.info(`waiting on aggregation completed.`); - - log.info(`calling getUsageStats...`); - const [{ stats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - const initialCount = getUsageCount( - initialStats, - `get /api/reporting/jobs/download/{docId}:printable_pdf` - ); - expect( - getUsageCount(stats, `get /api/reporting/jobs/download/{docId}:printable_pdf`) - ).to.be(3 + initialCount); - }); - - it('deleting', async () => { - log.info(`sending 1 delete request...`); - - try { - await supertest - .delete('/api/reporting/jobs/delete/krazcyw4156m0763b503j7f9') - .auth('test_user', 'changeme') - .set('kbn-xsrf', 'xxx'); - } catch (error) { - log.error(error); - } - log.info(`delete request completed.`); - - log.info(`waiting on internal stats aggregation...`); - await waitOnAggregation(); - log.info(`waiting on aggregation completed.`); - - log.info(`calling getUsageStats...`); - const [{ stats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - const initialCount = getUsageCount( - initialStats, - `delete /api/reporting/jobs/delete/{docId}:csv_searchsource` - ); - expect( - getUsageCount(stats, `delete /api/reporting/jobs/delete/{docId}:csv_searchsource`) - ).to.be(1 + initialCount); - }); - }); - - describe('API counters: job generation', () => { - let stats: UsageStatsPayloadTestFriendly; - - before(async () => { - // call generation APIs - await Promise.all([ - postCsv(), - postCsv(), - postPng(), - postPng(), - postPdf(), - postPdf(), - postPdf(), - downloadCsv(), - ]); - - await waitOnAggregation(); - - [{ stats }] = await usageAPI.getTelemetryStats({ unencrypted: true }); - }); - - it('PNG', async () => { - expect(getUsageCount(stats, 'post /api/reporting/generate/pngV2')).to.be(2); - }); - - it('PDF', async () => { - expect(getUsageCount(stats, 'post /api/reporting/generate/printablePdfV2')).to.be(3); - }); - - it('CSV', async () => { - expect(getUsageCount(stats, 'post /api/reporting/generate/csv_searchsource')).to.be(2); - }); - - it('Download CSV', async () => { - expect( - getUsageCount(stats, 'post /api/reporting/v1/generate/immediate/csv_searchsource') - ).to.be(1); - }); - }); - - // helpers - const waitOnAggregation = async () => { - await new Promise((resolve) => { - setTimeout(resolve, 8000); - }); - }; - - const getUsageCount = ( - checkUsage: UsageStatsPayloadTestFriendly, - counterName: string - ): number => { - return ( - checkUsage.stack_stats.kibana.plugins.usage_counters.dailyEvents.find( - (item: any) => item.counterName === counterName - )?.total || 0 - ); - }; - - const postCsv = () => - reportingAPI.postJobJSON(`/api/reporting/generate/csv_searchsource`, { - jobParams: - `(browserTimezone:UTC,` + - `columns:!(order_date,category,customer_full_name,taxful_total_price,currency),objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true))` + - `,filter:!((meta:(field:order_date,index:aac3e500-f2c7-11ea-8250-fb138aa491e7,params:()),query:(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-02T12:28:40.866Z'` + - `,lte:'2019-07-18T20:59:57.136Z'))))),index:aac3e500-f2c7-11ea-8250-fb138aa491e7,parent:(filter:!(),highlightAll:!t,index:aac3e500-f2c7-11ea-8250-fb138aa491e7` + - `,query:(language:kuery,query:''),version:!t),sort:!((order_date:desc)),trackTotalHits:!t)` + - `)`, - }); - - const postPng = () => - reportingAPI.postJobJSON(`/api/reporting/generate/pngV2`, { - jobParams: createPngV2Params(1600), - }); - - const postPdf = () => - reportingAPI.postJobJSON(`/api/reporting/generate/printablePdfV2`, { - jobParams: createPdfV2Params(1600), - }); - - const downloadCsv = () => - reportingAPI.downloadCsv( - reportingAPI.REPORTING_USER_USERNAME, - reportingAPI.REPORTING_USER_PASSWORD, - { - searchSource: { - query: { query: '', language: 'kuery' }, - index: '5193f870-d861-11e9-a311-0fa548c5f953', - filter: [], - }, - browserTimezone: 'UTC', - title: 'testfooyu78yt90-', - } - ); - }); -} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts index f43ca39136dcb..5bb4b15eefe7e 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage/index.ts @@ -15,6 +15,5 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./new_jobs')); loadTestFile(require.resolve('./error_codes')); - loadTestFile(require.resolve('./api_counters')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts index 22be1d776e792..30f58524b46f8 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts @@ -6,8 +6,9 @@ */ import expect from '@kbn/expect'; -import { pick } from 'lodash'; +import { INTERNAL_ROUTES } from '@kbn/reporting-plugin/common/constants/routes'; import { ReportApiJSON } from '@kbn/reporting-plugin/common/types'; +import { pick } from 'lodash'; import { FtrProviderContext } from '../ftr_provider_context'; const apiResponseFields = [ @@ -81,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { // call the job count api const { text: countText } = await supertestNoAuth - .get(`/api/reporting/jobs/count`) + .get(INTERNAL_ROUTES.JOBS.COUNT) .set('kbn-xsrf', 'xxx'); expect(countText).to.be('1'); @@ -109,7 +110,7 @@ export default function ({ getService }: FtrProviderContext) { // call the listing api const { text: listText } = await supertestNoAuth - .get(`/api/reporting/jobs/list?page=0&ids=${job.id}`) + .get(`${INTERNAL_ROUTES.JOBS.LIST}?page=0&ids=${job.id}`) .set('kbn-xsrf', 'xxx'); // verify the top item in the list @@ -156,7 +157,7 @@ export default function ({ getService }: FtrProviderContext) { // call the listing api const { text: listText, status } = await supertestNoAuth - .get(`/api/reporting/jobs/list?page=0`) + .get(`${INTERNAL_ROUTES.JOBS.LIST}?page=0`) .set('kbn-xsrf', 'xxx'); expect(status).to.be(200); @@ -203,7 +204,7 @@ export default function ({ getService }: FtrProviderContext) { `); const { text: infoText, status } = await supertestNoAuth - .get(`/api/reporting/jobs/info/${job.id}`) + .get(`${INTERNAL_ROUTES.JOBS.INFO_PREFIX}/${job.id}`) .set('kbn-xsrf', 'xxx'); expect(status).to.be(200); diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index 181af370e42c6..c7dd12cc5adab 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -5,15 +5,12 @@ * 2.0. */ -import rison from '@kbn/rison'; -import { - API_GET_ILM_POLICY_STATUS, - API_MIGRATE_ILM_POLICY_URL, -} from '@kbn/reporting-plugin/common/constants'; +import { INTERNAL_ROUTES } from '@kbn/reporting-plugin/common/constants/routes'; import { JobParamsCSV } from '@kbn/reporting-plugin/server/export_types/csv_searchsource/types'; import { JobParamsDownloadCSV } from '@kbn/reporting-plugin/server/export_types/csv_searchsource_immediate/types'; import { JobParamsPNGDeprecated } from '@kbn/reporting-plugin/server/export_types/png/types'; import { JobParamsPDFDeprecated } from '@kbn/reporting-plugin/server/export_types/printable_pdf/types'; +import rison from '@kbn/rison'; import { FtrProviderContext } from '../ftr_provider_context'; function removeWhitespace(str: string) { @@ -137,7 +134,7 @@ export function createScenarios({ getService }: Pick { return await supertestWithoutAuth - .post(`/api/reporting/v1/generate/immediate/csv_searchsource`) + .post(INTERNAL_ROUTES.DOWNLOAD_CSV) .auth(username, password) .set('kbn-xsrf', 'xxx') .send(job); @@ -200,7 +197,7 @@ export function createScenarios({ getService }: Pick { log.debug('ReportingAPI.checkIlmMigrationStatus'); const { body } = await supertestWithoutAuth - .get(API_GET_ILM_POLICY_STATUS) + .get(INTERNAL_ROUTES.MIGRATE.GET_ILM_POLICY_STATUS) .auth(username, password) .set('kbn-xsrf', 'xxx') .expect(200); @@ -234,7 +231,7 @@ export function createScenarios({ getService }: Pick