Skip to content

Commit

Permalink
[Search] Handle insufficient privileges nicely on Serverless (#196160)
Browse files Browse the repository at this point in the history
## Summary
This adds a couple of callouts and disables unprivileged actions, so we
don't bombard the user with ugly error messages when they click buttons
or navigate to pages.


It also:
 - Fixes a couple of TODO docLinks that were broken (oops)
- Adds an errorhandler on all serverless search API routes so we surface
issues to the user

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
sphilipse and kibanamachine authored Oct 15, 2024
1 parent c218e7c commit 1f9bff8
Show file tree
Hide file tree
Showing 37 changed files with 400 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function entryToDisplaylistItem(entry: ConfigEntryView): { description: string;
interface ConnectorConfigurationProps {
connector: Connector;
hasPlatinumLicense: boolean;
isDisabled?: boolean;
isLoading: boolean;
saveConfig: (configuration: Record<string, string | number | boolean | null>) => void;
saveAndSync?: (configuration: Record<string, string | number | boolean | null>) => void;
Expand Down Expand Up @@ -89,6 +90,7 @@ export const ConnectorConfigurationComponent: FC<
children,
connector,
hasPlatinumLicense,
isDisabled,
isLoading,
saveConfig,
saveAndSync,
Expand Down Expand Up @@ -207,6 +209,7 @@ export const ConnectorConfigurationComponent: FC<
data-test-subj="entSearchContent-connector-configuration-editConfiguration"
data-telemetry-id="entSearchContent-connector-overview-configuration-editConfiguration"
onClick={() => setIsEditing(!isEditing)}
isDisabled={isDisabled}
>
{i18n.translate(
'searchConnectors.configurationConnector.config.editButton.title',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ interface ConnectorContentSchedulingProps {
hasPlatinumLicense: boolean;
hasChanges: boolean;
hasIngestionError: boolean;
isDisabled?: boolean;
setHasChanges: (changes: boolean) => void;
shouldShowAccessControlSync: boolean;
shouldShowIncrementalSync: boolean;
Expand All @@ -81,6 +82,7 @@ export const ConnectorSchedulingComponent: React.FC<ConnectorContentSchedulingPr
hasChanges,
hasIngestionError,
hasPlatinumLicense,
isDisabled,
setHasChanges,
shouldShowAccessControlSync,
shouldShowIncrementalSync,
Expand Down Expand Up @@ -140,6 +142,7 @@ export const ConnectorSchedulingComponent: React.FC<ConnectorContentSchedulingPr
updateConnectorStatus={updateConnectorStatus}
updateScheduling={updateScheduling}
dataTelemetryIdPrefix={dataTelemetryIdPrefix}
isDisabled={isDisabled}
/>
</EuiFlexItem>
{shouldShowIncrementalSync && (
Expand All @@ -153,6 +156,7 @@ export const ConnectorSchedulingComponent: React.FC<ConnectorContentSchedulingPr
updateConnectorStatus={updateConnectorStatus}
updateScheduling={updateScheduling}
dataTelemetryIdPrefix={dataTelemetryIdPrefix}
isDisabled={isDisabled}
/>
</EuiFlexItem>
)}
Expand Down Expand Up @@ -186,6 +190,7 @@ export const ConnectorSchedulingComponent: React.FC<ConnectorContentSchedulingPr
updateConnectorStatus={updateConnectorStatus}
updateScheduling={updateScheduling}
dataTelemetryIdPrefix={dataTelemetryIdPrefix}
isDisabled={isDisabled}
/>
</SchedulePanel>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ConnectorContentSchedulingProps {
dataTelemetryIdPrefix: string;
hasPlatinumLicense?: boolean;
hasSyncTypeChanges: boolean;
isDisabled?: boolean;
setHasChanges: (hasChanges: boolean) => void;
setHasSyncTypeChanges: (state: boolean) => void;
type: SyncJobType;
Expand Down Expand Up @@ -104,6 +105,7 @@ export const ConnectorContentScheduling: React.FC<ConnectorContentSchedulingProp
setHasSyncTypeChanges,
hasPlatinumLicense = false,
hasSyncTypeChanges,
isDisabled,
type,
updateConnectorStatus,
updateScheduling,
Expand All @@ -120,7 +122,9 @@ export const ConnectorContentScheduling: React.FC<ConnectorContentSchedulingProp
!connector.configuration.use_document_level_security?.value;

const isEnableSwitchDisabled =
type === SyncJobType.ACCESS_CONTROL && (!hasPlatinumLicense || isDocumentLevelSecurityDisabled);
(type === SyncJobType.ACCESS_CONTROL &&
(!hasPlatinumLicense || isDocumentLevelSecurityDisabled)) ||
Boolean(isDisabled);

return (
<>
Expand Down Expand Up @@ -217,7 +221,7 @@ export const ConnectorContentScheduling: React.FC<ConnectorContentSchedulingProp
<ConnectorCronEditor
hasSyncTypeChanges={hasSyncTypeChanges}
setHasSyncTypeChanges={setHasSyncTypeChanges}
disabled={isGated}
disabled={isGated || Boolean(isDisabled)}
scheduling={scheduling[type]}
onReset={() => {
setScheduling({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { ApiKey } from '@kbn/security-plugin-types-common';
import { useQuery } from '@tanstack/react-query';
import React, { useEffect, useState } from 'react';
import { ApiKeySelectableTokenField } from '@kbn/security-api-key-management';
import {
Expand All @@ -32,6 +30,7 @@ import { useKibanaServices } from '../../hooks/use_kibana';
import { MANAGEMENT_API_KEYS } from '../../../../common/routes';
import { CreateApiKeyFlyout } from './create_api_key_flyout';
import './api_key.scss';
import { useGetApiKeys } from '../../hooks/api/use_api_key';

function isCreatedResponse(
value: SecurityCreateApiKeyResponse | SecurityUpdateApiKeyResponse
Expand All @@ -45,15 +44,16 @@ function isCreatedResponse(
export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: string) => void }) => {
const { http, user } = useKibanaServices();
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
const { data } = useQuery({
queryKey: ['apiKey'],
queryFn: () => http.fetch<{ apiKeys: ApiKey[] }>('/internal/serverless_search/api_keys'),
});
const { data } = useGetApiKeys();

const [apiKey, setApiKey] = useState<SecurityCreateApiKeyResponse | undefined>(undefined);
const saveApiKey = (value: SecurityCreateApiKeyResponse) => {
setApiKey(value);
};

// Prevent flickering in the most common case of having access to manage api keys
const canManageOwnApiKey = !data || data.canManageOwnApiKey;

useEffect(() => {
if (apiKey) {
setClientApiKey(apiKey.encoded);
Expand Down Expand Up @@ -101,7 +101,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri
</EuiStep>
</EuiPanel>
) : (
<EuiPanel>
<EuiPanel color={'plain'}>
<EuiTitle size="xs">
<h3>
{i18n.translate('xpack.serverlessSearch.apiKey.panel.title', {
Expand All @@ -117,6 +117,16 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri
})}
</EuiText>
<EuiSpacer size="l" />
{!canManageOwnApiKey && (
<>
<EuiBadge iconType="warningFilled">
{i18n.translate('xpack.serverlessSearch.apiKey.panel.noUserPrivileges', {
defaultMessage: "You don't have access to manage API keys",
})}
</EuiBadge>
<EuiSpacer size="m" />
</>
)}
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
Expand All @@ -127,6 +137,7 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri
size="s"
fill
onClick={() => setIsFlyoutOpen(true)}
disabled={!canManageOwnApiKey}
data-test-subj="new-api-key-button"
aria-label={i18n.translate(
'xpack.serverlessSearch.apiKey.newButton.ariaLabel',
Expand All @@ -143,24 +154,29 @@ export const ApiKeyPanel = ({ setClientApiKey }: { setClientApiKey: (value: stri
</EuiButton>
</span>
</EuiFlexItem>
<EuiFlexItem>
<span>
<EuiButton
iconType="popout"
size="s"
href={http.basePath.prepend(MANAGEMENT_API_KEYS)}
target="_blank"
data-test-subj="manage-api-keys-button"
aria-label={i18n.translate('xpack.serverlessSearch.apiKey.manage.ariaLabel', {
defaultMessage: 'Manage API keys',
})}
>
{i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', {
defaultMessage: 'Manage',
})}
</EuiButton>
</span>
</EuiFlexItem>
{canManageOwnApiKey && (
<EuiFlexItem>
<span>
<EuiButton
iconType="popout"
size="s"
href={http.basePath.prepend(MANAGEMENT_API_KEYS)}
target="_blank"
data-test-subj="manage-api-keys-button"
aria-label={i18n.translate(
'xpack.serverlessSearch.apiKey.manage.ariaLabel',
{
defaultMessage: 'Manage API keys',
}
)}
>
{i18n.translate('xpack.serverlessSearch.apiKey.manageLabel', {
defaultMessage: 'Manage',
})}
</EuiButton>
</span>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,33 @@ import { ConnectorSchedulingComponent } from '@kbn/search-connectors/components/
import { useConnectorScheduling } from '../../../hooks/api/use_update_connector_scheduling';

interface ConnectorSchedulingPanels {
canManageConnectors: boolean;
connector: Connector;
}
export const ConnectorScheduling: React.FC<ConnectorSchedulingPanels> = ({ connector }) => {
export const ConnectorScheduling: React.FC<ConnectorSchedulingPanels> = ({
canManageConnectors,
connector,
}) => {
const [hasChanges, setHasChanges] = useState<boolean>(false);
const { isLoading, mutate } = useConnectorScheduling(connector.id);
const hasIncrementalSyncFeature = connector?.features?.incremental_sync ?? false;
const shouldShowIncrementalSync =
hasIncrementalSyncFeature && (connector?.features?.incremental_sync?.enabled ?? false);
return (
<ConnectorSchedulingComponent
connector={connector}
dataTelemetryIdPrefix="serverlessSearch"
hasChanges={hasChanges}
hasIngestionError={connector?.status === ConnectorStatus.ERROR}
hasPlatinumLicense={false}
setHasChanges={setHasChanges}
shouldShowAccessControlSync={false}
shouldShowIncrementalSync={shouldShowIncrementalSync}
updateConnectorStatus={isLoading}
updateScheduling={mutate}
/>
<>
<ConnectorSchedulingComponent
connector={connector}
isDisabled={!canManageConnectors}
dataTelemetryIdPrefix="serverlessSearch"
hasChanges={hasChanges}
hasIngestionError={connector?.status === ConnectorStatus.ERROR}
hasPlatinumLicense={false}
setHasChanges={setHasChanges}
shouldShowAccessControlSync={false}
shouldShowIncrementalSync={shouldShowIncrementalSync}
updateConnectorStatus={isLoading}
updateScheduling={mutate}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { OPTIONAL_LABEL } from '../../../../../common/i18n_string';
import { useCreateApiKey } from '../../../hooks/api/use_create_api_key';
import { useGetApiKeys } from '../../../hooks/api/use_api_key';
interface ApiKeyPanelProps {
connector: Connector;
}
export const ApiKeyPanel: React.FC<ApiKeyPanelProps> = ({ connector }) => {
const { data, isLoading, mutate } = useCreateApiKey();
const { data: apiKeysData } = useGetApiKeys();
return (
<EuiPanel hasBorder>
<EuiFlexGroup direction="row" justifyContent="spaceBetween" alignItems="center">
Expand Down Expand Up @@ -59,7 +61,7 @@ export const ApiKeyPanel: React.FC<ApiKeyPanelProps> = ({ connector }) => {
<span>
<EuiButton
data-test-subj="serverlessSearchApiKeyPanelNewApiKeyButton"
isDisabled={!connector.index_name}
isDisabled={!connector.index_name || !apiKeysData?.canManageOwnApiKey}
isLoading={isLoading}
iconType="plusInCircle"
color="primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import { useEditConnectorConfiguration } from '../../../hooks/api/use_connector_

interface ConnectorConfigFieldsProps {
connector: Connector;
isDisabled: boolean;
}

export const ConnectorConfigFields: React.FC<ConnectorConfigFieldsProps> = ({ connector }) => {
export const ConnectorConfigFields: React.FC<ConnectorConfigFieldsProps> = ({
connector,
isDisabled,
}) => {
const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id);
const { queryKey } = useConnector(connector.id);
const queryClient = useQueryClient();
Expand Down Expand Up @@ -53,6 +57,7 @@ export const ConnectorConfigFields: React.FC<ConnectorConfigFieldsProps> = ({ co
<ConnectorConfigurationComponent
connector={connector}
hasPlatinumLicense={false}
isDisabled={isDisabled}
isLoading={isLoading}
saveConfig={mutate}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import { ConnectionDetails } from './connection_details_panel';
import { ConnectorIndexnamePanel } from './connector_index_name_panel';

interface ConnectorConfigurationPanels {
canManageConnectors: boolean;
connector: Connector;
}

export const ConnectorConfigurationPanels: React.FC<ConnectorConfigurationPanels> = ({
canManageConnectors,
connector,
}) => {
const { data, isLoading, isSuccess, mutate, reset } = useEditConnectorConfiguration(connector.id);
Expand All @@ -37,6 +39,7 @@ export const ConnectorConfigurationPanels: React.FC<ConnectorConfigurationPanels
<>
<EuiPanel hasBorder>
<ConnectorConfigurationComponent
isDisabled={!canManageConnectors}
connector={connector}
hasPlatinumLicense={false}
isLoading={isLoading}
Expand All @@ -46,7 +49,7 @@ export const ConnectorConfigurationPanels: React.FC<ConnectorConfigurationPanels
</EuiPanel>
<EuiSpacer />
<EuiPanel hasBorder>
<ConnectorIndexnamePanel connector={connector} />
<ConnectorIndexnamePanel canManageConnectors={canManageConnectors} connector={connector} />
</EuiPanel>
<EuiSpacer />
<ConnectionDetails
Expand Down
Loading

0 comments on commit 1f9bff8

Please sign in to comment.