From d644f193b0d2778785fff622248bd7ef28a11ed7 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 15 Sep 2021 14:38:51 -0400 Subject: [PATCH] [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> --- .../common/types/timeline/note/index.ts | 6 + .../server/lib/timeline/constants.ts | 5 + .../lib/timeline/routes/notes/persist_note.ts | 12 +- .../create_timelines/helpers.test.ts | 2 +- .../timelines/create_timelines/helpers.ts | 2 - .../timelines/import_timelines/index.test.ts | 46 +--- .../saved_object/notes/field_migrator.ts | 18 ++ .../lib/timeline/saved_object/notes/index.ts | 18 +- .../saved_object/notes/persist_notes.ts | 14 +- .../saved_object/notes/saved_object.ts | 219 ++++++++++++------ .../timeline/saved_object/timelines/index.ts | 2 +- .../saved_object_mappings/migrations/index.ts | 1 + .../migrations/notes.test.ts | 40 ++++ .../saved_object_mappings/migrations/notes.ts | 43 ++++ .../migrations/timelines.ts | 15 +- .../migrations/utils.test.ts | 50 +++- .../saved_object_mappings/migrations/utils.ts | 24 +- .../timeline/saved_object_mappings/notes.ts | 5 +- .../security_solution/timeline_migrations.ts | 131 ++++++++--- .../timelines/7.15.0/data.json.gz | Bin 2682 -> 3118 bytes .../timelines/7.15.0/mappings.json | 2 + 21 files changed, 480 insertions(+), 175 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/field_migrator.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts diff --git a/x-pack/plugins/security_solution/common/types/timeline/note/index.ts b/x-pack/plugins/security_solution/common/types/timeline/note/index.ts index 074e4132efdf..4bda81d75d92 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/note/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/note/index.ts @@ -31,6 +31,12 @@ export const SavedNoteRuntimeType = runtimeTypes.intersection([ export interface SavedNote extends runtimeTypes.TypeOf {} +/** + * 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; + /** * Note Saved object type with metadata */ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/constants.ts b/x-pack/plugins/security_solution/server/lib/timeline/constants.ts index 9e761a1f5c31..e38096bc2e82 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/constants.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts index 32fd87f39620..ad94f06f2d34 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/persist_note.ts @@ -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 } }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts index f5e5b7dfb8ae..c76b0858a6e4 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.test.ts @@ -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', diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts index e202230bf5cc..b393c753853f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/create_timelines/helpers.ts @@ -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 diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts index 2f51b23d7367..e0962e1fdce2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/index.test.ts @@ -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, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/field_migrator.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/field_migrator.ts new file mode 100644 index 000000000000..608c104440e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/field_migrator.ts @@ -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 }, +]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts index 34914517da68..81941853c57a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/index.ts @@ -28,13 +28,17 @@ export interface Notes { search: string | null, sort: SortNote | null ) => Promise; - persistNote: ( - request: FrameworkRequest, - noteId: string | null, - version: string | null, - note: SavedNote, - overrideOwner: boolean - ) => Promise; + persistNote: ({ + request, + noteId, + note, + overrideOwner, + }: { + request: FrameworkRequest; + noteId: string | null; + note: SavedNote; + overrideOwner: boolean; + }) => Promise; convertSavedObjectToSavedNote: ( savedObject: unknown, timelineVersion?: string | undefined | null diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts index 58b4e33444d9..612c9083cb34 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/persist_notes.ts @@ -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, + }); }) ?? [] ); }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts index 91caaa8cc8a8..29a2aa809b80 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts @@ -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 => { 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 => { +export const persistNote = async ({ + request, + noteId, + note, + overrideOwner = true, +}: { + request: FrameworkRequest; + noteId: string | null; + note: SavedNote; + overrideOwner?: boolean; +}): Promise => { 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({ + data: noteWithCreator, + }); + + const createdNote = await savedObjectsClient.create( + 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( + noteSavedObjectType, + noteId + ); + + const noteWithCreator = overrideOwner ? pickSavedNote(noteId, note, userInfo) : note; + + const { + transformedFields: migratedPatchAttributes, + references, + } = noteFieldsMigrator.extractFieldsToReferences({ + 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( + 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(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; }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts index d35a023e8df5..d25d2ece7d24 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/index.ts @@ -426,7 +426,7 @@ export const persistTimeline = async ( } }; -const createTimeline = async ({ +export const createTimeline = async ({ timelineId, timeline, savedObjectsClient, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts index 0b8c698bc3ea..e4c8858321e1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/index.ts @@ -6,3 +6,4 @@ */ export { timelinesMigrations } from './timelines'; +export { notesMigrations } from './notes'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts new file mode 100644 index 000000000000..0aa847cac34c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.test.ts @@ -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' }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts new file mode 100644 index 000000000000..a8d753e916af --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/notes.ts @@ -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 +): SavedObjectSanitizedDoc => { + 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, +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/timelines.ts index 7c26df4a475e..45733d7737b6 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/timelines.ts @@ -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 = { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts index cdf4124dc9c4..02e3fca996d5 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.test.ts @@ -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' }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts index 8efa6bb0ec93..ff9b56e6ae2c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/migrations/utils.ts @@ -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 = ({ + doc, + attributes, + docReferences, + migratedReferences, +}: { + doc: SavedObjectUnsanitizedDoc; + attributes: object; + docReferences: SavedObjectReference[]; + migratedReferences: SavedObjectReference[]; +}): SavedObjectSanitizedDoc => ({ + ...doc, + attributes: { + ...attributes, + }, + references: [...docReferences, ...migratedReferences], +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts index 5815747d3e72..387f78e5059f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings/notes.ts @@ -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, }; diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts index 820e0945f151..9863ebb7ba64 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_migrations.ts @@ -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; } +interface NoteWithoutTimelineId { + 'siem-ui-timeline-note': Omit; +} + 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( + 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( - 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( + 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"); + }); }); }); }); diff --git a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/data.json.gz index 9f45c0303ff7a687ce5aaaba1f1b38049659f729..e942ef732b22a21382848acce3ba2e8c8e5456fa 100644 GIT binary patch literal 3118 zcmV+}4AJu+iwFo~;Xq*k17u-zVJ>QOZ*BnXU2SulIu`!kU%~jx?%s)9i8l#<+TOk0 zyP4T`yWQN*&eokqAQ3ag-~vvY+sS`lNq_?wV<&;c$)(9mFgnta&Utin^a$J^FEnkE zU9I?JrcGGvO-|&EOB$}_ogdJ!flDL$8I>hzyiU9uW#Wz(C;Uxtk z!c3@Uo4yqCxuO0lHKrL|uC~gh6;yYlbr?1ZBf8}6B`K$gx1erVmRE|ePx|hD8eBv) zoF?m#PZ>|WBv?sDJn~rR#YMs|XjatBk~MF%?9q&0#L1N$1Ha@^*6i_Z%~2h^5ItLP zje7Ev=FwHQ7Bw{YO3TD;rEWTp=F!=}%P&7$}^!Y^+uUCLFc7CFruaa2IffNYHB^g(@#E>}f z5>DmksV2Lw=Csf^k0RNfFuW4|^u@f`j3ST^(kuz0i^(A1D(u}eyj~> zEyxziSJX{~x{PZpD(}SeNa5-`_tt6@3XE?=gD%&~4hS*Xc*ea
    $AS#Vi{XP4JM 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 diff --git a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/mappings.json b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/mappings.json index da616b9d19f6..7561dbb8dc6d 100644 --- a/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/mappings.json +++ b/x-pack/test/functional/es_archives/security_solution/timelines/7.15.0/mappings.json @@ -3,6 +3,8 @@ "value": { "aliases": { ".kibana": { + }, + ".kibana_7.15.0": { } }, "index": ".kibana_1",