mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
33263b25c2
commit
4a0ccdb6c4
11 changed files with 194 additions and 68 deletions
|
@ -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 [
|
||||
|
|
|
@ -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() });
|
||||
|
|
|
@ -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(
|
||||
() =>
|
||||
|
|
|
@ -48,7 +48,7 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
|
|||
findAllByIds: jest.fn(),
|
||||
deleteById: jest.fn(),
|
||||
search: jest.fn(),
|
||||
checkIfSLOExists: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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*]"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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}]`
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue