mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Epic] Knowledge Base - API integration tests (#8737) (#197290)](https://github.com/elastic/kibana/pull/197290) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Ievgen Sorokopud","email":"ievgen.sorokopud@elastic.co"},"sourceCommit":{"committedDate":"2024-10-23T19:58:09Z","message":"[Epic] Knowledge Base - API integration tests (#8737) (#197290)\n\n## Summary\r\n\r\nThis is a followup to the main Knowledge Base changes where we've:\r\n1. Fixed the issue with access control to KB entries via bulk actions\r\nAPIs\r\n2. Added the RBAC validation for the bulk actions API\r\n3. Added integration tests to cover the bulk actions API\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- Genai KB integration tests: [100 ESS + 100\r\nServerless](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7208)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"fd538614631c85c7cd3580e7d3270b9d38c57713","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team: SecuritySolution","backport:prev-minor","Feature:Security Assistant","Team:Security Generative AI","v8.16.0"],"title":"[Epic] Knowledge Base - API integration tests (#8737)","number":197290,"url":"https://github.com/elastic/kibana/pull/197290","mergeCommit":{"message":"[Epic] Knowledge Base - API integration tests (#8737) (#197290)\n\n## Summary\r\n\r\nThis is a followup to the main Knowledge Base changes where we've:\r\n1. Fixed the issue with access control to KB entries via bulk actions\r\nAPIs\r\n2. Added the RBAC validation for the bulk actions API\r\n3. Added integration tests to cover the bulk actions API\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- Genai KB integration tests: [100 ESS + 100\r\nServerless](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7208)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"fd538614631c85c7cd3580e7d3270b9d38c57713"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197290","number":197290,"mergeCommit":{"message":"[Epic] Knowledge Base - API integration tests (#8737) (#197290)\n\n## Summary\r\n\r\nThis is a followup to the main Knowledge Base changes where we've:\r\n1. Fixed the issue with access control to KB entries via bulk actions\r\nAPIs\r\n2. Added the RBAC validation for the bulk actions API\r\n3. Added integration tests to cover the bulk actions API\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- Genai KB integration tests: [100 ESS + 100\r\nServerless](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/7208)\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"fd538614631c85c7cd3580e7d3270b9d38c57713"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Ievgen Sorokopud <ievgen.sorokopud@elastic.co>
This commit is contained in:
parent
23d02d744d
commit
495de32666
8 changed files with 666 additions and 38 deletions
|
@ -117,8 +117,13 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
|
|||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'users',
|
||||
nested: {
|
||||
path: 'users',
|
||||
query: {
|
||||
exists: {
|
||||
field: 'users',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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<EsKnowledgeBaseEntrySchema>({
|
||||
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<EsKnowledgeBaseEntrySchema>({
|
||||
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) {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<PerformKnowledgeBaseEntryBulkActionResponse> => {
|
||||
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<PerformKnowledgeBaseEntryBulkActionResponse> => {
|
||||
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;
|
||||
};
|
|
@ -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<KnowledgeBaseEntryResponse> => {
|
||||
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<KnowledgeBaseEntryResponse> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -51,5 +51,6 @@
|
|||
"@kbn/security-plugin",
|
||||
"@kbn/ftr-common-functional-ui-services",
|
||||
"@kbn/spaces-plugin",
|
||||
"@kbn/elastic-assistant-plugin",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue