diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts index 21fbb8c3c11e7..3d023a6fc3811 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/esql/use_esql_overall_stats_data.ts @@ -91,7 +91,8 @@ const getESQLDocumentCountStats = async ( ` | EVAL _timestamp_= TO_DOUBLE(DATE_TRUNC(${intervalMs} millisecond, ${getSafeESQLName( timeFieldName )})) - | stats rows = count(*) by _timestamp_` + | stats rows = count(*) by _timestamp_ + | LIMIT 1000` ); const request = { diff --git a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts index 87ec80568dbdd..32b579fdeb71a 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/data_stream/documents_data_writer.ts @@ -117,8 +117,13 @@ export class DocumentsDataWriter implements DocumentsDataWriter { { bool: { must_not: { - exists: { - field: 'users', + nested: { + path: 'users', + query: { + exists: { + field: 'users', + }, + }, }, }, }, diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts index ce3f0c8c92693..cfb2303010756 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts @@ -28,12 +28,16 @@ import { } from '../../../ai_assistant_data_clients/knowledge_base/types'; import { ElasticAssistantPluginRouter } from '../../../types'; import { buildResponse } from '../../utils'; -import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms'; +import { + transformESSearchToKnowledgeBaseEntry, + transformESToKnowledgeBase, +} from '../../../ai_assistant_data_clients/knowledge_base/transforms'; import { getUpdateScript, transformToCreateSchema, transformToUpdateSchema, } from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry'; +import { getKBUserFilter } from './utils'; export interface BulkOperationError { message: string; @@ -179,8 +183,19 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug const spaceId = ctx.elasticAssistant.getSpaceId(); // Authenticated user null check completed in `performChecks()` above const authenticatedUser = ctx.elasticAssistant.getCurrentUser() as AuthenticatedUser; + const userFilter = getKBUserFilter(authenticatedUser); + const manageGlobalKnowledgeBaseAIAssistant = + kbDataClient?.options.manageGlobalKnowledgeBaseAIAssistant; if (body.create && body.create.length > 0) { + // RBAC validation + body.create.forEach((entry) => { + const isGlobal = entry.users != null && entry.users.length === 0; + if (isGlobal && !manageGlobalKnowledgeBaseAIAssistant) { + throw new Error(`User lacks privileges to create global knowledge base entries`); + } + }); + const result = await kbDataClient?.findDocuments({ perPage: 100, page: 1, @@ -199,6 +214,44 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug } } + const validateDocumentsModification = async ( + documentIds: string[], + operation: 'delete' | 'update' + ) => { + if (!documentIds.length) { + return; + } + const documentsFilter = documentIds.map((id) => `_id:${id}`).join(' OR '); + const entries = await kbDataClient?.findDocuments({ + page: 1, + perPage: 100, + filter: `${documentsFilter} AND ${userFilter}`, + }); + const availableEntries = entries + ? transformESSearchToKnowledgeBaseEntry(entries.data) + : []; + availableEntries.forEach((entry) => { + // RBAC validation + const isGlobal = entry.users != null && entry.users.length === 0; + if (isGlobal && !manageGlobalKnowledgeBaseAIAssistant) { + throw new Error( + `User lacks privileges to ${operation} global knowledge base entries` + ); + } + }); + const availableIds = availableEntries.map((doc) => doc.id); + const nonAvailableIds = documentIds.filter((id) => !availableIds.includes(id)); + if (nonAvailableIds.length > 0) { + throw new Error(`Could not find documents to ${operation}: ${nonAvailableIds}.`); + } + }; + + await validateDocumentsModification(body.delete?.ids ?? [], 'delete'); + await validateDocumentsModification( + body.update?.map((entry) => entry.id) ?? [], + 'update' + ); + const writer = await kbDataClient?.getWriter(); const changedAt = new Date().toISOString(); const { @@ -214,11 +267,11 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug spaceId, user: authenticatedUser, entry, + global: entry.users != null && entry.users.length === 0, }) ), documentsToDelete: body.delete?.ids, documentsToUpdate: body.update?.map((entry) => - // TODO: KB-RBAC check, required when users != null as entry will either be created globally if empty transformToUpdateSchema({ user: authenticatedUser, updatedAt: changedAt, @@ -241,9 +294,10 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug return buildBulkResponse(response, { // @ts-ignore-next-line TS2322 - updated: docsUpdated, + updated: transformESToKnowledgeBase(docsUpdated), created: created?.data ? transformESSearchToKnowledgeBaseEntry(created?.data) : [], deleted: docsDeleted ?? [], + skipped: [], errors, }); } catch (err) { diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts index 26b6a93cec9fa..37c65ebb9a314 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts @@ -36,6 +36,6 @@ export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AllInferenceEndpointsTable export const PIPELINE_URL = 'ingest/ingest_pipelines'; export const PRECONFIGURED_ENDPOINTS = { - ELSER: '.elser-2', - E5: '.multi-e5-small', + ELSER: '.elser-2-elasticsearch', + E5: '.multilingual-e5-small-elasticsearch', }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.test.tsx index 22c509ca22989..f5f4a0b7e8bdc 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/delete_action.test.tsx @@ -44,7 +44,7 @@ describe('Delete Action', () => { }); it('disable the delete action for preconfigured endpoint', () => { - const preconfiguredMockItem = { ...mockItem, endpoint: '.elser-2' }; + const preconfiguredMockItem = { ...mockItem, endpoint: '.multilingual-e5-small-elasticsearch' }; render(); expect(screen.getByTestId('inferenceUIDeleteAction')).toBeDisabled(); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx index 91cc303ed4568..85718478f65fd 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx @@ -46,7 +46,7 @@ const inferenceEndpoints = [ task_settings: {}, }, { - inference_id: '.elser-2', + inference_id: '.elser-2-elasticsearch', task_type: 'sparse_embedding', service: 'elasticsearch', service_settings: { @@ -57,7 +57,7 @@ const inferenceEndpoints = [ task_settings: {}, }, { - inference_id: '.multi-e5-small', + inference_id: '.multilingual-e5-small-elasticsearch', task_type: 'text_embedding', service: 'elasticsearch', service_settings: { @@ -80,8 +80,8 @@ describe('When the tabular page is loaded', () => { render(); const rows = screen.getAllByRole('row'); - expect(rows[1]).toHaveTextContent('.elser-2'); - expect(rows[2]).toHaveTextContent('.multi-e5-small'); + expect(rows[1]).toHaveTextContent('.elser-2-elasticsearch'); + expect(rows[2]).toHaveTextContent('.multilingual-e5-small-elasticsearch'); expect(rows[3]).toHaveTextContent('local-model'); expect(rows[4]).toHaveTextContent('my-elser-model-05'); expect(rows[5]).toHaveTextContent('third-party-model'); diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts index 7cd44a21ce236..2ecb368c2ba7b 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts @@ -6,6 +6,7 @@ */ import expect from 'expect'; +import { KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE } from '@kbn/elastic-assistant-plugin/common/constants'; import { FtrProviderContext } from '../../../../../ftr_provider_context'; import { createEntry, createEntryForUser } from '../utils/create_entry'; import { findEntries } from '../utils/find_entry'; @@ -18,7 +19,11 @@ import { import { removeServerGeneratedProperties } from '../utils/remove_server_generated_properties'; import { MachineLearningProvider } from '../../../../../../functional/services/ml'; import { documentEntry, indexEntry, globalDocumentEntry } from './mocks/entries'; -import { secOnlySpacesAll } from '../utils/auth/users'; +import { secOnlySpacesAll, secOnlySpacesAllAssistantMinimalAll } from '../utils/auth/users'; +import { + bulkActionKnowledgeBaseEntries, + bulkActionKnowledgeBaseEntriesForUser, +} from '../utils/bulk_actions_entry'; export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); @@ -42,8 +47,6 @@ export default ({ getService }: FtrProviderContext) => { }); describe('Create Entries', () => { - // TODO: KB-RBAC: Added stubbed admin tests for when RBAC is enabled. Hopefully this helps :] - // NOTE: Will need to update each section with the expected user, can use `createEntryForUser()` helper describe('Admin User', () => { it('should create a new document entry for the current user', async () => { const entry = await createEntry({ supertest, log, entry: documentEntry }); @@ -135,16 +138,18 @@ export default ({ getService }: FtrProviderContext) => { expect(removeServerGeneratedProperties(entry)).toEqual(expectedDocumentEntry); }); - // TODO: KB-RBAC: Action not currently limited without RBAC - it.skip('should not be able to create a global entry', async () => { - const entry = await createEntry({ supertest, log, entry: globalDocumentEntry }); - - const expectedDocumentEntry = { - ...globalDocumentEntry, - users: [{ name: 'elastic' }], - }; - - expect(removeServerGeneratedProperties(entry)).toEqual(expectedDocumentEntry); + it('should not be able to create a global entry', async () => { + const response = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: globalDocumentEntry, + user: secOnlySpacesAllAssistantMinimalAll, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: 'User lacks privileges to create global knowledge base entries', + }); }); }); }); @@ -188,5 +193,444 @@ export default ({ getService }: FtrProviderContext) => { expect(entries.total).toEqual(0); }); }); + + describe('Bulk Actions', () => { + describe('General', () => { + it(`should throw an error for more than ${KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE} actions`, async () => { + const entry = await createEntry({ supertest, log, entry: documentEntry }); + const updatedDocumentEntry = { + id: entry.id, + ...documentEntry, + text: 'This is a sample of updated document entry', + }; + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { + create: [documentEntry], + update: [updatedDocumentEntry], + delete: { + ids: Array(KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE).fill('fake-document-id'), + }, + }, + expectedHttpCode: 400, + }); + expect(response).toEqual({ + status_code: 400, + message: `More than ${KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE} ids sent for bulk edit action.`, + }); + }); + + it('should perform create, update and delete actions for the current user', async () => { + const entry1 = await createEntry({ supertest, log, entry: documentEntry }); + const entry2 = await createEntry({ supertest, log, entry: globalDocumentEntry }); + + const updatedDocumentEntry = { + id: entry2.id, + ...globalDocumentEntry, + text: 'This is a sample of updated document entry', + }; + const expectedUpdatedDocumentEntry = { + ...globalDocumentEntry, + text: 'This is a sample of updated document entry', + }; + + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { + create: [indexEntry], + update: [updatedDocumentEntry], + delete: { ids: [entry1.id] }, + }, + }); + + const expectedCreatedIndexEntry = { + ...indexEntry, + users: [{ name: 'elastic' }], + }; + + expect(response.attributes.summary.succeeded).toEqual(3); + expect(response.attributes.summary.total).toEqual(3); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(expectedCreatedIndexEntry)]) + ); + expect(response.attributes.results.updated).toEqual( + expect.arrayContaining([expect.objectContaining(expectedUpdatedDocumentEntry)]) + ); + expect(response.attributes.results.deleted).toEqual(expect.arrayContaining([entry1.id])); + }); + }); + + describe('Create Entries', () => { + it('should create a new document entry for the current user', async () => { + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { create: [documentEntry] }, + }); + + const expectedDocumentEntry = { + ...documentEntry, + users: [{ name: 'elastic' }], + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should create a new index entry for the current user', async () => { + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { create: [indexEntry] }, + }); + + const expectedIndexEntry = { + ...indexEntry, + inputSchema: [], + outputFields: [], + users: [{ name: 'elastic' }], + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(expectedIndexEntry)]) + ); + }); + + it('should create a new global entry for all users', async () => { + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { create: [globalDocumentEntry] }, + }); + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(globalDocumentEntry)]) + ); + }); + + it('should create a new global entry for all users in another space', async () => { + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { create: [globalDocumentEntry] }, + space: 'space-x', + }); + + const expectedDocumentEntry = { + ...globalDocumentEntry, + namespace: 'space-x', + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should create own private document even if user does not have `manage_global_knowledge_base` privileges', async () => { + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { create: [documentEntry] }, + user: secOnlySpacesAllAssistantMinimalAll, + }); + + const expectedDocumentEntry = { + ...documentEntry, + users: [{ name: secOnlySpacesAllAssistantMinimalAll.username }], + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.created).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should not create global document if user does not have `manage_global_knowledge_base` privileges', async () => { + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { create: [globalDocumentEntry] }, + user: secOnlySpacesAllAssistantMinimalAll, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: 'User lacks privileges to create global knowledge base entries', + }); + }); + }); + + describe('Update Entries', () => { + it('should update own document entry', async () => { + const entry = await createEntry({ supertest, log, entry: documentEntry }); + const updatedDocumentEntry = { + id: entry.id, + ...documentEntry, + text: 'This is a sample of updated document entry', + }; + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { update: [updatedDocumentEntry] }, + }); + + const expectedDocumentEntry = { + ...documentEntry, + users: [{ name: 'elastic' }], + text: 'This is a sample of updated document entry', + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.updated).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should not update private document entry created by another user', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: documentEntry, + user: secOnlySpacesAll, + }); + + const updatedDocumentEntry = { + id: entry.id, + ...documentEntry, + text: 'This is a sample of updated document entry', + }; + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { update: [updatedDocumentEntry] }, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: `Could not find documents to update: ${entry.id}.`, + }); + }); + + it('should update own global document entry', async () => { + const entry = await createEntry({ supertest, log, entry: globalDocumentEntry }); + const updatedDocumentEntry = { + id: entry.id, + ...globalDocumentEntry, + text: 'This is a sample of updated global document entry', + }; + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { update: [updatedDocumentEntry] }, + }); + + const expectedDocumentEntry = { + ...globalDocumentEntry, + text: 'This is a sample of updated global document entry', + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.updated).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should update global document entry created by another user', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: globalDocumentEntry, + user: secOnlySpacesAll, + }); + const updatedDocumentEntry = { + id: entry.id, + ...globalDocumentEntry, + text: 'This is a sample of updated global document entry', + }; + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { update: [updatedDocumentEntry] }, + }); + + const expectedDocumentEntry = { + ...globalDocumentEntry, + text: 'This is a sample of updated global document entry', + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.updated).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should update own private document even if user does not have `manage_global_knowledge_base` privileges', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: documentEntry, + user: secOnlySpacesAllAssistantMinimalAll, + }); + + const updatedDocumentEntry = { + id: entry.id, + ...documentEntry, + text: 'This is a sample of updated document entry', + }; + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { update: [updatedDocumentEntry] }, + user: secOnlySpacesAllAssistantMinimalAll, + }); + + const expectedDocumentEntry = { + ...documentEntry, + users: [{ name: secOnlySpacesAllAssistantMinimalAll.username }], + text: 'This is a sample of updated document entry', + }; + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.updated).toEqual( + expect.arrayContaining([expect.objectContaining(expectedDocumentEntry)]) + ); + }); + + it('should not update global document if user does not have `manage_global_knowledge_base` privileges', async () => { + const entry = await createEntry({ supertest, log, entry: globalDocumentEntry }); + const updatedDocumentEntry = { + id: entry.id, + ...globalDocumentEntry, + text: 'This is a sample of updated global document entry', + }; + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { update: [updatedDocumentEntry] }, + user: secOnlySpacesAllAssistantMinimalAll, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: 'User lacks privileges to update global knowledge base entries', + }); + }); + }); + + describe('Delete Entries', () => { + it('should delete own document entry', async () => { + const entry = await createEntry({ supertest, log, entry: documentEntry }); + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { delete: { ids: [entry.id] } }, + }); + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.deleted).toEqual(expect.arrayContaining([entry.id])); + }); + + it('should not delete private document entry created by another user', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: documentEntry, + user: secOnlySpacesAll, + }); + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { delete: { ids: [entry.id] } }, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: `Could not find documents to delete: ${entry.id}.`, + }); + }); + + it('should delete own global document entry', async () => { + const entry = await createEntry({ supertest, log, entry: globalDocumentEntry }); + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { delete: { ids: [entry.id] } }, + }); + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.deleted).toEqual(expect.arrayContaining([entry.id])); + }); + + it('should delete global document entry created by another user', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: globalDocumentEntry, + user: secOnlySpacesAll, + }); + const response = await bulkActionKnowledgeBaseEntries({ + supertest, + log, + payload: { delete: { ids: [entry.id] } }, + }); + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.deleted).toEqual(expect.arrayContaining([entry.id])); + }); + + it('should delete own private document even if user does not have `manage_global_knowledge_base` privileges', async () => { + const entry = await createEntryForUser({ + supertestWithoutAuth, + log, + entry: documentEntry, + user: secOnlySpacesAllAssistantMinimalAll, + }); + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { delete: { ids: [entry.id] } }, + user: secOnlySpacesAllAssistantMinimalAll, + }); + + expect(response.attributes.summary.succeeded).toEqual(1); + expect(response.attributes.summary.total).toEqual(1); + expect(response.attributes.results.deleted).toEqual(expect.arrayContaining([entry.id])); + }); + + it('should not delete global document if user does not have `manage_global_knowledge_base` privileges', async () => { + const entry = await createEntry({ supertest, log, entry: globalDocumentEntry }); + const response = await bulkActionKnowledgeBaseEntriesForUser({ + supertestWithoutAuth, + log, + payload: { delete: { ids: [entry.id] } }, + user: secOnlySpacesAllAssistantMinimalAll, + expectedHttpCode: 500, + }); + expect(response).toEqual({ + status_code: 500, + message: 'User lacks privileges to delete global knowledge base entries', + }); + }); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts index d83a2791d3409..9e81e7d11fffd 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/roles.ts @@ -179,6 +179,26 @@ export const securitySolutionOnlyReadSpacesAll: Role = { }, }; +export const securitySolutionOnlyAllSpacesAllAssistantMinimalAll: Role = { + name: 'sec_only_all_spaces_all_assistant_minimal_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + siem: ['all'], + securitySolutionAssistant: ['minimal_all'], + securitySolutionAttackDiscovery: ['all'], + aiAssistantManagementSelection: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + export const roles = [ noKibanaPrivileges, globalRead, @@ -193,6 +213,7 @@ export const allRoles = [ securitySolutionOnlyRead, securitySolutionOnlyAllSpacesAll, securitySolutionOnlyAllSpacesAllWithReadESIndices, + securitySolutionOnlyAllSpacesAllAssistantMinimalAll, securitySolutionOnlyReadSpacesAll, securitySolutionOnlyAllSpace2, securitySolutionOnlyReadSpace2, diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/users.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/users.ts index 6e0d790072df1..62fe17bacc76a 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/users.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/auth/users.ts @@ -17,6 +17,7 @@ import { securitySolutionOnlyAllSpace2, securitySolutionOnlyReadSpace2, securitySolutionOnlyAllSpacesAllWithReadESIndices, + securitySolutionOnlyAllSpacesAllAssistantMinimalAll, } from './roles'; import { User } from './types'; @@ -86,6 +87,12 @@ export const secOnlySpacesAllEsReadAll: User = { roles: [securitySolutionOnlyAllSpacesAllWithReadESIndices.name], }; +export const secOnlySpacesAllAssistantMinimalAll: User = { + username: 'sec_only_all_spaces_all_assistant_minimal_all', + password: 'sec_only_all_spaces_all_assistant_minimal_all', + roles: [securitySolutionOnlyAllSpacesAllAssistantMinimalAll.name], +}; + export const allUsers = [ superUser, secOnly, @@ -94,6 +101,7 @@ export const allUsers = [ noKibanaPrivileges, secOnlySpacesAll, secOnlySpacesAllEsReadAll, + secOnlySpacesAllAssistantMinimalAll, secOnlyReadSpacesAll, secOnlySpace2, secOnlyReadSpace2, diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/bulk_actions_entry.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/bulk_actions_entry.ts new file mode 100644 index 0000000000000..a709070d56fef --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/bulk_actions_entry.ts @@ -0,0 +1,101 @@ +/* + * 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 { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import type { ToolingLog } from '@kbn/tooling-log'; +import type SuperTest from 'supertest'; +import { + ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION, + KnowledgeBaseEntryCreateProps, + KnowledgeBaseEntryUpdateProps, + PerformKnowledgeBaseEntryBulkActionResponse, +} from '@kbn/elastic-assistant-common'; +import type { User } from './auth/types'; + +import { routeWithNamespace } from '../../../../../../common/utils/security_solution'; + +/** + * Performs bulk actions on Knowledge Base entries + * @param supertest The supertest deps + * @param log The tooling logger + * @param payload The bulk action payload + * @param space The Kibana Space to update the entry in (optional) + * @param expectedHttpCode The expected http status code (optional) + */ +export const bulkActionKnowledgeBaseEntries = async ({ + supertest, + log, + payload, + space, + expectedHttpCode = 200, +}: { + supertest: SuperTest.Agent; + log: ToolingLog; + payload: { + create?: KnowledgeBaseEntryCreateProps[]; + update?: KnowledgeBaseEntryUpdateProps[]; + delete?: { ids: string[] }; + }; + space?: string; + expectedHttpCode?: number; +}): Promise => { + const route = routeWithNamespace( + ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION, + space + ); + const response = await supertest + .post(route) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .send(payload) + .expect(expectedHttpCode); + + return response.body; +}; + +/** + * Performs bulk actions on Knowledge Base entries for a given User + * @param supertest The supertest deps + * @param log The tooling logger + * @param payload The bulk action payload + * @param user The user to update the entry on behalf of + * @param space The Kibana Space to update the entry in (optional) + * @param expectedHttpCode The expected http status code (optional) + */ +export const bulkActionKnowledgeBaseEntriesForUser = async ({ + supertestWithoutAuth, + log, + payload, + user, + space, + expectedHttpCode = 200, +}: { + supertestWithoutAuth: SuperTest.Agent; + log: ToolingLog; + payload: { + create?: KnowledgeBaseEntryCreateProps[]; + update?: KnowledgeBaseEntryUpdateProps[]; + delete?: { ids: string[] }; + }; + user: User; + space?: string; + expectedHttpCode?: number; +}): Promise => { + const route = routeWithNamespace( + ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BULK_ACTION, + space + ); + const response = await supertestWithoutAuth + .post(route) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '1') + .send(payload) + .expect(expectedHttpCode); + + return response.body; +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/create_entry.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/create_entry.ts index f69c42dcbd9bd..3b4507d0c4ba0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/create_entry.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/create_entry.ts @@ -23,33 +23,30 @@ import { routeWithNamespace } from '../../../../../../common/utils/security_solu * @param log The tooling logger * @param entry The entry to create * @param space The Kibana Space to create the entry in (optional) + * @param expectedHttpCode The expected http status code (optional) */ export const createEntry = async ({ supertest, log, entry, space, + expectedHttpCode = 200, }: { supertest: SuperTest.Agent; log: ToolingLog; entry: KnowledgeBaseEntryCreateProps; space?: string; + expectedHttpCode?: number; }): Promise => { const route = routeWithNamespace(ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL, space); const response = await supertest .post(route) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .send(entry); - if (response.status !== 200) { - throw new Error( - `Unexpected non 200 ok when attempting to create entry: ${JSON.stringify( - response.status - )},${JSON.stringify(response, null, 4)}` - ); - } else { - return response.body; - } + .send(entry) + .expect(expectedHttpCode); + + return response.body; }; /** @@ -59,6 +56,7 @@ export const createEntry = async ({ * @param entry The entry to create * @param user The user to create the entry on behalf of * @param space The Kibana Space to create the entry in (optional) + * @param expectedHttpCode The expected http status code (optional) */ export const createEntryForUser = async ({ supertestWithoutAuth, @@ -66,12 +64,14 @@ export const createEntryForUser = async ({ entry, user, space, + expectedHttpCode = 200, }: { supertestWithoutAuth: SuperTest.Agent; log: ToolingLog; entry: KnowledgeBaseEntryCreateProps; user: User; space?: string; + expectedHttpCode?: number; }): Promise => { const route = routeWithNamespace(ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL, space); const response = await supertestWithoutAuth @@ -79,14 +79,8 @@ export const createEntryForUser = async ({ .auth(user.username, user.password) .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '1') - .send(entry); - if (response.status !== 200) { - throw new Error( - `Unexpected non 200 ok when attempting to create entry: ${JSON.stringify( - response.status - )},${JSON.stringify(response, null, 4)}` - ); - } else { - return response.body; - } + .send(entry) + .expect(expectedHttpCode); + + return response.body; }; diff --git a/x-pack/test/security_solution_api_integration/tsconfig.json b/x-pack/test/security_solution_api_integration/tsconfig.json index b7a320dd19720..d6af5b428dead 100644 --- a/x-pack/test/security_solution_api_integration/tsconfig.json +++ b/x-pack/test/security_solution_api_integration/tsconfig.json @@ -50,5 +50,6 @@ "@kbn/search-types", "@kbn/security-plugin", "@kbn/ftr-common-functional-ui-services", + "@kbn/elastic-assistant-plugin", ] }