From 5f1bb1835e80b4c3201923a09e9f8343852549f1 Mon Sep 17 00:00:00 2001 From: Philippe Oberti Date: Fri, 25 Oct 2024 15:37:23 -0500 Subject: [PATCH 1/3] [Security Solution][Notes] - fix createdBy filter for notes management page (#197706) (cherry picked from commit 1065bbf03ca0583935d9b482939823e6b46c3c52) # Conflicts: # x-pack/plugins/security_solution/tsconfig.json # x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts --- oas_docs/output/kibana.serverless.yaml | 2 +- oas_docs/output/kibana.yaml | 2 +- .../upselling/messages/index.tsx | 4 +- .../timeline/get_notes/get_notes_route.gen.ts | 2 +- .../get_notes/get_notes_route.schema.yaml | 2 +- ...imeline_api_2023_10_31.bundled.schema.yaml | 2 +- ...imeline_api_2023_10_31.bundled.schema.yaml | 2 +- .../public/common/mock/global_state.ts | 2 +- .../security_solution/public/notes/api/api.ts | 6 +-- ...sx => created_by_filter_dropdown.test.tsx} | 25 ++++++---- ...own.tsx => created_by_filter_dropdown.tsx} | 39 ++++++++++----- .../notes/components/search_row.test.tsx | 8 +++- .../public/notes/components/search_row.tsx | 4 +- .../public/notes/components/test_ids.ts | 2 +- .../public/notes/components/utility_bar.tsx | 14 +++--- .../notes/pages/note_management_page.tsx | 14 +++--- .../public/notes/store/notes.slice.test.ts | 18 +++---- .../public/notes/store/notes.slice.ts | 22 ++++----- .../server/lib/timeline/routes/index.ts | 10 +++- .../lib/timeline/routes/notes/get_notes.ts | 48 ++++++++++++++++--- .../security_solution/server/routes/index.ts | 2 +- .../plugins/security_solution/tsconfig.json | 3 ++ .../saved_objects/tests/notes.ts | 37 ++++++++++---- 23 files changed, 180 insertions(+), 90 deletions(-) rename x-pack/plugins/security_solution/public/notes/components/{user_filter_dropdown.test.tsx => created_by_filter_dropdown.test.tsx} (74%) rename x-pack/plugins/security_solution/public/notes/components/{user_filter_dropdown.tsx => created_by_filter_dropdown.tsx} (68%) diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index ecfc211c98e1e..7027968029393 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -15012,7 +15012,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 508148435fbea..8f8760c0569f3 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -18442,7 +18442,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/x-pack/packages/security-solution/upselling/messages/index.tsx b/x-pack/packages/security-solution/upselling/messages/index.tsx index 4bda9477f13c0..1283671911402 100644 --- a/x-pack/packages/security-solution/upselling/messages/index.tsx +++ b/x-pack/packages/security-solution/upselling/messages/index.tsx @@ -48,8 +48,8 @@ export const ALERT_SUPPRESSION_RULE_DETAILS = i18n.translate( ); export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) => - i18n.translate('securitySolutionPackages.noteManagement.userFilter.upsell', { - defaultMessage: 'Upgrade to {requiredLicense} to make use of user filters', + i18n.translate('securitySolutionPackages.noteManagement.createdByFilter.upsell', { + defaultMessage: 'Upgrade to {requiredLicense} to make use of createdBy filter', values: { requiredLicense, }, diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts index 151fb05f41856..0ee6445dd71e3 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.gen.ts @@ -54,7 +54,7 @@ export const GetNotesRequestQuery = z.object({ sortField: z.string().nullable().optional(), sortOrder: z.string().nullable().optional(), filter: z.string().nullable().optional(), - userFilter: z.string().nullable().optional(), + createdByFilter: z.string().nullable().optional(), associatedFilter: AssociatedFilterType.optional(), }); export type GetNotesRequestQueryInput = z.input; diff --git a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml index e635018c293cf..9f7436bc31162 100644 --- a/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/timeline/get_notes/get_notes_route.schema.yaml @@ -52,7 +52,7 @@ paths: type: string nullable: true - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 070d7be235ef5..e0cd03ae54039 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -97,7 +97,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 9f5882f0f4072..49c3ff4644f83 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -97,7 +97,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 5874062f05523..6473f6fa5a67e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -550,7 +550,7 @@ export const mockGlobalState: State = { direction: 'desc' as const, }, filter: '', - userFilter: '', + createdByFilter: '', associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], diff --git a/x-pack/plugins/security_solution/public/notes/api/api.ts b/x-pack/plugins/security_solution/public/notes/api/api.ts index 917974a154884..892b01e3d17f0 100644 --- a/x-pack/plugins/security_solution/public/notes/api/api.ts +++ b/x-pack/plugins/security_solution/public/notes/api/api.ts @@ -43,7 +43,7 @@ export const fetchNotes = async ({ sortField, sortOrder, filter, - userFilter, + createdByFilter, associatedFilter, search, }: { @@ -52,7 +52,7 @@ export const fetchNotes = async ({ sortField: string; sortOrder: string; filter: string; - userFilter: string; + createdByFilter: string; associatedFilter: AssociatedFilter; search: string; }) => { @@ -63,7 +63,7 @@ export const fetchNotes = async ({ sortField, sortOrder, filter, - userFilter, + createdByFilter, associatedFilter, search, }, diff --git a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.test.tsx b/x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.test.tsx similarity index 74% rename from x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.test.tsx rename to x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.test.tsx index b095036e58632..301e59db1bfc1 100644 --- a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.test.tsx @@ -7,8 +7,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; -import { UserFilterDropdown } from './user_filter_dropdown'; -import { USER_SELECT_TEST_ID } from './test_ids'; +import { CreatedByFilterDropdown } from './created_by_filter_dropdown'; +import { CREATED_BY_SELECT_TEST_ID } from './test_ids'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; import { useLicense } from '../../common/hooks/use_license'; import { useUpsellingMessage } from '../../common/hooks/use_upselling'; @@ -32,16 +32,25 @@ describe('UserFilterDropdown', () => { jest.clearAllMocks(); (useSuggestUsers as jest.Mock).mockReturnValue({ isLoading: false, - data: [{ user: { username: 'test' } }, { user: { username: 'elastic' } }], + data: [ + { + uid: '1', + user: { username: 'test' }, + }, + { + uid: '2', + user: { username: 'elastic' }, + }, + ], }); (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => true }); (useUpsellingMessage as jest.Mock).mockReturnValue('upsellingMessage'); }); it('should render the component enabled', () => { - const { getByTestId } = render(); + const { getByTestId } = render(); - const dropdown = getByTestId(USER_SELECT_TEST_ID); + const dropdown = getByTestId(CREATED_BY_SELECT_TEST_ID); expect(dropdown).toBeInTheDocument(); expect(dropdown).not.toHaveClass('euiComboBox-isDisabled'); @@ -50,13 +59,13 @@ describe('UserFilterDropdown', () => { it('should render the dropdown disabled', async () => { (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => false }); - const { getByTestId } = render(); + const { getByTestId } = render(); - expect(getByTestId(USER_SELECT_TEST_ID)).toHaveClass('euiComboBox-isDisabled'); + expect(getByTestId(CREATED_BY_SELECT_TEST_ID)).toHaveClass('euiComboBox-isDisabled'); }); it('should call the correct action when select a user', async () => { - const { getByTestId } = render(); + const { getByTestId } = render(); const userSelect = getByTestId('comboBoxSearchInput'); userSelect.focus(); diff --git a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx b/x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx rename to x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.tsx index 78f4ef6dd2ac8..4b962e4c1ef60 100644 --- a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/created_by_filter_dropdown.tsx @@ -13,15 +13,26 @@ import { i18n } from '@kbn/i18n'; import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; import { useLicense } from '../../common/hooks/use_license'; import { useUpsellingMessage } from '../../common/hooks/use_upselling'; -import { USER_SELECT_TEST_ID } from './test_ids'; +import { CREATED_BY_SELECT_TEST_ID } from './test_ids'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; -import { userFilterUsers } from '..'; +import { userFilterCreatedBy } from '..'; -export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', { - defaultMessage: 'Users', +export const CREATED_BY = i18n.translate('xpack.securitySolution.notes.createdByDropdownLabel', { + defaultMessage: 'Created by', }); -export const UserFilterDropdown = React.memo(() => { +interface User { + /** + * uuid of the UserProfile + */ + id: string; + /** + * full_name || email || username of the UserProfile + */ + label: string; +} + +export const CreatedByFilterDropdown = React.memo(() => { const dispatch = useDispatch(); const isPlatinumPlus = useLicense().isPlatinumPlus(); const upsellingMessage = useUpsellingMessage('note_management_user_filter'); @@ -30,19 +41,21 @@ export const UserFilterDropdown = React.memo(() => { searchTerm: '', enabled: isPlatinumPlus, }); - const users = useMemo( + + const users: User[] = useMemo( () => (data || []).map((userProfile: UserProfileWithAvatar) => ({ - label: userProfile.user.full_name || userProfile.user.username, + id: userProfile.uid, + label: userProfile.user.full_name || userProfile.user.email || userProfile.user.username, })), [data] ); - const [selectedUser, setSelectedUser] = useState>>(); + const [selectedUser, setSelectedUser] = useState>>(); const onChange = useCallback( - (user: Array>) => { + (user: Array>) => { setSelectedUser(user); - dispatch(userFilterUsers(user.length > 0 ? user[0].label : '')); + dispatch(userFilterCreatedBy(user.length > 0 ? (user[0].id as string) : '')); }, [dispatch] ); @@ -50,14 +63,14 @@ export const UserFilterDropdown = React.memo(() => { const dropdown = useMemo( () => ( ), [isLoading, isPlatinumPlus, onChange, selectedUser, users] @@ -76,4 +89,4 @@ export const UserFilterDropdown = React.memo(() => { ); }); -UserFilterDropdown.displayName = 'UserFilterDropdown'; +CreatedByFilterDropdown.displayName = 'CreatedByFilterDropdown'; diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx index 447ade158306b..cdae928b4ad87 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.test.tsx @@ -9,7 +9,11 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SearchRow } from './search_row'; -import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID, USER_SELECT_TEST_ID } from './test_ids'; +import { + ASSOCIATED_NOT_SELECT_TEST_ID, + SEARCH_BAR_TEST_ID, + CREATED_BY_SELECT_TEST_ID, +} from './test_ids'; import { AssociatedFilter } from '../../../common/notes/constants'; import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users'; import { TestProviders } from '../../common/mock'; @@ -43,7 +47,7 @@ describe('SearchRow', () => { ); expect(getByTestId(SEARCH_BAR_TEST_ID)).toBeInTheDocument(); - expect(getByTestId(USER_SELECT_TEST_ID)).toBeInTheDocument(); + expect(getByTestId(CREATED_BY_SELECT_TEST_ID)).toBeInTheDocument(); expect(getByTestId(ASSOCIATED_NOT_SELECT_TEST_ID)).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx index 3c4093f913acf..ea006f9a3f01f 100644 --- a/x-pack/plugins/security_solution/public/notes/components/search_row.tsx +++ b/x-pack/plugins/security_solution/public/notes/components/search_row.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; -import { UserFilterDropdown } from './user_filter_dropdown'; +import { CreatedByFilterDropdown } from './created_by_filter_dropdown'; import { ASSOCIATED_NOT_SELECT_TEST_ID, SEARCH_BAR_TEST_ID } from './test_ids'; import { userFilterAssociatedNotes, userSearchedNotes } from '..'; import { AssociatedFilter } from '../../../common/notes/constants'; @@ -65,7 +65,7 @@ export const SearchRow = React.memo(() => { - + { const pagination = useSelector(selectNotesPagination); const sort = useSelector(selectNotesTableSort); const selectedItems = useSelector(selectNotesTableSelectedIds); - const notesUserFilters = useSelector(selectNotesTableUserFilters); - const notesAssociatedFilters = useSelector(selectNotesTableAssociatedFilter); + const notesCreatedByFilter = useSelector(selectNotesTableCreatedByFilter); + const notesAssociatedFilter = useSelector(selectNotesTableAssociatedFilter); const resultsCount = useMemo(() => { const { perPage, page, total } = pagination; const startOfCurrentPage = perPage * (page - 1) + 1; @@ -87,8 +87,8 @@ export const NotesUtilityBar = React.memo(() => { sortField: sort.field, sortOrder: sort.direction, filter: '', - userFilter: notesUserFilters, - associatedFilter: notesAssociatedFilters, + createdByFilter: notesCreatedByFilter, + associatedFilter: notesAssociatedFilter, search: notesSearch, }) ); @@ -98,8 +98,8 @@ export const NotesUtilityBar = React.memo(() => { pagination.perPage, sort.field, sort.direction, - notesUserFilters, - notesAssociatedFilters, + notesCreatedByFilter, + notesAssociatedFilter, notesSearch, ]); return ( diff --git a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx index 4795d6146be4d..3060e5ccf93d9 100644 --- a/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx +++ b/x-pack/plugins/security_solution/public/notes/pages/note_management_page.tsx @@ -36,7 +36,7 @@ import { selectNotesTablePendingDeleteIds, selectFetchNotesError, ReqStatus, - selectNotesTableUserFilters, + selectNotesTableCreatedByFilter, selectNotesTableAssociatedFilter, } from '..'; import type { NotesState } from '..'; @@ -121,8 +121,8 @@ export const NoteManagementPage = () => { const pagination = useSelector(selectNotesPagination); const sort = useSelector(selectNotesTableSort); const notesSearch = useSelector(selectNotesTableSearch); - const notesUserFilters = useSelector(selectNotesTableUserFilters); - const notesAssociatedFilters = useSelector(selectNotesTableAssociatedFilter); + const notesCreatedByFilter = useSelector(selectNotesTableCreatedByFilter); + const notesAssociatedFilter = useSelector(selectNotesTableAssociatedFilter); const pendingDeleteIds = useSelector(selectNotesTablePendingDeleteIds); const isDeleteModalVisible = pendingDeleteIds.length > 0; const fetchNotesStatus = useSelector(selectFetchNotesStatus); @@ -138,8 +138,8 @@ export const NoteManagementPage = () => { sortField: sort.field, sortOrder: sort.direction, filter: '', - userFilter: notesUserFilters, - associatedFilter: notesAssociatedFilters, + createdByFilter: notesCreatedByFilter, + associatedFilter: notesAssociatedFilter, search: notesSearch, }) ); @@ -149,8 +149,8 @@ export const NoteManagementPage = () => { pagination.perPage, sort.field, sort.direction, - notesUserFilters, - notesAssociatedFilters, + notesCreatedByFilter, + notesAssociatedFilter, notesSearch, ]); diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts index 65fa293bd824a..46bcec9b448f9 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.test.ts @@ -38,7 +38,7 @@ import { selectNotesTableSort, selectSortedNotesByDocumentId, selectSortedNotesBySavedObjectId, - selectNotesTableUserFilters, + selectNotesTableCreatedByFilter, selectNotesTableAssociatedFilter, userClosedDeleteModal, userFilteredNotes, @@ -49,7 +49,7 @@ import { userSelectedRow, userSelectedNotesForDeletion, userSortedNotes, - userFilterUsers, + userFilterCreatedBy, userClosedCreateErrorToast, userFilterAssociatedNotes, } from './notes.slice'; @@ -104,7 +104,7 @@ const initialNonEmptyState: NotesState = { direction: 'desc' as const, }, filter: '', - userFilter: '', + createdByFilter: '', associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], @@ -508,13 +508,13 @@ describe('notesSlice', () => { }); }); - describe('userFilterUsers', () => { + describe('userFilterCreatedBy', () => { it('should set correct value to filter users', () => { - const action = { type: userFilterUsers.type, payload: 'abc' }; + const action = { type: userFilterCreatedBy.type, payload: 'abc' }; expect(notesReducer(initalEmptyState, action)).toEqual({ ...initalEmptyState, - userFilter: 'abc', + createdByFilter: 'abc', }); }); }); @@ -866,12 +866,12 @@ describe('notesSlice', () => { expect(selectNotesTableSearch(state)).toBe('test search'); }); - it('should select user filter', () => { + it('should select createdBy filter', () => { const state = { ...mockGlobalState, - notes: { ...initialNotesState, userFilter: 'abc' }, + notes: { ...initialNotesState, createdByFilter: 'abc' }, }; - expect(selectNotesTableUserFilters(state)).toBe('abc'); + expect(selectNotesTableCreatedByFilter(state)).toBe('abc'); }); it('should select associated filter', () => { diff --git a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts index 28bf609a4f210..259a14b208969 100644 --- a/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts +++ b/x-pack/plugins/security_solution/public/notes/store/notes.slice.ts @@ -58,7 +58,7 @@ export interface NotesState extends EntityState { direction: 'asc' | 'desc'; }; filter: string; - userFilter: string; + createdByFilter: string; search: string; associatedFilter: AssociatedFilter; selectedIds: string[]; @@ -94,7 +94,7 @@ export const initialNotesState: NotesState = notesAdapter.getInitialState({ direction: 'desc', }, filter: '', - userFilter: '', + createdByFilter: '', associatedFilter: AssociatedFilter.all, search: '', selectedIds: [], @@ -129,13 +129,13 @@ export const fetchNotes = createAsyncThunk< sortField: string; sortOrder: string; filter: string; - userFilter: string; + createdByFilter: string; associatedFilter: AssociatedFilter; search: string; }, {} >('notes/fetchNotes', async (args) => { - const { page, perPage, sortField, sortOrder, filter, userFilter, associatedFilter, search } = + const { page, perPage, sortField, sortOrder, filter, createdByFilter, associatedFilter, search } = args; const res = await fetchNotesApi({ page, @@ -143,7 +143,7 @@ export const fetchNotes = createAsyncThunk< sortField, sortOrder, filter, - userFilter, + createdByFilter, associatedFilter, search, }); @@ -169,7 +169,7 @@ export const deleteNotes = createAsyncThunk { state.filter = action.payload; }, - userFilterUsers: (state: NotesState, action: { payload: string }) => { - state.userFilter = action.payload; + userFilterCreatedBy: (state: NotesState, action: { payload: string }) => { + state.createdByFilter = action.payload; }, userFilterAssociatedNotes: (state: NotesState, action: { payload: AssociatedFilter }) => { state.associatedFilter = action.payload; @@ -332,7 +332,7 @@ export const selectNotesTableSelectedIds = (state: State) => state.notes.selecte export const selectNotesTableSearch = (state: State) => state.notes.search; -export const selectNotesTableUserFilters = (state: State) => state.notes.userFilter; +export const selectNotesTableCreatedByFilter = (state: State) => state.notes.createdByFilter; export const selectNotesTableAssociatedFilter = (state: State) => state.notes.associatedFilter; @@ -423,7 +423,7 @@ export const { userSelectedPerPage, userSortedNotes, userFilteredNotes, - userFilterUsers, + userFilterCreatedBy, userFilterAssociatedNotes, userSearchedNotes, userSelectedRow, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts index 905e28872a5a4..90ec0bce1be00 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { StartServicesAccessor } from '@kbn/core-lifecycle-server'; +import type { StartPlugins } from '../../../plugin_contract'; import type { SecuritySolutionPluginRouter } from '../../../types'; import type { ConfigType } from '../../..'; import { @@ -27,7 +29,11 @@ import { persistNoteRoute, deleteNoteRoute, getNotesRoute } from './notes'; import { persistPinnedEventRoute } from './pinned_events'; -export function registerTimelineRoutes(router: SecuritySolutionPluginRouter, config: ConfigType) { +export function registerTimelineRoutes( + router: SecuritySolutionPluginRouter, + config: ConfigType, + startServices: StartServicesAccessor +) { createTimelinesRoute(router); patchTimelinesRoute(router); @@ -46,7 +52,7 @@ export function registerTimelineRoutes(router: SecuritySolutionPluginRouter, con persistNoteRoute(router); deleteNoteRoute(router); - getNotesRoute(router); + getNotesRoute(router, startServices); persistPinnedEventRoute(router); } diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts index 7b8c732ae54ca..0cd7853b38a1b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/notes/get_notes.ts @@ -15,6 +15,9 @@ import type { } from '@kbn/core-saved-objects-api-server'; import type { KueryNode } from '@kbn/es-query'; import { nodeBuilder, nodeTypes } from '@kbn/es-query'; +import type { StartServicesAccessor } from '@kbn/core-lifecycle-server'; +import type { UserProfile } from '@kbn/core-user-profile-common'; +import type { StartPlugins } from '../../../../plugin_contract'; import { AssociatedFilter } from '../../../../../common/notes/constants'; import { timelineSavedObjectType } from '../../saved_object_mappings'; import type { SecuritySolutionPluginRouter } from '../../../../types'; @@ -27,7 +30,10 @@ import { noteSavedObjectType } from '../../saved_object_mappings/notes'; import { GetNotesRequestQuery, type GetNotesResponse } from '../../../../../common/api/timeline'; /* eslint-disable complexity */ -export const getNotesRoute = (router: SecuritySolutionPluginRouter) => { +export const getNotesRoute = ( + router: SecuritySolutionPluginRouter, + startServices: StartServicesAccessor +) => { router.versioned .get({ path: NOTE_URL, @@ -139,11 +145,41 @@ export const getNotesRoute = (router: SecuritySolutionPluginRouter) => { const filterKueryNodeArray = [filterAsKueryNode]; // retrieve all the notes created by a specific user - const userFilter = queryParams?.userFilter; - if (userFilter) { - filterKueryNodeArray.push( - nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, userFilter) - ); + // the createdByFilter value is the uuid of the user + const createdByFilter = queryParams?.createdByFilter; // now uuid + if (createdByFilter) { + // because the notes createdBy property can be either full_name, email or username + // see pickSaveNote (https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/server/lib/timeline/saved_object/notes/saved_object.ts#L302) + // which uses the getUserDisplayName (https://github.com/elastic/kibana/blob/main/packages/kbn-user-profile-components/src/user_profile.ts#L138) + const [_, { security }] = await startServices(); + const users: UserProfile[] = await security.userProfiles.bulkGet({ + uids: new Set([createdByFilter]), + }); + // once we retrieve the user by the uuid we can search all the notes that have the createdBy property with full_name, email or username values + if (users && users.length > 0) { + const { + user: { email, full_name: fullName, username: userName }, + } = users[0]; + const createdByNodeArray = []; + if (fullName) { + createdByNodeArray.push( + nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, fullName) + ); + } + if (userName) { + createdByNodeArray.push( + nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, userName) + ); + } + if (email) { + createdByNodeArray.push( + nodeBuilder.is(`${noteSavedObjectType}.attributes.createdBy`, email) + ); + } + filterKueryNodeArray.push(nodeBuilder.or(createdByNodeArray)); + } else { + throw new Error(`User with uid ${createdByFilter} not found`); + } } const associatedFilter = queryParams?.associatedFilter; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 6f245bd04a02b..119e0788fa913 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -99,7 +99,7 @@ export const initRoutes = ( registerResolverRoutes(router, getStartServices, config); - registerTimelineRoutes(router, config); + registerTimelineRoutes(router, config, getStartServices); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals // POST /api/detection_engine/signals/status diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index a79fb2772757d..d7eea2ee4fa76 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -230,5 +230,8 @@ "@kbn/core-security-server-mocks", "@kbn/serverless", "@kbn/core-user-profile-browser", + "@kbn/data-stream-adapter", + "@kbn/core-lifecycle-server", + "@kbn/core-user-profile-common", ] } diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts index 027c0a20262a8..b69ce55b36ce5 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts @@ -408,37 +408,56 @@ export default function ({ getService }: FtrProviderContext) { expect(notes[2].eventId).to.be('1'); }); - // TODO figure out why this test is failing on CI but not locally - it.skip('should retrieve all notes that have been created by a specific user', async () => { + // skipped https://github.com/elastic/kibana/issues/196896 + describe('@skipInServerless', () => { + // TODO we need to figure out how to retrieve the uid of the current user in the test environment + it.skip('should retrieve all notes that have been created by a specific user', async () => { + await Promise.all([ + createNote(supertest, { text: 'first note' }), + createNote(supertest, { text: 'second note' }), + ]); + + const response = await supertest + .get('/api/note?createdByFilter=elastic') + .set('kbn-xsrf', 'true') + .set('elastic-api-version', '2023-10-31'); + const { totalCount } = response.body as GetNotesResult; + + expect(totalCount).to.be(2); + }); + + // TODO we need to figure out how to create another user in the test environment + it.skip('should return nothing if no notes have been created by that user', async () => { await Promise.all([ createNote(supertest, { text: 'first note' }), createNote(supertest, { text: 'second note' }), ]); const response = await supertest - .get('/api/note?userFilter=elastic') + .get('/api/note?createdByFilter=user1') .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31'); const { totalCount } = response.body; - expect(totalCount).to.be(2); + expect(totalCount).to.be(0); }); - it('should return nothing if no notes have been created by that user', async () => { + it('should return error if user does not exist', async () => { await Promise.all([ createNote(supertest, { text: 'first note' }), createNote(supertest, { text: 'second note' }), ]); const response = await supertest - .get('/api/note?userFilter=user1') + .get(`${NOTE_URL}?createdByFilter=wrong_user`) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31'); - const { totalCount } = response.body; - - expect(totalCount).to.be(0); + expect(response.body).to.not.have.property('totalCount'); + expect(response.body).to.not.have.property('notes'); + expect(response.body.message).to.be('User with uid wrong_user not found'); + expect(response.body.status_code).to.be(500); }); it('should retrieve all notes that have an association with a document only', async () => { From b74eb973f7a699f2a48da8ec3acca834a1dafbd9 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:43:57 +0000 Subject: [PATCH 2/3] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/security_solution/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index d7eea2ee4fa76..7153df9e95b61 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -230,7 +230,6 @@ "@kbn/core-security-server-mocks", "@kbn/serverless", "@kbn/core-user-profile-browser", - "@kbn/data-stream-adapter", "@kbn/core-lifecycle-server", "@kbn/core-user-profile-common", ] From 42a117b402682c62579029a280e4a35d479bf9c0 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:24:14 +0000 Subject: [PATCH 3/3] [CI] Auto-commit changed files from 'make api-docs && make api-docs-staging' --- oas_docs/output/kibana.serverless.staging.yaml | 2 +- oas_docs/output/kibana.staging.yaml | 2 +- .../test_suites/investigation/saved_objects/tests/notes.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index ecfc211c98e1e..7027968029393 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -15012,7 +15012,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index 508148435fbea..8f8760c0569f3 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -18442,7 +18442,7 @@ paths: nullable: true type: string - in: query - name: userFilter + name: createdByFilter schema: nullable: true type: string diff --git a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts index b69ce55b36ce5..cc4b2750808c0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/investigation/saved_objects/tests/notes.ts @@ -423,7 +423,8 @@ export default function ({ getService }: FtrProviderContext) { .set('elastic-api-version', '2023-10-31'); const { totalCount } = response.body as GetNotesResult; - expect(totalCount).to.be(2); + expect(totalCount).to.be(2); + }); }); // TODO we need to figure out how to create another user in the test environment