[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:
parent
4309356b78
commit
d644f193b0
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 } },
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
]);
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}) ?? []
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -426,7 +426,7 @@ export const persistTimeline = async (
|
|||
}
|
||||
};
|
||||
|
||||
const createTimeline = async ({
|
||||
export const createTimeline = async ({
|
||||
timelineId,
|
||||
timeline,
|
||||
savedObjectsClient,
|
||||
|
|
|
@ -6,3 +6,4 @@
|
|||
*/
|
||||
|
||||
export { timelinesMigrations } from './timelines';
|
||||
export { notesMigrations } from './notes';
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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 = {
|
||||
|
|
|
@ -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' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Binary file not shown.
|
@ -3,6 +3,8 @@
|
|||
"value": {
|
||||
"aliases": {
|
||||
".kibana": {
|
||||
},
|
||||
".kibana_7.15.0": {
|
||||
}
|
||||
},
|
||||
"index": ".kibana_1",
|
||||
|
|
Loading…
Reference in a new issue