[8.18] [Security Assistant] Fix Knowledge Base API (#211367) (#212456)

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Security Assistant] Fix Knowledge Base API
(#211367)](https://github.com/elastic/kibana/pull/211367)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Patryk
Kopyciński","email":"contact@patrykkopycinski.com"},"sourceCommit":{"committedDate":"2025-02-25T23:00:00Z","message":"[Security
Assistant] Fix Knowledge Base API (#211367)\n\n## Summary\n\nFixes bugs
related to Security Assistant Knowledge Base
API\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Hannah Mudge <Heenawter@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra <4283304+mbondyra@users.noreply.github.com>\nCo-authored-by:
Davis Plumlee
<56367316+dplumlee@users.noreply.github.com>\nCo-authored-by: Jatin
Kathuria <jatin.kathuria@elastic.co>\nCo-authored-by: Chris Cowan
<chris@elastic.co>\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Arturo
Lidueña <arturo.liduena@elastic.co>\nCo-authored-by: Jon
<jon@elastic.co>\nCo-authored-by: Rodney Norris
<rodney.norris@elastic.co>\nCo-authored-by: Elena Shostak
<165678770+elena-shostak@users.noreply.github.com>\nCo-authored-by:
Stratoula Kalafateli <efstratia.kalafateli@elastic.co>\nCo-authored-by:
Irene Blanco <irene.blanco@elastic.co>\nCo-authored-by: Cauê Marcondes
<55978943+cauemarcondes@users.noreply.github.com>\nCo-authored-by:
Carlos Crespo
<crespocarlos@users.noreply.github.com>","sha":"c822109a492fe4dcf38ca5aa6d87b2a95bf075c4","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Feature:Security
Assistant","Team:Security Generative
AI","backport:version","v8.18.0","v9.1.0"],"title":"[Security Assistant]
Fix Knowledge Base
API","number":211367,"url":"https://github.com/elastic/kibana/pull/211367","mergeCommit":{"message":"[Security
Assistant] Fix Knowledge Base API (#211367)\n\n## Summary\n\nFixes bugs
related to Security Assistant Knowledge Base
API\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Hannah Mudge <Heenawter@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra <4283304+mbondyra@users.noreply.github.com>\nCo-authored-by:
Davis Plumlee
<56367316+dplumlee@users.noreply.github.com>\nCo-authored-by: Jatin
Kathuria <jatin.kathuria@elastic.co>\nCo-authored-by: Chris Cowan
<chris@elastic.co>\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Arturo
Lidueña <arturo.liduena@elastic.co>\nCo-authored-by: Jon
<jon@elastic.co>\nCo-authored-by: Rodney Norris
<rodney.norris@elastic.co>\nCo-authored-by: Elena Shostak
<165678770+elena-shostak@users.noreply.github.com>\nCo-authored-by:
Stratoula Kalafateli <efstratia.kalafateli@elastic.co>\nCo-authored-by:
Irene Blanco <irene.blanco@elastic.co>\nCo-authored-by: Cauê Marcondes
<55978943+cauemarcondes@users.noreply.github.com>\nCo-authored-by:
Carlos Crespo
<crespocarlos@users.noreply.github.com>","sha":"c822109a492fe4dcf38ca5aa6d87b2a95bf075c4"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/211367","number":211367,"mergeCommit":{"message":"[Security
Assistant] Fix Knowledge Base API (#211367)\n\n## Summary\n\nFixes bugs
related to Security Assistant Knowledge Base
API\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by:
Hannah Mudge <Heenawter@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra <4283304+mbondyra@users.noreply.github.com>\nCo-authored-by:
Davis Plumlee
<56367316+dplumlee@users.noreply.github.com>\nCo-authored-by: Jatin
Kathuria <jatin.kathuria@elastic.co>\nCo-authored-by: Chris Cowan
<chris@elastic.co>\nCo-authored-by: Elastic Machine
<elasticmachine@users.noreply.github.com>\nCo-authored-by: Arturo
Lidueña <arturo.liduena@elastic.co>\nCo-authored-by: Jon
<jon@elastic.co>\nCo-authored-by: Rodney Norris
<rodney.norris@elastic.co>\nCo-authored-by: Elena Shostak
<165678770+elena-shostak@users.noreply.github.com>\nCo-authored-by:
Stratoula Kalafateli <efstratia.kalafateli@elastic.co>\nCo-authored-by:
Irene Blanco <irene.blanco@elastic.co>\nCo-authored-by: Cauê Marcondes
<55978943+cauemarcondes@users.noreply.github.com>\nCo-authored-by:
Carlos Crespo
<crespocarlos@users.noreply.github.com>","sha":"c822109a492fe4dcf38ca5aa6d87b2a95bf075c4"}}]}]
BACKPORT-->

---------

Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
Kibana Machine 2025-02-26 12:13:47 +11:00 committed by GitHub
parent 22bfe943cd
commit ee44e4d0fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1085 additions and 178 deletions

View file

@ -36382,7 +36382,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateProps'
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps'
required: true
responses:
'200':
@ -42773,6 +42773,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -42787,6 +42790,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryResponseFields'
@ -42794,6 +42798,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -42821,8 +42828,7 @@ components:
type: object
properties:
kbResource:
description: Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
type: string
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseResource'
source:
description: Source document name or filepath
type: string
@ -42847,6 +42853,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
name:
@ -42927,6 +42936,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -42941,6 +42953,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryResponseFields'
@ -42948,6 +42961,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -43007,6 +43023,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
name:
@ -43186,6 +43205,18 @@ components:
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryUpdateFields'
discriminator:
propertyName: type
Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps:
anyOf:
- $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryCreateFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryCreateFields'
discriminator:
propertyName: type
Security_AI_Assistant_API_KnowledgeBaseResource:
description: Knowledge Base resource name for grouping entries, e.g. 'security_labs', 'user', etc
enum:
- security_labs
- user
type: string
Security_AI_Assistant_API_KnowledgeBaseResponse:
description: AI assistant KnowledgeBase.
type: object

View file

@ -20468,7 +20468,7 @@ paths:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateProps'
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps'
required: true
responses:
'200':
@ -31033,6 +31033,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -31047,6 +31050,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryResponseFields'
@ -31054,6 +31058,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -31081,8 +31088,7 @@ components:
type: object
properties:
kbResource:
description: Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
type: string
$ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseResource'
source:
description: Source document name or filepath
type: string
@ -31107,6 +31113,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
name:
@ -31187,6 +31196,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -31201,6 +31213,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryResponseFields'
@ -31208,6 +31221,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -31267,6 +31283,9 @@ components:
allOf:
- type: object
properties:
global:
description: Whether this Knowledge Base Entry is global, defaults to false
type: boolean
id:
$ref: '#/components/schemas/Security_AI_Assistant_API_NonEmptyString'
name:
@ -31446,6 +31465,18 @@ components:
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryUpdateFields'
discriminator:
propertyName: type
Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps:
anyOf:
- $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryCreateFields'
- $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryCreateFields'
discriminator:
propertyName: type
Security_AI_Assistant_API_KnowledgeBaseResource:
description: Knowledge Base resource name for grouping entries, e.g. 'security_labs', 'user', etc
enum:
- security_labs
- user
type: string
Security_AI_Assistant_API_KnowledgeBaseResponse:
description: AI assistant KnowledgeBase.
type: object

View file

@ -746,7 +746,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/KnowledgeBaseEntryUpdateProps'
$ref: '#/components/schemas/KnowledgeBaseEntryUpdateRouteProps'
required: true
responses:
'200':
@ -1301,6 +1301,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1317,6 +1320,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/ResponseFields'
- $ref: '#/components/schemas/DocumentEntryResponseFields'
@ -1324,6 +1328,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1353,10 +1360,7 @@ components:
type: object
properties:
kbResource:
description: >-
Knowledge Base resource name for grouping entries, e.g. 'esql',
'lens-docs', etc
type: string
$ref: '#/components/schemas/KnowledgeBaseResource'
source:
description: Source document name or filepath
type: string
@ -1381,6 +1385,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
id:
$ref: '#/components/schemas/NonEmptyString'
name:
@ -1463,6 +1470,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1479,6 +1489,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/ResponseFields'
- $ref: '#/components/schemas/IndexEntryResponseFields'
@ -1486,6 +1497,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1553,6 +1567,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
id:
$ref: '#/components/schemas/NonEmptyString'
name:
@ -1736,6 +1753,20 @@ components:
- $ref: '#/components/schemas/IndexEntryUpdateFields'
discriminator:
propertyName: type
KnowledgeBaseEntryUpdateRouteProps:
anyOf:
- $ref: '#/components/schemas/DocumentEntryCreateFields'
- $ref: '#/components/schemas/IndexEntryCreateFields'
discriminator:
propertyName: type
KnowledgeBaseResource:
description: >-
Knowledge Base resource name for grouping entries, e.g. 'security_labs',
'user', etc
enum:
- security_labs
- user
type: string
KnowledgeBaseResponse:
description: AI assistant KnowledgeBase.
type: object

View file

@ -746,7 +746,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/KnowledgeBaseEntryUpdateProps'
$ref: '#/components/schemas/KnowledgeBaseEntryUpdateRouteProps'
required: true
responses:
'200':
@ -1301,6 +1301,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1317,6 +1320,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/ResponseFields'
- $ref: '#/components/schemas/DocumentEntryResponseFields'
@ -1324,6 +1328,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1353,10 +1360,7 @@ components:
type: object
properties:
kbResource:
description: >-
Knowledge Base resource name for grouping entries, e.g. 'esql',
'lens-docs', etc
type: string
$ref: '#/components/schemas/KnowledgeBaseResource'
source:
description: Source document name or filepath
type: string
@ -1381,6 +1385,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
id:
$ref: '#/components/schemas/NonEmptyString'
name:
@ -1463,6 +1470,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1479,6 +1489,7 @@ components:
required:
- name
- namespace
- global
- users
- $ref: '#/components/schemas/ResponseFields'
- $ref: '#/components/schemas/IndexEntryResponseFields'
@ -1486,6 +1497,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
name:
description: Name of the Knowledge Base Entry
type: string
@ -1553,6 +1567,9 @@ components:
allOf:
- type: object
properties:
global:
description: 'Whether this Knowledge Base Entry is global, defaults to false'
type: boolean
id:
$ref: '#/components/schemas/NonEmptyString'
name:
@ -1736,6 +1753,20 @@ components:
- $ref: '#/components/schemas/IndexEntryUpdateFields'
discriminator:
propertyName: type
KnowledgeBaseEntryUpdateRouteProps:
anyOf:
- $ref: '#/components/schemas/DocumentEntryCreateFields'
- $ref: '#/components/schemas/IndexEntryCreateFields'
discriminator:
propertyName: type
KnowledgeBaseResource:
description: >-
Knowledge Base resource name for grouping entries, e.g. 'security_labs',
'user', etc
enum:
- security_labs
- user
type: string
KnowledgeBaseResponse:
description: AI assistant KnowledgeBase.
type: object

View file

@ -48,15 +48,20 @@ export const KnowledgeBaseEntryErrorSchema = z
})
.strict();
/**
* Knowledge Base resource name for grouping entries, e.g. 'security_labs', 'user', etc
*/
export type KnowledgeBaseResource = z.infer<typeof KnowledgeBaseResource>;
export const KnowledgeBaseResource = z.enum(['security_labs', 'user']);
export type KnowledgeBaseResourceEnum = typeof KnowledgeBaseResource.enum;
export const KnowledgeBaseResourceEnum = KnowledgeBaseResource.enum;
/**
* Metadata about a Knowledge Base Entry
*/
export type Metadata = z.infer<typeof Metadata>;
export const Metadata = z.object({
/**
* Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
*/
kbResource: z.string(),
kbResource: KnowledgeBaseResource,
/**
* Source document name or filepath
*/
@ -96,6 +101,10 @@ export const BaseDefaultableFields = z.object({
* Kibana Space, defaults to 'default' space
*/
namespace: z.string().optional(),
/**
* Whether this Knowledge Base Entry is global, defaults to false
*/
global: z.boolean().optional(),
/**
* Users who have access to the Knowledge Base Entry, defaults to current user. Empty array provides access to all users.
*/
@ -153,10 +162,7 @@ export const DocumentEntryRequiredFields = z.object({
* Entry type
*/
type: z.literal('document'),
/**
* Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
*/
kbResource: z.string(),
kbResource: KnowledgeBaseResource,
/**
* Source document name or filepath
*/
@ -253,6 +259,12 @@ export const KnowledgeBaseEntryUpdateProps = z.discriminatedUnion('type', [
IndexEntryUpdateFields,
]);
export type KnowledgeBaseEntryUpdateRouteProps = z.infer<typeof KnowledgeBaseEntryUpdateRouteProps>;
export const KnowledgeBaseEntryUpdateRouteProps = z.discriminatedUnion('type', [
DocumentEntryCreateFields,
IndexEntryCreateFields,
]);
export type KnowledgeBaseEntryResponse = z.infer<typeof KnowledgeBaseEntryResponse>;
export const KnowledgeBaseEntryResponse = z.discriminatedUnion('type', [DocumentEntry, IndexEntry]);

View file

@ -50,8 +50,7 @@ components:
- "required"
properties:
kbResource:
type: string
description: Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
$ref: "#/components/schemas/KnowledgeBaseResource"
source:
type: string
description: Source document name or filepath
@ -95,6 +94,9 @@ components:
namespace:
type: string
description: Kibana Space, defaults to 'default' space
global:
type: boolean
description: Whether this Knowledge Base Entry is global, defaults to false
users:
type: array
description: Users who have access to the Knowledge Base Entry, defaults to current user. Empty array provides access to all users.
@ -164,6 +166,13 @@ components:
- $ref: "#/components/schemas/BaseResponseProps"
- $ref: "#/components/schemas/ResponseFields"
KnowledgeBaseResource:
description: Knowledge Base resource name for grouping entries, e.g. 'security_labs', 'user', etc
type: string
enum:
- security_labs
- user
###########
# Document Knowledge Base Entry
###########
@ -180,8 +189,7 @@ components:
enum: [document]
description: Entry type
kbResource:
type: string
description: Knowledge Base resource name for grouping entries, e.g. 'esql', 'lens-docs', etc
$ref: "#/components/schemas/KnowledgeBaseResource"
source:
type: string
description: Source document name or filepath
@ -308,6 +316,14 @@ components:
- $ref: "#/components/schemas/DocumentEntryUpdateFields"
- $ref: "#/components/schemas/IndexEntryUpdateFields"
# Don't allow passing id to the update route body
KnowledgeBaseEntryUpdateRouteProps:
discriminator:
propertyName: type
anyOf:
- $ref: "#/components/schemas/DocumentEntryCreateFields"
- $ref: "#/components/schemas/IndexEntryCreateFields"
KnowledgeBaseEntryResponse:
discriminator:
propertyName: type

View file

@ -19,7 +19,7 @@ import { z } from '@kbn/zod';
import {
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
KnowledgeBaseEntryUpdateProps,
KnowledgeBaseEntryUpdateRouteProps,
DeleteResponseFields,
} from './common_attributes.gen';
import { NonEmptyString } from '../../common_attributes.gen';
@ -83,7 +83,7 @@ export type UpdateKnowledgeBaseEntryRequestParamsInput = z.input<
export type UpdateKnowledgeBaseEntryRequestBody = z.infer<
typeof UpdateKnowledgeBaseEntryRequestBody
>;
export const UpdateKnowledgeBaseEntryRequestBody = KnowledgeBaseEntryUpdateProps;
export const UpdateKnowledgeBaseEntryRequestBody = KnowledgeBaseEntryUpdateRouteProps;
export type UpdateKnowledgeBaseEntryRequestBodyInput = z.input<
typeof UpdateKnowledgeBaseEntryRequestBody
>;

View file

@ -81,7 +81,8 @@ paths:
content:
application/json:
schema:
$ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryUpdateProps'
$ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryUpdateRouteProps'
responses:
200:
description: Successful request returning the updated Knowledge Base Entry

View file

@ -11,6 +11,7 @@ import {
KnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import { z } from '@kbn/zod';
import { isArray } from 'lodash';
export const isSystemEntry = (
entry: KnowledgeBaseEntryResponse
@ -25,7 +26,8 @@ export const isSystemEntry = (
export const isGlobalEntry = (
entry: KnowledgeBaseEntryResponse
): entry is KnowledgeBaseEntryResponse => entry.users != null && !entry.users.length;
): entry is KnowledgeBaseEntryResponse =>
entry.global ?? (isArray(entry.users) && !entry.users.length);
export const isKnowledgeBaseEntryCreateProps = (
entry: unknown

View file

@ -85,6 +85,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_1',
updatedAt: '2024-10-23T17:33:15.933Z',
updatedBy: 'u_user_id_1',
global: false,
users: [{ name: 'Test User 1' }],
name: 'Test Entry 1',
namespace: 'default',
@ -99,6 +100,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_2',
updatedAt: '2024-10-25T09:55:56.596Z',
updatedBy: 'u_user_id_2',
global: true,
users: [],
name: 'Test Entry 2',
namespace: 'default',
@ -114,6 +116,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_1',
updatedAt: '2024-10-25T09:55:56.596Z',
updatedBy: 'u_user_id_1',
global: false,
users: [{ name: 'Test User 1' }],
name: 'Test Entry 3',
namespace: 'default',
@ -129,6 +132,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_3',
updatedAt: '2024-10-23T17:33:15.933Z',
updatedBy: 'u_user_id_3',
global: true,
users: [],
name: 'Test Entry 4',
namespace: 'default',
@ -232,6 +236,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_1',
updatedAt: '2024-10-23T17:33:15.933Z',
updatedBy: 'u_user_id_1',
global: false,
users: [{ name: 'Test User 1' }],
name: 'A',
namespace: 'default',
@ -246,6 +251,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_2',
updatedAt: '2024-10-25T09:55:56.596Z',
updatedBy: 'u_user_id_2',
global: true,
users: [],
name: 'b',
namespace: 'default',
@ -261,6 +267,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_2',
updatedAt: '2024-10-25T09:55:56.596Z',
updatedBy: 'u_user_id_2',
global: true,
users: [],
name: 'B',
namespace: 'default',
@ -276,6 +283,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
createdBy: 'u_user_id_1',
updatedAt: '2024-10-25T09:55:56.596Z',
updatedBy: 'u_user_id_1',
global: false,
users: [{ name: 'Test User 1' }],
name: 'a',
namespace: 'default',
@ -468,7 +476,7 @@ describe('KnowledgeBaseSettingsManagement', () => {
});
expect(mockCreateEntry).toHaveBeenCalledTimes(0);
expect(mockUpdateEntry).toHaveBeenCalledWith([{ ...mockData[0], name: updatedName }]);
});
}, 100000000);
it('does not create a duplicate index entry when switching sharing option twice', async () => {
(useFlyoutModalVisibility as jest.Mock).mockReturnValue({

View file

@ -54,7 +54,7 @@ export const documentEntry: EsDocumentEntry = {
namespace: 'default',
semantic_text: 'test',
type: 'document',
kb_resource: 'test',
kb_resource: 'user',
required: true,
source: 'test',
text: 'test',
@ -105,7 +105,7 @@ export const getCreateKnowledgeBaseEntrySchemaMock = (
source: 'test',
text: 'test',
name: 'test',
kbResource: 'test',
kbResource: 'user',
...restProps,
};
}
@ -135,7 +135,7 @@ export const getUpdateKnowledgeBaseEntrySchemaMock = (
type: 'document',
source: 'test',
text: 'test',
kbResource: 'test',
kbResource: 'user',
id: entryId,
});
@ -146,7 +146,7 @@ export const getKnowledgeBaseEntryMock = (
type: 'document',
text: 'test',
source: 'test',
kbResource: 'test',
kbResource: 'user',
required: true,
}
): KnowledgeBaseEntryResponse => ({
@ -157,6 +157,7 @@ export const getKnowledgeBaseEntryMock = (
createdAt: '2020-04-20T15:25:31.830Z',
updatedAt: '2020-04-20T15:25:31.830Z',
namespace: 'default',
global: false,
users: [
{
name: 'my_username',

View file

@ -132,6 +132,15 @@ export const getPostEvaluateRequest = ({ body }: { body: PostEvaluateRequestBody
path: ELASTIC_AI_ASSISTANT_EVALUATE_URL,
});
export const getKnowledgeBaseEntryGetRequest = (
id: string = '04128c15-0d1b-4716-a4c5-46997ac7f3bd'
) =>
requestMock.create({
method: 'get',
path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
params: { id },
});
export const getKnowledgeBaseEntryFindRequest = () =>
requestMock.create({
method: 'get',

View file

@ -79,7 +79,7 @@ describe('createKnowledgeBaseEntry', () => {
source: 'test',
text: 'test',
name: 'test',
kb_resource: 'test',
kb_resource: 'user',
required: false,
vector: undefined,
},

View file

@ -47,7 +47,6 @@ export const createKnowledgeBaseEntry = async ({
user,
knowledgeBaseEntry,
logger,
global = false,
telemetry,
}: CreateKnowledgeBaseEntryParams): Promise<KnowledgeBaseEntryResponse | null> => {
const createdAt = new Date().toISOString();
@ -56,7 +55,6 @@ export const createKnowledgeBaseEntry = async ({
spaceId,
user,
entry: knowledgeBaseEntry as unknown as KnowledgeBaseEntryCreateProps,
global,
});
const telemetryPayload = {
entryType: body.type,
@ -112,14 +110,12 @@ interface TransformToUpdateSchemaProps {
user: AuthenticatedUser;
updatedAt: string;
entry: KnowledgeBaseEntryUpdateProps;
global?: boolean;
}
export const transformToUpdateSchema = ({
user,
updatedAt,
entry,
global = false,
}: TransformToUpdateSchemaProps): UpdateKnowledgeBaseEntrySchema => {
const base = {
id: entry.id,
@ -127,7 +123,8 @@ export const transformToUpdateSchema = ({
updated_by: user.profile_uid ?? 'unknown',
name: entry.name,
type: entry.type,
users: global
global: entry.global,
users: entry.global
? []
: [
{
@ -142,6 +139,7 @@ export const transformToUpdateSchema = ({
return {
...base,
...restEntry,
users: restEntry.users ?? base.users,
query_description: queryDescription,
input_schema:
entry.inputSchema?.map((schema) => ({
@ -177,7 +175,6 @@ interface TransformToCreateSchemaProps {
spaceId: string;
user: AuthenticatedUser;
entry: KnowledgeBaseEntryCreateProps;
global?: boolean;
}
export const transformToCreateSchema = ({
@ -185,7 +182,6 @@ export const transformToCreateSchema = ({
spaceId,
user,
entry,
global = false,
}: TransformToCreateSchemaProps): CreateKnowledgeBaseEntrySchema => {
const base = {
'@timestamp': createdAt,
@ -196,7 +192,8 @@ export const transformToCreateSchema = ({
name: entry.name,
namespace: spaceId,
type: entry.type,
users: global
global: entry.global,
users: entry.global
? []
: [
{
@ -211,6 +208,7 @@ export const transformToCreateSchema = ({
return {
...base,
...restEntry,
users: restEntry.users ?? base.users,
query_description: queryDescription,
input_schema:
entry.inputSchema?.map((schema) => ({

View file

@ -28,6 +28,7 @@ import {
} from '../../lib/langchain/content_loaders/security_labs_loader';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { newContentReferencesStoreMock } from '@kbn/elastic-assistant-common/impl/content_references/content_references_store/__mocks__/content_references_store.mock';
import { KnowledgeBaseResource } from '@kbn/elastic-assistant-common';
jest.mock('../../lib/langchain/content_loaders/security_labs_loader');
jest.mock('p-retry');
const date = '2023-03-28T22:27:28.159Z';
@ -334,7 +335,7 @@ describe('AIAssistantKnowledgeBaseDataClient', () => {
const documents = [
{
pageContent: 'Document 1',
metadata: { kbResource: 'user', source: 'user', required: false },
metadata: { kbResource: KnowledgeBaseResource.enum.user, source: 'user', required: false },
},
];
it('should add documents to the knowledge base', async () => {
@ -412,7 +413,7 @@ describe('AIAssistantKnowledgeBaseDataClient', () => {
expect(results).toHaveLength(1);
expect(results[0].pageContent).toBe('test');
expect(results[0].metadata.kbResource).toBe('test');
expect(results[0].metadata.kbResource).toBe('user');
});
it('should swallow errors during search', async () => {
@ -505,7 +506,10 @@ describe('AIAssistantKnowledgeBaseDataClient', () => {
mockOptions.manageGlobalKnowledgeBaseAIAssistant = false;
await expect(
client.createKnowledgeBaseEntry({ telemetry, knowledgeBaseEntry, global: true })
client.createKnowledgeBaseEntry({
telemetry,
knowledgeBaseEntry: { ...knowledgeBaseEntry, global: true },
})
).rejects.toThrow('User lacks privileges to create global knowledge base entries');
});
});

View file

@ -53,6 +53,7 @@ import {
} from './helpers';
import {
getKBUserFilter,
isGlobalEntry,
validateDocumentsModification,
} from '../../routes/knowledge_base/entries/utils';
import {
@ -434,8 +435,8 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
kbResource: doc.metadata.kbResource ?? 'unknown',
required: doc.metadata.required ?? false,
source: doc.metadata.source ?? 'unknown',
global,
},
global,
});
}),
authenticatedUser,
@ -658,11 +659,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
auditLogger,
knowledgeBaseEntry,
telemetry,
global = false,
}: {
auditLogger?: AuditLogger;
knowledgeBaseEntry: KnowledgeBaseEntryCreateProps;
global?: boolean;
telemetry: AnalyticsServiceSetup;
}): Promise<KnowledgeBaseEntryResponse | null> => {
const authenticatedUser = this.options.currentUser;
@ -673,7 +672,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
);
}
if (global && !this.options.manageGlobalKnowledgeBaseAIAssistant) {
if (isGlobalEntry(knowledgeBaseEntry) && !this.options.manageGlobalKnowledgeBaseAIAssistant) {
throw new Error('User lacks privileges to create global knowledge base entries');
}
@ -690,7 +689,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
spaceId: this.spaceId,
user: authenticatedUser,
knowledgeBaseEntry,
global,
telemetry,
});
};
@ -704,12 +702,14 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
public updateKnowledgeBaseEntry = async ({
auditLogger,
knowledgeBaseEntry,
telemetry,
}: {
auditLogger?: AuditLogger;
knowledgeBaseEntry: KnowledgeBaseEntryUpdateProps;
telemetry: AnalyticsServiceSetup;
}): Promise<{
errors: BulkOperationError[];
updatedEntry: KnowledgeBaseEntryResponse;
updatedEntry: KnowledgeBaseEntryResponse | null | undefined;
}> => {
const authenticatedUser = this.options.currentUser;
@ -734,7 +734,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
user: authenticatedUser,
updatedAt: changedAt,
entry: knowledgeBaseEntry,
global: knowledgeBaseEntry.users != null && knowledgeBaseEntry.users.length === 0,
}),
],
getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => getUpdateScript({ entry }),

View file

@ -22,6 +22,7 @@ describe('transforms', () => {
id: '1',
createdAt: documentEntry.created_at,
createdBy: documentEntry.created_by,
global: false,
updatedAt: documentEntry.updated_at,
updatedBy: documentEntry.updated_by,
type: documentEntry.type,
@ -47,6 +48,7 @@ describe('transforms', () => {
id: documentEntry.id,
createdAt: documentEntry.created_at,
createdBy: documentEntry.created_by,
global: false,
updatedAt: documentEntry.updated_at,
updatedBy: documentEntry.updated_by,
type: documentEntry.type,

View file

@ -12,6 +12,7 @@ import {
IndexEntry,
IndexEntryType,
KnowledgeBaseEntryResponse,
KnowledgeBaseResource,
} from '@kbn/elastic-assistant-common';
import { EsKnowledgeBaseEntrySchema, LegacyEsKnowledgeBaseEntrySchema } from './types';
@ -49,6 +50,7 @@ const transformEsSchemaToEntry = (
createdBy: esKbEntry.created_by,
updatedAt: esKbEntry.updated_at,
updatedBy: esKbEntry.updated_by,
global: !esKbEntry.users?.length,
users:
esKbEntry.users?.map((user) => ({
id: user.id,
@ -79,6 +81,7 @@ const transformEsSchemaToEntry = (
createdBy: esKbEntry.created_by,
updatedAt: esKbEntry.updated_at,
updatedBy: esKbEntry.updated_by,
global: !esKbEntry.users?.length,
users:
esKbEntry.users?.map((user) => ({
id: user.id,
@ -116,6 +119,7 @@ const getDocumentEntryFromLegacyKbEntry = (
createdBy: legacyEsKbDoc.created_by,
updatedAt: legacyEsKbDoc.updated_at,
updatedBy: legacyEsKbDoc.updated_by,
global: !legacyEsKbDoc.users?.length,
users:
legacyEsKbDoc.users?.map((user) => ({
id: user.id,
@ -125,7 +129,7 @@ const getDocumentEntryFromLegacyKbEntry = (
name: legacyEsKbDoc.text,
namespace: legacyEsKbDoc.namespace,
type: DocumentEntryType.value,
kbResource: legacyEsKbDoc.metadata?.kbResource ?? 'unknown',
kbResource: (legacyEsKbDoc.metadata?.kbResource as KnowledgeBaseResource) ?? 'user',
source: legacyEsKbDoc.metadata?.source ?? 'unknown',
required: legacyEsKbDoc.metadata?.required ?? false,
text: legacyEsKbDoc.text,

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import type { DocumentEntryType, IndexEntryType } from '@kbn/elastic-assistant-common';
import type {
DocumentEntryType,
IndexEntryType,
KnowledgeBaseResource,
} from '@kbn/elastic-assistant-common';
export type EsKnowledgeBaseEntrySchema = EsDocumentEntry | EsIndexEntry;
@ -23,7 +27,7 @@ export interface EsDocumentEntry {
name: string;
namespace: string;
type: DocumentEntryType;
kb_resource: string;
kb_resource: KnowledgeBaseResource;
required: boolean;
source: string;
text: string;

View file

@ -47,7 +47,7 @@ describe('Perform bulk action route', () => {
docs_deleted: [],
errors: [],
});
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
bulkActionAnonymizationFieldsRoute(server.router, logger);
});

View file

@ -165,7 +165,7 @@ export const bulkActionAnonymizationFieldsRoute = (
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -33,7 +33,7 @@ describe('Find user anonymization fields route', () => {
clients.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindAnonymizationFieldsResultWithSingleHit())
);
context.elasticAssistant.getCurrentUser.mockReturnValue({
context.elasticAssistant.getCurrentUser.mockResolvedValue({
username: 'my_username',
authentication_realm: {
type: 'my_realm_type',
@ -41,7 +41,7 @@ describe('Find user anonymization fields route', () => {
},
} as AuthenticatedUser);
logger = loggingSystemMock.createLogger();
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
findAnonymizationFieldsRoute(server.router, logger);
});

View file

@ -58,7 +58,7 @@ export const findAnonymizationFieldsRoute = (
const { query } = request;
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -84,7 +84,7 @@ const mockCurrentAd = transformESSearchToAttackDiscovery(getAttackDiscoverySearc
describe('getAttackDiscoveryRoute', () => {
beforeEach(() => {
jest.clearAllMocks();
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValue(mockDataClient);
getAttackDiscoveryRoute(server.router);
@ -105,7 +105,7 @@ describe('getAttackDiscoveryRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValue(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getAttackDiscoveryRequest('connector-id'),
requestContextMock.convertContext(context)

View file

@ -51,7 +51,7 @@ export const getAttackDiscoveryRoute = (router: IRouter<ElasticAssistantRequestH
try {
const dataClient = await assistantContext.getAttackDiscoveryDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
const connectorId = decodeURIComponent(request.params.connectorId);
if (authenticatedUser == null) {
return resp.error({

View file

@ -51,7 +51,7 @@ describe('cancelAttackDiscoveryRoute', () => {
...mockCurrentAd,
status: 'canceled',
});
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValue(mockDataClient);
cancelAttackDiscoveryRoute(server.router);
@ -70,7 +70,7 @@ describe('cancelAttackDiscoveryRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValue(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getCancelAttackDiscoveryRequest('connector-id'),
requestContextMock.convertContext(context)

View file

@ -57,7 +57,7 @@ export const cancelAttackDiscoveryRoute = (
try {
const dataClient = await assistantContext.getAttackDiscoveryDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
const connectorId = decodeURIComponent(request.params.connectorId);
if (authenticatedUser == null) {
return resp.error({

View file

@ -72,7 +72,7 @@ const runningAd = {
describe('postAttackDiscoveryRoute', () => {
beforeEach(() => {
jest.clearAllMocks();
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getAttackDiscoveryDataClient.mockResolvedValue(mockDataClient);
context.elasticAssistant.actions = actionsMock.createStart();
postAttackDiscoveryRoute(server.router);
@ -93,7 +93,7 @@ describe('postAttackDiscoveryRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValue(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
postAttackDiscoveryRequest(mockRequestBody),
requestContextMock.convertContext(context)

View file

@ -73,7 +73,7 @@ export const postAttackDiscoveryRoute = (
const actions = (await context.elasticAssistant).actions;
const actionsClient = await actions.getActionsClientWithRequest(request);
const dataClient = await assistantContext.getAttackDiscoveryDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
if (authenticatedUser == null) {
return resp.error({
body: `Authenticated user not found`,

View file

@ -77,7 +77,7 @@ export const chatCompleteRoute = (
(await ctx.elasticAssistant.llmTasks.retrieveDocumentationAvailable()) ?? false;
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -63,7 +63,7 @@ describe('getDefendInsightRoute', () => {
mockDataClient = getDefaultDataClient();
mockCurrentInsight = transformESSearchToDefendInsights(getDefendInsightsSearchEsMock())[0];
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getDefendInsightsDataClient.mockResolvedValue(mockDataClient);
getDefendInsightRoute(server.router);
(updateDefendInsightLastViewedAt as jest.Mock).mockResolvedValue(mockCurrentInsight);
@ -109,7 +109,7 @@ describe('getDefendInsightRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getDefendInsightRequest('insight-id1'),
requestContextMock.convertContext(context)

View file

@ -72,7 +72,7 @@ export const getDefendInsightRoute = (router: IRouter<ElasticAssistantRequestHan
}
const dataClient = await assistantContext.getDefendInsightsDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
if (authenticatedUser == null) {
return resp.error({
body: `Authenticated user not found`,

View file

@ -63,7 +63,7 @@ describe('getDefendInsightsRoute', () => {
mockDataClient = getDefaultDataClient();
mockCurrentInsights = transformESSearchToDefendInsights(getDefendInsightsSearchEsMock());
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getDefendInsightsDataClient.mockResolvedValue(mockDataClient);
getDefendInsightsRoute(server.router);
(updateDefendInsightsLastViewedAt as jest.Mock).mockResolvedValue(mockCurrentInsights);
@ -109,7 +109,7 @@ describe('getDefendInsightsRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getDefendInsightsRequest({ connector_id: 'connector-id1' }),
requestContextMock.convertContext(context)

View file

@ -75,7 +75,7 @@ export const getDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestHa
const dataClient = await assistantContext.getDefendInsightsDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
if (authenticatedUser == null) {
return resp.error({
body: `Authenticated user not found`,

View file

@ -106,7 +106,7 @@ describe('postDefendInsightsRoute', () => {
});
(isDefendInsightsEnabled as jest.Mock).mockResolvedValue(true);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getDefendInsightsDataClient.mockResolvedValue(mockDataClient);
context.elasticAssistant.actions = actionsMock.createStart();
@ -141,7 +141,7 @@ describe('postDefendInsightsRoute', () => {
});
it('should handle missing authenticated user', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
postDefendInsightsRequest(mockRequestBody),
requestContextMock.convertContext(context)

View file

@ -102,7 +102,7 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
const actions = assistantContext.actions;
const actionsClient = await actions.getActionsClientWithRequest(request);
const dataClient = await assistantContext.getDefendInsightsDataClient();
const authenticatedUser = assistantContext.getCurrentUser();
const authenticatedUser = await assistantContext.getCurrentUser();
if (authenticatedUser == null) {
return resp.error({
body: `Authenticated user not found`,

View file

@ -49,7 +49,7 @@ export const getEvaluateRoute = (router: IRouter<ElasticAssistantRequestHandlerC
const logger = assistantContext.logger.get('evaluate');
// Perform license, authenticated user and evaluation FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
capability: 'assistantModelEvaluation',
context: ctx,
request,

View file

@ -46,7 +46,7 @@ describe('Post Evaluate Route', () => {
beforeEach(() => {
jest.clearAllMocks();
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
postEvaluateRoute(server.router, mockGetElser);
});

View file

@ -103,7 +103,7 @@ export const postEvaluateRoute = (
const savedObjectsClient = ctx.elasticAssistant.savedObjectsClient;
// Perform license, authenticated user and evaluation FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
capability: 'assistantModelEvaluation',
context: ctx,
request,

View file

@ -439,12 +439,12 @@ type PerformChecks =
isSuccess: false;
response: IKibanaResponse;
};
export const performChecks = ({
export const performChecks = async ({
capability,
context,
request,
response,
}: PerformChecksParams): PerformChecks => {
}: PerformChecksParams): Promise<PerformChecks> => {
const assistantResponse = buildResponse(response);
if (!hasAIAssistantLicense(context.licensing.license)) {
@ -458,7 +458,7 @@ export const performChecks = ({
};
}
const currentUser = context.elasticAssistant.getCurrentUser();
const currentUser = await context.elasticAssistant.getCurrentUser();
if (currentUser == null) {
return {

View file

@ -156,9 +156,9 @@ describe('Bulk actions knowledge base entry route', () => {
);
});
test('handles all three bulk update actions at once', async () => {
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments
.mockResolvedValueOnce(Promise.resolve(getEmptyFindResult()))
.mockResolvedValue(Promise.resolve(getFindKnowledgeBaseEntriesResultWithSingleHit()));
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindKnowledgeBaseEntriesResultWithSingleHit())
);
const response = await server.inject(
getBulkActionKnowledgeBaseEntryRequest({
create: [getCreateKnowledgeBaseEntrySchemaMock()],
@ -201,7 +201,7 @@ describe('Bulk actions knowledge base entry route', () => {
);
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getBulkActionKnowledgeBaseEntryRequest({
create: [getCreateKnowledgeBaseEntrySchemaMock()],
@ -214,11 +214,9 @@ describe('Bulk actions knowledge base entry route', () => {
describe('unhappy paths', () => {
test('catches error if creation throws', async () => {
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockImplementation(
async () => {
throw new Error('Test error');
}
);
mockBulk.mockImplementationOnce(async () => {
throw new Error('Test error');
});
const response = await server.inject(
getBulkActionKnowledgeBaseEntryRequest({
create: [getCreateKnowledgeBaseEntrySchemaMock()],

View file

@ -48,7 +48,7 @@ import {
transformToCreateSchema,
transformToUpdateSchema,
} from '../../../ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
import { validateDocumentsModification } from './utils';
import { isGlobalEntry, validateDocumentsModification } from './utils';
export interface BulkOperationError {
message: string;
@ -200,7 +200,7 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
const logger = ctx.elasticAssistant.logger;
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,
@ -241,28 +241,10 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
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) {
if (isGlobalEntry(entry) && !manageGlobalKnowledgeBaseAIAssistant) {
throw new Error(`User lacks privileges to create global knowledge base entries`);
}
});
const result = await kbDataClient?.findDocuments<EsKnowledgeBaseEntrySchema>({
perPage: 100,
page: 1,
filter: `users:{ id: "${authenticatedUser?.profile_uid}" }`,
fields: [],
});
if (result?.data != null && result.total > 0) {
return assistantResponse.error({
statusCode: 409,
body: `Knowledge Base Entry id's: "${transformESSearchToKnowledgeBaseEntry(
result.data
)
.map((c) => c.id)
.join(',')}" already exists`,
});
}
}
await validateDocumentsModification(
@ -293,7 +275,6 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
spaceId,
user: authenticatedUser,
entry,
global: entry.users != null && entry.users.length === 0,
})
),
documentsToDelete: body.delete?.ids,
@ -302,7 +283,6 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
user: authenticatedUser,
updatedAt: changedAt,
entry,
global: entry.users != null && entry.users.length === 0,
})
),
getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => getUpdateScript({ entry }),

View file

@ -38,7 +38,7 @@ describe('Create knowledge base entry route', () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
createKnowledgeBaseEntryRoute(server.router);
});
@ -52,7 +52,7 @@ describe('Create knowledge base entry route', () => {
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getCreateKnowledgeBaseEntryRequest(),
requestContextMock.convertContext(context)

View file

@ -48,7 +48,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
const logger = ctx.elasticAssistant.logger;
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,
@ -61,8 +61,10 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`);
const createResponse = await kbDataClient?.createKnowledgeBaseEntry({
knowledgeBaseEntry: request.body,
global: request.body.users != null && request.body.users.length === 0,
knowledgeBaseEntry: {
...request.body,
...(request.body.global ? { users: [] } : {}),
},
auditLogger: ctx.elasticAssistant.auditLogger,
telemetry: ctx.elasticAssistant.telemetry,
});

View file

@ -33,7 +33,7 @@ describe('Delete knowledge base entry route', () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
deleteKnowledgeBaseEntryRoute(server.router);
});
@ -47,7 +47,7 @@ describe('Delete knowledge base entry route', () => {
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getDeleteKnowledgeBaseEntryRequest({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd' }),
requestContextMock.convertContext(context)

View file

@ -52,7 +52,7 @@ export const deleteKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
const logger = ctx.elasticAssistant.logger;
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -26,7 +26,7 @@ describe('Find Knowledge Base Entries route', () => {
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindKnowledgeBaseEntriesResultWithSingleHit())
);

View file

@ -16,6 +16,7 @@ import {
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_FIND,
FindKnowledgeBaseEntriesRequestQuery,
FindKnowledgeBaseEntriesResponse,
KnowledgeBaseResource,
} from '@kbn/elastic-assistant-common';
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
import { estypes } from '@elastic/elasticsearch';
@ -59,7 +60,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,
@ -107,7 +108,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
const systemEntries = [
{
bucketId: 'securityLabsId',
kbResource: SECURITY_LABS_RESOURCE,
kbResource: SECURITY_LABS_RESOURCE as KnowledgeBaseResource,
name: 'Security Labs',
required: true,
},
@ -138,6 +139,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
createdBy: entry.created_by,
updatedAt: entry.updated_at,
updatedBy: entry.updated_by,
global: true,
users: [],
name,
namespace: entry.namespace,

View file

@ -0,0 +1,75 @@
/*
* 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 { getKnowledgeBaseEntryGetRequest } from '../../../__mocks__/request';
import { serverMock } from '../../../__mocks__/server';
import { requestContextMock } from '../../../__mocks__/request_context';
import {
getEmptyFindResult,
getFindKnowledgeBaseEntriesResultWithSingleHit,
} from '../../../__mocks__/response';
import { getKnowledgeBaseEntryRoute } from './get_route';
import type { AuthenticatedUser } from '@kbn/core-security-common';
const mockUser = {
username: 'my_username',
authentication_realm: {
type: 'my_realm_type',
name: 'my_realm_name',
},
} as AuthenticatedUser;
describe('Get Knowledge Base Entry route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
getKnowledgeBaseEntryRoute(server.router);
});
describe('status codes', () => {
test('returns 200 if entry exists', async () => {
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindKnowledgeBaseEntriesResultWithSingleHit())
);
const response = await server.inject(
getKnowledgeBaseEntryGetRequest('04128c15-0d1b-4716-a4c5-46997ac7f3bd'),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(200);
});
test('returns 404 if entry does not exists', async () => {
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getEmptyFindResult())
);
const response = await server.inject(
getKnowledgeBaseEntryGetRequest('04128c15-0d1b-4716-a4c5-46997ac7f3bd'),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(404);
});
test('catches error if search throws error', async () => {
clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient.findDocuments.mockImplementation(
async () => {
throw new Error('Test error');
}
);
const response = await server.inject(
getKnowledgeBaseEntryGetRequest(),
requestContextMock.convertContext(context)
);
expect(response.status).toEqual(500);
expect(response.body).toEqual({
message: 'Test error',
status_code: 500,
});
});
});
});

View file

@ -0,0 +1,95 @@
/*
* 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 type { IKibanaResponse } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
ReadKnowledgeBaseEntryRequestParams,
ReadKnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
import { ElasticAssistantPluginRouter } from '../../../types';
import { buildResponse } from '../../utils';
import { performChecks } from '../../helpers';
import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms';
import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types';
import { getKBUserFilter } from './utils';
export const getKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRouter) => {
router.versioned
.get({
access: 'public',
path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
security: {
authz: {
requiredPrivileges: ['elasticAssistant'],
},
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: {
params: buildRouteValidationWithZod(ReadKnowledgeBaseEntryRequestParams),
},
},
},
async (
context,
request,
response
): Promise<IKibanaResponse<ReadKnowledgeBaseEntryResponse>> => {
const assistantResponse = buildResponse(response);
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license, authenticated user and FF checks
const checkResponse = await performChecks({
context: ctx,
request,
response,
});
if (!checkResponse.isSuccess) {
return checkResponse.response;
}
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient();
const currentUser = checkResponse.currentUser;
const userFilter = getKBUserFilter(currentUser);
const systemFilter = ` AND _id: "${request.params.id}"`;
const result = await kbDataClient?.findDocuments<EsKnowledgeBaseEntrySchema>({
perPage: 1,
page: 1,
sortField: 'created_at',
sortOrder: 'desc',
filter: `${userFilter}${systemFilter}`,
fields: ['*'],
});
if (!result?.data?.hits.hits.length) {
return response.notFound();
}
return response.ok({
body: transformESSearchToKnowledgeBaseEntry(result.data)[0],
});
} catch (err) {
const error = transformError(err);
return assistantResponse.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -45,7 +45,7 @@ describe('Update knowledge base entry route', () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
updateKnowledgeBaseEntryRoute(server.router);
});
@ -66,7 +66,7 @@ describe('Update knowledge base entry route', () => {
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getUpdateKnowledgeBaseEntryRequest({
params: { id: '1' },

View file

@ -15,7 +15,7 @@ import {
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
import {
KnowledgeBaseEntryResponse,
KnowledgeBaseEntryUpdateProps,
KnowledgeBaseEntryUpdateRouteProps,
} from '@kbn/elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen';
import { ElasticAssistantPluginRouter } from '../../../types';
import { buildResponse } from '../../utils';
@ -39,7 +39,7 @@ export const updateKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
validate: {
request: {
params: buildRouteValidationWithZod(UpdateKnowledgeBaseEntryRequestParams),
body: buildRouteValidationWithZod(KnowledgeBaseEntryUpdateProps),
body: buildRouteValidationWithZod(KnowledgeBaseEntryUpdateRouteProps),
},
},
},
@ -50,7 +50,7 @@ export const updateKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
const logger = ctx.elasticAssistant.logger;
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,
@ -63,8 +63,13 @@ export const updateKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient();
const updateResponse = await kbDataClient?.updateKnowledgeBaseEntry({
knowledgeBaseEntry: { ...request.body, id: request.params.id },
knowledgeBaseEntry: {
...request.body,
id: request.params.id,
...(request.body.global ? { users: [] } : {}),
},
auditLogger: ctx.elasticAssistant.auditLogger,
telemetry: ctx.elasticAssistant.telemetry,
});
if (updateResponse?.updatedEntry) {
@ -74,7 +79,7 @@ export const updateKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
}
return assistantResponse.error({
body: updateResponse?.errors?.[0].message ?? `Knowledge Base Entry was not created`,
body: updateResponse?.errors?.[0].message ?? `Knowledge Base Entry was not updated`,
statusCode: 400,
});
} catch (err) {

View file

@ -6,6 +6,11 @@
*/
import { AuthenticatedUser } from '@kbn/core-security-common';
import {
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import { isArray } from 'lodash';
import { AIAssistantKnowledgeBaseDataClient } from '../../../ai_assistant_data_clients/knowledge_base';
import { transformESSearchToKnowledgeBaseEntry } from '../../../ai_assistant_data_clients/knowledge_base/transforms';
import { EsKnowledgeBaseEntrySchema } from '../../../ai_assistant_data_clients/knowledge_base/types';
@ -28,6 +33,9 @@ export const getKBUserFilter = (user: AuthenticatedUser | null) => {
return `(${globalFilter}${userFilter})`;
};
export const isGlobalEntry = (entry: KnowledgeBaseEntryResponse | KnowledgeBaseEntryCreateProps) =>
entry.global ?? (isArray(entry.users) && !entry.users.length);
export const validateDocumentsModification = async (
kbDataClient: AIAssistantKnowledgeBaseDataClient | null,
authenticatedUser: AuthenticatedUser | null,
@ -50,8 +58,7 @@ export const validateDocumentsModification = async (
const availableEntries = entries ? transformESSearchToKnowledgeBaseEntry(entries.data) : [];
availableEntries.forEach((entry) => {
// RBAC validation
const isGlobal = entry.users != null && entry.users.length === 0;
if (isGlobal && !manageGlobalKnowledgeBaseAIAssistant) {
if (isGlobalEntry(entry) && !manageGlobalKnowledgeBaseAIAssistant) {
throw new Error(`User lacks privileges to ${operation} global knowledge base entries`);
}
});

View file

@ -27,7 +27,7 @@ describe('Get Knowledge Base Status Route', () => {
beforeEach(() => {
server = serverMock.create();
({ context } = requestContextMock.createTools());
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getAIAssistantKnowledgeBaseDataClient = jest.fn().mockResolvedValue({
getKnowledgeBaseDocuments: jest.fn().mockResolvedValue([]),
indexTemplateAndPattern: {

View file

@ -30,7 +30,7 @@ describe('Post Knowledge Base Route', () => {
beforeEach(() => {
server = serverMock.create();
({ context } = requestContextMock.createTools());
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser);
context.elasticAssistant.getAIAssistantKnowledgeBaseDataClient = jest.fn().mockResolvedValue({
setupKnowledgeBase: jest.fn(),
indexTemplateAndPattern: {

View file

@ -69,7 +69,7 @@ export const postActionsConnectorExecuteRoute = (
let onLlmResponse;
try {
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -43,7 +43,7 @@ describe('Perform bulk action route', () => {
docs_deleted: [],
errors: [],
});
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
bulkPromptsRoute(server.router, logger);
});

View file

@ -159,7 +159,7 @@ export const bulkPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -33,7 +33,7 @@ describe('Find user prompts route', () => {
clients.elasticAssistant.getAIAssistantPromptsDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindPromptsResultWithSingleHit())
);
context.elasticAssistant.getCurrentUser.mockReturnValue({
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce({
username: 'my_username',
authentication_realm: {
type: 'my_realm_type',
@ -41,7 +41,7 @@ describe('Find user prompts route', () => {
},
} as AuthenticatedUser);
logger = loggingSystemMock.createLogger();
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
findPromptsRoute(server.router, logger);
});

View file

@ -47,7 +47,7 @@ export const findPromptsRoute = (router: ElasticAssistantPluginRouter, logger: L
const { query } = request;
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -38,6 +38,9 @@ import {
getDefendInsightsRoute,
postDefendInsightsRoute,
} from './defend_insights';
import { deleteKnowledgeBaseEntryRoute } from './knowledge_base/entries/delete_route';
import { updateKnowledgeBaseEntryRoute } from './knowledge_base/entries/update_route';
import { getKnowledgeBaseEntryRoute } from './knowledge_base/entries/get_route';
export const registerRoutes = (
router: ElasticAssistantPluginRouter,
@ -71,8 +74,11 @@ export const registerRoutes = (
postKnowledgeBaseRoute(router);
// Knowledge Base Entries
getKnowledgeBaseEntryRoute(router);
findKnowledgeBaseEntriesRoute(router);
createKnowledgeBaseEntryRoute(router);
updateKnowledgeBaseEntryRoute(router);
deleteKnowledgeBaseEntryRoute(router);
bulkActionKnowledgeBaseEntriesRoute(router);
// Actions Connector Execute (LLM Wrapper)

View file

@ -56,7 +56,26 @@ export class RequestContextFactory implements IRequestContextFactory {
const getSpaceId = (): string =>
startPlugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_NAMESPACE_STRING;
const getCurrentUser = () => coreContext.security.authc.getCurrentUser();
const getCurrentUser = async () => {
let contextUser = coreContext.security.authc.getCurrentUser();
if (contextUser && !contextUser?.profile_uid) {
try {
const users = await coreContext.elasticsearch.client.asCurrentUser.security.getUser({
username: contextUser.username,
with_profile_uid: true,
});
if (users[contextUser.username].profile_uid) {
contextUser = { ...contextUser, profile_uid: users[contextUser.username].profile_uid };
}
} catch (e) {
this.logger.error(`Failed to get user profile_uid: ${e}`);
}
}
return contextUser;
};
return {
core: coreContext,
@ -86,7 +105,7 @@ export class RequestContextFactory implements IRequestContextFactory {
// Note: modelIdOverride is used here to enable setting up the KB using a different ELSER model, which
// is necessary for testing purposes (`pt_tiny_elser`).
getAIAssistantKnowledgeBaseDataClient: memoize(async (params) => {
const currentUser = getCurrentUser();
const currentUser = await getCurrentUser();
const { securitySolutionAssistant } = await coreStart.capabilities.resolveCapabilities(
request,
@ -105,8 +124,8 @@ export class RequestContextFactory implements IRequestContextFactory {
});
}),
getAttackDiscoveryDataClient: memoize(() => {
const currentUser = getCurrentUser();
getAttackDiscoveryDataClient: memoize(async () => {
const currentUser = await getCurrentUser();
return this.assistantService.createAttackDiscoveryDataClient({
spaceId: getSpaceId(),
licensing: context.licensing,
@ -115,8 +134,8 @@ export class RequestContextFactory implements IRequestContextFactory {
});
}),
getDefendInsightsDataClient: memoize(() => {
const currentUser = getCurrentUser();
getDefendInsightsDataClient: memoize(async () => {
const currentUser = await getCurrentUser();
return this.assistantService.createDefendInsightsDataClient({
spaceId: getSpaceId(),
licensing: context.licensing,
@ -125,8 +144,8 @@ export class RequestContextFactory implements IRequestContextFactory {
});
}),
getAIAssistantPromptsDataClient: memoize(() => {
const currentUser = getCurrentUser();
getAIAssistantPromptsDataClient: memoize(async () => {
const currentUser = await getCurrentUser();
return this.assistantService.createAIAssistantPromptsDataClient({
spaceId: getSpaceId(),
licensing: context.licensing,
@ -135,8 +154,8 @@ export class RequestContextFactory implements IRequestContextFactory {
});
}),
getAIAssistantAnonymizationFieldsDataClient: memoize(() => {
const currentUser = getCurrentUser();
getAIAssistantAnonymizationFieldsDataClient: memoize(async () => {
const currentUser = await getCurrentUser();
return this.assistantService.createAIAssistantAnonymizationFieldsDataClient({
spaceId: getSpaceId(),
licensing: context.licensing,
@ -146,7 +165,7 @@ export class RequestContextFactory implements IRequestContextFactory {
}),
getAIAssistantConversationsDataClient: memoize(async (params) => {
const currentUser = getCurrentUser();
const currentUser = await getCurrentUser();
return this.assistantService.createAIAssistantConversationsDataClient({
spaceId: getSpaceId(),
licensing: context.licensing,

View file

@ -32,7 +32,7 @@ describe('Append conversation messages route', () => {
clients.elasticAssistant.getAIAssistantConversationsDataClient.appendConversationMessages.mockResolvedValue(
getConversationMock(getQueryConversationParams())
); // successful append
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
appendConversationMessageRoute(server.router);
});

View file

@ -45,7 +45,7 @@ export const appendConversationMessageRoute = (router: ElasticAssistantPluginRou
const { id } = request.params;
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -47,7 +47,7 @@ describe('Perform bulk action route', () => {
docs_deleted: [],
errors: [],
});
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
bulkActionConversationsRoute(server.router, logger);
});

View file

@ -158,7 +158,7 @@ export const bulkActionConversationsRoute = (
request.events.completed$.subscribe(() => abortController.abort());
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -38,7 +38,7 @@ describe('Create conversation route', () => {
context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue(
elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
createConversationRoute(server.router);
});
@ -52,7 +52,7 @@ describe('Create conversation route', () => {
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getCreateConversationRequest(),
requestContextMock.convertContext(context)

View file

@ -44,7 +44,7 @@ export const createConversationRoute = (router: ElasticAssistantPluginRouter): v
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -28,7 +28,7 @@ describe('Delete conversation route', () => {
clients.elasticAssistant.getAIAssistantConversationsDataClient.getConversation.mockResolvedValue(
getConversationMock(getQueryConversationParams())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
deleteConversationRoute(server.router);
});

View file

@ -42,7 +42,7 @@ export const deleteConversationRoute = (router: ElasticAssistantPluginRouter) =>
const { id } = request.params;
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -22,7 +22,7 @@ describe('Find user conversations route', () => {
clients.elasticAssistant.getAIAssistantConversationsDataClient.findDocuments.mockResolvedValue(
Promise.resolve(getFindConversationsResultWithSingleHit())
);
context.elasticAssistant.getCurrentUser.mockReturnValue({
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce({
username: 'my_username',
authentication_realm: {
type: 'my_realm_type',

View file

@ -49,7 +49,7 @@ export const findUserConversationsRoute = (router: ElasticAssistantPluginRouter)
const { query } = request;
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,
@ -59,7 +59,7 @@ export const findUserConversationsRoute = (router: ElasticAssistantPluginRouter)
}
const dataClient = await ctx.elasticAssistant.getAIAssistantConversationsDataClient();
const currentUser = checkResponse.currentUser;
const currentUser = await checkResponse.currentUser;
const additionalFilter = query.filter ? ` AND ${query.filter}` : '';
const userFilter = currentUser?.username

View file

@ -29,7 +29,7 @@ describe('Read conversation route', () => {
clients.elasticAssistant.getAIAssistantConversationsDataClient.getConversation.mockResolvedValue(
getConversationMock(getQueryConversationParams())
);
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
readConversationRoute(server.router);
});
@ -68,7 +68,7 @@ describe('Read conversation route', () => {
});
test('returns 401 Unauthorized when request context getCurrentUser is not defined', async () => {
context.elasticAssistant.getCurrentUser.mockReturnValueOnce(null);
context.elasticAssistant.getCurrentUser.mockResolvedValueOnce(null);
const response = await server.inject(
getConversationReadRequest(),
requestContextMock.convertContext(context)

View file

@ -45,7 +45,7 @@ export const readConversationRoute = (router: ElasticAssistantPluginRouter) => {
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -34,7 +34,7 @@ describe('Update conversation route', () => {
getConversationMock(getQueryConversationParams())
); // successful update
context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser1);
context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser1);
updateConversationRoute(server.router);
});

View file

@ -48,7 +48,7 @@ export const updateConversationRoute = (router: ElasticAssistantPluginRouter) =>
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
// Perform license and authenticated user checks
const checkResponse = performChecks({
const checkResponse = await performChecks({
context: ctx,
request,
response,

View file

@ -134,7 +134,7 @@ export interface ElasticAssistantApiRequestHandlerContext {
logger: Logger;
getServerBasePath: () => string;
getSpaceId: () => string;
getCurrentUser: () => AuthenticatedUser | null;
getCurrentUser: () => Promise<AuthenticatedUser | null>;
getAIAssistantConversationsDataClient: (
params?: GetAIAssistantConversationsDataClientParams
) => Promise<AIAssistantConversationsDataClient | null>;

View file

@ -24,6 +24,9 @@ import {
bulkActionKnowledgeBaseEntries,
bulkActionKnowledgeBaseEntriesForUser,
} from '../utils/bulk_actions_entry';
import { getEntry } from '../utils/get_entry';
import { deleteEntry } from '../utils/delete_entry';
import { updateEntry } from '../utils/update_entry';
export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
@ -194,6 +197,333 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('Get Entry', () => {
it('should see other users global entries', async () => {
const users = [secOnlySpacesAll];
const createdEntries = await Promise.all(
users.map((user) =>
createEntryForUser({
supertestWithoutAuth,
log,
entry: globalDocumentEntry,
user,
})
)
);
const entry = await getEntry({
supertest,
supertestWithoutAuth,
params: { id: createdEntries[0].id },
log,
});
expect(removeServerGeneratedProperties(entry)).toEqual(globalDocumentEntry);
});
it('should not see other users private entries', async () => {
const users = [secOnlySpacesAll];
const createdEntries = await Promise.all(
users.map((user) =>
createEntryForUser({
supertestWithoutAuth,
log,
entry: documentEntry,
user,
})
)
);
let entry;
try {
entry = await getEntry({
supertest,
supertestWithoutAuth,
params: { id: createdEntries[0].id },
log,
});
// eslint-disable-next-line no-empty
} catch (e) {}
expect(entry).toBeUndefined();
});
});
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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: updatedDocumentEntry,
});
const expectedDocumentEntry = {
...documentEntry,
users: [{ name: 'elastic' }],
text: 'This is a sample of updated document entry',
};
expect(response).toMatchObject(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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: 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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: updatedDocumentEntry,
});
const expectedDocumentEntry = {
...globalDocumentEntry,
text: 'This is a sample of updated global document entry',
};
expect(response).toMatchObject(expectedDocumentEntry);
});
it('should update own global document entry and make it private', async () => {
const entry = await createEntry({ supertest, log, entry: globalDocumentEntry });
const updatedDocumentEntry = {
id: entry.id,
...globalDocumentEntry,
global: false,
text: 'This is a sample of updated global document entry',
};
const response = await updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: updatedDocumentEntry,
});
const expectedDocumentEntry = {
...globalDocumentEntry,
users: [{ name: 'elastic' }],
global: false,
text: 'This is a sample of updated global document entry',
};
expect(response).toMatchObject(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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: updatedDocumentEntry,
});
const expectedDocumentEntry = {
...globalDocumentEntry,
text: 'This is a sample of updated global document entry',
};
expect(response).toMatchObject(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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: updatedDocumentEntry,
user: secOnlySpacesAllAssistantMinimalAll,
});
const expectedDocumentEntry = {
...documentEntry,
users: [{ name: secOnlySpacesAllAssistantMinimalAll.username }],
text: 'This is a sample of updated document entry',
};
expect(response).toMatchObject(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 updateEntry({
supertest,
supertestWithoutAuth,
log,
entry: 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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: entry.id },
});
expect(response.id).toEqual(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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: 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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: entry.id },
});
expect(response.id).toEqual(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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: entry.id },
});
expect(response.id).toEqual(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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: entry.id },
user: secOnlySpacesAllAssistantMinimalAll,
});
expect(response.id).toEqual(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 deleteEntry({
supertest,
supertestWithoutAuth,
log,
params: { id: entry.id },
user: secOnlySpacesAllAssistantMinimalAll,
expectedHttpCode: 500,
});
expect(response).toEqual({
status_code: 500,
message: 'User lacks privileges to delete global knowledge base entries',
});
});
});
describe('Bulk Actions', () => {
describe('General', () => {
it(`should throw an error for more than ${KNOWLEDGE_BASE_ENTRIES_TABLE_MAX_PAGE_SIZE} actions`, async () => {

View file

@ -20,12 +20,14 @@ export const documentEntry: DocumentEntryCreateFields = {
kbResource: 'user',
namespace: 'default',
text: 'This is a sample document entry',
global: false,
users: undefined,
};
export const globalDocumentEntry: DocumentEntryCreateFields = {
...documentEntry,
name: 'Sample Global Document Entry',
global: true,
users: [],
};
@ -38,4 +40,5 @@ export const indexEntry: IndexEntryCreateFields = {
description: 'This is a sample index entry',
queryDescription: 'Use sample-field to search in sample-index',
users: undefined,
global: false,
};

View file

@ -0,0 +1,72 @@
/*
* 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 {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
DeleteKnowledgeBaseEntryRequestParamsInput,
DeleteKnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import type { User } from './auth/types';
import { routeWithNamespace } from '../../../../../../common/utils/security_solution';
/**
* Delete Knowledge Base Entry
* @param supertest The supertest deps
* @param supertestWithoutAuth The supertest deps
* @param log The tooling logger
* @param params Params for delete API (optional)
* @param space The Kibana Space to delete entries in (optional)
* @param user The user to perform search on behalf of
*/
export const deleteEntry = async ({
supertest,
supertestWithoutAuth,
log,
user,
params,
space,
expectedHttpCode = 200,
}: {
supertest: SuperTest.Agent;
supertestWithoutAuth: SuperTest.Agent;
log: ToolingLog;
user?: User;
params: DeleteKnowledgeBaseEntryRequestParamsInput;
space?: string;
expectedHttpCode?: number;
}): Promise<DeleteKnowledgeBaseEntryResponse> => {
const route = routeWithNamespace(
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
space
).replace('{id}', params.id);
let request = (user ? supertestWithoutAuth : supertest)
.delete(route)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.public.v1);
if (user) {
request = request.auth(user.username, user.password);
}
const response = await request.send().expect(expectedHttpCode);
if (response.status !== expectedHttpCode) {
throw new Error(
`Unexpected non ${expectedHttpCode} ok when attempting to delete entry: ${JSON.stringify(
response.status
)},${JSON.stringify(response, null, 4)}`
);
} else {
return response.body;
}
};

View file

@ -0,0 +1,65 @@
/*
* 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 {
ReadKnowledgeBaseEntryResponse,
API_VERSIONS,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
ReadKnowledgeBaseEntryRequestParamsInput,
} from '@kbn/elastic-assistant-common';
import type { User } from './auth/types';
import { routeWithNamespace } from '../../../../../../common/utils/security_solution';
/**
* Get Knowledge Base Entry
* @param supertest The supertest deps
* @param supertestWithoutAuth The supertest deps
* @param log The tooling logger
* @param params Params for find API (optional)
* @param space The Kibana Space to find entries in (optional)
* @param user The user to perform search on behalf of
*/
export const getEntry = async ({
supertest,
supertestWithoutAuth,
log,
user,
params,
space,
}: {
supertest: SuperTest.Agent;
supertestWithoutAuth: SuperTest.Agent;
log: ToolingLog;
user?: User;
params: ReadKnowledgeBaseEntryRequestParamsInput;
space?: string;
}): Promise<ReadKnowledgeBaseEntryResponse> => {
const route = routeWithNamespace(
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
space
).replace('{id}', params.id);
const request = user ? supertestWithoutAuth.auth(user.username, user.password) : supertest;
const response = await request
.get(route)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.public.v1)
.send();
if (response.status !== 200) {
throw new Error(
`Unexpected non 200 ok when attempting to find entries: ${JSON.stringify(
response.status
)},${JSON.stringify(response, null, 4)}`
);
} else {
return response.body;
}
};

View file

@ -0,0 +1,64 @@
/*
* 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 {
API_VERSIONS,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
KnowledgeBaseEntryUpdateProps,
KnowledgeBaseEntryResponse,
} from '@kbn/elastic-assistant-common';
import type { User } from './auth/types';
import { routeWithNamespace } from '../../../../../../common/utils/security_solution';
/**
* Updates a Knowledge Base Entry
* @param supertest The supertest deps
* @param supertestWithoutAuth The supertest deps
* @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 updateEntry = async ({
supertest,
supertestWithoutAuth,
log,
entry,
space,
user,
expectedHttpCode = 200,
}: {
supertest: SuperTest.Agent;
supertestWithoutAuth: SuperTest.Agent;
log: ToolingLog;
entry: KnowledgeBaseEntryUpdateProps;
space?: string;
user?: User;
expectedHttpCode?: number;
}): Promise<KnowledgeBaseEntryResponse> => {
const route = routeWithNamespace(
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_ENTRIES_URL_BY_ID,
space
).replace('{id}', entry.id);
let request = (user ? supertestWithoutAuth : supertest)
.put(route)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, API_VERSIONS.public.v1);
if (user) {
request = request.auth(user.username, user.password);
}
const response = await request.send(entry).expect(expectedHttpCode);
return response.body;
};