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

[Security AI Assistant] Fixed license issue for Knowledge Base resources initialization #198239

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface UseKnowledgeBaseStatusParams {
http: HttpSetup;
resource?: string;
toasts?: IToasts;
enabled: boolean;
}

/**
Expand All @@ -36,13 +37,15 @@ export const useKnowledgeBaseStatus = ({
http,
resource,
toasts,
enabled,
}: UseKnowledgeBaseStatusParams): UseQueryResult<ReadKnowledgeBaseResponse, IHttpFetchError> => {
return useQuery(
KNOWLEDGE_BASE_STATUS_QUERY_KEY,
async ({ signal }) => {
return getKnowledgeBaseStatus({ http, resource, signal });
},
{
enabled,
retry: false,
keepPreviousData: true,
// Deprecated, hoist to `queryCache` w/in `QueryClient. See: https://stackoverflow.com/a/76961109
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ describe('use chat send', () => {
assistantTelemetry: {
reportAssistantMessageSent,
},
assistantAvailability: {
isAssistantEnabled: true,
},
});
});
it('handleOnChatCleared clears the conversation', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ export const useChatSend = ({
setSelectedPromptContexts,
setCurrentConversation,
}: UseChatSendProps): UseChatSend => {
const { assistantTelemetry, toasts } = useAssistantContext();
const {
assistantTelemetry,
toasts,
assistantAvailability: { isAssistantEnabled },
} = useAssistantContext();
const [userPrompt, setUserPrompt] = useState<string | null>(null);

const { isLoading, sendMessage, abortStream } = useSendMessage();
const { clearConversation, removeLastMessage } = useConversation();
const { data: kbStatus } = useKnowledgeBaseStatus({ http });
const { data: kbStatus } = useKnowledgeBaseStatus({ http, enabled: isAssistantEnabled });
const isSetupComplete =
kbStatus?.elser_exists &&
kbStatus?.index_exists &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const mockUseAssistantContext = {
},
setAllSystemPrompts: jest.fn(),
setConversations: jest.fn(),
assistantAvailability: {
isAssistantEnabled: true,
},
};

jest.mock('../assistant_context', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,16 @@ interface Props {
*/
export const KnowledgeBaseSettings: React.FC<Props> = React.memo(
({ knowledgeBase, setUpdatedKnowledgeBaseSettings, modalMode = false }) => {
const { http, toasts } = useAssistantContext();
const { data: kbStatus, isLoading, isFetching } = useKnowledgeBaseStatus({ http });
const {
http,
toasts,
assistantAvailability: { isAssistantEnabled },
} = useAssistantContext();
const {
data: kbStatus,
isLoading,
isFetching,
} = useKnowledgeBaseStatus({ http, enabled: isAssistantEnabled });
const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http, toasts });

// Resource enabled state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@ interface Params {
export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ dataViews }) => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
assistantAvailability: { hasManageGlobalKnowledgeBase },
assistantAvailability: { hasManageGlobalKnowledgeBase, isAssistantEnabled },
http,
toasts,
} = useAssistantContext();
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({ http });
const { data: kbStatus, isFetched } = useKnowledgeBaseStatus({
http,
enabled: isAssistantEnabled,
});
const isKbSetup = isKnowledgeBaseSetup(kbStatus);

const [deleteKBItem, setDeleteKBItem] = useState<DocumentEntry | IndexEntry | null>(null);
Expand Down Expand Up @@ -159,7 +162,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
} = useKnowledgeBaseEntries({
http,
toasts,
enabled: enableKnowledgeBaseByDefault,
enabled: enableKnowledgeBaseByDefault && isAssistantEnabled,
});

// Flyout Save/Cancel Actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ interface Props {
*
*/
export const SetupKnowledgeBaseButton: React.FC<Props> = React.memo(({ display }: Props) => {
const { http, toasts } = useAssistantContext();
const {
http,
toasts,
assistantAvailability: { isAssistantEnabled },
} = useAssistantContext();

const { data: kbStatus } = useKnowledgeBaseStatus({ http });
const { data: kbStatus } = useKnowledgeBaseStatus({ http, enabled: isAssistantEnabled });
const { mutate: setupKB, isLoading: isSettingUpKB } = useSetupKnowledgeBase({ http, toasts });

const isSetupInProgress = kbStatus?.is_setup_in_progress || isSettingUpKB;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('createResourceInstallationHelper', () => {
async () => (await getContextInitialized(helper)) === false
);

expect(logger.error).toHaveBeenCalledWith(`Error initializing resources test1 - fail`);
expect(logger.warn).toHaveBeenCalledWith(`Error initializing resources test1 - fail`);
expect(await helper.getInitializedResources('test1')).toEqual({
result: false,
error: `fail`,
Expand Down Expand Up @@ -204,7 +204,7 @@ describe('createResourceInstallationHelper', () => {
async () => (await getContextInitialized(helper)) === false
);

expect(logger.error).toHaveBeenCalledWith(`Error initializing resources default - first error`);
expect(logger.warn).toHaveBeenCalledWith(`Error initializing resources default - first error`);
expect(await helper.getInitializedResources(DEFAULT_NAMESPACE_STRING)).toEqual({
result: false,
error: `first error`,
Expand All @@ -221,9 +221,7 @@ describe('createResourceInstallationHelper', () => {
return logger.error.mock.calls.length === 1;
});

expect(logger.error).toHaveBeenCalledWith(
`Error initializing resources default - second error`
);
expect(logger.warn).toHaveBeenCalledWith(`Error initializing resources default - second error`);

// the second retry is throttled so this is never called
expect(logger.info).not.toHaveBeenCalledWith('test1_default successfully retried');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function createResourceInstallationHelper(
return errorResult(commonInitError);
}
} catch (err) {
logger.error(`Error initializing resources ${namespace} - ${err.message}`);
logger.warn(`Error initializing resources ${namespace} - ${err.message}`);
return errorResult(err.message);
}
};
Expand Down Expand Up @@ -113,7 +113,7 @@ export function createResourceInstallationHelper(
const key = namespace;
return (
initializedResources.has(key)
? initializedResources.get(key)
? await initializedResources.get(key)
: errorResult(`Unrecognized spaceId ${key}`)
) as InitializationPromise;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@ import { AIAssistantService, AIAssistantServiceOpts } from '.';
import { retryUntil } from './create_resource_installation_helper.test';
import { mlPluginMock } from '@kbn/ml-plugin/public/mocks';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';

jest.mock('../ai_assistant_data_clients/conversations', () => ({
AIAssistantConversationsDataClient: jest.fn(),
}));

const licensing = Promise.resolve(
licensingMock.createRequestHandlerContext({
license: { type: 'enterprise' },
})
);
let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>;
const clusterClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

Expand Down Expand Up @@ -191,6 +197,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});

expect(AIAssistantConversationsDataClient).toHaveBeenCalledWith({
Expand Down Expand Up @@ -221,6 +228,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});

expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalled();
Expand Down Expand Up @@ -274,11 +282,13 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
}),
assistantService.createAIAssistantConversationsDataClient({
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
}),
]);

Expand Down Expand Up @@ -340,6 +350,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});

expect(AIAssistantConversationsDataClient).toHaveBeenCalledWith({
Expand Down Expand Up @@ -400,6 +411,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});
};

Expand Down Expand Up @@ -472,6 +484,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});
};

Expand Down Expand Up @@ -513,6 +526,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'test',
currentUser: mockUser1,
licensing,
});

expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled();
Expand Down Expand Up @@ -560,6 +574,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'test',
currentUser: mockUser1,
licensing,
});

expect(clusterClient.indices.putIndexTemplate).not.toHaveBeenCalled();
Expand Down Expand Up @@ -607,6 +622,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'test',
currentUser: mockUser1,
licensing,
});

expect(AIAssistantConversationsDataClient).not.toHaveBeenCalled();
Expand Down Expand Up @@ -752,6 +768,7 @@ describe('AI Assistant Service', () => {
logger,
spaceId: 'default',
currentUser: mockUser1,
licensing,
});

await retryUntil(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { AuthenticatedUser, Logger, ElasticsearchClient } from '@kbn/core/s
import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
import { Subject } from 'rxjs';
import { LicensingApiRequestHandlerContext } from '@kbn/licensing-plugin/server';
import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration';
import { getDefaultAnonymizationFields } from '../../common/anonymization';
import { AssistantResourceNames, GetElser } from '../types';
Expand All @@ -36,6 +37,7 @@ import {
} from '../ai_assistant_data_clients/knowledge_base';
import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence';
import { createGetElserId, createPipeline, pipelineExists } from './helpers';
import { hasAIAssistantLicense } from '../routes/helpers';

const TOTAL_FIELDS_LIMIT = 2500;

Expand All @@ -56,6 +58,7 @@ export interface CreateAIAssistantClientParams {
logger: Logger;
spaceId: string;
currentUser: AuthenticatedUser | null;
licensing: Promise<LicensingApiRequestHandlerContext>;
}

export type CreateDataStream = (params: {
Expand Down Expand Up @@ -237,7 +240,7 @@ export class AIAssistantService {
pluginStop$: this.options.pluginStop$,
});
} catch (error) {
this.options.logger.error(`Error initializing AI assistant resources: ${error.message}`);
this.options.logger.warn(`Error initializing AI assistant resources: ${error.message}`);
this.initialized = false;
this.isInitializing = false;
return errorResult(error.message);
Expand Down Expand Up @@ -282,6 +285,8 @@ export class AIAssistantService {
};

private async checkResourcesInstallation(opts: CreateAIAssistantClientParams) {
const licensing = await opts.licensing;
if (!hasAIAssistantLicense(licensing.license)) return null;
// Check if resources installation has succeeded
const { result: initialized, error } = await this.getSpaceResourcesInitializationPromise(
opts.spaceId
Expand Down Expand Up @@ -502,7 +507,7 @@ export class AIAssistantService {
await this.createDefaultAnonymizationFields(spaceId);
}
} catch (error) {
this.options.logger.error(
this.options.logger.warn(
`Error initializing AI assistant namespace level resources: ${error.message}`
);
throw error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
EsAnonymizationFieldsSchema,
UpdateAnonymizationFieldSchema,
} from '../../ai_assistant_data_clients/anonymization_fields/types';
import { UPGRADE_LICENSE_MESSAGE, hasAIAssistantLicense } from '../helpers';
import { performChecks } from '../helpers';

export interface BulkOperationError {
message: string;
Expand Down Expand Up @@ -162,22 +162,18 @@ export const bulkActionAnonymizationFieldsRoute = (
request.events.completed$.subscribe(() => abortController.abort());
try {
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const license = ctx.licensing.license;
if (!hasAIAssistantLicense(license)) {
return response.forbidden({
body: {
message: UPGRADE_LICENSE_MESSAGE,
},
});
}
// Perform license and authenticated user checks
const checkResponse = performChecks({
context: ctx,
request,
response,
});

const authenticatedUser = ctx.elasticAssistant.getCurrentUser();
if (authenticatedUser == null) {
return assistantResponse.error({
body: `Authenticated user not found`,
statusCode: 401,
});
if (!checkResponse.isSuccess) {
return checkResponse.response;
}
const authenticatedUser = checkResponse.currentUser;

const dataClient =
await ctx.elasticAssistant.getAIAssistantAnonymizationFieldsDataClient();

Expand All @@ -199,20 +195,20 @@ export const bulkActionAnonymizationFieldsRoute = (
}

const writer = await dataClient?.getWriter();
const changedAt = new Date().toISOString();
const createdAt = new Date().toISOString();
const {
errors,
docs_created: docsCreated,
docs_updated: docsUpdated,
docs_deleted: docsDeleted,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = await writer!.bulk({
documentsToCreate: body.create?.map((f) =>
transformToCreateScheme(authenticatedUser, changedAt, f)
documentsToCreate: body.create?.map((doc) =>
transformToCreateScheme(authenticatedUser, createdAt, doc)
),
documentsToDelete: body.delete?.ids,
documentsToUpdate: body.update?.map((f) =>
transformToUpdateScheme(authenticatedUser, changedAt, f)
documentsToUpdate: body.update?.map((doc) =>
transformToUpdateScheme(authenticatedUser, createdAt, doc)
),
getUpdateScript: (document: UpdateAnonymizationFieldSchema) =>
getUpdateScript({ anonymizationField: document, isPatch: true }),
Expand Down
Loading