[RAM] Remove allow slack channels (#161674)

## Summary

Remove allow slack channels feature for 8.9 until we have a better way
to deal with channels


### 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
This commit is contained in:
Xavier Mouligneau 2023-07-11 13:25:20 -04:00 committed by GitHub
parent f4e9cd15d5
commit dd292b70b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 9 additions and 349 deletions

View file

@ -11,9 +11,7 @@ export const SlackApiSecretsSchema = schema.object({
token: schema.string({ minLength: 1 }),
});
export const SlackApiConfigSchema = schema.object({
allowedChannels: schema.maybe(schema.arrayOf(schema.string())),
});
export const SlackApiConfigSchema = schema.object({}, { defaultValue: {} });
export const GetChannelsParamsSchema = schema.object({
subAction: schema.literal('getChannels'),

View file

@ -48,16 +48,14 @@ describe('SlackActionFields renders', () => {
secrets: {
token: 'some token',
},
config: {
allowedChannels: ['foo', 'bar'],
},
config: {},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
isDeprecated: false,
};
const { container } = render(
render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
@ -65,17 +63,6 @@ describe('SlackActionFields renders', () => {
expect(screen.getByTestId('secrets.token-input')).toBeInTheDocument();
expect(screen.getByTestId('secrets.token-input')).toHaveValue('some token');
expect(screen.getByTestId('config.allowedChannels-input')).toBeInTheDocument();
const allowedChannels: string[] = [];
container
.querySelectorAll('[data-test-subj="config.allowedChannels-input"] .euiBadge')
.forEach((node) => {
const channel = node.getAttribute('title');
if (channel) {
allowedChannels.push(channel);
}
});
expect(allowedChannels).toEqual(['foo', 'bar']);
});
it('connector validation succeeds when connector config is valid for Web API type', async () => {
@ -105,9 +92,6 @@ describe('SlackActionFields renders', () => {
secrets: {
token: 'some token',
},
config: {
allowedChannels: [],
},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
@ -116,62 +100,4 @@ describe('SlackActionFields renders', () => {
isValid: true,
});
});
it('Allowed Channels combobox should be disable when there is NO token', async () => {
const actionConnector = {
secrets: {
token: '',
},
config: {
allowedChannels: ['foo', 'bar'],
},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
isDeprecated: false,
};
const { container } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);
expect(
container.querySelector(
'[data-test-subj="config.allowedChannels-input"].euiComboBox-isDisabled'
)
).toBeInTheDocument();
});
it('Allowed Channels combobox should NOT be disable when there is token', async () => {
const actionConnector = {
secrets: {
token: 'qwertyuiopasdfghjklzxcvbnm',
},
config: {
allowedChannels: ['foo', 'bar'],
},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
isDeprecated: false,
};
(useFetchChannels as jest.Mock).mockImplementation(() => ({
channels: [{ label: 'foo' }, { label: 'bar' }, { label: 'hello' }, { label: 'world' }],
isLoading: false,
}));
const { container } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);
expect(
container.querySelector(
'[data-test-subj="config.allowedChannels-input"].euiComboBox-isDisabled'
)
).not.toBeInTheDocument();
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect, useMemo, useState } from 'react';
import React from 'react';
import {
ActionConnectorFieldsProps,
ConfigFieldSchema,
@ -13,18 +13,12 @@ import {
SimpleConnectorForm,
useKibana,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EuiComboBoxOptionOption, EuiLink } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from '@kbn/core/public';
import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { debounce, isEmpty } from 'lodash';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as i18n from './translations';
import { useFetchChannels } from './use_fetch_channels';
/** wait this many ms after the user completes typing before applying the filter input */
const INPUT_TIMEOUT = 250;
const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] => [
{
@ -42,31 +36,6 @@ const getSecretsFormSchema = (docLinks: DocLinksStart): SecretsFieldSchema[] =>
},
];
const getConfigFormSchemaAfterSecrets = (
options: EuiComboBoxOptionOption[],
isLoading: boolean,
isDisabled: boolean
): ConfigFieldSchema[] => [
{
id: 'allowedChannels',
isRequired: false,
label: i18n.ALLOWED_CHANNELS,
helpText: (
<FormattedMessage
id="xpack.stackConnectors.components.slack_api.allowedChannelsText"
defaultMessage="By default, the connector can access all channels within the scope of the Slack app."
/>
),
type: 'COMBO_BOX',
euiFieldProps: {
isDisabled,
isLoading,
noSuggestions: false,
options,
},
},
];
const NO_SCHEMA: ConfigFieldSchema[] = [];
export const SlackActionFieldsComponents: React.FC<ActionConnectorFieldsProps> = ({
@ -75,39 +44,12 @@ export const SlackActionFieldsComponents: React.FC<ActionConnectorFieldsProps> =
}) => {
const { docLinks } = useKibana().services;
const form = useFormContext();
const { setFieldValue } = form;
const [formData] = useFormData({ form });
const [authToken, setAuthToken] = useState('');
const { channels, isLoading } = useFetchChannels({ authToken });
const configFormSchemaAfterSecrets = useMemo(
() => getConfigFormSchemaAfterSecrets(channels, isLoading, channels.length === 0),
[channels, isLoading]
);
const debounceSetToken = debounce(setAuthToken, INPUT_TIMEOUT);
useEffect(() => {
if (formData.secrets && formData.secrets.token !== authToken) {
debounceSetToken(formData.secrets.token);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formData.secrets]);
useEffect(() => {
if (isEmpty(authToken) && channels.length > 0) {
setFieldValue('config.allowedChannels', []);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authToken]);
return (
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={NO_SCHEMA}
secretsFormSchema={getSecretsFormSchema(docLinks)}
configFormSchemaAfterSecrets={configFormSchemaAfterSecrets}
/>
);
};

View file

@ -55,23 +55,9 @@ describe('validate config', () => {
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [message]: definition for this key is missing"`
);
expect(() => {
validateConfig(connectorType, { allowedChannels: 'foo' }, { configurationUtilities });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [allowedChannels]: could not parse array value from json input"`
);
});
test('should validate when config are valid', () => {
expect(() => {
validateConfig(
connectorType,
{ allowedChannels: ['foo', 'bar'] },
{ configurationUtilities }
);
}).not.toThrow();
expect(() => {
validateConfig(connectorType, {}, { configurationUtilities });
}).not.toThrow();

View file

@ -105,7 +105,6 @@ const slackApiExecutor = async ({
const externalService = createExternalService(
{
config,
secrets,
},
logger,

View file

@ -177,50 +177,5 @@ describe('Slack API service', () => {
status: 'error',
});
});
test('should NOT by pass allowed channels when present', async () => {
service = createExternalService(
{
secrets: { token: 'token' },
config: { allowedChannels: ['foo', 'bar'] },
},
logger,
configurationUtilities
);
expect(
await service.postMessage({ channels: ['general', 'privat'], text: 'a message' })
).toEqual({
actionId: SLACK_API_CONNECTOR_ID,
serviceMessage:
'The channel "general,privat" is not included in the allowed channels list "foo,bar"',
message: 'error posting slack message',
status: 'error',
});
});
test('should allowed channels to be persisted', async () => {
service = createExternalService(
{
secrets: { token: 'token' },
config: { allowedChannels: ['foo', 'bar', 'general', 'privat'] },
},
logger,
configurationUtilities
);
requestMock.mockImplementation(() => postMessageResponse);
await service.postMessage({ channels: ['general', 'privat'], text: 'a message' });
expect(requestMock).toHaveBeenCalledTimes(1);
expect(requestMock).toHaveBeenNthCalledWith(1, {
axios,
logger,
configurationUtilities,
method: 'post',
url: 'chat.postMessage',
data: { channel: 'general', text: 'a message' },
});
});
});
});

View file

@ -106,12 +106,11 @@ const buildSlackExecutorSuccessResponse = <T extends SlackAPiResponse>({
};
export const createExternalService = (
{ config, secrets }: { config?: { allowedChannels?: string[] }; secrets: { token: string } },
{ secrets }: { secrets: { token: string } },
logger: Logger,
configurationUtilities: ActionsConfigurationUtilities
): SlackApiService => {
const { token } = secrets;
const { allowedChannels } = config || { allowedChannels: [] };
if (!token) {
throw Error(`[Action][${SLACK_CONNECTOR_NAME}]: Wrong configuration.`);
}
@ -170,23 +169,6 @@ export const createExternalService = (
}
result.data.channels = channels;
const responseData = result.data;
if ((allowedChannels ?? []).length > 0) {
const allowedChannelsList = channels.filter((channel: ChannelsResponse) =>
allowedChannels?.includes(channel.name)
);
allowedChannels?.forEach((ac) => {
if (!allowedChannelsList.find((c: ChannelsResponse) => c.name === ac)) {
allowedChannelsList.push({
id: '-1',
name: ac,
is_channel: true,
is_archived: false,
is_private: false,
});
}
});
responseData.channels = allowedChannelsList;
}
return buildSlackExecutorSuccessResponse<GetChannelsResponse>({
slackApiResponseData: responseData,
@ -201,19 +183,6 @@ export const createExternalService = (
text,
}: PostMessageSubActionParams): Promise<ConnectorTypeExecutorResult<unknown>> => {
try {
if (
allowedChannels &&
allowedChannels.length > 0 &&
!channels.every((c) => allowedChannels?.includes(c))
) {
return buildSlackExecutorErrorResponse({
slackApiError: {
message: `The channel "${channels.join()}" is not included in the allowed channels list "${allowedChannels.join()}"`,
},
logger,
});
}
const result: AxiosResponse<PostMessageResponse> = await request({
axios: axiosInstance,
method: 'post',

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import { PluginInitializerContext, Plugin, CoreSetup, Logger } from '@kbn/core/server';
import { PluginInitializerContext, Plugin, CoreSetup } from '@kbn/core/server';
import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
import { registerConnectorTypes } from './connector_types';
import { getSlackApiChannelsRoute, getWellKnownEmailServiceRoute } from './routes';
import { getWellKnownEmailServiceRoute } from './routes';
export interface ConnectorsPluginsSetup {
actions: ActionsPluginSetupContract;
}
@ -18,18 +18,13 @@ export interface ConnectorsPluginsStart {
}
export class StackConnectorsPlugin implements Plugin<void, void> {
private readonly logger: Logger;
constructor(context: PluginInitializerContext) {
this.logger = context.logger.get();
}
constructor(context: PluginInitializerContext) {}
public setup(core: CoreSetup<ConnectorsPluginsStart>, plugins: ConnectorsPluginsSetup) {
const router = core.http.createRouter();
const { actions } = plugins;
getWellKnownEmailServiceRoute(router);
getSlackApiChannelsRoute(router, actions.getActionsConfigurationUtilities(), this.logger);
registerConnectorTypes({
actions,

View file

@ -1,109 +0,0 @@
/*
* 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 { schema } from '@kbn/config-schema';
import {
IRouter,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
Logger,
} from '@kbn/core/server';
import axios, { AxiosResponse } from 'axios';
import { request } from '@kbn/actions-plugin/server/lib/axios_utils';
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
import { INTERNAL_BASE_STACK_CONNECTORS_API_PATH } from '../../common';
import { SLACK_URL } from '../../common/slack_api/constants';
import { ChannelsResponse, GetChannelsResponse } from '../../common/slack_api/types';
const bodySchema = schema.object({
authToken: schema.string(),
});
const RE_TRY = 5;
const LIMIT = 1000;
export const getSlackApiChannelsRoute = (
router: IRouter,
configurationUtilities: ActionsConfigurationUtilities,
logger: Logger
) => {
router.post(
{
path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_slack_api/channels`,
validate: {
body: bodySchema,
},
},
handler
);
async function handler(
ctx: RequestHandlerContext,
req: KibanaRequest<unknown, unknown, { authToken: string }>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
const { authToken } = req.body;
const axiosInstance = axios.create({
baseURL: SLACK_URL,
headers: {
Authorization: `Bearer ${authToken}`,
'Content-type': 'application/json; charset=UTF-8',
},
});
const fetchChannels = (cursor: string = ''): Promise<AxiosResponse<GetChannelsResponse>> => {
return request<GetChannelsResponse>({
axios: axiosInstance,
configurationUtilities,
logger,
method: 'get',
url: `conversations.list?exclude_archived=true&types=public_channel,private_channel&limit=${LIMIT}${
cursor.length > 0 ? `&cursor=${cursor}` : ''
}`,
});
};
let numberOfFetch = 0;
let cursor = '';
const channels: ChannelsResponse[] = [];
let result: AxiosResponse<GetChannelsResponse> = {
data: { ok: false, channels },
status: 0,
statusText: '',
headers: {},
config: {},
};
while (numberOfFetch < RE_TRY) {
result = await fetchChannels(cursor);
if (result.data.ok && (result.data?.channels ?? []).length > 0) {
channels.push(...(result.data?.channels ?? []));
}
if (
result.data.ok &&
result.data.response_metadata &&
result.data.response_metadata.next_cursor &&
result.data.response_metadata.next_cursor.length > 0
) {
numberOfFetch += 1;
cursor = result.data.response_metadata.next_cursor;
} else {
break;
}
}
return res.ok({
body: {
...result.data,
channels,
},
});
}
};

View file

@ -6,4 +6,3 @@
*/
export { getWellKnownEmailServiceRoute } from './get_well_known_email_service';
export { getSlackApiChannelsRoute } from './get_slack_api_channels';