From 3984bad117310ffed0700ff10dfe2a7d0dad6eee Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Wed, 23 Oct 2024 21:58:09 +0200 Subject: [PATCH] [Epic] Knowledge Base - API integration tests (#8737) (#197290) ## Summary This is a followup to the main Knowledge Base changes where we've: 1. Fixed the issue with access control to KB entries via bulk actions APIs 2. Added the RBAC validation for the bulk actions API 3. Added integration tests to cover the bulk actions API ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - Genai KB integration tests: [100 ESS + 100 Serverless](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7208) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit fd538614631c85c7cd3580e7d3270b9d38c57713) # Conflicts: # x-pack/test/security_solution_api_integration/tsconfig.json --- .../lib/data_stream/documents_data_writer.ts | 9 +- .../entries/bulk_actions_route.ts | 60 ++- .../trial_license_complete_tier/entries.ts | 470 +++++++++++++++++- .../entries/utils/auth/roles.ts | 21 + .../entries/utils/auth/users.ts | 8 + .../entries/utils/bulk_actions_entry.ts | 101 ++++ .../entries/utils/create_entry.ts | 34 +- .../tsconfig.json | 1 + 8 files changed, 666 insertions(+), 38 deletions(-) create mode 100644 x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/bulk_actions_entry.ts 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/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", ] }