[Security Solution] Fixes Assistant Connector and Actions RBAC Flow (#164382)

## Summary

Resolves https://github.com/elastic/kibana/issues/159374 by ensuring
that if a user doesn't have the appropriate `Connectors & Actions`
privileges, they will be shown the appropriate messaging and any UI
controls for adding Connectors will be disabled or unavailable.

#### Connectors and Actions `NONE` or Connectors and Actions `READ` if
*NO* existing connectors exist:

<p align="center">
<img width="500"
src="d9535ae9-a31e-499b-9b18-6004e3db64de"
/>
</p> 

#### Connectors and Actions `READ` if existing connector count > 0:

`Add Connector...` option isn't available:

<p align="center">
<img width="500"
src="bd6a06a7-ffa2-4cfc-a2b7-844da99cb171"
/>
</p> 

<p align="center">
<img width="500"
src="4681086e-1015-45b9-9afb-ff604c52cd38"
/>
</p> 



Also addresses:

* Fixes disabled state of header connector selector for setup flows.
* Adds `AssistantAvailability` interface to `AssistantContext` for
exposing ui feature controls like `Connectors & Actions` privileges.
* Hides `Add new connector...` option if user doesn't have `ALL`
`Connectors & Actions` privileges.
* Hoists dependencies from `assistant/index.tsx` to `connector_setup` as
it was already fetching dependencies from `useAssistantContext`.

Note: `ConnectorButton` and `ConnectorMissingCallout` should probably be
combined into a single component and show appropriate messaging given
the user's `Connectors & Actions` privileges. I kept them separate for
now as to not modify the control flow around the two components (till we
can further refactor `assistant/index.tsx`), which means the missing
connector callout is sort of doing double duty at the moment.


### Checklist

Delete any items that are not applicable to this PR.

- [X] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [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
This commit is contained in:
Garrett Spong 2023-08-23 15:22:39 -06:00 committed by GitHub
parent d12b2b7d71
commit db7ac1bb41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 290 additions and 89 deletions

View file

@ -82,6 +82,7 @@ export const AssistantHeader: React.FC<Props> = ({
<EuiFlexItem grow={false}>
<AssistantTitle
{...currentTitle}
isDisabled={isDisabled}
docLinks={docLinks}
selectedConversation={currentConversation}
/>

View file

@ -29,11 +29,12 @@ import { ConnectorSelectorInline } from '../../connectorland/connector_selector_
* information about the assistant feature and access to documentation.
*/
export const AssistantTitle: React.FC<{
isDisabled?: boolean;
title: string | JSX.Element;
titleIcon: string;
docLinks: Omit<DocLinksStart, 'links'>;
selectedConversation: Conversation | undefined;
}> = ({ title, titleIcon, docLinks, selectedConversation }) => {
}> = ({ isDisabled = false, title, titleIcon, docLinks, selectedConversation }) => {
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
@ -116,7 +117,7 @@ export const AssistantTitle: React.FC<{
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ConnectorSelectorInline
isDisabled={selectedConversation === undefined}
isDisabled={isDisabled || selectedConversation === undefined}
onConnectorModalVisibilityChange={() => {}}
onConnectorSelectionChange={() => {}}
selectedConnectorId={selectedConnectorId}

View file

@ -71,7 +71,6 @@ const AssistantComponent: React.FC<Props> = ({
setConversationId,
}) => {
const {
actionTypeRegistry,
assistantTelemetry,
augmentMessageCodeBlocks,
conversations,
@ -98,11 +97,7 @@ const AssistantComponent: React.FC<Props> = ({
const { createConversation } = useConversation();
// Connector details
const {
data: connectors,
isSuccess: areConnectorsFetched,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({ http });
const defaultConnectorId = useMemo(() => getDefaultConnector(connectors)?.id, [connectors]);
const defaultProvider = useMemo(
() =>
@ -171,14 +166,10 @@ const AssistantComponent: React.FC<Props> = ({
}, [areConnectorsFetched, connectors?.length, currentConversation, setLastConversationId]);
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
actionTypeRegistry,
http,
refetchConnectors,
conversation: blockBotConversation,
onSetupComplete: () => {
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
},
conversation: blockBotConversation,
isConnectorConfigured: !!connectors?.length,
});
const currentTitle: { title: string | JSX.Element; titleIcon: string } =
@ -475,6 +466,7 @@ const AssistantComponent: React.FC<Props> = ({
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<ConnectorMissingCallout
isConnectorConfigured={connectors?.length > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>

View file

@ -11,15 +11,23 @@ import React from 'react';
import { AssistantProvider, useAssistantContext } from '.';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import { AssistantAvailability } from '../..';
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockGetInitialConversations = jest.fn(() => ({}));
const mockGetComments = jest.fn(() => []);
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
const ContextWrapper: React.FC = ({ children }) => (
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -33,7 +33,7 @@ import {
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
} from './constants';
import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings';
import { AssistantTelemetry } from './types';
import { AssistantAvailability, AssistantTelemetry } from './types';
export interface ShowAssistantOverlayProps {
showOverlay: boolean;
@ -48,6 +48,7 @@ type ShowAssistantOverlay = ({
}: ShowAssistantOverlayProps) => void;
export interface AssistantProviderProps {
actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
baseAllow: string[];
@ -79,6 +80,7 @@ export interface AssistantProviderProps {
export interface UseAssistantContext {
actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
allQuickPrompts: QuickPrompt[];
@ -126,6 +128,7 @@ const AssistantContext = React.createContext<UseAssistantContext | undefined>(un
export const AssistantProvider: React.FC<AssistantProviderProps> = ({
actionTypeRegistry,
assistantAvailability,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,
@ -244,6 +247,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
const value = useMemo(
() => ({
actionTypeRegistry,
assistantAvailability,
assistantTelemetry,
augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [],
@ -279,6 +283,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
}),
[
actionTypeRegistry,
assistantAvailability,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,

View file

@ -62,3 +62,14 @@ export interface AssistantTelemetry {
reportAssistantMessageSent: (params: { conversationId: string; role: string }) => void;
reportAssistantQuickPrompt: (params: { conversationId: string; promptTitle: string }) => void;
}
export interface AssistantAvailability {
// True when user is Enterprise, or Security Complete PLI for serverless. When false, the Assistant is disabled and unavailable
isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable
hasAssistantPrivilege: boolean;
// When true, user has `All` privilege for `Connectors and Actions` (show/execute/delete/save ui capabilities)
hasConnectorsAllPrivilege: boolean;
// When true, user has `Read` privilege for `Connectors and Actions` (show/execute ui capabilities)
hasConnectorsReadPrivilege: boolean;
}

View file

@ -5,32 +5,46 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
import { GenAiLogo } from '@kbn/stack-connectors-plugin/public/common';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
export interface ConnectorButtonProps {
setIsConnectorModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
setIsConnectorModalVisible?: React.Dispatch<React.SetStateAction<boolean>>;
}
/**
* Simple button component for adding a connector. Note: component is basic and does not handle connector
* add logic. Must pass in `setIsConnectorModalVisible`, see ConnectorSetup component if wanting to manage
* connector add logic.
* add logic. See ConnectorSetup component if wanting to manage connector add logic.
*/
export const ConnectorButton: React.FC<ConnectorButtonProps> = React.memo<ConnectorButtonProps>(
({ setIsConnectorModalVisible }) => {
const { assistantAvailability } = useAssistantContext();
const title = assistantAvailability.hasConnectorsAllPrivilege
? i18n.ADD_CONNECTOR_TITLE
: i18n.ADD_CONNECTOR_MISSING_PRIVILEGES_TITLE;
const description = assistantAvailability.hasConnectorsAllPrivilege
? i18n.ADD_CONNECTOR_DESCRIPTION
: i18n.ADD_CONNECTOR_MISSING_PRIVILEGES_DESCRIPTION;
const onClick = useCallback(() => {
setIsConnectorModalVisible?.(true);
}, [setIsConnectorModalVisible]);
return (
<EuiFlexGroup gutterSize="l" justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiCard
data-test-subj="connectorButton"
layout="horizontal"
icon={<EuiIcon size="xl" type={GenAiLogo} />}
title={i18n.ADD_CONNECTOR_TITLE}
description={i18n.ADD_CONNECTOR_DESCRIPTION}
onClick={() => setIsConnectorModalVisible(true)}
title={title}
description={description}
onClick={assistantAvailability.hasConnectorsAllPrivilege ? onClick : undefined}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -0,0 +1,77 @@
/*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { ConnectorMissingCallout } from '.';
import { AssistantAvailability } from '../../..';
import { TestProviders } from '../../mock/test_providers/test_providers';
describe('connectorMissingCallout', () => {
describe('when connectors and actions privileges', () => {
describe('are `READ`', () => {
const assistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
it('should show connector privileges required button if no connectors exist', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={false}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).toBeInTheDocument();
});
it('should NOT show connector privileges required button if at least one connector exists', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).not.toBeInTheDocument();
});
});
describe('are `NONE`', () => {
const assistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: false,
hasConnectorsReadPrivilege: false,
isAssistantEnabled: true,
};
it('should show connector privileges required button', async () => {
const { queryByTestId } = render(
<TestProviders assistantAvailability={assistantAvailability}>
<ConnectorMissingCallout
isConnectorConfigured={true}
isSettingsModalVisible={false}
setIsSettingsModalVisible={jest.fn()}
/>
</TestProviders>
);
expect(queryByTestId('connectorButton')).toBeInTheDocument();
});
});
});
});

View file

@ -12,22 +12,24 @@ import { FormattedMessage } from '@kbn/i18n-react';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
import { CONVERSATIONS_TAB } from '../../assistant/settings/assistant_settings';
import { ConnectorButton } from '../connector_button';
interface Props {
isConnectorConfigured: boolean;
isSettingsModalVisible: boolean;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
}
/**
* Error callout to be displayed when there is no connector configured for a conversation. Includes deep-link
* to conversation settings to quickly resolve.
* to conversation settings to quickly resolve. Falls back to <ConnectorButton /> connector if privileges aren't met.
*
* TODO: Add 'quick fix' button to just pick a connector
* TODO: Add setting for 'default connector' so we can auto-resolve and not even show this
*/
export const ConnectorMissingCallout: React.FC<Props> = React.memo(
({ isSettingsModalVisible, setIsSettingsModalVisible }) => {
const { setSelectedSettingsTab } = useAssistantContext();
({ isConnectorConfigured, isSettingsModalVisible, setIsSettingsModalVisible }) => {
const { assistantAvailability, setSelectedSettingsTab } = useAssistantContext();
const onConversationSettingsClicked = useCallback(() => {
if (!isSettingsModalVisible) {
@ -36,28 +38,40 @@ export const ConnectorMissingCallout: React.FC<Props> = React.memo(
}
}, [isSettingsModalVisible, setIsSettingsModalVisible, setSelectedSettingsTab]);
// Show missing callout if user has all privileges or read privileges and at least 1 connector configured
const showMissingCallout =
assistantAvailability.hasConnectorsAllPrivilege ||
(assistantAvailability.hasConnectorsReadPrivilege && isConnectorConfigured);
return (
<EuiCallOut
color="danger"
iconType="controlsVertical"
size="m"
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
>
<p>
{' '}
<FormattedMessage
defaultMessage="Select a connector above or from the {link} to continue"
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
values={{
link: (
<EuiLink onClick={onConversationSettingsClicked}>
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
</EuiLink>
),
}}
/>
</p>
</EuiCallOut>
<>
{showMissingCallout ? (
<EuiCallOut
data-test-subj="connectorMissingCallout"
color="danger"
iconType="controlsVertical"
size="m"
title={i18n.MISSING_CONNECTOR_CALLOUT_TITLE}
>
<p>
{' '}
<FormattedMessage
defaultMessage="Select a connector above or from the {link} to continue"
id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription"
values={{
link: (
<EuiLink onClick={onConversationSettingsClicked}>
{i18n.MISSING_CONNECTOR_CONVERSATION_SETTINGS_LINK}
</EuiLink>
),
}}
/>
</p>
</EuiCallOut>
) : (
<ConnectorButton />
)}
</>
);
}
);

View file

@ -23,6 +23,7 @@ import {
import { useLoadConnectors } from '../use_load_connectors';
import * as i18n from '../translations';
import { useLoadActionTypes } from '../use_load_action_types';
import { useAssistantContext } from '../../assistant_context';
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
interface Props {
@ -47,6 +48,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
selectedConnectorId,
onConnectorSelectionChange,
}) => {
const { assistantAvailability } = useAssistantContext();
// Connector Modal State
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
const { data: actionTypes } = useLoadActionTypes({ http });
@ -68,6 +70,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const isLoading = isLoadingActionTypes || isFetchingActionTypes;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const addNewConnectorOption = useMemo(() => {
return {
@ -113,6 +116,15 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
);
}, [connectors]);
// Only include add new connector option if user has privilege
const allConnectorOptions = useMemo(
() =>
assistantAvailability.hasConnectorsAllPrivilege
? [...connectorOptions, addNewConnectorOption]
: [...connectorOptions],
[addNewConnectorOption, assistantAvailability.hasConnectorsAllPrivilege, connectorOptions]
);
const cleanupAndCloseModal = useCallback(() => {
onConnectorModalVisibilityChange?.(false);
setIsConnectorModalVisible(false);
@ -139,11 +151,11 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
<EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true}
disabled={isDisabled}
disabled={localIsDisabled}
hasDividers={true}
isLoading={isLoading}
onChange={onChange}
options={[...connectorOptions, addNewConnectorOption]}
options={allConnectorOptions}
valueOfSelected={selectedConnectorId ?? ''}
/>
{isConnectorModalVisible && (

View file

@ -85,7 +85,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
onConnectorSelectionChange,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { actionTypeRegistry, http } = useAssistantContext();
const { actionTypeRegistry, assistantAvailability, http } = useAssistantContext();
const { setApiConfig } = useConversation();
// Connector Modal State
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
@ -111,6 +111,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
const selectedConnectorName =
connectors?.find((c) => c.id === selectedConnectorId)?.name ??
i18n.INLINE_CONNECTOR_PLACEHOLDER;
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
const addNewConnectorOption = useMemo(() => {
return {
@ -160,6 +161,15 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
);
}, [connectors]);
// Only include add new connector option if user has privilege
const allConnectorOptions = useMemo(
() =>
assistantAvailability.hasConnectorsAllPrivilege
? [...connectorOptions, addNewConnectorOption]
: [...connectorOptions],
[addNewConnectorOption, assistantAvailability.hasConnectorsAllPrivilege, connectorOptions]
);
const cleanupAndCloseModal = useCallback(() => {
onConnectorModalVisibilityChange?.(false);
setIsConnectorModalVisible(false);
@ -236,13 +246,13 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
<EuiSuperSelect
aria-label={i18n.CONNECTOR_SELECTOR_TITLE}
compressed={true}
disabled={isDisabled}
disabled={localIsDisabled}
hasDividers={true}
isLoading={isLoading}
isOpen={isOpen}
onBlur={handleOnBlur}
onChange={onChange}
options={[...connectorOptions, addNewConnectorOption]}
options={allConnectorOptions}
placeholder={placeholderComponent}
valueOfSelected={selectedConnectorId}
/>
@ -254,7 +264,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
data-test-subj="connectorSelectorPlaceholderButton"
iconSide={'right'}
iconType="arrowDown"
isDisabled={isDisabled}
isDisabled={localIsDisabled}
onClick={onConnectorClick}
size="xs"
>

View file

@ -13,8 +13,7 @@ import styled from 'styled-components';
import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
import { HttpSetup } from '@kbn/core-http-browser';
import { ActionType, ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
import { ActionType } from '@kbn/triggers-actions-ui-plugin/public';
import {
GEN_AI_CONNECTOR_ID,
OpenAiProviderType,
@ -29,6 +28,7 @@ import { useConversation } from '../../assistant/use_conversation';
import { clearPresentationData, conversationHasNoPresentationData } from './helpers';
import * as i18n from '../translations';
import { useAssistantContext } from '../../assistant_context';
import { useLoadConnectors } from '../use_load_connectors';
const ConnectorButtonWrapper = styled.div`
margin-bottom: 10px;
@ -43,21 +43,13 @@ interface Config {
}
export interface ConnectorSetupProps {
isConnectorConfigured: boolean;
actionTypeRegistry: ActionTypeRegistryContract;
conversation?: Conversation;
http: HttpSetup;
onSetupComplete?: () => void;
refetchConnectors?: () => void;
}
export const useConnectorSetup = ({
actionTypeRegistry,
conversation = WELCOME_CONVERSATION,
http,
isConnectorConfigured = false,
onSetupComplete,
refetchConnectors,
}: ConnectorSetupProps): {
comments: EuiCommentProps[];
prompt: React.ReactElement;
@ -65,7 +57,13 @@ export const useConnectorSetup = ({
const { appendMessage, setApiConfig, setConversation } = useConversation();
const bottomRef = useRef<HTMLDivElement | null>(null);
// Access all conversations so we can add connector to all on initial setup
const { conversations } = useAssistantContext();
const { actionTypeRegistry, conversations, http } = useAssistantContext();
const {
data: connectors,
isSuccess: areConnectorsFetched,
refetch: refetchConnectors,
} = useLoadConnectors({ http });
const isConnectorConfigured = areConnectorsFetched && !!connectors?.length;
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
const [showAddConnectorButton, setShowAddConnectorButton] = useState<boolean>(() => {

View file

@ -73,17 +73,17 @@ export const ADD_CONNECTOR_DESCRIPTION = i18n.translate(
}
);
export const CONNECTOR_ADDED_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle',
export const ADD_CONNECTOR_MISSING_PRIVILEGES_TITLE = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesTitle',
{
defaultMessage: 'Generative AI Connector added!',
defaultMessage: 'Generative AI Connector Required',
}
);
export const CONNECTOR_ADDED_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription',
export const ADD_CONNECTOR_MISSING_PRIVILEGES_DESCRIPTION = i18n.translate(
'xpack.elasticAssistant.assistant.connectors.addConnectorButton.missingPrivilegesDescription',
{
defaultMessage: 'Ready to continue the conversation...',
defaultMessage: 'Please contact your administrator to enable a Generative AI Connector.',
}
);

View file

@ -15,9 +15,10 @@ import { ThemeProvider } from 'styled-components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AssistantProvider, AssistantProviderProps } from '../../assistant_context';
import { Conversation } from '../../assistant_context/types';
import { AssistantAvailability, Conversation } from '../../assistant_context/types';
interface Props {
assistantAvailability?: AssistantAvailability;
children: React.ReactNode;
getInitialConversations?: () => Record<string, Conversation>;
providerContext?: Partial<AssistantProviderProps>;
@ -28,8 +29,16 @@ window.HTMLElement.prototype.scrollIntoView = jest.fn();
const mockGetInitialConversations = () => ({});
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
/** A utility for wrapping children in the providers required to run tests */
export const TestProvidersComponent: React.FC<Props> = ({
assistantAvailability = mockAssistantAvailability,
children,
getInitialConversations = mockGetInitialConversations,
providerContext,
@ -56,6 +65,7 @@ export const TestProvidersComponent: React.FC<Props> = ({
<QueryClientProvider client={queryClient}>
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={assistantAvailability}
augmentMessageCodeBlocks={jest.fn().mockReturnValue([])}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -89,8 +89,16 @@ export type {
QueryType,
} from './impl/assistant/use_conversation/helpers';
/** serialized conversations */
export type { AssistantTelemetry, Conversation, Message } from './impl/assistant_context/types';
export type {
/** Feature Availability Interface */
AssistantAvailability,
/** Telemetry Interface */
AssistantTelemetry,
/** Conversation Interface */
Conversation,
/** Message Interface */
Message,
} from './impl/assistant_context/types';
/** Interface for defining system/user prompts */
export type { Prompt } from './impl/assistant/types';

View file

@ -7,7 +7,7 @@
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { AssistantProvider } from '@kbn/elastic-assistant';
import { AssistantAvailability, AssistantProvider } from '@kbn/elastic-assistant';
import { I18nProvider } from '@kbn/i18n-react';
import { euiDarkVars } from '@kbn/ui-theme';
import React from 'react';
@ -32,11 +32,19 @@ export const TestProvidersComponent: React.FC<Props> = ({ children }) => {
reportDataQualityIndexChecked: jest.fn(),
reportDataQualityCheckAllCompleted: jest.fn(),
};
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
return (
<I18nProvider>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -42,6 +42,7 @@ import { PROMPT_CONTEXTS } from '../assistant/content/prompt_contexts';
import { BASE_SECURITY_QUICK_PROMPTS } from '../assistant/content/quick_prompts';
import { BASE_SECURITY_SYSTEM_PROMPTS } from '../assistant/content/prompts/system';
import { useAnonymizationStore } from '../assistant/use_anonymization_store';
import { useAssistantAvailability } from '../assistant/use_assistant_availability';
interface StartAppComponent {
children: React.ReactNode;
@ -70,6 +71,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
upselling,
} = services;
const assistantAvailability = useAssistantAvailability();
const { conversations, setConversations } = useConversationStore();
const { defaultAllow, defaultAllowReplacement, setDefaultAllow, setDefaultAllowReplacement } =
useAnonymizationStore();
@ -96,6 +98,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement}

View file

@ -14,15 +14,31 @@ export interface UseAssistantAvailability {
isAssistantEnabled: boolean;
// When true, the Assistant is hidden and unavailable
hasAssistantPrivilege: boolean;
// When true, user has `All` privilege for `Connectors and Actions` (show/execute/delete/save ui capabilities)
hasConnectorsAllPrivilege: boolean;
// When true, user has `Read` privilege for `Connectors and Actions` (show/execute ui capabilities)
hasConnectorsReadPrivilege: boolean;
}
export const useAssistantAvailability = (): UseAssistantAvailability => {
const isEnterprise = useLicense().isEnterprise();
const capabilities = useKibana().services.application.capabilities;
const isAssistantEnabled = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true;
const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true;
// Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts
// `READ` ui capabilities defined as: { ui: ['show', 'execute'] }
const hasConnectorsReadPrivilege =
capabilities.actions?.show === true && capabilities.actions?.execute === true;
// `ALL` ui capabilities defined as: { ui: ['show', 'execute', 'save', 'delete'] }
const hasConnectorsAllPrivilege =
hasConnectorsReadPrivilege &&
capabilities.actions?.delete === true &&
capabilities.actions?.save === true;
return {
hasAssistantPrivilege,
hasConnectorsAllPrivilege,
hasConnectorsReadPrivilege,
isAssistantEnabled: isEnterprise,
hasAssistantPrivilege: isAssistantEnabled,
};
};

View file

@ -8,6 +8,7 @@
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
import React from 'react';
import type { AssistantAvailability } from '@kbn/elastic-assistant';
import { AssistantProvider } from '@kbn/elastic-assistant';
interface Props {
@ -22,10 +23,17 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({ children }) =>
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockHttp = httpServiceMock.createStartContract({ basePath: '/test' });
mockHttp.get.mockResolvedValue([]);
const mockAssistantAvailability: AssistantAvailability = {
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
};
return (
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
augmentMessageCodeBlocks={jest.fn(() => [])}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -28,9 +28,12 @@ describe('useAssistant', () => {
let hookResult: RenderHookResult<UseAssistantParams, UseAssistantResult>;
it(`should return showAssistant true and a value for promptContextId`, () => {
jest
.mocked(useAssistantAvailability)
.mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true });
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest
.mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });
@ -42,9 +45,12 @@ describe('useAssistant', () => {
});
it(`should return showAssistant false if hasAssistantPrivilege is false`, () => {
jest
.mocked(useAssistantAvailability)
.mockReturnValue({ hasAssistantPrivilege: false, isAssistantEnabled: true });
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: false,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest
.mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });
@ -56,9 +62,12 @@ describe('useAssistant', () => {
});
it('returns anonymized prompt context data', async () => {
jest
.mocked(useAssistantAvailability)
.mockReturnValue({ hasAssistantPrivilege: true, isAssistantEnabled: true });
jest.mocked(useAssistantAvailability).mockReturnValue({
hasAssistantPrivilege: true,
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
isAssistantEnabled: true,
});
jest
.mocked(useAssistantOverlay)
.mockReturnValue({ showAssistantOverlay: jest.fn, promptContextId: '123' });

View file

@ -114,6 +114,8 @@ describe('Details Panel Component', () => {
describe('DetailsPanel: rendering', () => {
beforeEach(() => {
(useAssistantAvailability as jest.Mock).mockReturnValue({
hasConnectorsAllPrivilege: true,
hasConnectorsReadPrivilege: true,
hasAssistantPrivilege: false,
isAssistantEnabled: true,
});

View file

@ -11933,8 +11933,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "{total} champs dans ce contexte sont disponibles pour être inclus dans la conversation",
"xpack.elasticAssistant.assistant.apiErrorTitle": "Une erreur sest produite lors de lenvoi de votre message. Si le problème persiste, veuillez tester la configuration du connecteur.",
"xpack.elasticAssistant.assistant.clearChat": "Effacer le chat",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "Prêt à continuer la conversation...",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "Connecteur d'intelligence artificielle générative ajouté.",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "Configurez un connecteur pour continuer la conversation",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "Ajouter un connecteur d'intelligence artificielle générative",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "Sélecteur de conversation",

View file

@ -11947,8 +11947,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "このコンテキストの{total}個のフィールドを会話に含めることができます",
"xpack.elasticAssistant.assistant.apiErrorTitle": "メッセージの送信中にエラーが発生しました。問題が解決しない場合は、コネクター構成をテストしてください。",
"xpack.elasticAssistant.assistant.clearChat": "チャットを消去",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "会話を続行する準備ができました...",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "生成AIコネクターが追加されました。",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "会話を続行するようにコネクターを構成",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "生成AIコネクターを追加",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "会話セレクター",

View file

@ -11947,8 +11947,6 @@
"xpack.elasticAssistant.dataAnonymizationEditor.stats.availableStat.availableTooltip": "可在对话中包含此上下文中的 {total} 个字段",
"xpack.elasticAssistant.assistant.apiErrorTitle": "发送消息时出错。如果问题持续存在,请测试连接器配置。",
"xpack.elasticAssistant.assistant.clearChat": "清除聊天",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedDescription": "准备继续对话……",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.connectorAddedTitle": "已添加生成式 AI 连接器!",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.description": "配置连接器以继续对话",
"xpack.elasticAssistant.assistant.connectors.addConnectorButton.title": "添加生成式 AI 连接器",
"xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel": "对话选择器",