From 84c7dbe5c04b7571898bb9a103440f9dbef82a8e Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 12 Nov 2024 15:08:40 -0500 Subject: [PATCH] feat(slo): Assert user has correct source index privileges when creating, updating or reseting an SLO (#199233) --- .../__snapshots__/reset_slo.test.ts.snap | 22 +++--- .../slo/server/services/create_slo.test.ts | 36 +++++++++ .../slo/server/services/create_slo.ts | 29 +++---- .../slo/server/services/mocks/index.ts | 2 +- .../slo/server/services/reset_slo.test.ts | 75 ++++++++++++------- .../slo/server/services/reset_slo.ts | 13 ++-- .../server/services/slo_repository.test.ts | 4 +- .../slo/server/services/slo_repository.ts | 6 +- .../slo/server/services/update_slo.test.ts | 39 +++++++++- .../slo/server/services/update_slo.ts | 12 +-- ...ected_indicator_source_index_privileges.ts | 24 ++++++ 11 files changed, 194 insertions(+), 68 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts diff --git a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap index 90690a4989586..ec81df9f08fdd 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap +++ b/x-pack/plugins/observability_solution/slo/server/services/__snapshots__/reset_slo.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSLO resets all associated resources 1`] = ` +exports[`ResetSLO happy path resets all associated resources 1`] = ` [MockFunction] { "calls": Array [ Array [ @@ -16,7 +16,7 @@ exports[`ResetSLO resets all associated resources 1`] = ` } `; -exports[`ResetSLO resets all associated resources 2`] = ` +exports[`ResetSLO happy path resets all associated resources 2`] = ` [MockFunction] { "calls": Array [ Array [ @@ -32,7 +32,7 @@ exports[`ResetSLO resets all associated resources 2`] = ` } `; -exports[`ResetSLO resets all associated resources 3`] = ` +exports[`ResetSLO happy path resets all associated resources 3`] = ` [MockFunction] { "calls": Array [ Array [ @@ -48,7 +48,7 @@ exports[`ResetSLO resets all associated resources 3`] = ` } `; -exports[`ResetSLO resets all associated resources 4`] = ` +exports[`ResetSLO happy path resets all associated resources 4`] = ` [MockFunction] { "calls": Array [ Array [ @@ -64,7 +64,7 @@ exports[`ResetSLO resets all associated resources 4`] = ` } `; -exports[`ResetSLO resets all associated resources 5`] = ` +exports[`ResetSLO happy path resets all associated resources 5`] = ` [MockFunction] { "calls": Array [ Array [ @@ -115,7 +115,7 @@ exports[`ResetSLO resets all associated resources 5`] = ` } `; -exports[`ResetSLO resets all associated resources 6`] = ` +exports[`ResetSLO happy path resets all associated resources 6`] = ` [MockFunction] { "calls": Array [ Array [ @@ -178,7 +178,7 @@ exports[`ResetSLO resets all associated resources 6`] = ` } `; -exports[`ResetSLO resets all associated resources 7`] = ` +exports[`ResetSLO happy path resets all associated resources 7`] = ` [MockFunction] { "calls": Array [ Array [ @@ -194,7 +194,7 @@ exports[`ResetSLO resets all associated resources 7`] = ` } `; -exports[`ResetSLO resets all associated resources 8`] = ` +exports[`ResetSLO happy path resets all associated resources 8`] = ` [MockFunction] { "calls": Array [ Array [ @@ -542,7 +542,7 @@ exports[`ResetSLO resets all associated resources 8`] = ` } `; -exports[`ResetSLO resets all associated resources 9`] = ` +exports[`ResetSLO happy path resets all associated resources 9`] = ` [MockFunction] { "calls": Array [ Array [ @@ -605,7 +605,7 @@ exports[`ResetSLO resets all associated resources 9`] = ` } `; -exports[`ResetSLO resets all associated resources 10`] = ` +exports[`ResetSLO happy path resets all associated resources 10`] = ` [MockFunction] { "calls": Array [ Array [ @@ -621,7 +621,7 @@ exports[`ResetSLO resets all associated resources 10`] = ` } `; -exports[`ResetSLO resets all associated resources 11`] = ` +exports[`ResetSLO happy path resets all associated resources 11`] = ` [MockFunction] { "calls": Array [ Array [ diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts index 84edf74f18aa5..342b1a4190748 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.test.ts @@ -23,6 +23,7 @@ import { } from './mocks'; import { SLORepository } from './slo_repository'; import { TransformManager } from './transform_manager'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; describe('CreateSLO', () => { let mockEsClient: ElasticsearchClientMock; @@ -55,11 +56,19 @@ describe('CreateSLO', () => { }); describe('happy path', () => { + beforeEach(() => { + mockRepository.exists.mockResolvedValue(false); + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + it('calls the expected services', async () => { const sloParams = createSLOParams({ id: 'unique-id', indicator: createAPMTransactionErrorRateIndicator(), }); + mockTransformManager.install.mockResolvedValue('slo-id-revision'); mockSummaryTransformManager.install.mockResolvedValue('slo-summary-id-revision'); @@ -157,6 +166,33 @@ describe('CreateSLO', () => { }); describe('unhappy path', () => { + beforeEach(() => { + mockRepository.exists.mockResolvedValue(false); + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('throws a SLOIdConflict error when the SLO already exists', async () => { + mockRepository.exists.mockResolvedValue(true); + + const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + + await expect(createSLO.execute(sloParams)).rejects.toThrowError(/SLO \[.*\] already exists/); + }); + + it('throws a SecurityException error when the user does not have the required privileges', async () => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + + const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); + + await expect(createSLO.execute(sloParams)).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [metrics-apm*]" + ); + }); + it('rollbacks completed operations when rollup transform install fails', async () => { mockTransformManager.install.mockRejectedValue(new Error('Rollup transform install error')); const sloParams = createSLOParams({ indicator: createAPMTransactionErrorRateIndicator() }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts index 3845ec2ddbd4f..e7c09c352bd66 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/create_slo.ts @@ -4,30 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { IScopedClusterClient } from '@kbn/core/server'; +import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient, IBasePath, Logger } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { ALL_VALUE, CreateSLOParams, CreateSLOResponse } from '@kbn/slo-schema'; import { asyncForEach } from '@kbn/std'; import { v4 as uuidv4 } from 'uuid'; -import { IngestPutPipelineRequest } from '@elastic/elasticsearch/lib/api/types'; import { + SLO_MODEL_VERSION, + SLO_SUMMARY_TEMP_INDEX_NAME, getSLOPipelineId, getSLOSummaryPipelineId, getSLOSummaryTransformId, getSLOTransformId, - SLO_MODEL_VERSION, - SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; import { Duration, DurationUnit, SLODefinition } from '../domain/models'; import { validateSLO } from '../domain/services'; -import { SecurityException, SLOIdConflict } from '../errors'; +import { SLOIdConflict, SecurityException } from '../errors'; import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; import { getTransformQueryComposite } from './utils/get_transform_compite_query'; export class CreateSLO { @@ -46,16 +46,11 @@ export class CreateSLO { const slo = this.toSLO(params); validateSLO(slo); - const rollbackOperations = []; - - const sloAlreadyExists = await this.repository.checkIfSLOExists(slo); - - if (sloAlreadyExists) { - throw new SLOIdConflict(`SLO [${slo.id}] already exists`); - } + await this.assertSLOInexistant(slo); + await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient); + const rollbackOperations = []; const createPromise = this.repository.create(slo); - rollbackOperations.push(() => this.repository.deleteById(slo.id, true)); const rollupTransformId = getSLOTransformId(slo.id, slo.revision); @@ -123,6 +118,12 @@ export class CreateSLO { return this.toResponse(slo); } + private async assertSLOInexistant(slo: SLODefinition) { + const exists = await this.repository.exists(slo.id); + if (exists) { + throw new SLOIdConflict(`SLO [${slo.id}] already exists`); + } + } async createTempSummaryDocument(slo: SLODefinition) { return await retryTransientEsErrors( () => diff --git a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts index dc458fcdb813e..ab8230cfec463 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/mocks/index.ts @@ -48,7 +48,7 @@ const createSLORepositoryMock = (): jest.Mocked => { findAllByIds: jest.fn(), deleteById: jest.fn(), search: jest.fn(), - checkIfSLOExists: jest.fn(), + exists: jest.fn(), }; }; diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts index 4e66d992b46cd..ab806f221a888 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.test.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; import { + ElasticsearchClientMock, elasticsearchServiceMock, httpServiceMock, loggingSystemMock, ScopedClusterClientMock, } from '@kbn/core/server/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; - import { SLO_MODEL_VERSION } from '../../common/constants'; import { createSLO } from './fixtures/slo'; import { @@ -31,7 +31,7 @@ describe('ResetSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; let mockSummaryTransformManager: jest.Mocked; - let mockEsClient: jest.Mocked; + let mockEsClient: ElasticsearchClientMock; let mockScopedClusterClient: ScopedClusterClientMock; let loggerMock: jest.Mocked; let resetSLO: ResetSLO; @@ -60,37 +60,62 @@ describe('ResetSLO', () => { jest.useRealTimers(); }); - it('resets all associated resources', async () => { - const slo = createSLO({ id: 'irrelevant', version: 1 }); - mockRepository.findById.mockResolvedValueOnce(slo); - mockRepository.update.mockImplementation((v) => Promise.resolve(v)); + describe('happy path', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('resets all associated resources', async () => { + const slo = createSLO({ id: 'irrelevant', version: 1 }); + mockRepository.findById.mockResolvedValueOnce(slo); + mockRepository.update.mockImplementation((v) => Promise.resolve(v)); + + await resetSLO.execute(slo.id); + + // delete existing resources and data + expect(mockSummaryTransformManager.stop).toMatchSnapshot(); + expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); - await resetSLO.execute(slo.id); + expect(mockTransformManager.stop).toMatchSnapshot(); + expect(mockTransformManager.uninstall).toMatchSnapshot(); - // delete existing resources and data - expect(mockSummaryTransformManager.stop).toMatchSnapshot(); - expect(mockSummaryTransformManager.uninstall).toMatchSnapshot(); + expect(mockEsClient.deleteByQuery).toMatchSnapshot(); - expect(mockTransformManager.stop).toMatchSnapshot(); - expect(mockTransformManager.uninstall).toMatchSnapshot(); + // install resources + expect(mockSummaryTransformManager.install).toMatchSnapshot(); + expect(mockSummaryTransformManager.start).toMatchSnapshot(); - expect(mockEsClient.deleteByQuery).toMatchSnapshot(); + expect(mockScopedClusterClient.asSecondaryAuthUser.ingest.putPipeline).toMatchSnapshot(); - // install resources - expect(mockSummaryTransformManager.install).toMatchSnapshot(); - expect(mockSummaryTransformManager.start).toMatchSnapshot(); + expect(mockTransformManager.install).toMatchSnapshot(); + expect(mockTransformManager.start).toMatchSnapshot(); - expect(mockScopedClusterClient.asSecondaryAuthUser.ingest.putPipeline).toMatchSnapshot(); + expect(mockEsClient.index).toMatchSnapshot(); - expect(mockTransformManager.install).toMatchSnapshot(); - expect(mockTransformManager.start).toMatchSnapshot(); + expect(mockRepository.update).toHaveBeenCalledWith({ + ...slo, + version: SLO_MODEL_VERSION, + updatedAt: expect.anything(), + }); + }); + }); + + describe('unhappy path', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + }); - expect(mockEsClient.index).toMatchSnapshot(); + it('throws a SecurityException error when the user does not have the required privileges', async () => { + const slo = createSLO({ id: 'irrelevant', version: 1 }); + mockRepository.findById.mockResolvedValueOnce(slo); - expect(mockRepository.update).toHaveBeenCalledWith({ - ...slo, - version: SLO_MODEL_VERSION, - updatedAt: expect.anything(), + await expect(resetSLO.execute(slo.id)).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [metrics-apm*]" + ); }); }); }); diff --git a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts index 634f02c8f6f90..c9da382c2d6ce 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/reset_slo.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { ElasticsearchClient, IBasePath, Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { resetSLOResponseSchema } from '@kbn/slo-schema'; import { - getSLOPipelineId, - getSLOSummaryPipelineId, - getSLOSummaryTransformId, - getSLOTransformId, SLO_DESTINATION_INDEX_PATTERN, SLO_MODEL_VERSION, SLO_SUMMARY_DESTINATION_INDEX_PATTERN, SLO_SUMMARY_TEMP_INDEX_NAME, + getSLOPipelineId, + getSLOSummaryPipelineId, + getSLOSummaryTransformId, + getSLOTransformId, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; @@ -23,6 +23,7 @@ import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; export class ResetSLO { constructor( @@ -39,6 +40,8 @@ export class ResetSLO { public async execute(sloId: string) { const slo = await this.repository.findById(sloId); + await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient); + const summaryTransformId = getSLOSummaryTransformId(slo.id, slo.revision); await this.summaryTransformManager.stop(summaryTransformId); await this.summaryTransformManager.uninstall(summaryTransformId); diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts index 243b2b5e9958b..633ee359ca53f 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.test.ts @@ -88,7 +88,7 @@ describe('KibanaSavedObjectsSLORepository', () => { soClientMock.create.mockResolvedValueOnce(aStoredSLO(slo)); const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock); - await repository.checkIfSLOExists(slo); + await repository.exists(slo.id); expect(soClientMock.find).toHaveBeenCalledWith({ type: SO_SLO_TYPE, @@ -117,7 +117,7 @@ describe('KibanaSavedObjectsSLORepository', () => { soClientMock.find.mockResolvedValueOnce(soFindResponse([slo])); const repository = new KibanaSavedObjectsSLORepository(soClientMock, loggerMock); - await expect(await repository.checkIfSLOExists(slo)).toEqual(true); + await expect(await repository.exists(slo.id)).toEqual(true); expect(soClientMock.find).toHaveBeenCalledWith({ type: SO_SLO_TYPE, perPage: 0, diff --git a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts index 35266ea993bfb..4f9cf439e8ed1 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/slo_repository.ts @@ -15,7 +15,7 @@ import { SLONotFound } from '../errors'; import { SO_SLO_TYPE } from '../saved_objects'; export interface SLORepository { - checkIfSLOExists(slo: SLODefinition): Promise; + exists(id: string): Promise; create(slo: SLODefinition): Promise; update(slo: SLODefinition): Promise; findAllByIds(ids: string[]): Promise; @@ -31,11 +31,11 @@ export interface SLORepository { export class KibanaSavedObjectsSLORepository implements SLORepository { constructor(private soClient: SavedObjectsClientContract, private logger: Logger) {} - async checkIfSLOExists(slo: SLODefinition) { + async exists(id: string) { const findResponse = await this.soClient.find({ type: SO_SLO_TYPE, perPage: 0, - filter: `slo.attributes.id:(${slo.id})`, + filter: `slo.attributes.id:(${id})`, }); return findResponse.total > 0; diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts index dccfe5f97d633..9417e4779a5e2 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ElasticsearchClient } from '@kbn/core/server'; import { + ElasticsearchClientMock, elasticsearchServiceMock, httpServiceMock, loggingSystemMock, @@ -16,6 +16,7 @@ import { MockedLogger } from '@kbn/logging-mocks'; import { UpdateSLOParams } from '@kbn/slo-schema'; import { cloneDeep, omit, pick } from 'lodash'; +import { SecurityHasPrivilegesResponse } from '@elastic/elasticsearch/lib/api/types'; import { getSLOSummaryTransformId, getSLOTransformId, @@ -42,7 +43,7 @@ import { UpdateSLO } from './update_slo'; describe('UpdateSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; - let mockEsClient: jest.Mocked; + let mockEsClient: ElasticsearchClientMock; let mockScopedClusterClient: ScopedClusterClientMock; let mockLogger: jest.Mocked; let mockSummaryTransformManager: jest.Mocked; @@ -69,6 +70,8 @@ describe('UpdateSLO', () => { describe('when the update payload does not change the original SLO', () => { function expectNoCallsToAnyMocks() { + expect(mockEsClient.security.hasPrivileges).not.toBeCalled(); + expect(mockTransformManager.stop).not.toBeCalled(); expect(mockTransformManager.uninstall).not.toBeCalled(); expect(mockTransformManager.install).not.toBeCalled(); @@ -192,6 +195,12 @@ describe('UpdateSLO', () => { }); describe('handles breaking changes', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + it('consideres a settings change as a breaking change', async () => { const slo = createSLO(); mockRepository.findById.mockResolvedValueOnce(slo); @@ -302,6 +311,32 @@ describe('UpdateSLO', () => { }); describe('when error happens during the update', () => { + beforeEach(() => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: true, + } as SecurityHasPrivilegesResponse); + }); + + it('throws a SecurityException error when the user does not have the required privileges on the source index', async () => { + mockEsClient.security.hasPrivileges.mockResolvedValue({ + has_all_requested: false, + } as SecurityHasPrivilegesResponse); + + const originalSlo = createSLO({ + id: 'original-id', + indicator: createAPMTransactionErrorRateIndicator(), + }); + mockRepository.findById.mockResolvedValueOnce(originalSlo); + + const newIndicator = createAPMTransactionErrorRateIndicator({ index: 'new-index-*' }); + + await expect( + updateSLO.execute(originalSlo.id, { indicator: newIndicator }) + ).rejects.toThrowError( + "Missing ['read', 'view_index_metadata'] privileges on the source index [new-index-*]" + ); + }); + it('restores the previous SLO definition when updated summary transform install fails', async () => { const originalSlo = createSLO({ id: 'original-id', diff --git a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts index 9418bfb1ea91a..d1dfb2e70e00c 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/update_slo.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { ElasticsearchClient, IBasePath, Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ElasticsearchClient, IBasePath, IScopedClusterClient, Logger } from '@kbn/core/server'; import { UpdateSLOParams, UpdateSLOResponse, updateSLOResponseSchema } from '@kbn/slo-schema'; import { asyncForEach } from '@kbn/std'; import { isEqual, pick } from 'lodash'; import { + SLO_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + SLO_SUMMARY_TEMP_INDEX_NAME, getSLOPipelineId, getSLOSummaryPipelineId, getSLOSummaryTransformId, getSLOTransformId, - SLO_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_DESTINATION_INDEX_PATTERN, - SLO_SUMMARY_TEMP_INDEX_NAME, } from '../../common/constants'; import { getSLOPipelineTemplate } from '../assets/ingest_templates/slo_pipeline_template'; import { getSLOSummaryPipelineTemplate } from '../assets/ingest_templates/slo_summary_pipeline_template'; @@ -27,6 +27,7 @@ import { retryTransientEsErrors } from '../utils/retry'; import { SLORepository } from './slo_repository'; import { createTempSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary'; import { TransformManager } from './transform_manager'; +import { assertExpectedIndicatorSourceIndexPrivileges } from './utils/assert_expected_indicator_source_index_privileges'; export class UpdateSLO { constructor( @@ -68,8 +69,9 @@ export class UpdateSLO { validateSLO(updatedSlo); - const rollbackOperations = []; + await assertExpectedIndicatorSourceIndexPrivileges(updatedSlo, this.esClient); + const rollbackOperations = []; await this.repository.update(updatedSlo); rollbackOperations.push(() => this.repository.update(originalSlo)); diff --git a/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts b/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts new file mode 100644 index 0000000000000..d3633865eafb8 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/server/services/utils/assert_expected_indicator_source_index_privileges.ts @@ -0,0 +1,24 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core/server'; +import { SLODefinition } from '../../domain/models'; +import { SecurityException } from '../../errors'; + +export async function assertExpectedIndicatorSourceIndexPrivileges( + slo: SLODefinition, + esClient: ElasticsearchClient +) { + const privileges = await esClient.security.hasPrivileges({ + index: [{ names: slo.indicator.params.index, privileges: ['read', 'view_index_metadata'] }], + }); + if (!privileges.has_all_requested) { + throw new SecurityException( + `Missing ['read', 'view_index_metadata'] privileges on the source index [${slo.indicator.params.index}]` + ); + } +}