Skip to content

Commit

Permalink
[RAM] Add the feature for slack api to have allowed list on channels (#…
Browse files Browse the repository at this point in the history
…159534)

## Summary

This will enable our user to create a slack api connector with the
ability to only allow some channels as an allowed list. Our user will
only be able to edit this input if the secrets is enter like the `test`
button work.

<img width="695" alt="image"
src="https://github.com/elastic/kibana/assets/189600/1c9997ae-806d-43ec-98d5-98f78bd9f2d0">


### 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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

---------

Co-authored-by: Lisa Cawley <lcawley@elastic.co>
  • Loading branch information
XavierM and lcawl committed Jun 21, 2023
1 parent b122d39 commit 1149fe4
Show file tree
Hide file tree
Showing 23 changed files with 726 additions and 205 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/actions/server/mocks.ts
Expand Up @@ -30,6 +30,7 @@ const createSetupMock = () => {
getSubActionConnectorClass: jest.fn(),
getCaseConnectorClass: jest.fn(),
getActionsHealth: jest.fn(),
getActionsConfigurationUtilities: jest.fn(),
};
return mock;
};
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/actions/server/plugin.ts
Expand Up @@ -67,7 +67,7 @@ import {
ActionsRequestHandlerContext,
} from './types';

import { getActionsConfigurationUtilities } from './actions_config';
import { ActionsConfigurationUtilities, getActionsConfigurationUtilities } from './actions_config';

import { defineRoutes } from './routes';
import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task';
Expand Down Expand Up @@ -129,6 +129,7 @@ export interface PluginSetupContract {
getSubActionConnectorClass: <Config, Secrets>() => IServiceAbstract<Config, Secrets>;
getCaseConnectorClass: <Config, Secrets>() => IServiceAbstract<Config, Secrets>;
getActionsHealth: () => { hasPermanentEncryptionKey: boolean };
getActionsConfigurationUtilities: () => ActionsConfigurationUtilities;
}

export interface PluginStartContract {
Expand Down Expand Up @@ -370,6 +371,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
hasPermanentEncryptionKey: plugins.encryptedSavedObjects.canEncrypt,
};
},
getActionsConfigurationUtilities: () => actionsConfigUtils,
};
}

Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/stack_connectors/common/slack_api/lib.ts
Expand Up @@ -8,10 +8,10 @@
import type { ActionTypeExecutorResult as ConnectorTypeExecutorResult } from '@kbn/actions-plugin/server/types';
import { i18n } from '@kbn/i18n';

export function successResult(
export function successResult<T = unknown>(
actionId: string,
data: unknown
): ConnectorTypeExecutorResult<unknown> {
data: T
): ConnectorTypeExecutorResult<T> {
return { status: 'ok', data, actionId };
}

Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/stack_connectors/common/slack_api/schema.ts
Expand Up @@ -11,6 +11,10 @@ export const SlackApiSecretsSchema = schema.object({
token: schema.string({ minLength: 1 }),
});

export const SlackApiConfigSchema = schema.object({
allowedChannels: schema.maybe(schema.arrayOf(schema.string())),
});

export const GetChannelsParamsSchema = schema.object({
subAction: schema.literal('getChannels'),
});
Expand Down
48 changes: 30 additions & 18 deletions x-pack/plugins/stack_connectors/common/slack_api/types.ts
Expand Up @@ -14,52 +14,64 @@ import {
PostMessageSubActionParamsSchema,
SlackApiSecretsSchema,
SlackApiParamsSchema,
SlackApiConfigSchema,
} from './schema';

export type SlackApiSecrets = TypeOf<typeof SlackApiSecretsSchema>;
export type SlackApiConfig = TypeOf<typeof SlackApiConfigSchema>;

export type PostMessageParams = TypeOf<typeof PostMessageParamsSchema>;
export type PostMessageSubActionParams = TypeOf<typeof PostMessageSubActionParamsSchema>;
export type SlackApiParams = TypeOf<typeof SlackApiParamsSchema>;
export type SlackApiConnectorType = ConnectorType<{}, SlackApiSecrets, SlackApiParams, unknown>;
export type SlackApiConnectorType = ConnectorType<
SlackApiConfig,
SlackApiSecrets,
SlackApiParams,
unknown
>;

export type SlackApiExecutorOptions = ConnectorTypeExecutorOptions<
{},
SlackApiConfig,
SlackApiSecrets,
SlackApiParams
>;

export type SlackExecutorOptions = ConnectorTypeExecutorOptions<
{},
SlackApiConfig,
SlackApiSecrets,
SlackApiParams
>;

export type SlackApiActionParams = TypeOf<typeof SlackApiParamsSchema>;

export interface GetChannelsResponse {
ok: true;
error?: string;
channels?: Array<{
id: string;
name: string;
is_channel: boolean;
is_archived: boolean;
is_private: boolean;
}>;
}

export interface PostMessageResponse {
export interface SlackAPiResponse {
ok: boolean;
channel?: string;
error?: string;
message?: {
text: string;
};
response_metadata?: {
next_cursor: string;
};
}

export interface ChannelsResponse {
id: string;
name: string;
is_channel: boolean;
is_archived: boolean;
is_private: boolean;
}
export interface GetChannelsResponse extends SlackAPiResponse {
channels?: ChannelsResponse[];
}

export interface PostMessageResponse extends SlackAPiResponse {
channel?: string;
}

export interface SlackApiService {
getChannels: () => Promise<ConnectorTypeExecutorResult<unknown>>;
getChannels: () => Promise<ConnectorTypeExecutorResult<GetChannelsResponse | void>>;
postMessage: ({
channels,
text,
Expand Down
Expand Up @@ -20,13 +20,14 @@ import type {
SlackApiActionParams,
SlackApiSecrets,
PostMessageParams,
SlackApiConfig,
} from '../../../common/slack_api/types';
import { SLACK_API_CONNECTOR_ID } from '../../../common/slack_api/constants';
import { SlackActionParams } from '../types';
import { subtype } from '../slack/slack';

export const getConnectorType = (): ConnectorTypeModel<
unknown,
SlackApiConfig,
SlackApiSecrets,
PostMessageParams
> => ({
Expand Down
Expand Up @@ -7,36 +7,75 @@

import React from 'react';
import { act, render, fireEvent, screen } from '@testing-library/react';
import SlackActionFields from './slack_connectors';
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';

import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../lib/test_utils';
import SlackActionFields from './slack_connectors';
import { useFetchChannels } from './use_fetch_channels';

jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
jest.mock('./use_fetch_channels');

(useKibana as jest.Mock).mockImplementation(() => ({
services: {
docLinks: {
links: {
alerting: { slackApiAction: 'url' },
},
},
notifications: {
toasts: {
addSuccess: jest.fn(),
addDanger: jest.fn(),
},
},
},
}));

(useFetchChannels as jest.Mock).mockImplementation(() => ({
channels: [],
isLoading: false,
}));

describe('SlackActionFields renders', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});

it('all connector fields is rendered for web_api type', async () => {
const actionConnector = {
secrets: {
token: 'some token',
},
config: {
allowedChannels: ['foo', 'bar'],
},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
config: {},
isDeprecated: false,
};

render(
const { container } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<SlackActionFields readOnly={false} isEdit={false} registerPreSubmitValidator={() => {}} />
</ConnectorFormTestProvider>
);

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 () => {
Expand Down Expand Up @@ -66,6 +105,9 @@ describe('SlackActionFields renders', () => {
secrets: {
token: 'some token',
},
config: {
allowedChannels: [],
},
id: 'test',
actionTypeId: '.slack',
name: 'slack',
Expand All @@ -74,4 +116,62 @@ 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();
});
});

0 comments on commit 1149fe4

Please sign in to comment.