[Security Solution][Timeline] Notes migrations (#111900)

* Starting migration class

* Fleshing out migrator

* Adding migration tests

* Refactoring

* Adding migrator to each client

* gzipping file

* Fixing cypress tests

* Cleaning up types and adding additional test

* Starting notes migrations

* Finishing notes references migration

* gzipping data.json

* Fixing unit tests

* Updating the archive and fixing spelling

* Adding await

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jonathan Buttner 2021-09-15 14:38:51 -04:00 committed by GitHub
parent 4309356b78
commit d644f193b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 480 additions and 175 deletions

View file

@ -31,6 +31,12 @@ export const SavedNoteRuntimeType = runtimeTypes.intersection([
export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {}
/**
* This type represents a note type stored in a saved object that does not include any fields that reference
* other saved objects.
*/
export type NoteWithoutExternalRefs = Omit<SavedNote, 'timelineId'>;
/**
* Note Saved object type with metadata
*/

View file

@ -15,3 +15,8 @@ export const SAVED_QUERY_ID_REF_NAME = 'savedQueryId';
* https://github.com/elastic/kibana/blob/master/src/plugins/data/public/query/saved_query/saved_query_service.ts#L54
*/
export const SAVED_QUERY_TYPE = 'query';
/**
* The reference name for the timeline ID field within the notes and pinned events saved object definition
*/
export const TIMELINE_ID_REF_NAME = 'timelineId';

View file

@ -42,18 +42,16 @@ export const persistNoteRoute = (
const frameworkRequest = await buildFrameworkRequest(context, security, request);
const { note } = request.body;
const noteId = request.body?.noteId ?? null;
const version = request.body?.version ?? null;
const res = await persistNote(
frameworkRequest,
const res = await persistNote({
request: frameworkRequest,
noteId,
version,
{
note: {
...note,
timelineId: note.timelineId || null,
},
true
);
overrideOwner: true,
});
return response.ok({
body: { data: { persistNote: res } },

View file

@ -119,7 +119,7 @@ describe('createTimelines', () => {
});
test('persistNotes', () => {
expect((persistNotes as jest.Mock).mock.calls[0][4]).toEqual([
expect((persistNotes as jest.Mock).mock.calls[0][3]).toEqual([
{
created: 1603885051655,
createdBy: 'elastic',

View file

@ -54,7 +54,6 @@ export const createTimelines = async ({
isImmutable
);
const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId;
const newTimelineVersion = responseTimeline.timeline.version;
let myPromises: unknown[] = [];
if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) {
@ -73,7 +72,6 @@ export const createTimelines = async ({
persistNotes(
frameworkRequest,
timelineSavedObjectId ?? newTimelineSavedObjectId,
newTimelineVersion,
existingNoteIds,
notes,
overrideNotesOwner

View file

@ -257,25 +257,13 @@ describe('import timelines', () => {
test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][1]).toBeNull();
});
test('should provide new timeline version when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][1]).toBeNull();
});
test('should provide note content when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version);
expect(mockPersistNote.mock.calls[0][0].noteId).toBeNull();
});
test('should provide new notes with original author info when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][3]).toEqual({
expect(mockPersistNote.mock.calls[0][0].note).toEqual({
eventId: undefined,
note: 'original note',
created: '1584830796960',
@ -284,7 +272,7 @@ describe('import timelines', () => {
updatedBy: 'original author A',
timelineId: mockCreatedTimeline.savedObjectId,
});
expect(mockPersistNote.mock.calls[1][3]).toEqual({
expect(mockPersistNote.mock.calls[1][0].note).toEqual({
eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId,
note: 'original event note',
created: '1584830796960',
@ -293,7 +281,7 @@ describe('import timelines', () => {
updatedBy: 'original author B',
timelineId: mockCreatedTimeline.savedObjectId,
});
expect(mockPersistNote.mock.calls[2][3]).toEqual({
expect(mockPersistNote.mock.calls[2][0].note).toEqual({
eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId,
note: 'event note2',
created: '1584830796960',
@ -310,7 +298,7 @@ describe('import timelines', () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][3]).toEqual({
expect(mockPersistNote.mock.calls[0][0].note).toEqual({
created: mockUniqueParsedObjects[0].globalNotes[0].created,
createdBy: mockUniqueParsedObjects[0].globalNotes[0].createdBy,
updated: mockUniqueParsedObjects[0].globalNotes[0].updated,
@ -319,7 +307,7 @@ describe('import timelines', () => {
note: mockUniqueParsedObjects[0].globalNotes[0].note,
timelineId: mockCreatedTimeline.savedObjectId,
});
expect(mockPersistNote.mock.calls[1][3]).toEqual({
expect(mockPersistNote.mock.calls[1][0].note).toEqual({
created: mockUniqueParsedObjects[0].eventNotes[0].created,
createdBy: mockUniqueParsedObjects[0].eventNotes[0].createdBy,
updated: mockUniqueParsedObjects[0].eventNotes[0].updated,
@ -328,7 +316,7 @@ describe('import timelines', () => {
note: mockUniqueParsedObjects[0].eventNotes[0].note,
timelineId: mockCreatedTimeline.savedObjectId,
});
expect(mockPersistNote.mock.calls[2][3]).toEqual({
expect(mockPersistNote.mock.calls[2][0].note).toEqual({
created: mockUniqueParsedObjects[0].eventNotes[1].created,
createdBy: mockUniqueParsedObjects[0].eventNotes[1].createdBy,
updated: mockUniqueParsedObjects[0].eventNotes[1].updated,
@ -640,19 +628,13 @@ describe('import timeline templates', () => {
test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][1]).toBeNull();
});
test('should provide new timeline version when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version);
expect(mockPersistNote.mock.calls[0][0].noteId).toBeNull();
});
test('should exclude event notes when creating notes', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][3]).toEqual({
expect(mockPersistNote.mock.calls[0][0].note).toEqual({
eventId: undefined,
note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note,
created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created,
@ -792,19 +774,13 @@ describe('import timeline templates', () => {
test('should provide noteSavedObjectId when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][1]).toBeNull();
});
test('should provide new timeline version when Creating notes for a timeline', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version);
expect(mockPersistNote.mock.calls[0][0].noteId).toBeNull();
});
test('should exclude event notes when creating notes', async () => {
const mockRequest = await getImportTimelinesRequest();
await server.inject(mockRequest, context);
expect(mockPersistNote.mock.calls[0][3]).toEqual({
expect(mockPersistNote.mock.calls[0][0].note).toEqual({
eventId: undefined,
note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note,
created: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].created,

View file

@ -0,0 +1,18 @@
/*
* 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 { TIMELINE_ID_REF_NAME } from '../../constants';
import { timelineSavedObjectType } from '../../saved_object_mappings';
import { FieldMigrator } from '../../utils/migrator';
/**
* A migrator to handle moving specific fields that reference the timeline saved object to the references field within a note saved
* object.
*/
export const noteFieldsMigrator = new FieldMigrator([
{ path: 'timelineId', type: timelineSavedObjectType, name: TIMELINE_ID_REF_NAME },
]);

View file

@ -28,13 +28,17 @@ export interface Notes {
search: string | null,
sort: SortNote | null
) => Promise<ResponseNotes>;
persistNote: (
request: FrameworkRequest,
noteId: string | null,
version: string | null,
note: SavedNote,
overrideOwner: boolean
) => Promise<ResponseNote>;
persistNote: ({
request,
noteId,
note,
overrideOwner,
}: {
request: FrameworkRequest;
noteId: string | null;
note: SavedNote;
overrideOwner: boolean;
}) => Promise<ResponseNote>;
convertSavedObjectToSavedNote: (
savedObject: unknown,
timelineVersion?: string | undefined | null

View file

@ -13,7 +13,6 @@ import { NoteResult } from '../../../../../common/types/timeline/note';
export const persistNotes = async (
frameworkRequest: FrameworkRequest,
timelineSavedObjectId: string,
timelineVersion?: string | null,
existingNoteIds?: string[],
newNotes?: NoteResult[],
overrideOwner: boolean = true
@ -26,13 +25,12 @@ export const persistNotes = async (
timelineSavedObjectId,
overrideOwner
);
return persistNote(
frameworkRequest,
overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null,
timelineVersion ?? null,
newNote,
overrideOwner
);
return persistNote({
request: frameworkRequest,
noteId: overrideOwner ? existingNoteIds?.find((nId) => nId === note.noteId) ?? null : null,
note: newNote,
overrideOwner,
});
}) ?? []
);
};

View file

@ -25,11 +25,13 @@ import {
NoteResult,
ResponseNotes,
ResponseNote,
NoteWithoutExternalRefs,
} from '../../../../../common/types/timeline/note';
import { FrameworkRequest } from '../../../framework';
import { noteSavedObjectType } from '../../saved_object_mappings/notes';
import { convertSavedObjectToSavedTimeline, pickSavedTimeline } from '../timelines';
import { createTimeline } from '../timelines';
import { timelineSavedObjectType } from '../../saved_object_mappings';
import { noteFieldsMigrator } from './field_migrator';
export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) => {
const savedObjectsClient = request.context.core.savedObjects.client;
@ -42,8 +44,7 @@ export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) =
export const deleteNoteByTimelineId = async (request: FrameworkRequest, timelineId: string) => {
const options: SavedObjectsFindOptions = {
type: noteSavedObjectType,
search: timelineId,
searchFields: ['timelineId'],
hasReference: { type: timelineSavedObjectType, id: timelineId },
};
const notesToBeDeleted = await getAllSavedNote(request, options);
const savedObjectsClient = request.context.core.savedObjects.client;
@ -81,8 +82,7 @@ export const getNotesByTimelineId = async (
): Promise<NoteSavedObject[]> => {
const options: SavedObjectsFindOptions = {
type: noteSavedObjectType,
search: timelineId,
searchFields: ['timelineId'],
hasReference: { type: timelineSavedObjectType, id: timelineId },
};
const notesByTimelineId = await getAllSavedNote(request, options);
return notesByTimelineId.notes;
@ -106,62 +106,29 @@ export const getAllNotes = async (
return getAllSavedNote(request, options);
};
export const persistNote = async (
request: FrameworkRequest,
noteId: string | null,
version: string | null,
note: SavedNote,
overrideOwner: boolean = true
): Promise<ResponseNote> => {
export const persistNote = async ({
request,
noteId,
note,
overrideOwner = true,
}: {
request: FrameworkRequest;
noteId: string | null;
note: SavedNote;
overrideOwner?: boolean;
}): Promise<ResponseNote> => {
try {
const savedObjectsClient = request.context.core.savedObjects.client;
if (noteId == null) {
const timelineVersionSavedObject =
note.timelineId == null
? await (async () => {
const timelineResult = convertSavedObjectToSavedTimeline(
await savedObjectsClient.create(
timelineSavedObjectType,
pickSavedTimeline(null, {}, request.user)
)
);
note.timelineId = timelineResult.savedObjectId;
return timelineResult.version;
})()
: null;
// Create new note
return {
code: 200,
message: 'success',
note: convertSavedObjectToSavedNote(
await savedObjectsClient.create(
noteSavedObjectType,
overrideOwner ? pickSavedNote(noteId, note, request.user) : note
),
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
),
};
return await createNote({
request,
noteId,
note,
overrideOwner,
});
}
// Update existing note
const existingNote = await getSavedNote(request, noteId);
return {
code: 200,
message: 'success',
note: convertSavedObjectToSavedNote(
await savedObjectsClient.update(
noteSavedObjectType,
noteId,
overrideOwner ? pickSavedNote(noteId, note, request.user) : note,
{
version: existingNote.version || undefined,
}
)
),
};
return await updateNote({ request, noteId, note, overrideOwner });
} catch (err) {
if (getOr(null, 'output.statusCode', err) === 403) {
const noteToReturn: NoteResult = {
@ -181,22 +148,142 @@ export const persistNote = async (
}
};
const createNote = async ({
request,
noteId,
note,
overrideOwner = true,
}: {
request: FrameworkRequest;
noteId: string | null;
note: SavedNote;
overrideOwner?: boolean;
}) => {
const savedObjectsClient = request.context.core.savedObjects.client;
const userInfo = request.user;
const shallowCopyOfNote = { ...note };
let timelineVersion: string | undefined;
if (note.timelineId == null) {
const { timeline: timelineResult } = await createTimeline({
timelineId: null,
timeline: {},
savedObjectsClient,
userInfo,
});
shallowCopyOfNote.timelineId = timelineResult.savedObjectId;
timelineVersion = timelineResult.version;
}
const noteWithCreator = overrideOwner
? pickSavedNote(noteId, shallowCopyOfNote, userInfo)
: shallowCopyOfNote;
const {
transformedFields: migratedAttributes,
references,
} = noteFieldsMigrator.extractFieldsToReferences<NoteWithoutExternalRefs>({
data: noteWithCreator,
});
const createdNote = await savedObjectsClient.create<NoteWithoutExternalRefs>(
noteSavedObjectType,
migratedAttributes,
{
references,
}
);
const repopulatedSavedObject = noteFieldsMigrator.populateFieldsFromReferences(createdNote);
const convertedNote = convertSavedObjectToSavedNote(repopulatedSavedObject, timelineVersion);
// Create new note
return {
code: 200,
message: 'success',
note: convertedNote,
};
};
const updateNote = async ({
request,
noteId,
note,
overrideOwner = true,
}: {
request: FrameworkRequest;
noteId: string;
note: SavedNote;
overrideOwner?: boolean;
}) => {
const savedObjectsClient = request.context.core.savedObjects.client;
const userInfo = request.user;
const existingNote = await savedObjectsClient.get<NoteWithoutExternalRefs>(
noteSavedObjectType,
noteId
);
const noteWithCreator = overrideOwner ? pickSavedNote(noteId, note, userInfo) : note;
const {
transformedFields: migratedPatchAttributes,
references,
} = noteFieldsMigrator.extractFieldsToReferences<NoteWithoutExternalRefs>({
data: noteWithCreator,
existingReferences: existingNote.references,
});
const updatedNote = await savedObjectsClient.update(
noteSavedObjectType,
noteId,
migratedPatchAttributes,
{
version: existingNote.version || undefined,
references,
}
);
const populatedNote = noteFieldsMigrator.populateFieldsFromReferencesForPatch({
dataBeforeRequest: note,
dataReturnedFromRequest: updatedNote,
});
const convertedNote = convertSavedObjectToSavedNote(populatedNote);
return {
code: 200,
message: 'success',
note: convertedNote,
};
};
const getSavedNote = async (request: FrameworkRequest, NoteId: string) => {
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId);
const savedObject = await savedObjectsClient.get<NoteWithoutExternalRefs>(
noteSavedObjectType,
NoteId
);
return convertSavedObjectToSavedNote(savedObject);
const populatedNote = noteFieldsMigrator.populateFieldsFromReferences(savedObject);
return convertSavedObjectToSavedNote(populatedNote);
};
const getAllSavedNote = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => {
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObjects = await savedObjectsClient.find(options);
const savedObjects = await savedObjectsClient.find<NoteWithoutExternalRefs>(options);
return {
totalCount: savedObjects.total,
notes: savedObjects.saved_objects.map((savedObject) =>
convertSavedObjectToSavedNote(savedObject)
),
notes: savedObjects.saved_objects.map((savedObject) => {
const populatedNote = noteFieldsMigrator.populateFieldsFromReferences(savedObject);
return convertSavedObjectToSavedNote(populatedNote);
}),
};
};
@ -233,11 +320,9 @@ const pickSavedNote = (
if (noteId == null) {
savedNote.created = new Date().valueOf();
savedNote.createdBy = userInfo?.username ?? UNAUTHENTICATED_USER;
savedNote.updated = new Date().valueOf();
savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER;
} else if (noteId != null) {
savedNote.updated = new Date().valueOf();
savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER;
}
savedNote.updated = new Date().valueOf();
savedNote.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER;
return savedNote;
};

View file

@ -426,7 +426,7 @@ export const persistTimeline = async (
}
};
const createTimeline = async ({
export const createTimeline = async ({
timelineId,
timeline,
savedObjectsClient,

View file

@ -6,3 +6,4 @@
*/
export { timelinesMigrations } from './timelines';
export { notesMigrations } from './notes';

View file

@ -0,0 +1,40 @@
/*
* 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 { TIMELINE_ID_REF_NAME } from '../../constants';
import { migrateNoteTimelineIdToReferences, TimelineId } from './notes';
describe('notes migrations', () => {
describe('7.16.0 timelineId', () => {
it('removes the timelineId from the migrated document', () => {
const migratedDoc = migrateNoteTimelineIdToReferences({
id: '1',
type: 'awesome',
attributes: { timelineId: '123' },
});
expect(migratedDoc.attributes).toEqual({});
expect(migratedDoc.references).toEqual([
// importing the timeline saved object type from the timeline saved object causes a circular import and causes the jest tests to fail
{ id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' },
]);
});
it('preserves additional fields when migrating timeline id', () => {
const migratedDoc = migrateNoteTimelineIdToReferences({
id: '1',
type: 'awesome',
attributes: ({ awesome: 'yes', timelineId: '123' } as unknown) as TimelineId,
});
expect(migratedDoc.attributes).toEqual({ awesome: 'yes' });
expect(migratedDoc.references).toEqual([
{ id: '123', name: TIMELINE_ID_REF_NAME, type: 'siem-ui-timeline' },
]);
});
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 {
SavedObjectMigrationMap,
SavedObjectSanitizedDoc,
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
import { timelineSavedObjectType } from '..';
import { TIMELINE_ID_REF_NAME } from '../../constants';
import { createMigratedDoc, createReference } from './utils';
export interface TimelineId {
timelineId?: string | null;
}
export const migrateNoteTimelineIdToReferences = (
doc: SavedObjectUnsanitizedDoc<TimelineId>
): SavedObjectSanitizedDoc<unknown> => {
const { timelineId, ...restAttributes } = doc.attributes;
const { references: docReferences = [] } = doc;
const timelineIdReferences = createReference(
timelineId,
TIMELINE_ID_REF_NAME,
timelineSavedObjectType
);
return createMigratedDoc({
doc,
attributes: restAttributes,
docReferences,
migratedReferences: timelineIdReferences,
});
};
export const notesMigrations: SavedObjectMigrationMap = {
'7.16.0': migrateNoteTimelineIdToReferences,
};

View file

@ -11,7 +11,7 @@ import {
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
import { SAVED_QUERY_ID_REF_NAME, SAVED_QUERY_TYPE } from '../../constants';
import { createReference } from './utils';
import { createMigratedDoc, createReference } from './utils';
export interface SavedQueryId {
savedQueryId?: string | null;
@ -29,13 +29,12 @@ export const migrateSavedQueryIdToReferences = (
SAVED_QUERY_TYPE
);
return {
...doc,
attributes: {
...restAttributes,
},
references: [...docReferences, ...savedQueryIdReferences],
};
return createMigratedDoc({
doc,
attributes: restAttributes,
docReferences,
migratedReferences: savedQueryIdReferences,
});
};
export const timelinesMigrations: SavedObjectMigrationMap = {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { createReference } from './utils';
import { createMigratedDoc, createReference } from './utils';
describe('migration utils', () => {
describe('createReference', () => {
@ -23,4 +23,52 @@ describe('migration utils', () => {
expect(createReference(null, 'name', 'type')).toHaveLength(0);
});
});
describe('createMigratedDoc', () => {
it('overwrites the attributes of the original doc', () => {
const doc = {
id: '1',
attributes: {
hello: '1',
},
type: 'yes',
};
expect(
createMigratedDoc({ doc, attributes: {}, docReferences: [], migratedReferences: [] })
).toEqual({
id: '1',
attributes: {},
type: 'yes',
references: [],
});
});
it('combines the references', () => {
const doc = {
id: '1',
attributes: {
hello: '1',
},
type: 'yes',
};
expect(
createMigratedDoc({
doc,
attributes: {},
docReferences: [{ id: '1', name: 'name', type: 'type' }],
migratedReferences: [{ id: '5', name: 'name5', type: 'type5' }],
})
).toEqual({
id: '1',
attributes: {},
type: 'yes',
references: [
{ id: '1', name: 'name', type: 'type' },
{ id: '5', name: 'name5', type: 'type5' },
],
});
});
});
});

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { SavedObjectReference } from 'kibana/server';
import {
SavedObjectReference,
SavedObjectSanitizedDoc,
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
export function createReference(
id: string | null | undefined,
@ -14,3 +18,21 @@ export function createReference(
): SavedObjectReference[] {
return id != null ? [{ id, name, type }] : [];
}
export const createMigratedDoc = <T>({
doc,
attributes,
docReferences,
migratedReferences,
}: {
doc: SavedObjectUnsanitizedDoc<T>;
attributes: object;
docReferences: SavedObjectReference[];
migratedReferences: SavedObjectReference[];
}): SavedObjectSanitizedDoc<unknown> => ({
...doc,
attributes: {
...attributes,
},
references: [...docReferences, ...migratedReferences],
});

View file

@ -6,14 +6,12 @@
*/
import { SavedObjectsType } from '../../../../../../../src/core/server';
import { notesMigrations } from './migrations';
export const noteSavedObjectType = 'siem-ui-timeline-note';
export const noteSavedObjectMappings: SavedObjectsType['mappings'] = {
properties: {
timelineId: {
type: 'keyword',
},
eventId: {
type: 'keyword',
},
@ -40,4 +38,5 @@ export const noteType: SavedObjectsType = {
hidden: false,
namespaceType: 'single',
mappings: noteSavedObjectMappings,
migrations: notesMigrations,
};

View file

@ -7,6 +7,7 @@
import expect from '@kbn/expect';
import { SavedTimeline } from '../../../../plugins/security_solution/common/types/timeline';
import { SavedNote } from '../../../../plugins/security_solution/common/types/timeline/note';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getSavedObjectFromES } from './utils';
@ -15,6 +16,10 @@ interface TimelineWithoutSavedQueryId {
'siem-ui-timeline': Omit<SavedTimeline, 'savedQueryId'>;
}
interface NoteWithoutTimelineId {
'siem-ui-timeline-note': Omit<SavedNote, 'timelineId'>;
}
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@ -23,48 +28,106 @@ export default function ({ getService }: FtrProviderContext) {
const es = getService('es');
describe('7.16.0', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
describe('notes timelineId', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
});
it('removes the timelineId in the saved object', async () => {
const timelines = await getSavedObjectFromES<NoteWithoutTimelineId>(
es,
'siem-ui-timeline-note',
{
ids: {
values: [
'siem-ui-timeline-note:989002c0-126e-11ec-83d2-db1096c73738',
'siem-ui-timeline-note:f09b5980-1271-11ec-83d2-db1096c73738',
],
},
}
);
expect(
timelines.body.hits.hits[0]._source?.['siem-ui-timeline-note']
).to.not.have.property('timelineId');
expect(
timelines.body.hits.hits[1]._source?.['siem-ui-timeline-note']
).to.not.have.property('timelineId');
});
it('preserves the eventId in the saved object after migration', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.notes[0].eventId).to.be('Edo00XsBEVtyvU-8LGNe');
});
it('returns the timelineId in the response', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '6484cc90-126e-11ec-83d2-db1096c73738' });
expect(resp.body.data.getOneTimeline.notes[0].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
expect(resp.body.data.getOneTimeline.notes[1].timelineId).to.be(
'6484cc90-126e-11ec-83d2-db1096c73738'
);
});
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
});
describe('savedQueryId', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
});
it('removes the savedQueryId', async () => {
const timelines = await getSavedObjectFromES<TimelineWithoutSavedQueryId>(
es,
'siem-ui-timeline',
{
ids: { values: ['siem-ui-timeline:8dc70950-1012-11ec-9ad3-2d7c6600c0f7'] },
}
);
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/security_solution/timelines/7.15.0'
);
});
expect(timelines.body.hits.hits[0]._source?.['siem-ui-timeline']).to.not.have.property(
'savedQueryId'
);
});
it('removes the savedQueryId', async () => {
const timelines = await getSavedObjectFromES<TimelineWithoutSavedQueryId>(
es,
'siem-ui-timeline',
{
ids: { values: ['siem-ui-timeline:8dc70950-1012-11ec-9ad3-2d7c6600c0f7'] },
}
);
it('preserves the title in the saved object after migration', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' })
.set('kbn-xsrf', 'true');
expect(timelines.body.hits.hits[0]._source?.['siem-ui-timeline']).to.not.have.property(
'savedQueryId'
);
});
expect(resp.body.data.getOneTimeline.title).to.be('Awesome Timeline');
});
it('preserves the title in the saved object after migration', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' });
it('returns the savedQueryId in the response', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' })
.set('kbn-xsrf', 'true');
expect(resp.body.data.getOneTimeline.title).to.be('Awesome Timeline');
});
expect(resp.body.data.getOneTimeline.savedQueryId).to.be("It's me");
it('returns the savedQueryId in the response', async () => {
const resp = await supertest
.get('/api/timeline')
.query({ id: '8dc70950-1012-11ec-9ad3-2d7c6600c0f7' });
expect(resp.body.data.getOneTimeline.savedQueryId).to.be("It's me");
});
});
});
});

View file

@ -3,6 +3,8 @@
"value": {
"aliases": {
".kibana": {
},
".kibana_7.15.0": {
}
},
"index": ".kibana_1",