Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Security GenAI] When indices referenced in KB index entries are deleted from OUTSIDE the AI Assistant KB UI, there is not indication to the user (#197156) (#197722) #197895

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const useInlineActions = <T extends { isDefault?: boolean | undefined }>(
actions: [
{
name: i18n.EDIT_BUTTON,
'data-test-subj': 'edit-button',
description: i18n.EDIT_BUTTON,
icon: 'pencil',
type: 'icon',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const mockDataViews = {
{ name: 'field-2', esTypes: ['text'] },
{ name: 'field-3', esTypes: ['semantic_text'] },
]),
getExistingIndices: jest.fn().mockResolvedValue(['index-2']),
} as unknown as DataViewsContract;
const queryClient = new QueryClient();
const wrapper = (props: { children: React.ReactNode }) => (
Expand All @@ -65,7 +66,22 @@ const wrapper = (props: { children: React.ReactNode }) => (
describe('KnowledgeBaseSettingsManagement', () => {
const mockData = [
{ id: '1', name: 'Test Entry 1', type: 'document', kbResource: 'user', users: [{ id: 'hi' }] },
{ id: '2', name: 'Test Entry 2', type: 'index', kbResource: 'global', users: [] },
{
id: '2',
name: 'Test Entry 2',
type: 'index',
kbResource: 'global',
users: [],
index: 'missing-index',
},
{
id: '3',
name: 'Test Entry 3',
type: 'index',
kbResource: 'private',
users: [{ id: 'fake-user' }],
index: 'index-2',
},
];

beforeEach(() => {
Expand Down Expand Up @@ -241,4 +257,24 @@ describe('KnowledgeBaseSettingsManagement', () => {
});
expect(screen.queryByTestId('delete-entry-confirmation')).not.toBeInTheDocument();
});

it('shows warning icon for index entries with missing indices', async () => {
render(<KnowledgeBaseSettingsManagement dataViews={mockDataViews} />, {
wrapper,
});

await waitFor(() => expect(screen.getByTestId('missing-index-icon')).toBeInTheDocument());

expect(screen.getAllByTestId('missing-index-icon').length).toEqual(1);

fireEvent.mouseOver(screen.getByTestId('missing-index-icon'));

await waitFor(() => screen.getByTestId('missing-index-tooltip'));

expect(
screen.getByText(
'The index assigned to this knowledge base entry is unavailable. Check the permissions on the configured index, or that the index has not been deleted. You can update the index to be used for this knowledge entry, or delete the entry entirely.'
)
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from '@kbn/elastic-assistant-common';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import useAsync from 'react-use/lib/useAsync';
import { KnowledgeBaseTour } from '../../tour/knowledge_base';
import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management';
import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries';
Expand Down Expand Up @@ -173,10 +174,22 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
toasts,
enabled: enableKnowledgeBaseByDefault,
});

const { value: existingIndices } = useAsync(() => {
const indices: string[] = [];
entries.data.forEach((entry) => {
if (entry.type === 'index') {
indices.push(entry.index);
}
});
return dataViews.getExistingIndices(indices);
}, [entries.data]);

const { getColumns } = useKnowledgeBaseTable();
const columns = useMemo(
() =>
getColumns({
existingIndices,
isDeleteEnabled: (entry: KnowledgeBaseEntryResponse) => {
return (
!isSystemEntry(entry) && (isGlobalEntry(entry) ? hasManageGlobalKnowledgeBase : true)
Expand All @@ -197,7 +210,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
openFlyout();
},
}),
[entries.data, getColumns, hasManageGlobalKnowledgeBase, openFlyout]
[entries.data, existingIndices, getColumns, hasManageGlobalKnowledgeBase, openFlyout]
);

// Refresh button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('IndexEntryEditor', () => {
{ name: 'field-2', esTypes: ['text'] },
{ name: 'field-3', esTypes: ['semantic_text'] },
]),
getExistingIndices: jest.fn().mockResolvedValue(['index-1']),
} as unknown as DataViewsContract;

const defaultProps = {
Expand Down Expand Up @@ -147,4 +148,20 @@ describe('IndexEntryEditor', () => {
expect(getByRole('combobox', { name: i18n.ENTRY_FIELD_PLACEHOLDER })).toBeDisabled();
});
});

it('fetches index options and updates on selection 2', async () => {
(mockDataViews.getExistingIndices as jest.Mock).mockResolvedValue([]);
const { getByText } = render(
<IndexEntryEditor
{...defaultProps}
entry={{ ...defaultProps.entry, index: 'missing-index' }}
/>
);

await waitFor(() => {
expect(mockDataViews.getExistingIndices).toHaveBeenCalled();
});

expect(getByText("Index doesn't exist")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
}));
}, [dataViews]);

const { value: isMissingIndex } = useAsync(async () => {
if (!entry?.index?.length) return false;

return !(await dataViews.getExistingIndices([entry.index])).length;
}, [entry?.index]);

const indexFields = useAsync(
async () =>
dataViews.getFieldsForWildcard({
Expand Down Expand Up @@ -251,11 +257,17 @@ export const IndexEntryEditor: React.FC<Props> = React.memo(
fullWidth
/>
</EuiFormRow>
<EuiFormRow label={i18n.ENTRY_INDEX_NAME_INPUT_LABEL} fullWidth>
<EuiFormRow
label={i18n.ENTRY_INDEX_NAME_INPUT_LABEL}
fullWidth
isInvalid={isMissingIndex}
error={isMissingIndex && <>{i18n.MISSING_INDEX_ERROR}</>}
>
<EuiComboBox
data-test-subj="index-combobox"
aria-label={i18n.ENTRY_INDEX_NAME_INPUT_LABEL}
isClearable={true}
isInvalid={isMissingIndex}
singleSelection={{ asPlainText: true }}
onCreateOption={onCreateIndexOption}
fullWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,18 @@ export const PRIVATE = i18n.translate(
defaultMessage: 'Private',
}
);

export const MISSING_INDEX_ERROR = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.missingIndexError',
{
defaultMessage: `Index doesn't exist`,
}
);

export const MISSING_INDEX_TOOLTIP_CONTENT = i18n.translate(
'xpack.elasticAssistant.assistant.settings.knowledgeBaseSettingsManagement.missingIndexTootipContent',
{
defaultMessage:
'The index assigned to this knowledge base entry is unavailable. Check the permissions on the configured index, or that the index has not been deleted. You can update the index to be used for this knowledge entry, or delete the entry entirely.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
* 2.0.
*/

import { EuiAvatar, EuiBadge, EuiBasicTableColumn, EuiIcon, EuiText } from '@elastic/eui';
import {
EuiAvatar,
EuiBadge,
EuiBasicTableColumn,
EuiIcon,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback, useMemo } from 'react';
import { FormattedDate } from '@kbn/i18n-react';
Expand Down Expand Up @@ -77,6 +84,39 @@ const AuthorColumn = ({ entry }: { entry: KnowledgeBaseEntryResponse }) => {
);
};

const NameColumn = ({
entry,
existingIndices,
}: {
entry: KnowledgeBaseEntryResponse;
existingIndices?: string[];
}) => {
let showMissingIndexWarning = false;
if (existingIndices && entry.type === 'index') {
showMissingIndexWarning = !existingIndices.includes(entry.index);
}
return (
<>
<EuiText size={'s'}>{entry.name}</EuiText>
{showMissingIndexWarning && (
<EuiToolTip
data-test-subj="missing-index-tooltip"
content={i18n.MISSING_INDEX_TOOLTIP_CONTENT}
>
<EuiIcon
data-test-subj="missing-index-icon"
type="warning"
color="danger"
css={css`
margin-left: 10px;
`}
/>
</EuiToolTip>
)}
</>
);
};

export const useKnowledgeBaseTable = () => {
const getActions = useInlineActions<KnowledgeBaseEntryResponse & { isDefault?: undefined }>();

Expand All @@ -97,11 +137,13 @@ export const useKnowledgeBaseTable = () => {

const getColumns = useCallback(
({
existingIndices,
isDeleteEnabled,
isEditEnabled,
onDeleteActionClicked,
onEditActionClicked,
}: {
existingIndices?: string[];
isDeleteEnabled: (entry: KnowledgeBaseEntryResponse) => boolean;
isEditEnabled: (entry: KnowledgeBaseEntryResponse) => boolean;
onDeleteActionClicked: (entry: KnowledgeBaseEntryResponse) => void;
Expand All @@ -115,7 +157,9 @@ export const useKnowledgeBaseTable = () => {
},
{
name: i18n.COLUMN_NAME,
render: ({ name }: KnowledgeBaseEntryResponse) => name,
render: (entry: KnowledgeBaseEntryResponse) => (
<NameColumn entry={entry} existingIndices={existingIndices} />
),
sortable: ({ name }: KnowledgeBaseEntryResponse) => name,
width: '30%',
},
Expand Down