[8.x] [Epic] Knowledge Base - API integration tests (#8737) (#197290) (#197525)

# 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:
Kibana Machine 2024-10-24 08:43:28 +11:00 committed by GitHub
parent 23d02d744d
commit 495de32666
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 666 additions and 38 deletions

View file

@ -117,8 +117,13 @@ export class DocumentsDataWriter implements DocumentsDataWriter {
{
bool: {
must_not: {
exists: {
field: 'users',
nested: {
path: 'users',
query: {
exists: {
field: 'users',
},
},
},
},
},

View file

@ -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) {

View file

@ -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',
});
});
});
});
});
};

View file

@ -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,

View file

@ -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,

View file

@ -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;
};

View file

@ -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;
};

View file

@ -51,5 +51,6 @@
"@kbn/security-plugin",
"@kbn/ftr-common-functional-ui-services",
"@kbn/spaces-plugin",
"@kbn/elastic-assistant-plugin",
]
}