From d669c83be8d7d79e157786dce9742a55c3e69a6b Mon Sep 17 00:00:00 2001 From: Viduni Wickramarachchi Date: Wed, 4 Dec 2024 15:21:25 -0500 Subject: [PATCH] [Obs AI Assistant] Manual migration for routes with access tag (#202817) ## Summary ### Problem `tags: [access:ai_assistant]` is deprecated. ### Solution All the routes that use this tag needs to be migrated to the `authz`: `requiredPrivileges` property. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../server/routes/chat/route.ts | 24 ++-- .../server/routes/connectors/route.ts | 6 +- .../server/routes/conversations/route.ts | 36 +++-- .../server/routes/functions/route.ts | 18 ++- .../server/routes/knowledge_base/route.ts | 60 ++++++--- .../server/routes/types.ts | 1 - .../common/config.ts | 13 +- .../common/users/users.ts | 14 +- .../tests/chat/chat.spec.ts | 24 ++++ .../tests/complete/complete.spec.ts | 25 +++- .../tests/connectors/connectors.spec.ts | 22 +++- .../tests/conversations/conversations.spec.ts | 124 ++++++++++++++++++ .../knowledge_base/knowledge_base.spec.ts | 57 ++++++++ .../knowledge_base_setup.spec.ts | 25 +++- .../knowledge_base_status.spec.ts | 22 +++- .../knowledge_base_user_instructions.spec.ts | 38 ++++++ 16 files changed, 449 insertions(+), 60 deletions(-) diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts index e80e6fa156b06..5ef72dc8e7b56 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts @@ -126,8 +126,10 @@ async function initializeChatRequest({ const chatRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, params: t.type({ body: t.intersection([ @@ -174,8 +176,10 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const chatRecallRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat/recall', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, params: t.type({ body: t.type({ @@ -282,8 +286,10 @@ async function chatComplete( const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat/complete', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, params: chatCompleteInternalRt, handler: async (resources): Promise => { @@ -293,8 +299,10 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ const publicChatCompleteRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /api/observability_ai_assistant/chat/complete 2023-10-31', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, params: chatCompletePublicRt, handler: async (resources): Promise => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/connectors/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/connectors/route.ts index 24d63d3f7fa06..80bc877e6f5f9 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/connectors/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/connectors/route.ts @@ -10,8 +10,10 @@ import { createObservabilityAIAssistantServerRoute } from '../create_observabili const listConnectorsRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'GET /internal/observability_ai_assistant/connectors', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { request, plugins } = resources; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/conversations/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/conversations/route.ts index 7e59be004cac9..e320376bc7357 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/conversations/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/conversations/route.ts @@ -17,8 +17,10 @@ const getConversationRoute = createObservabilityAIAssistantServerRoute({ conversationId: t.string, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { service, request, params } = resources; @@ -40,8 +42,10 @@ const findConversationsRoute = createObservabilityAIAssistantServerRoute({ query: t.string, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise<{ conversations: Conversation[] }> => { const { service, request, params } = resources; @@ -63,8 +67,10 @@ const createConversationRoute = createObservabilityAIAssistantServerRoute({ conversation: conversationCreateRt, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { service, request, params } = resources; @@ -89,8 +95,10 @@ const updateConversationRoute = createObservabilityAIAssistantServerRoute({ conversation: conversationUpdateRt, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { service, request, params } = resources; @@ -115,8 +123,10 @@ const updateConversationTitle = createObservabilityAIAssistantServerRoute({ title: t.string, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { service, request, params } = resources; @@ -143,8 +153,10 @@ const deleteConversationRoute = createObservabilityAIAssistantServerRoute({ conversationId: t.string, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const { service, request, params } = resources; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts index 1571487765c09..c5f571769dfb6 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts @@ -22,8 +22,10 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({ scopes: t.union([t.array(assistantScopeType), assistantScopeType]), }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async ( resources @@ -97,8 +99,10 @@ const functionRecallRoute = createObservabilityAIAssistantServerRoute({ }), ]), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async ( resources @@ -132,8 +136,10 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ labels: t.record(t.string, t.string), }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts index 37e9248a0c624..4ff94393bc525 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/knowledge_base/route.ts @@ -20,8 +20,10 @@ import { Instruction, KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../. const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({ endpoint: 'GET /internal/observability_ai_assistant/kb/status', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async ({ service, @@ -54,11 +56,15 @@ const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({ }), }), options: { - tags: ['access:ai_assistant'], timeout: { idleSocket: moment.duration(20, 'minutes').asMilliseconds(), }, }, + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, + }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -74,8 +80,10 @@ const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({ const resetKnowledgeBase = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/reset', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise<{ result: string }> => { const client = await resources.service.getClient({ request: resources.request }); @@ -92,8 +100,10 @@ const resetKnowledgeBase = createObservabilityAIAssistantServerRoute({ const semanticTextMigrationKnowledgeBase = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/kb/semantic_text_migration', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -108,8 +118,10 @@ const semanticTextMigrationKnowledgeBase = createObservabilityAIAssistantServerR const getKnowledgeBaseUserInstructions = createObservabilityAIAssistantServerRoute({ endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async ( resources @@ -137,8 +149,10 @@ const saveKnowledgeBaseUserInstruction = createObservabilityAIAssistantServerRou public: toBooleanRt, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -156,8 +170,10 @@ const saveKnowledgeBaseUserInstruction = createObservabilityAIAssistantServerRou const getKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ endpoint: 'GET /internal/observability_ai_assistant/kb/entries', - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, params: t.type({ query: t.type({ @@ -207,8 +223,10 @@ const saveKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({ params: t.type({ body: knowledgeBaseEntryRt, }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -238,8 +256,10 @@ const deleteKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({ entryId: t.string, }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); @@ -259,8 +279,10 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({ entries: t.array(knowledgeBaseEntryRt), }), }), - options: { - tags: ['access:ai_assistant'], + security: { + authz: { + requiredPrivileges: ['ai_assistant'], + }, }, handler: async (resources): Promise => { const client = await resources.service.getClient({ request: resources.request }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts index 645da146dfb89..38a8f0632920b 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/types.ts @@ -85,5 +85,4 @@ export interface ObservabilityAIAssistantRouteCreateOptions { payload?: number; idleSocket?: number; }; - tags: Array<'access:ai_assistant'>; } diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts b/x-pack/test/observability_ai_assistant_api_integration/common/config.ts index 427258a6e2910..6505ad3e94d64 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/config.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/common/config.ts @@ -11,7 +11,7 @@ import { ObservabilityAIAssistantFtrConfigName } from '../configs'; import { getApmSynthtraceEsClient } from './create_synthtrace_client'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; import { getScopedApiClient } from './observability_ai_assistant_api_client'; -import { editor, secondaryEditor, viewer } from './users/users'; +import { editor, secondaryEditor, unauthorizedUser, viewer } from './users/users'; export interface ObservabilityAIAssistantFtrConfig { name: ObservabilityAIAssistantFtrConfigName; @@ -33,6 +33,16 @@ export type ObservabilityAIAssistantAPIClient = Awaited< export type ObservabilityAIAssistantServices = Awaited>['services']; +export class ForbiddenApiError extends Error { + status: number; + + constructor(message: string = 'Forbidden') { + super(message); + this.name = 'ForbiddenApiError'; + this.status = 403; + } +} + export function createObservabilityAIAssistantAPIConfig({ config, license, @@ -67,6 +77,7 @@ export function createObservabilityAIAssistantAPIConfig({ viewer: getScopedApiClientForUsername(viewer.username), editor: getScopedApiClientForUsername(editor.username), secondaryEditor: getScopedApiClientForUsername(secondaryEditor.username), + unauthorizedUser: getScopedApiClientForUsername(unauthorizedUser.username), }; }, }, diff --git a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts b/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts index 898954a9bfb97..2dc5a433517f3 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/common/users/users.ts @@ -6,10 +6,14 @@ */ import { kbnTestConfig } from '@kbn/test'; + const password = kbnTestConfig.getUrlParts().password!; +export const UNAUTHORIZED_USERNAME = 'unauthorized_user'; +export const UNAUTHORIZED_USER_PASSWORD = 'unauthorized_password'; + export interface User { - username: 'elastic' | 'editor' | 'viewer' | 'secondary_editor'; + username: 'elastic' | 'editor' | 'viewer' | 'secondary_editor' | 'unauthorized_user'; password: string; roles: string[]; } @@ -32,4 +36,10 @@ export const viewer: User = { roles: ['viewer'], }; -export const allUsers = [editor, secondaryEditor, viewer]; +export const unauthorizedUser: User = { + username: UNAUTHORIZED_USERNAME, + password: UNAUTHORIZED_USER_PASSWORD, + roles: [], +}; + +export const allUsers = [editor, secondaryEditor, viewer, unauthorizedUser]; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts index d514d6ddb7025..cedd4c286dc1a 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/chat/chat.spec.ts @@ -11,10 +11,12 @@ import { PassThrough } from 'stream'; import { createLlmProxy, LlmProxy } from '../../common/create_llm_proxy'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const log = getService('log'); + const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); const CHAT_API_URL = `/internal/observability_ai_assistant/chat`; @@ -183,5 +185,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { `Token limit reached. Token limit is 8192, but the current conversation has 11036 tokens.` ); }); + + describe('security roles and access privileges', () => { + it('should deny access for users without the ai_assistant privilege', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: `POST ${CHAT_API_URL}`, + params: { + body: { + name: 'my_api_call', + messages, + connectorId, + functions: [], + scopes: ['all'], + }, + }, + }); + throw new ForbiddenApiError('Expected unauthorizedUser() to throw a 403 Forbidden error'); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index 2eb7c6f986cfd..86e357e2e7760 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -32,6 +32,7 @@ import { getConversationUpdatedEvent, } from '../conversations/helpers'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -39,7 +40,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); - const COMPLETE_API_URL = `/internal/observability_ai_assistant/chat/complete`; + const COMPLETE_API_URL = '/internal/observability_ai_assistant/chat/complete'; const messages: Message[] = [ { @@ -486,5 +487,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { // todo it.skip('executes a function', async () => {}); + + describe('security roles and access privileges', () => { + it('should deny access for users without the ai_assistant privilege', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'POST /internal/observability_ai_assistant/chat/complete', + params: { + body: { + messages, + connectorId, + persist: false, + screenContexts: [], + scopes: ['all'], + }, + }, + }); + throw new ForbiddenApiError('Expected unauthorizedUser() to throw a 403 Forbidden error'); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts index 41700b21555fa..42e1f8751719e 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/connectors/connectors.spec.ts @@ -9,12 +9,15 @@ import expect from '@kbn/expect'; import type { Agent as SuperTestAgent } from 'supertest'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); const supertest = getService('supertest'); const log = getService('log'); + const CONNECTOR_API_URL = '/internal/observability_ai_assistant/connectors'; + describe('List connectors', () => { before(async () => { await deleteAllActionConnectors(supertest); @@ -27,14 +30,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('Returns a 2xx for enterprise license', async () => { await observabilityAIAssistantAPIClient .editor({ - endpoint: 'GET /internal/observability_ai_assistant/connectors', + endpoint: `GET ${CONNECTOR_API_URL}`, }) .expect(200); }); it('returns an empty list of connectors', async () => { const res = await observabilityAIAssistantAPIClient.editor({ - endpoint: 'GET /internal/observability_ai_assistant/connectors', + endpoint: `GET ${CONNECTOR_API_URL}`, }); expect(res.body.length).to.be(0); @@ -44,13 +47,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { const connectorId = await createProxyActionConnector({ supertest, log, port: 1234 }); const res = await observabilityAIAssistantAPIClient.editor({ - endpoint: 'GET /internal/observability_ai_assistant/connectors', + endpoint: `GET ${CONNECTOR_API_URL}`, }); expect(res.body.length).to.be(1); await deleteActionConnector({ supertest, connectorId, log }); }); + + describe('security roles and access privileges', () => { + it('should deny access for users without the ai_assistant privilege', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: `GET ${CONNECTOR_API_URL}`, + }); + throw new ForbiddenApiError('Expected unauthorizedUser() to throw a 403 Forbidden error'); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts index 71eb37d357696..bb85e99b99500 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/conversations/conversations.spec.ts @@ -14,6 +14,7 @@ import { } from '@kbn/observability-ai-assistant-plugin/common/types'; import type { FtrProviderContext } from '../../common/ftr_provider_context'; import type { SupertestReturnType } from '../../common/observability_ai_assistant_api_client'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); @@ -250,5 +251,128 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); }); + + describe('security roles and access privileges', () => { + describe('should deny access for users without the ai_assistant privilege', () => { + let createResponse: Awaited< + SupertestReturnType<'POST /internal/observability_ai_assistant/conversation'> + >; + before(async () => { + createResponse = await observabilityAIAssistantAPIClient + .editor({ + endpoint: 'POST /internal/observability_ai_assistant/conversation', + params: { + body: { + conversation: conversationCreate, + }, + }, + }) + .expect(200); + }); + + after(async () => { + await observabilityAIAssistantAPIClient + .editor({ + endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }) + .expect(200); + }); + + it('POST /internal/observability_ai_assistant/conversation', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'POST /internal/observability_ai_assistant/conversation', + params: { + body: { + conversation: conversationCreate, + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('POST /internal/observability_ai_assistant/conversations', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'POST /internal/observability_ai_assistant/conversations', + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('PUT /internal/observability_ai_assistant/conversation/{conversationId}', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + body: { + conversation: merge(omit(conversationUpdate, 'conversation.id'), { + conversation: { id: createResponse.body.conversation.id }, + }), + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('GET /internal/observability_ai_assistant/conversation/{conversationId}', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('DELETE /internal/observability_ai_assistant/conversation/{conversationId}', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', + params: { + path: { + conversationId: createResponse.body.conversation.id, + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts index 8d8c2e2417686..9d80db3baeae6 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base.spec.ts @@ -15,6 +15,7 @@ import { deleteInferenceEndpoint, deleteKnowledgeBaseModel, } from './helpers'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); @@ -210,6 +211,62 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(entries[0].title).to.eql('My title b'); }); }); + + describe('security roles and access privileges', () => { + describe('should deny access for users without the ai_assistant privilege', () => { + it('POST /internal/observability_ai_assistant/kb/entries/save', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'POST /internal/observability_ai_assistant/kb/entries/save', + params: { + body: { + id: 'my-doc-id-1', + title: 'My title', + text: 'My content', + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('GET /internal/observability_ai_assistant/kb/entries', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'GET /internal/observability_ai_assistant/kb/entries', + params: { + query: { query: '', sortBy: 'title', sortDirection: 'asc' }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('DELETE /internal/observability_ai_assistant/kb/entries/{entryId}', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}', + params: { + path: { entryId: 'my-doc-id-1' }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts index 7903f4b53966a..0d7625bb63ed3 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_setup.spec.ts @@ -13,18 +13,21 @@ import { TINY_ELSER, deleteInferenceEndpoint, } from './helpers'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); + const KNOWLEDGE_BASE_SETUP_API_URL = '/internal/observability_ai_assistant/kb/setup'; + describe('/internal/observability_ai_assistant/kb/setup', () => { it('returns model info when successful', async () => { await createKnowledgeBaseModel(ml); const res = await observabilityAIAssistantAPIClient .admin({ - endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + endpoint: `POST ${KNOWLEDGE_BASE_SETUP_API_URL}`, params: { query: { model_id: TINY_ELSER.id, @@ -43,7 +46,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns error message if model is not deployed', async () => { const res = await observabilityAIAssistantAPIClient .admin({ - endpoint: 'POST /internal/observability_ai_assistant/kb/setup', + endpoint: `POST ${KNOWLEDGE_BASE_SETUP_API_URL}`, params: { query: { model_id: TINY_ELSER.id, @@ -60,5 +63,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { // @ts-expect-error expect(res.body.statusCode).to.be(500); }); + + describe('security roles and access privileges', () => { + it('should deny access for users without the ai_assistant privilege', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: `POST ${KNOWLEDGE_BASE_SETUP_API_URL}`, + params: { + query: { + model_id: TINY_ELSER.id, + }, + }, + }); + throw new ForbiddenApiError('Expected unauthorizedUser() to throw a 403 Forbidden error'); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts index 8c10a6128d302..3f66931ca0719 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_status.spec.ts @@ -13,12 +13,15 @@ import { TINY_ELSER, deleteInferenceEndpoint, } from './helpers'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const ml = getService('ml'); const es = getService('es'); const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); + const KNOWLEDGE_BASE_STATUS_API_URL = '/internal/observability_ai_assistant/kb/status'; + describe('/internal/observability_ai_assistant/kb/status', () => { beforeEach(async () => { await createKnowledgeBaseModel(ml); @@ -41,7 +44,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns correct status after knowledge base is setup', async () => { const res = await observabilityAIAssistantAPIClient - .editor({ endpoint: 'GET /internal/observability_ai_assistant/kb/status' }) + .editor({ endpoint: `GET ${KNOWLEDGE_BASE_STATUS_API_URL}` }) .expect(200); expect(res.body.ready).to.be(true); @@ -54,7 +57,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const res = await observabilityAIAssistantAPIClient .editor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/status', + endpoint: `GET ${KNOWLEDGE_BASE_STATUS_API_URL}`, }) .expect(200); @@ -70,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const res = await observabilityAIAssistantAPIClient .editor({ - endpoint: 'GET /internal/observability_ai_assistant/kb/status', + endpoint: `GET ${KNOWLEDGE_BASE_STATUS_API_URL}`, }) .expect(200); @@ -80,5 +83,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Inference endpoint not found [obs_ai_assistant_kb_inference]' ); }); + + describe('security roles and access privileges', () => { + it('should deny access for users without the ai_assistant privilege', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: `GET ${KNOWLEDGE_BASE_STATUS_API_URL}`, + }); + throw new ForbiddenApiError('Expected unauthorizedUser() to throw a 403 Forbidden error'); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); }); } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts index cde2c9e4b4a83..d5022a052d781 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/knowledge_base_user_instructions.spec.ts @@ -23,6 +23,7 @@ import { getConversationCreatedEvent } from '../conversations/helpers'; import { LlmProxy, createLlmProxy } from '../../common/create_llm_proxy'; import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors'; import { User } from '../../common/users/users'; +import { ForbiddenApiError } from '../../common/config'; export default function ApiTest({ getService }: FtrProviderContext) { const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient'); @@ -362,5 +363,42 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(res2).to.be(''); }); }); + + describe('security roles and access privileges', () => { + describe('should deny access for users without the ai_assistant privilege', () => { + it('PUT /internal/observability_ai_assistant/kb/user_instructions', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions', + params: { + body: { + id: 'test-instruction', + text: 'Test user instruction', + public: true, + }, + }, + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + + it('GET /internal/observability_ai_assistant/kb/user_instructions', async () => { + try { + await observabilityAIAssistantAPIClient.unauthorizedUser({ + endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions', + }); + throw new ForbiddenApiError( + 'Expected unauthorizedUser() to throw a 403 Forbidden error' + ); + } catch (e) { + expect(e.status).to.be(403); + } + }); + }); + }); }); }