From 8522d9cb872dedd2ed8da9aa6fd7242582947c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 26 Feb 2025 00:00:00 +0100 Subject: [PATCH] [Security Assistant] Fix Knowledge Base API (#211367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes bugs related to Security Assistant Knowledge Base API --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Hannah Mudge Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Co-authored-by: Jatin Kathuria Co-authored-by: Chris Cowan Co-authored-by: Elastic Machine Co-authored-by: Arturo Lidueña Co-authored-by: Jon Co-authored-by: Rodney Norris Co-authored-by: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli Co-authored-by: Irene Blanco Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> Co-authored-by: Carlos Crespo (cherry picked from commit c822109a492fe4dcf38ca5aa6d87b2a95bf075c4) --- oas_docs/output/kibana.serverless.yaml | 37 +- oas_docs/output/kibana.yaml | 37 +- ...sistant_api_2023_10_31.bundled.schema.yaml | 41 ++- ...sistant_api_2023_10_31.bundled.schema.yaml | 41 ++- .../entries/common_attributes.gen.ts | 28 +- .../entries/common_attributes.schema.yaml | 24 +- .../crud_knowledge_base_entries_route.gen.ts | 4 +- ...d_knowledge_base_entries_route.schema.yaml | 3 +- .../helpers.ts | 4 +- .../index.test.tsx | 10 +- .../knowledge_base_entry_schema.mock.ts | 9 +- .../server/__mocks__/request.ts | 9 + .../create_knowledge_base_entry.test.ts | 2 +- .../create_knowledge_base_entry.ts | 14 +- .../knowledge_base/index.test.ts | 10 +- .../knowledge_base/index.ts | 13 +- .../knowledge_base/transforms.test.ts | 2 + .../knowledge_base/transforms.ts | 6 +- .../knowledge_base/types.ts | 8 +- .../bulk_actions_route.test.ts | 2 +- .../bulk_actions_route.ts | 2 +- .../anonymization_fields/find_route.test.ts | 4 +- .../routes/anonymization_fields/find_route.ts | 2 +- .../get/get_attack_discovery.test.ts | 4 +- .../get/get_attack_discovery.ts | 2 +- .../cancel/cancel_attack_discovery.test.ts | 4 +- .../post/cancel/cancel_attack_discovery.ts | 2 +- .../post/post_attack_discovery.test.ts | 4 +- .../post/post_attack_discovery.ts | 2 +- .../server/routes/chat/chat_complete_route.ts | 2 +- .../get_defend_insight.test.ts | 4 +- .../defend_insights/get_defend_insight.ts | 2 +- .../get_defend_insights.test.ts | 4 +- .../defend_insights/get_defend_insights.ts | 2 +- .../post_defend_insights.test.ts | 4 +- .../defend_insights/post_defend_insights.ts | 2 +- .../server/routes/evaluate/get_evaluate.ts | 2 +- .../routes/evaluate/post_evaluate.test.ts | 2 +- .../server/routes/evaluate/post_evaluate.ts | 2 +- .../server/routes/helpers.ts | 6 +- .../entries/bulk_actions_route.test.ts | 16 +- .../entries/bulk_actions_route.ts | 26 +- .../entries/create_route.test.ts | 4 +- .../knowledge_base/entries/create_route.ts | 8 +- .../entries/delete_route.test.ts | 4 +- .../knowledge_base/entries/delete_route.ts | 2 +- .../knowledge_base/entries/find_route.test.ts | 2 +- .../knowledge_base/entries/find_route.ts | 6 +- .../knowledge_base/entries/get_route.test.ts | 75 ++++ .../knowledge_base/entries/get_route.ts | 95 +++++ .../entries/update_route.test.ts | 4 +- .../knowledge_base/entries/update_route.ts | 15 +- .../routes/knowledge_base/entries/utils.ts | 11 +- .../get_knowledge_base_status.test.ts | 2 +- .../post_knowledge_base.test.ts | 2 +- .../routes/post_actions_connector_execute.ts | 2 +- .../routes/prompts/bulk_actions_route.test.ts | 2 +- .../routes/prompts/bulk_actions_route.ts | 2 +- .../server/routes/prompts/find_route.test.ts | 4 +- .../server/routes/prompts/find_route.ts | 2 +- .../server/routes/register_routes.ts | 6 + .../server/routes/request_context_factory.ts | 41 ++- ...append_conversation_messages_route.test.ts | 2 +- .../append_conversation_messages_route.ts | 2 +- .../bulk_actions_route.test.ts | 2 +- .../user_conversations/bulk_actions_route.ts | 2 +- .../user_conversations/create_route.test.ts | 4 +- .../routes/user_conversations/create_route.ts | 2 +- .../user_conversations/delete_route.test.ts | 2 +- .../routes/user_conversations/delete_route.ts | 2 +- .../user_conversations/find_route.test.ts | 2 +- .../routes/user_conversations/find_route.ts | 4 +- .../user_conversations/read_route.test.ts | 4 +- .../routes/user_conversations/read_route.ts | 2 +- .../user_conversations/update_route.test.ts | 2 +- .../routes/user_conversations/update_route.ts | 2 +- .../plugins/elastic_assistant/server/types.ts | 2 +- .../trial_license_complete_tier/entries.ts | 330 ++++++++++++++++++ .../mocks/entries.ts | 3 + .../entries/utils/delete_entry.ts | 72 ++++ .../knowledge_base/entries/utils/get_entry.ts | 65 ++++ .../entries/utils/update_entry.ts | 64 ++++ 82 files changed, 1085 insertions(+), 178 deletions(-) create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.test.ts create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/delete_entry.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/get_entry.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/update_entry.ts diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 4e693b9ad6f4c..1042700c86b18 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -37999,7 +37999,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateProps' + $ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps' required: true responses: '200': @@ -44302,6 +44302,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 @@ -44316,6 +44319,7 @@ components: required: - name - namespace + - global - users - $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields' - $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryResponseFields' @@ -44323,6 +44327,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 @@ -44350,8 +44357,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 @@ -44376,6 +44382,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: @@ -44456,6 +44465,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 @@ -44470,6 +44482,7 @@ components: required: - name - namespace + - global - users - $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields' - $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryResponseFields' @@ -44477,6 +44490,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 @@ -44536,6 +44552,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: @@ -44715,6 +44734,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 diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index e60da71e7cc01..707e3ab31ec71 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -40544,7 +40544,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateProps' + $ref: '#/components/schemas/Security_AI_Assistant_API_KnowledgeBaseEntryUpdateRouteProps' required: true responses: '200': @@ -50879,6 +50879,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 @@ -50893,6 +50896,7 @@ components: required: - name - namespace + - global - users - $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields' - $ref: '#/components/schemas/Security_AI_Assistant_API_DocumentEntryResponseFields' @@ -50900,6 +50904,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 @@ -50927,8 +50934,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 @@ -50953,6 +50959,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: @@ -51033,6 +51042,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 @@ -51047,6 +51059,7 @@ components: required: - name - namespace + - global - users - $ref: '#/components/schemas/Security_AI_Assistant_API_ResponseFields' - $ref: '#/components/schemas/Security_AI_Assistant_API_IndexEntryResponseFields' @@ -51054,6 +51067,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 @@ -51113,6 +51129,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: @@ -51292,6 +51311,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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 763a7f0efb081..c57ef291cb74e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index b4c3e59a17a71..a36d5d7ef7bd0 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts index 8a9e4b89e971f..96d128d2b76d7 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.gen.ts @@ -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; +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; 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; +export const KnowledgeBaseEntryUpdateRouteProps = z.discriminatedUnion('type', [ + DocumentEntryCreateFields, + IndexEntryCreateFields, +]); + export type KnowledgeBaseEntryResponse = z.infer; export const KnowledgeBaseEntryResponse = z.discriminatedUnion('type', [DocumentEntry, IndexEntry]); diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml index 91b082e4aac44..7ff8395d06cd1 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/common_attributes.schema.yaml @@ -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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.gen.ts index d47f90b2446df..e4eb006efccaa 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.gen.ts @@ -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 >; diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml index 4c80999d2c926..db2dbf3616b3e 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/knowledge_base/entries/crud_knowledge_base_entries_route.schema.yaml @@ -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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts index 456eebfaffb57..0ef9e360e1a59 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/helpers.ts @@ -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 diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx index 4ad629d1ef7d3..222df5c87c855 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.test.tsx @@ -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({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts index 8171dd2b39249..e5af9a5d44f76 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/knowledge_base_entry_schema.mock.ts @@ -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', diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts index 326b37ab219a3..e3c32bbc5ab5f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/request.ts @@ -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', diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts index df6533d5d8df2..27f5483abc23d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.test.ts @@ -79,7 +79,7 @@ describe('createKnowledgeBaseEntry', () => { source: 'test', text: 'test', name: 'test', - kb_resource: 'test', + kb_resource: 'user', required: false, vector: undefined, }, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts index cda1781c3a1e9..59cd6a244999d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry.ts @@ -47,7 +47,6 @@ export const createKnowledgeBaseEntry = async ({ user, knowledgeBaseEntry, logger, - global = false, telemetry, }: CreateKnowledgeBaseEntryParams): Promise => { 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) => ({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts index b21ac92170e73..5fd50ec487e7e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.test.ts @@ -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'); }); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts index a4cad89abdc22..d1a770d741983 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/index.ts @@ -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 => { 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 }), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts index b0451774770b8..586806d010b93 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.test.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.ts index 16ef4ffb0595e..70c3835d5397f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/transforms.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts index 443d03941ccdd..a38c9a045bdcd 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/knowledge_base/types.ts @@ -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; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts index d3d1302247052..d7ec5f4b1c25f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts index 5804f195fb855..35e0f9de5e9bd 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/bulk_actions_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.test.ts index 7c2b1d330a3db..8a6f56fd2ec6a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts index e23c06dbb6428..404cf73c469cf 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/anonymization_fields/find_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts index ce07d66b9606e..fd5560d70b32e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts index cf6d07e4953cd..189701473aef9 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts @@ -51,7 +51,7 @@ export const getAttackDiscoveryRoute = (router: IRouter { ...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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts index 1c5fc36dc56cf..73247c66e4448 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts @@ -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({ diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts index d50987317b0e3..8728b30d98100 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts index 9b7640cd8899b..a3a36d75f69fa 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts @@ -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`, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts index 3d0d64bb74b73..726ea224692ee 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts index 1f0721daeb35e..992f4d731eece 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts index 50c547d1d31a2..3121155f15eed 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insight.ts @@ -72,7 +72,7 @@ export const getDefendInsightRoute = (router: IRouter { 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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.ts index d967ee6537c9a..f9ad0c3a8cdb8 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/get_defend_insights.ts @@ -75,7 +75,7 @@ export const getDefendInsightsRoute = (router: IRouter { }); (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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.ts index c8ba9e9819b19..46fb5a4301a28 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/defend_insights/post_defend_insights.ts @@ -102,7 +102,7 @@ export const postDefendInsightsRoute = (router: IRouter { beforeEach(() => { jest.clearAllMocks(); - context.elasticAssistant.getCurrentUser.mockReturnValue(mockUser); + context.elasticAssistant.getCurrentUser.mockResolvedValue(mockUser); postEvaluateRoute(server.router, mockGetElser); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index 226b7602b9d76..be250af80325e 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts index 346d17eb7bdfc..e8a359e335122 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts @@ -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 => { 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 { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.test.ts index eb06e34c33219..8f6290c4bcb4c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.test.ts @@ -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()], diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts index 1a9b6af6cb674..723d452e4198c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/bulk_actions_route.ts @@ -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({ - 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 }), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.test.ts index 909ca1e5cb6b2..06137bf90f105 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts index 5d3171f49533f..3b8e90fe0f0f0 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/create_route.ts @@ -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, }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.test.ts index 730807550545d..dcd3a76bfa1e3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.ts index 74ae67fdb17e5..0653660f757ad 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/delete_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.test.ts index 681a3fc2e08fa..55070b8609600 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.test.ts @@ -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()) ); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts index 6823c8d346b87..db3a21794528c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/find_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.test.ts new file mode 100644 index 0000000000000..0a40954b55657 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.test.ts @@ -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; + 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, + }); + }); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.ts new file mode 100644 index 0000000000000..7cf7cb7972db4 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/get_route.ts @@ -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> => { + 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({ + 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, + }); + } + } + ); +}; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.test.ts index 7f46434630172..6b70ed9cccf73 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.test.ts @@ -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' }, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.ts index 4ef4cbeaaea50..93761e3e01c82 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/update_route.ts @@ -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) { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts index 38da2c733edc0..2e24c7cbc09a3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts @@ -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`); } }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts index a31af7596977a..a3de571b5140d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_status.test.ts @@ -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: { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.test.ts index 69301ac7035a4..b2214bf4cd5f0 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/knowledge_base/post_knowledge_base.test.ts @@ -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: { diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts index 41ecb19c989ed..6bd1884ae893c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts @@ -69,7 +69,7 @@ export const postActionsConnectorExecuteRoute = ( let onLlmResponse; try { - const checkResponse = performChecks({ + const checkResponse = await performChecks({ context: ctx, request, response, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.test.ts index cb3d71b469589..918a5b691eb43 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts index d5898c6eb2450..5987d0d54bea2 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/bulk_actions_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.test.ts index 151c1622d0219..dd15c80547243 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts index c4b3e5720f74b..ea57bd5472203 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/prompts/find_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts index 0124dfc7969c2..211b4649fc7c3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/register_routes.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts index 25935c784e43b..e326e01ba77b3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/request_context_factory.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.test.ts index fb066f1245fe7..cd85828f3d22c 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.ts index 4d10105173548..ffbaed812cfab 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/append_conversation_messages_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.test.ts index d69f53ecaa6c0..602d93c548fe9 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts index 436b77fc26245..b66ea1baf5d76 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/bulk_actions_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.test.ts index 378cde4e9bf65..769b340df8d96 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts index 39971dca7480f..0fd4888548ade 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/create_route.ts @@ -45,7 +45,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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.test.ts index 8edc493c3239f..9a8c472b8e295 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts index 5679d8cb35c61..4460762dd3c8a 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/delete_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.test.ts index 2b20ab03371f6..42fe3b5e184da 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.test.ts @@ -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', diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts index 3295dad4ea5bf..d18de6eaac7ad 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/find_route.ts @@ -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 diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.test.ts index 705054cad9e00..64f5fd018d920 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.test.ts @@ -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) diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts index 3ccc2c93d8bd5..14d880123d823 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/read_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.test.ts index 138116398b818..a7fb7243d1728 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.test.ts @@ -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); }); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts index 674511611b041..ac3b9b4846ac7 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/user_conversations/update_route.ts @@ -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, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts index a979e1b20aba9..5730336d17b31 100755 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/types.ts @@ -134,7 +134,7 @@ export interface ElasticAssistantApiRequestHandlerContext { logger: Logger; getServerBasePath: () => string; getSpaceId: () => string; - getCurrentUser: () => AuthenticatedUser | null; + getCurrentUser: () => Promise; getAIAssistantConversationsDataClient: ( params?: GetAIAssistantConversationsDataClientParams ) => Promise; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts index 2ecb368c2ba7b..9ef35c1167138 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/entries.ts @@ -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 () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/mocks/entries.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/mocks/entries.ts index 570d9d8b5f30d..c1d8e5594e853 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/mocks/entries.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/mocks/entries.ts @@ -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, }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/delete_entry.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/delete_entry.ts new file mode 100644 index 0000000000000..8ec69fa8d430c --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/delete_entry.ts @@ -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 => { + 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; + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/get_entry.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/get_entry.ts new file mode 100644 index 0000000000000..8823ed938fe30 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/get_entry.ts @@ -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 => { + 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; + } +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/update_entry.ts b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/update_entry.ts new file mode 100644 index 0000000000000..bf20fcaadac4e --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/utils/update_entry.ts @@ -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 => { + 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; +};