[8.x] feat(slo): Assert user has correct source index privileges when creating, updating or reseting an SLO (#199233) (#199875)

# Backport

This will backport the following commits from `main` to `8.x`:
- [feat(slo): Assert user has correct source index privileges when
creating, updating or reseting an SLO
(#199233)](https://github.com/elastic/kibana/pull/199233)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Kevin
Delemme","email":"kevin.delemme@elastic.co"},"sourceCommit":{"committedDate":"2024-11-12T20:08:40Z","message":"feat(slo):
Assert user has correct source index privileges when creating, updating
or reseting an SLO
(#199233)","sha":"da85efe5093c148d4b91bcd3e21fd93c9f182a4f","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management","v8.17.0"],"title":"feat(slo):
Assert user has correct source index privileges when creating, updating
or reseting an
SLO","number":199233,"url":"https://github.com/elastic/kibana/pull/199233","mergeCommit":{"message":"feat(slo):
Assert user has correct source index privileges when creating, updating
or reseting an SLO
(#199233)","sha":"da85efe5093c148d4b91bcd3e21fd93c9f182a4f"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199233","number":199233,"mergeCommit":{"message":"feat(slo):
Assert user has correct source index privileges when creating, updating
or reseting an SLO
(#199233)","sha":"da85efe5093c148d4b91bcd3e21fd93c9f182a4f"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-11-21 01:41:52 +11:00 committed by GitHub
parent 33263b25c2
commit 4a0ccdb6c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 194 additions and 68 deletions

View file

@ -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 [

View file

@ -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() });

View file

@ -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);
await this.assertSLOInexistant(slo);
await assertExpectedIndicatorSourceIndexPrivileges(slo, this.esClient);
const rollbackOperations = [];
const sloAlreadyExists = await this.repository.checkIfSLOExists(slo);
if (sloAlreadyExists) {
throw new SLOIdConflict(`SLO [${slo.id}] already exists`);
}
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(
() =>

View file

@ -48,7 +48,7 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
findAllByIds: jest.fn(),
deleteById: jest.fn(),
search: jest.fn(),
checkIfSLOExists: jest.fn(),
exists: jest.fn(),
};
};

View file

@ -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<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
let mockEsClient: jest.Mocked<ElasticsearchClient>;
let mockEsClient: ElasticsearchClientMock;
let mockScopedClusterClient: ScopedClusterClientMock;
let loggerMock: jest.Mocked<MockedLogger>;
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);
});
await resetSLO.execute(slo.id);
it('resets all associated resources', async () => {
const slo = createSLO({ id: 'irrelevant', version: 1 });
mockRepository.findById.mockResolvedValueOnce(slo);
mockRepository.update.mockImplementation((v) => Promise.resolve(v));
// 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(),
expect(mockEsClient.index).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);
});
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);
await expect(resetSLO.execute(slo.id)).rejects.toThrowError(
"Missing ['read', 'view_index_metadata'] privileges on the source index [metrics-apm*]"
);
});
});
});

View file

@ -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);

View file

@ -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,

View file

@ -15,7 +15,7 @@ import { SLONotFound } from '../errors';
import { SO_SLO_TYPE } from '../saved_objects';
export interface SLORepository {
checkIfSLOExists(slo: SLODefinition): Promise<boolean>;
exists(id: string): Promise<boolean>;
create(slo: SLODefinition): Promise<SLODefinition>;
update(slo: SLODefinition): Promise<SLODefinition>;
findAllByIds(ids: string[]): Promise<SLODefinition[]>;
@ -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<StoredSLODefinition>({
type: SO_SLO_TYPE,
perPage: 0,
filter: `slo.attributes.id:(${slo.id})`,
filter: `slo.attributes.id:(${id})`,
});
return findResponse.total > 0;

View file

@ -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<SLORepository>;
let mockTransformManager: jest.Mocked<TransformManager>;
let mockEsClient: jest.Mocked<ElasticsearchClient>;
let mockEsClient: ElasticsearchClientMock;
let mockScopedClusterClient: ScopedClusterClientMock;
let mockLogger: jest.Mocked<MockedLogger>;
let mockSummaryTransformManager: jest.Mocked<TransformManager>;
@ -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',

View file

@ -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));

View file

@ -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}]`
);
}
}