mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
f4e9cd15d5
commit
dd292b70b7
10 changed files with 9 additions and 349 deletions
|
@ -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'),
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -105,7 +105,6 @@ const slackApiExecutor = async ({
|
|||
|
||||
const externalService = createExternalService(
|
||||
{
|
||||
config,
|
||||
secrets,
|
||||
},
|
||||
logger,
|
||||
|
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
|
@ -6,4 +6,3 @@
|
|||
*/
|
||||
|
||||
export { getWellKnownEmailServiceRoute } from './get_well_known_email_service';
|
||||
export { getSlackApiChannelsRoute } from './get_slack_api_channels';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue