From a79046ee3258c66d976e0590554b2f410a98c446 Mon Sep 17 00:00:00 2001 From: Ying Date: Fri, 23 May 2025 09:30:53 -0400 Subject: [PATCH] Adding space id to report template --- .../private/kbn-reporting/common/types.ts | 1 + .../plugins/private/reporting/server/core.ts | 2 +- .../reporting/server/lib/store/report.test.ts | 3 + .../reporting/server/lib/store/report.ts | 4 + .../reporting/server/lib/store/store.test.ts | 1 + .../common/generate/request_handler.test.ts | 1 + .../routes/common/generate/request_handler.ts | 6 +- .../routes/common/jobs/jobs_query.test.ts | 37 +++-- .../server/routes/common/jobs/jobs_query.ts | 16 ++- .../server/routes/internal/management/jobs.ts | 2 +- .../reporting_and_security/index.ts | 1 + .../reporting_and_security/list_jobs.ts | 128 ++++++++++++++++++ .../services/scenarios.ts | 42 +++++- 13 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/list_jobs.ts diff --git a/src/platform/packages/private/kbn-reporting/common/types.ts b/src/platform/packages/private/kbn-reporting/common/types.ts index 0b5f6636ccc41..8eed3f9e1de70 100644 --- a/src/platform/packages/private/kbn-reporting/common/types.ts +++ b/src/platform/packages/private/kbn-reporting/common/types.ts @@ -168,6 +168,7 @@ export interface ReportSource { */ kibana_name?: string; // for troubleshooting kibana_id?: string; // for troubleshooting + space_id?: string; timeout?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.queue.timeout max_attempts?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.capture.maxAttempts started_at?: string; // timestamp in UTC diff --git a/x-pack/platform/plugins/private/reporting/server/core.ts b/x-pack/platform/plugins/private/reporting/server/core.ts index b082dd526e9ef..35d36557f4bf7 100644 --- a/x-pack/platform/plugins/private/reporting/server/core.ts +++ b/x-pack/platform/plugins/private/reporting/server/core.ts @@ -390,7 +390,7 @@ export class ReportingCore { const spaceId = spacesService?.getSpaceId(request); if (spaceId !== DEFAULT_SPACE_ID) { - logger.info(`Request uses Space ID: ${spaceId}`); + logger.debug(`Request uses Space ID: ${spaceId}`); return spaceId; } else { logger.debug(`Request uses default Space`); diff --git a/x-pack/platform/plugins/private/reporting/server/lib/store/report.test.ts b/x-pack/platform/plugins/private/reporting/server/lib/store/report.test.ts index ca1294f663da8..613485587324c 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/store/report.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/store/report.test.ts @@ -21,6 +21,7 @@ describe('Class Report', () => { version: '7.14.0', browserTimezone: 'UTC', }, + space_id: 'a_space', meta: { objectType: 'test' }, timeout: 30000, }); @@ -36,6 +37,7 @@ describe('Class Report', () => { started_at: undefined, status: 'pending', timeout: 30000, + space_id: 'a_space', }); expect(report.toReportTaskJSON()).toMatchObject({ attempts: 0, @@ -55,6 +57,7 @@ describe('Class Report', () => { meta: { objectType: 'test' }, status: 'pending', timeout: 30000, + space_id: 'a_space', }); expect(report._id).toBeDefined(); diff --git a/x-pack/platform/plugins/private/reporting/server/lib/store/report.ts b/x-pack/platform/plugins/private/reporting/server/lib/store/report.ts index 6eb0960aedd93..3f83ac577fb62 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/store/report.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/store/report.ts @@ -35,6 +35,7 @@ export class Report implements Partial { public readonly created_at: ReportSource['created_at']; public readonly created_by: ReportSource['created_by']; public readonly payload: ReportSource['payload']; + public readonly space_id: ReportSource['space_id']; public readonly meta: ReportSource['meta']; @@ -81,6 +82,7 @@ export class Report implements Partial { this.payload = opts.payload; this.kibana_id = opts.kibana_id; this.kibana_name = opts.kibana_name; + this.space_id = opts.space_id; this.jobtype = opts.jobtype; this.max_attempts = opts.max_attempts; this.attempts = opts.attempts || 0; @@ -137,6 +139,7 @@ export class Report implements Partial { started_at: this.started_at, completed_at: this.completed_at, process_expiration: this.process_expiration, + space_id: this.space_id, output: this.output || null, metrics: this.metrics, }; @@ -184,6 +187,7 @@ export class Report implements Partial { queue_time_ms: this.queue_time_ms?.[0], execution_time_ms: this.execution_time_ms?.[0], migration_version: this.migration_version, + space_id: this.space_id, payload: omit(this.payload, 'headers'), output: omit(this.output, 'content'), metrics: this.metrics, diff --git a/x-pack/platform/plugins/private/reporting/server/lib/store/store.test.ts b/x-pack/platform/plugins/private/reporting/server/lib/store/store.test.ts index cece2608f4c48..1f321a66846cf 100644 --- a/x-pack/platform/plugins/private/reporting/server/lib/store/store.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/lib/store/store.test.ts @@ -181,6 +181,7 @@ describe('ReportingStore', () => { }, "process_expiration": undefined, "queue_time_ms": undefined, + "space_id": undefined, "started_at": undefined, "status": "pending", "timeout": 30000, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.test.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.test.ts index 2ded83071d6a3..12216b999045f 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.test.ts @@ -131,6 +131,7 @@ describe('Handle request to generate', () => { "output": null, "process_expiration": undefined, "queue_time_ms": undefined, + "space_id": "default", "started_at": undefined, "status": "pending", "timeout": undefined, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.ts index 2ba63a5d52655..7e0348d7b93cc 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/generate/request_handler.ts @@ -16,6 +16,7 @@ import type { BaseParams } from '@kbn/reporting-common/types'; import { cryptoFactory } from '@kbn/reporting-server'; import rison from '@kbn/rison'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { type Counters, getCounters } from '..'; import type { ReportingCore } from '../../..'; import { checkParamsVersion } from '../../../lib'; @@ -85,6 +86,8 @@ export class RequestHandler { // 3. Create a payload object by calling exportType.createJob(), and adding some automatic parameters const job = await exportType.createJob(jobParams, context, req); + const spaceId = reporting.getSpaceId(req, logger); + const payload = { ...job, headers, @@ -92,7 +95,7 @@ export class RequestHandler { objectType: jobParams.objectType, browserTimezone: jobParams.browserTimezone, version: jobParams.version, - spaceId: reporting.getSpaceId(req, logger), + spaceId, }; // 4. Add the report to ReportingStore to show as pending @@ -102,6 +105,7 @@ export class RequestHandler { created_by: user ? user.username : false, payload, migration_version: jobParams.version, + space_id: spaceId || DEFAULT_SPACE_ID, meta: { // telemetry fields objectType: jobParams.objectType, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.test.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.test.ts index d9fdf01388729..aa1dc81e69101 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.test.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.test.ts @@ -7,13 +7,20 @@ import { set } from '@kbn/safer-lodash-set'; -import { ElasticsearchClient } from '@kbn/core/server'; +import { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { JOB_STATUS } from '@kbn/reporting-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { createMockReportingCore } from '../../../test_helpers'; import { jobsQueryFactory } from './jobs_query'; +const fakeRawRequest = { + headers: { + authorization: `ApiKey skdjtq4u543yt3rhewrh`, + }, + path: '/', +} as unknown as KibanaRequest; + describe('jobsQuery', () => { let client: ReturnType; let jobsQuery: ReturnType; @@ -37,10 +44,11 @@ describe('jobsQuery', () => { }); it('should pass parameters in the request body', async () => { - await jobsQuery.list({ username: 'somebody' }, 1, 10, ['id1', 'id2']); - await jobsQuery.list({ username: 'somebody' }, 1, 10, null); + await jobsQuery.list(fakeRawRequest, { username: 'somebody' }, 1, 10, ['id1', 'id2']); + await jobsQuery.list(fakeRawRequest, { username: 'somebody' }, 1, 10, null); expect(client.search).toHaveBeenCalledTimes(2); + expect(client.search).toHaveBeenNthCalledWith( 1, expect.objectContaining({ @@ -52,6 +60,15 @@ describe('jobsQuery', () => { expect.arrayContaining([ { term: { created_by: 'somebody' } }, { ids: { values: ['id1', 'id2'] } }, + { + bool: { + should: [ + { term: { space_id: 'default' } }, + // also show all reports created before space_id was added + { bool: { must_not: { exists: { field: 'space_id' } } } }, + ], + }, + }, ]) ), }) @@ -70,7 +87,9 @@ describe('jobsQuery', () => { }); it('should return reports list', async () => { - await expect(jobsQuery.list({ username: 'somebody' }, 0, 10, [])).resolves.toEqual( + await expect( + jobsQuery.list(fakeRawRequest, { username: 'somebody' }, 0, 10, []) + ).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ id: 'id1', jobtype: 'pdf' }), expect.objectContaining({ id: 'id2', jobtype: 'csv' }), @@ -81,7 +100,9 @@ describe('jobsQuery', () => { it('should return an empty array when there are no hits', async () => { client.search.mockResponse({} as Awaited>); - await expect(jobsQuery.list({ username: 'somebody' }, 0, 10, [])).resolves.toHaveLength(0); + await expect( + jobsQuery.list(fakeRawRequest, { username: 'somebody' }, 0, 10, []) + ).resolves.toHaveLength(0); }); it('should reject if the report source is missing', async () => { @@ -89,9 +110,9 @@ describe('jobsQuery', () => { set>>({}, 'hits.hits', [{}]) ); - await expect(jobsQuery.list({ username: 'somebody' }, 0, 10, [])).rejects.toBeInstanceOf( - Error - ); + await expect( + jobsQuery.list(fakeRawRequest, { username: 'somebody' }, 0, 10, []) + ).rejects.toBeInstanceOf(Error); }); }); diff --git a/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.ts b/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.ts index b7f1cc6b48661..80cb1869829e4 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/common/jobs/jobs_query.ts @@ -6,11 +6,12 @@ */ import { TransportResult, errors, estypes } from '@elastic/elasticsearch'; -import type { ElasticsearchClient } from '@kbn/core/server'; +import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { JOB_STATUS } from '@kbn/reporting-common'; import type { ReportApiJSON, ReportSource } from '@kbn/reporting-common/types'; import { REPORTING_DATA_STREAM_WILDCARD_WITH_LEGACY } from '@kbn/reporting-server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import type { ReportingCore } from '../../..'; import { Report } from '../../../lib/store'; import { runtimeFieldKeys, runtimeFields } from '../../../lib/store/runtime_fields'; @@ -40,6 +41,7 @@ export type ReportContent = Pick export interface JobsQueryFactory { list( + req: KibanaRequest, user: ReportingUser, page: number, size: number, @@ -73,8 +75,9 @@ export function jobsQueryFactory( } return { - async list(user, page = 0, size = defaultSize, jobIds) { + async list(req, user, page = 0, size = defaultSize, jobIds) { const username = getUsername(user); + const spaceId = reportingCore.getSpaceId(req) || DEFAULT_SPACE_ID; const body = getSearchBody({ size, from: size * page, @@ -85,6 +88,15 @@ export function jobsQueryFactory( must: [ { term: { created_by: username } }, ...(jobIds ? [{ ids: { values: jobIds } }] : []), + { + bool: { + should: [ + { term: { space_id: spaceId } }, + // also show all reports created before space_id was added + { bool: { must_not: { exists: { field: 'space_id' } } } }, + ], + }, + }, ], }, }, diff --git a/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts b/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts index 492d579727c51..1f631f0f01582 100644 --- a/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts +++ b/x-pack/platform/plugins/private/reporting/server/routes/internal/management/jobs.ts @@ -57,7 +57,7 @@ export function registerJobInfoRoutesInternal(reporting: ReportingCore) { const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; - const results = await jobsQuery.list(user, page, size, jobIds); + const results = await jobsQuery.list(req, user, page, size, jobIds); counters.usageCounter(); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 6caffc5d562e1..fedad1bf589fd 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -25,6 +25,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./ilm_migration_apis')); loadTestFile(require.resolve('./security_roles_privileges')); loadTestFile(require.resolve('./spaces')); + loadTestFile(require.resolve('./list_jobs')); // CSV-specific loadTestFile(require.resolve('./csv/csv_v2')); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/list_jobs.ts b/x-pack/test/reporting_api_integration/reporting_and_security/list_jobs.ts new file mode 100644 index 0000000000000..3e517468817db --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/list_jobs.ts @@ -0,0 +1,128 @@ +/* + * 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 { ReportApiJSON } from '@kbn/reporting-common/types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const spacesService = getService('spaces'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const reportingAPI = getService('reportingAPI'); + + describe('List Reports', () => { + const spaceId = 'non_default_space'; + before(async () => { + await spacesService.create({ id: spaceId, name: spaceId }); + await kibanaServer.importExport.load( + `x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce_kibana_non_default_space`, + { space: spaceId } + ); + await reportingAPI.initEcommerce(); + await esArchiver.load('x-pack/test/functional/es_archives/reporting/archived_reports'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/archived_reports'); + await reportingAPI.teardownEcommerce(); + await reportingAPI.deleteAllReports(); + await spacesService.delete(spaceId); + }); + + it('should list reports filtered by current space or legacy reports with no space_id', async () => { + const res1 = await reportingAPI.generatePdf( + reportingAPI.REPORTING_USER_USERNAME, + reportingAPI.REPORTING_USER_PASSWORD, + { + browserTimezone: 'UTC', + title: 'test default-space PDF 1', + layout: { id: 'preserve_layout' }, + locatorParams: [{ id: 'canvas', version: '7.14.0', params: {} }], + objectType: 'dashboard', + version: '7.14.0', + } + ); + expect(res1.status).to.eql(200); + const reportDefaultSpace1Id = res1.body.job.id; + + const res2 = await reportingAPI.generatePdf( + reportingAPI.REPORTING_USER_USERNAME, + reportingAPI.REPORTING_USER_PASSWORD, + { + browserTimezone: 'UTC', + title: 'test default-space PDF 2', + layout: { id: 'preserve_layout' }, + locatorParams: [{ id: 'canvas', version: '7.14.0', params: {} }], + objectType: 'visualization', + version: '7.14.0', + } + ); + expect(res2.status).to.eql(200); + const reportDefaultSpace2Id = res2.body.job.id; + + const res3 = await reportingAPI.generatePdf( + reportingAPI.REPORTING_USER_USERNAME, + reportingAPI.REPORTING_USER_PASSWORD, + { + browserTimezone: 'UTC', + title: 'test custom space PDF', + layout: { id: 'preserve_layout' }, + locatorParams: [{ id: 'canvas', version: '7.14.0', params: {} }], + objectType: 'dashboard', + version: '7.14.0', + }, + spaceId + ); + expect(res3.status).to.eql(200); + const reportNonDefaultSpace1Id = res3.body.job.id; + + const listDefault = await reportingAPI.listReports( + reportingAPI.REPORTING_USER_USERNAME, + reportingAPI.REPORTING_USER_PASSWORD + ); + + const reportsInDefaultSpace = listDefault.body; + expect(reportsInDefaultSpace).to.have.length(4); + + const reportsInDefaultSpaceIds = reportsInDefaultSpace.map( + (report: ReportApiJSON) => report.id + ); + // listing should contain reports from the default space and legacy reports with no space_id + expect(reportsInDefaultSpaceIds).to.contain(reportDefaultSpace1Id); + expect(reportsInDefaultSpaceIds).to.contain(reportDefaultSpace2Id); + expect(reportsInDefaultSpaceIds).to.contain('krb7arhe164k0763b50bjm31'); + expect(reportsInDefaultSpaceIds).to.contain('kraz9db6154g0763b5141viu'); + + // listing should not contain reports from custom space + expect(reportsInDefaultSpaceIds).not.to.contain(reportNonDefaultSpace1Id); + + const listNonDefault = await reportingAPI.listReports( + reportingAPI.REPORTING_USER_USERNAME, + reportingAPI.REPORTING_USER_PASSWORD, + spaceId + ); + + const reportsInNonDefaultSpace = listNonDefault.body; + expect(reportsInNonDefaultSpace).to.have.length(3); + + const reportsInNonDefaultSpaceIds = reportsInNonDefaultSpace.map( + (report: ReportApiJSON) => report.id + ); + + // listing should contain reports from the custom space and legacy reports with no space_id + expect(reportsInNonDefaultSpaceIds).to.contain(reportNonDefaultSpace1Id); + expect(reportsInNonDefaultSpaceIds).to.contain('krb7arhe164k0763b50bjm31'); + expect(reportsInNonDefaultSpaceIds).to.contain('kraz9db6154g0763b5141viu'); + + // listing should not contain reports from default space + expect(reportsInNonDefaultSpaceIds).not.to.contain(reportDefaultSpace1Id); + expect(reportsInNonDefaultSpaceIds).not.to.contain(reportDefaultSpace2Id); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index 0800647d2abef..559a2fd5ba434 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -142,18 +142,30 @@ export function createScenarios({ getService }: Pick { + const generatePdf = async ( + username: string, + password: string, + job: JobParamsPDFV2, + spaceId: string = 'default' + ) => { const jobParams = rison.encode(job); + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; return await supertestWithoutAuth - .post(`/api/reporting/generate/printablePdfV2`) + .post(`${spacePrefix}/api/reporting/generate/printablePdfV2`) .auth(username, password) .set('kbn-xsrf', 'xxx') .send({ jobParams }); }; - const generatePng = async (username: string, password: string, job: JobParamsPNGV2) => { + const generatePng = async ( + username: string, + password: string, + job: JobParamsPNGV2, + spaceId: string = 'default' + ) => { const jobParams = rison.encode(job); + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; return await supertestWithoutAuth - .post(`/api/reporting/generate/pngV2`) + .post(`${spacePrefix}/api/reporting/generate/pngV2`) .auth(username, password) .set('kbn-xsrf', 'xxx') .send({ jobParams }); @@ -161,12 +173,13 @@ export function createScenarios({ getService }: Pick { const jobParams = rison.encode(job); - + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; return await supertestWithoutAuth - .post(`/api/reporting/generate/csv_searchsource`) + .post(`${spacePrefix}/api/reporting/generate/csv_searchsource`) .auth(username, password) .set('kbn-xsrf', 'xxx') .send({ jobParams }); @@ -213,6 +226,20 @@ export function createScenarios({ getService }: Pick { + const spacePrefix = spaceId !== 'default' ? `/s/${spaceId}` : ''; + return await supertestWithoutAuth + .get(`${spacePrefix}${INTERNAL_ROUTES.JOBS.LIST}?page=0`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + }; + const deleteAllReports = async () => { log.debug('ReportingAPI.deleteAllReports'); @@ -281,6 +308,7 @@ export function createScenarios({ getService }: Pick