Skip to content

Commit 9b1455c

Browse files
authored
[Obs AI Assistant] Make KB retrieval namespace specific (elastic#213505)
Closes elastic#213504 ## Summary ### Problem KB retrievals are not space specific at present. Therefore, users are able to view entries across spaces. ### Solution Filter by `namespace` when retrieving KB entries. ### 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] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [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)
1 parent eb653d2 commit 9b1455c

File tree

4 files changed

+173
-18
lines changed

4 files changed

+173
-18
lines changed

x-pack/platform/plugins/shared/observability_ai_assistant/server/service/client/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,12 @@ export class ObservabilityAIAssistantClient {
760760
sortBy: string;
761761
sortDirection: 'asc' | 'desc';
762762
}) => {
763-
return this.dependencies.knowledgeBaseService.getEntries({ query, sortBy, sortDirection });
763+
return this.dependencies.knowledgeBaseService.getEntries({
764+
query,
765+
sortBy,
766+
sortDirection,
767+
namespace: this.dependencies.namespace,
768+
});
764769
};
765770

766771
deleteKnowledgeBaseEntry = async (id: string) => {

x-pack/platform/plugins/shared/observability_ai_assistant/server/service/knowledge_base_service/index.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '../../../common/types';
2020
import { getAccessQuery, getUserAccessFilters } from '../util/get_access_query';
2121
import { getCategoryQuery } from '../util/get_category_query';
22+
import { getSpaceQuery } from '../util/get_space_query';
2223
import {
2324
createInferenceEndpoint,
2425
deleteInferenceEndpoint,
@@ -259,14 +260,17 @@ export class KnowledgeBaseService {
259260
query,
260261
sortBy,
261262
sortDirection,
263+
namespace,
262264
}: {
263265
query?: string;
264266
sortBy?: string;
265267
sortDirection?: 'asc' | 'desc';
268+
namespace: string;
266269
}): Promise<{ entries: KnowledgeBaseEntry[] }> => {
267270
if (!this.dependencies.config.enableKnowledgeBase) {
268271
return { entries: [] };
269272
}
273+
270274
try {
271275
const response = await this.dependencies.esClient.asInternalUser.search<
272276
KnowledgeBaseEntry & { doc_id?: string }
@@ -281,8 +285,12 @@ export class KnowledgeBaseService {
281285
: []),
282286
{
283287
// exclude user instructions
284-
bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } },
288+
bool: {
289+
must_not: { term: { type: KnowledgeBaseType.UserInstruction } },
290+
},
285291
},
292+
// filter by space
293+
...getSpaceQuery({ namespace }),
286294
],
287295
},
288296
},
@@ -425,6 +433,7 @@ export class KnowledgeBaseService {
425433
},
426434
refresh: 'wait_for',
427435
});
436+
428437
this.dependencies.logger.debug(`Entry added to knowledge base`);
429438
} catch (error) {
430439
this.dependencies.logger.debug(`Failed to add entry to knowledge base ${error}`);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
export function getSpaceQuery({ namespace }: { namespace?: string }) {
9+
return [
10+
{
11+
bool: {
12+
should: [
13+
{ term: { namespace } },
14+
{ bool: { must_not: { exists: { field: 'namespace' } } } },
15+
],
16+
},
17+
},
18+
];
19+
}

x-pack/test/api_integration/deployment_agnostic/apis/observability/ai_assistant/knowledge_base/knowledge_base.spec.ts

+138-16
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
2424
const retry = getService('retry');
2525
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi');
2626

27+
async function getEntries({
28+
query = '',
29+
sortBy = 'title',
30+
sortDirection = 'asc',
31+
spaceId,
32+
user = 'editor',
33+
}: {
34+
query?: string;
35+
sortBy?: string;
36+
sortDirection?: 'asc' | 'desc';
37+
spaceId?: string;
38+
user?: 'admin' | 'editor' | 'viewer';
39+
} = {}): Promise<KnowledgeBaseEntry[]> {
40+
const res = await observabilityAIAssistantAPIClient[user]({
41+
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
42+
params: {
43+
query: { query, sortBy, sortDirection },
44+
},
45+
spaceId,
46+
});
47+
expect(res.status).to.be(200);
48+
49+
return omitCategories(res.body.entries);
50+
}
51+
2752
describe('Knowledge base', function () {
2853
before(async () => {
2954
await importTinyElserModel(ml);
@@ -124,22 +149,6 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
124149
});
125150

126151
describe('when managing multiple entries', () => {
127-
async function getEntries({
128-
query = '',
129-
sortBy = 'title',
130-
sortDirection = 'asc',
131-
}: { query?: string; sortBy?: string; sortDirection?: 'asc' | 'desc' } = {}) {
132-
const res = await observabilityAIAssistantAPIClient.editor({
133-
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
134-
params: {
135-
query: { query, sortBy, sortDirection },
136-
},
137-
});
138-
expect(res.status).to.be(200);
139-
140-
return omitCategories(res.body.entries);
141-
}
142-
143152
beforeEach(async () => {
144153
await clearKnowledgeBase(es);
145154

@@ -200,6 +209,119 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
200209
});
201210
});
202211

212+
describe('when managing multiple entries across spaces', () => {
213+
const SPACE_A_ID = 'space_a';
214+
const SPACE_B_ID = 'space_b';
215+
216+
before(async () => {
217+
await clearKnowledgeBase(es);
218+
219+
const { status: importStatusForSpaceA } = await observabilityAIAssistantAPIClient.admin({
220+
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
221+
params: {
222+
body: {
223+
entries: [
224+
{
225+
id: 'my-doc-1',
226+
title: `Entry in Space A by Admin 1`,
227+
text: `This is a public entry in Space A created by Admin`,
228+
public: true,
229+
},
230+
{
231+
id: 'my-doc-2',
232+
title: `Entry in Space A by Admin 2`,
233+
text: `This is a private entry in Space A created by Admin`,
234+
public: false,
235+
},
236+
],
237+
},
238+
},
239+
spaceId: SPACE_A_ID,
240+
});
241+
expect(importStatusForSpaceA).to.be(200);
242+
243+
const { status: importStatusForSpaceB } = await observabilityAIAssistantAPIClient.admin({
244+
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
245+
params: {
246+
body: {
247+
entries: [
248+
{
249+
id: 'my-doc-3',
250+
title: `Entry in Space B by Admin 3`,
251+
text: `This is a public entry in Space B created by Admin`,
252+
public: true,
253+
},
254+
{
255+
id: 'my-doc-4',
256+
title: `Entry in Space B by Admin 4`,
257+
text: `This is a private entry in Space B created by Admin`,
258+
public: false,
259+
},
260+
],
261+
},
262+
},
263+
spaceId: SPACE_B_ID,
264+
});
265+
expect(importStatusForSpaceB).to.be(200);
266+
});
267+
268+
after(async () => {
269+
await clearKnowledgeBase(es);
270+
});
271+
272+
it('ensures users can only access entries relevant to their namespace', async () => {
273+
// User (admin) in space A should only see entries in space A
274+
const spaceAEntries = await getEntries({
275+
user: 'admin',
276+
spaceId: SPACE_A_ID,
277+
});
278+
279+
expect(spaceAEntries.length).to.be(2);
280+
281+
expect(spaceAEntries[0].id).to.equal('my-doc-1');
282+
expect(spaceAEntries[0].public).to.be(true);
283+
expect(spaceAEntries[0].title).to.equal('Entry in Space A by Admin 1');
284+
285+
expect(spaceAEntries[1].id).to.equal('my-doc-2');
286+
expect(spaceAEntries[1].public).to.be(false);
287+
expect(spaceAEntries[1].title).to.equal('Entry in Space A by Admin 2');
288+
289+
// User (admin) in space B should only see entries in space B
290+
const spaceBEntries = await getEntries({
291+
user: 'admin',
292+
spaceId: SPACE_B_ID,
293+
});
294+
295+
expect(spaceBEntries.length).to.be(2);
296+
297+
expect(spaceBEntries[0].id).to.equal('my-doc-3');
298+
expect(spaceBEntries[0].public).to.be(true);
299+
expect(spaceBEntries[0].title).to.equal('Entry in Space B by Admin 3');
300+
301+
expect(spaceBEntries[1].id).to.equal('my-doc-4');
302+
expect(spaceBEntries[1].public).to.be(false);
303+
expect(spaceBEntries[1].title).to.equal('Entry in Space B by Admin 4');
304+
});
305+
306+
it('should allow a user who is not the owner of the entries to access entries relevant to their namespace', async () => {
307+
// User (editor) in space B should only see entries in space B
308+
const spaceBEntries = await getEntries({
309+
user: 'editor',
310+
spaceId: SPACE_B_ID,
311+
});
312+
313+
expect(spaceBEntries.length).to.be(2);
314+
315+
expect(spaceBEntries[0].id).to.equal('my-doc-3');
316+
expect(spaceBEntries[0].public).to.be(true);
317+
expect(spaceBEntries[0].title).to.equal('Entry in Space B by Admin 3');
318+
319+
expect(spaceBEntries[1].id).to.equal('my-doc-4');
320+
expect(spaceBEntries[1].public).to.be(false);
321+
expect(spaceBEntries[1].title).to.equal('Entry in Space B by Admin 4');
322+
});
323+
});
324+
203325
describe('security roles and access privileges', () => {
204326
describe('should deny access for users without the ai_assistant privilege', () => {
205327
it('POST /internal/observability_ai_assistant/kb/entries/save', async () => {

0 commit comments

Comments
 (0)