From 9f183ef923b2437ed852c950cf6ab730392d750c Mon Sep 17 00:00:00 2001 From: Bruce Clarke <2120379+bclarkejr@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:10:30 -0400 Subject: [PATCH] Add support for Slack Apps in addition to webhook URLs (#28) * Updating CONTRIBUTING.md to reference the new default 'main' branch * Adding support for Slack App OAuth Tokens and adding a note about Webhook URLs being deprecated * Updating packages.json version * Wording suggestions Co-authored-by: Shane Welcher <32134114+swelcher@users.noreply.github.com> * Updating the UI with more information and using tabs to separate OAuth and Webhook methods * Minor wording touch ups Co-authored-by: Shane Welcher <32134114+swelcher@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- package.json | 2 +- src/app/README.md | 17 +- src/app/README/alert_actions.conf.spec | 9 + src/app/README/savedsearches.conf.spec | 4 + src/app/bin/slack.py | 61 +++++-- src/app/default/alert_actions.slap.conf | 1 + src/app/default/data/ui/alerts/slack.html | 12 ++ src/ui/pages/slack_alerts_setup/api.js | 2 + src/ui/pages/slack_alerts_setup/form.jsx | 208 ++++++++++++++++------ 10 files changed, 240 insertions(+), 78 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 021daf6..205cf76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,7 @@ If you want to learn more, please consult this [tutorial on how pull requests wo Here's an overview of how you can make a pull request against this project: 1. Fork the [Slack Alerts GitHub repository](https://github.com/splunk/slack-alerts) -2. Clone your fork using git and create a branch off master +2. Clone your fork using git and create a branch off `main` 3. Run all the tests to verify your environment 4. Make your changes, commit and push once your tests have passed 5. Submit a pull request through the GitHub website using the changes from your forked codebase diff --git a/package.json b/package.json index d8326a5..66f84b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splunk-slack-alerts", - "version": "2.2.0", + "version": "2.3.0", "description": "Slack alert action for Splunk Enterprise", "private": true, "splunk": { diff --git a/src/app/README.md b/src/app/README.md index 8046c75..6d21235 100644 --- a/src/app/README.md +++ b/src/app/README.md @@ -14,11 +14,24 @@ App installation requires admin privileges. In order to setup the app, navigate to "Settings" -> "Alert actions". Click on "Setup Slack Alerts". +On the setup screen, you'll need to supply a Slack App OAuth token. To set up a new Slack App for your +workspace, follow the instructions on https://api.slack.com/apps. + +Once you have the Slack App created, you **must** give the app permission to the `chat:write` and +`chat:write.public` OAuth scopes in your Slack workspace. These scopes allow your app to write messages +to every public channel and user in your workspace. The app will also be able to write to any private +channel that it is added to. + +#### Deprecated configuration option + +> This alert action was originally built using the Slack Webhook URL functionality. Slack has recently +> deprecated this feature in favor of the Slack App method above. **Webhook URL support may be +> removed in a future release of Slack.** For more information see +> https://slack.com/apps/A0F7XDUAZ-incoming-webhooks + On the setup screen you'll want to supply a Webhook URL. You can obtain this URL by configuring a custom integration for you Slack workspace. -For more information see https://slack.com/apps/A0F7XDUAZ-incoming-webhooks - ## Troubleshooting ### Known issue with Setup diff --git a/src/app/README/alert_actions.conf.spec b/src/app/README/alert_actions.conf.spec index b960670..2135f5b 100644 --- a/src/app/README/alert_actions.conf.spec +++ b/src/app/README/alert_actions.conf.spec @@ -1,13 +1,22 @@ [slack] +param.slack_app_oauth_token = +* The Slack App OAuth token that Splunk should use to send Slack alerts. This +* can be obtained by create a new Slack App for your workspace at +* https://api.slack.com/apps. +* This takes precendence over the deprecated webhook_url parameter (below). + param.webhook_url = +* DEPRECATED - Slack has deprecated this feature and will possibly be removed in the future. * The webhook URL to send the Slack message requests to. This can be obtained * by creating a new "Incoming webhook" integration in Slack. param.from_user = +* DEPRECATED - This is only used in the deprecated webhook_url parameter. * The name of the user sending the Slack message. By default this is "Splunk". param.from_user_icon = +* DEPRECATED - This is only used in the deprecated webhook_url parameter. * URL to an icon to show as the avatar for the Slack message. By default this is * a Splunk icon. diff --git a/src/app/README/savedsearches.conf.spec b/src/app/README/savedsearches.conf.spec index e83602f..721e005 100644 --- a/src/app/README/savedsearches.conf.spec +++ b/src/app/README/savedsearches.conf.spec @@ -20,6 +20,10 @@ action.slack.param.fields = * It is possible to use wildcards, such as "*" for all fields or "foo*" for * prefixed fields, etc. +action.slack.param.slack_app_oauth_token_override = +* Override the Slack App OAuth token for a single alert. This is useful when wanting +* to send some alerts to different Slack teams. + action.slack.param.webhook_url_override = * Override the Slack webhook URL for a single alert. This useful when wanting * to send some alerts to different Slack teams. diff --git a/src/app/bin/slack.py b/src/app/bin/slack.py index bfde4c5..44b1777 100644 --- a/src/app/bin/slack.py +++ b/src/app/bin/slack.py @@ -99,34 +99,59 @@ def build_slack_message(payload): def send_slack_message(payload): try: + req = {} config = payload.get('configuration') - url = config.get('webhook_url', '') - if config.get('webhook_url_override'): - url = config.get('webhook_url_override', '') - log("INFO Using webhook URL from webhook_url_override: %s" % url) - elif not url: - log("FATAL No webhook URL configured and no override specified") - return ERROR_CODE_VALIDATION_FAILED - else: - log("INFO Using configured webhook URL: %s" % url) + body = json.dumps(build_slack_message(payload)) - if not url.startswith('https:'): - log("FATAL Invalid webhook URL specified. The URL must use HTTPS.") - return ERROR_CODE_VALIDATION_FAILED + # Since Slack webhook URLs are deprecated, we will bias towards using Slack Apps, if they are provided + is_using_slack_app = (("slack_app_oauth_token" in config and config["slack_app_oauth_token"]) or ("slack_app_oauth_token_override" in config and config["slack_app_oauth_token_override"])) + if is_using_slack_app: + token = config.get("slack_app_oauth_token", "") + if config.get("slack_app_oauth_token_override"): + token = config.get("slack_app_oauth_token_override") + log("INFO Using Slack App OAuth token from slack_app_oauth_token_override: %s" % token) + else: + log("INFO Using configured Slack App OAuth token: %s" % token) - body = json.dumps(build_slack_message(payload)) + log('DEBUG Calling url="https://slack.com/api/chat.postMessage" with token=%s and body=%s' % (token, body)) + req = urllib.request.Request("https://slack.com/api/chat.postMessage", ensure_binary(body), {"Content-Type": "application/json", 'Authorization': "Bearer %s" % token}) + + # To preserve backwards compatibility, we will fallback to the webhook_url configuration, if a Slack App OAuth token is not provided + else: + url = config.get('webhook_url', '') + if config.get('webhook_url_override'): + url = config.get('webhook_url_override', '') + log("INFO Using webhook URL from webhook_url_override: %s" % url) + elif not url: + log("FATAL No webhook URL configured and no override specified") + return ERROR_CODE_VALIDATION_FAILED + else: + log("INFO Using configured webhook URL: %s" % url) + + if not url.startswith('https:'): + log("FATAL Invalid webhook URL specified. The URL must use HTTPS.") + return ERROR_CODE_VALIDATION_FAILED + + log('DEBUG Calling url="%s" with body=%s' % (url, body)) + req = urllib.request.Request(url, ensure_binary(body), {"Content-Type": "application/json"}) - log('DEBUG Calling url="%s" with body=%s' % (url, body)) - req = urllib.request.Request(url, ensure_binary(body), {"Content-Type": "application/json"}) try: res = urllib.request.urlopen(req) - body = res.read() + res_body = str(res.read()) log("INFO Slack API responded with HTTP status=%d" % res.code) - log("DEBUG Slack API response: %s" % body) + log("DEBUG Slack API response: %s" % res_body) if 200 <= res.code < 300: + if "invalid_auth" in res_body: + log("FATAL The Slack App OAuth token provided is invalid or does not have the permission to post messages to the channel provided.") + return ERROR_CODE_FORBIDDEN + if "channel_not_found" in res_body or "channel_is_archived" in res_body: + log("FATAL The channel provided was not found or is archived. If the channel is private, please make sure the Slack App is added to the channel.") + return ERROR_CODE_CHANNEL_NOT_FOUND + if "error" in res_body: + return ERROR_CODE_UNKNOWN return OK except urllib.error.HTTPError as e: - log("ERROR HTTP request to Slack webhook URL failed: %s" % e) + log("ERROR HTTP request to Slack API failed: %s" % e) try: res = e.read() log("ERROR Slack error response: %s" % res) diff --git a/src/app/default/alert_actions.slap.conf b/src/app/default/alert_actions.slap.conf index 757a6f7..aec704a 100644 --- a/src/app/default/alert_actions.slap.conf +++ b/src/app/default/alert_actions.slap.conf @@ -5,6 +5,7 @@ description = Send a message to a Slack channel icon_path = slack.png python.version = python3 payload_format = json +param.slack_app_oauth_token =<@= process.env.NODE_ENV !== 'production' && process.env.SLACK_APP_OAUTH_TOKEN ? " " + process.env.SLACK_APP_OAUTH_TOKEN : "" @> param.webhook_url =<@= process.env.NODE_ENV !== 'production' && process.env.SLACK_WEBHOOK_URL ? " " + process.env.SLACK_WEBHOOK_URL : "" @> param.from_user = Splunk param.from_user_icon = https://s3-us-west-1.amazonaws.com/ziegfried-apps/slack-alerts/splunk-icon.png diff --git a/src/app/default/data/ui/alerts/slack.html b/src/app/default/data/ui/alerts/slack.html index 6c7c5bb..864c53d 100644 --- a/src/app/default/data/ui/alerts/slack.html +++ b/src/app/default/data/ui/alerts/slack.html @@ -45,6 +45,18 @@
Advanced settings:
+
+ + +
+ + + You can override the Slack App OAuth Token here + if you need to send the alert message to a different Slack team. + +
+
+
diff --git a/src/ui/pages/slack_alerts_setup/api.js b/src/ui/pages/slack_alerts_setup/api.js index 75caf88..7a44318 100644 --- a/src/ui/pages/slack_alerts_setup/api.js +++ b/src/ui/pages/slack_alerts_setup/api.js @@ -13,6 +13,7 @@ export function loadAlertActionConfig() { .then((data) => { const d = data.entry[0].content; return { + slack_app_oauth_token: d['param.slack_app_oauth_token'], webhook_url: d['param.webhook_url'], from_user: d['param.from_user'], from_user_icon: d['param.from_user_icon'], @@ -26,6 +27,7 @@ export function updateAlertActionConfig(data) { { method: 'POST', body: [ + `param.slack_app_oauth_token=${encodeURIComponent(data.slack_app_oauth_token)}`, `param.webhook_url=${encodeURIComponent(data.webhook_url)}`, `param.from_user=${encodeURIComponent(data.from_user)}`, `param.from_user_icon=${encodeURIComponent(data.from_user_icon)}`, diff --git a/src/ui/pages/slack_alerts_setup/form.jsx b/src/ui/pages/slack_alerts_setup/form.jsx index 6f9d353..0185dee 100644 --- a/src/ui/pages/slack_alerts_setup/form.jsx +++ b/src/ui/pages/slack_alerts_setup/form.jsx @@ -1,8 +1,11 @@ import Button from '@splunk/react-ui/Button'; +import Code from '@splunk/react-ui/Code'; +import CollapsiblePanel from '@splunk/react-ui/CollapsiblePanel'; import ControlGroup from '@splunk/react-ui/ControlGroup'; import Heading from '@splunk/react-ui/Heading'; import Link from '@splunk/react-ui/Link'; import Paragraph from '@splunk/react-ui/Paragraph'; +import TabLayout from '@splunk/react-ui/TabLayout'; import Text from '@splunk/react-ui/Text'; import React, { useCallback } from 'react'; import { useSlackConfig, redirectToAlertListingPage } from './config'; @@ -12,73 +15,166 @@ import { ButtonGroup, FormWrapper } from './styles'; export function SetupForm() { const [{ loading, error, data, isDirty }, update, save] = useSlackConfig(); + const updateOauthToken = useCallback((e, { value }) => update({ ...data, slack_app_oauth_token: value })); const updateUrl = useCallback((e, { value }) => update({ ...data, webhook_url: value })); const updateUser = useCallback((e, { value }) => update({ ...data, from_user: value })); const updateUserIcon = useCallback((e, { value }) => update({ ...data, from_user_icon: value })); + const slackAppOauthToken = loading ? '' : data.slack_app_oauth_token; const webhookUrl = loading ? '' : data.webhook_url; const fromUserName = loading ? '' : data.from_user; const fromUserIcon = loading ? '' : data.from_user_icon; + const oauth_app_manifest = ` +display_information: + name: Splunk Alerts +features: + bot_user: + display_name: Splunk + always_online: false +oauth_config: + scopes: + bot: + - chat:write + - chat:write.public + - chat:write.customize +settings: + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false`.trim(); + return ( <> - Slack Incoming Webhook - - This alert action uses Slack's{' '} - - Incoming Webhooks - {' '} - to post messages from Splunk into Slack channels. You can set a default webhook URL here, - which will be used for all alerts be default. Each alert can override and use a different - webhook URL that has different permissions or can send to a different Slack workspace. - - - + + + The Slack App OAuth token is the preferred method for using the Slack alert action. + The webhook url method (in the second tab) is {' '} + + deprecated + {' '} + and may be removed in a future release of Slack. + + Slack App OAuth Token + + This alert action uses{' '} + + custom Slack Apps + {' '} + to post messages from Splunk into Slack channels. You can set a default Slack App OAuth + token here, which will be used for all alerts by default. Each alert can override and + use a different Slack App OAuth Token that has different permissions or can send to a + different Slack workspace. + + + Once you have the Slack App created, you must give the app permission to the{' '} + chat:write, chat:write.public, and chat:write.customize{' '} + OAuth scopes in your Slack workspace. You can grant these permissions in the "OAuth & + Permissions" tab at the link above. + + + These scopes allow your app to write messages to every public channel and user in + your workspace. The app will also be able to write to any private channel, but it + will need to be explicitly granted access to each private channel. + + + + Slack also lets you set OAuth scopes using the "App Manifest" tab. The code below + can be pasted into the App Manifest YAML input box to set all of the permissions above. + + + + + + Configure Slack App OAuth token + + } + > + + + + + + + Slack has deprecated the "Incoming Webhooks" feature in favor of using{' '} + + Slack Apps + .{' '} + Slack may remove the incoming webhook feature in a future update. If you provide both a + "Slack App OAuth Token" and an "Incoming Webhook", then the "Slack App OAuth Token" will + take precedence. You can still override the OAuth token by using the "webhook_url_override" + parameter on an individual alert. + + Slack Incoming Webhook + + This alert action uses Slack's deprecated{' '} - Configure Slack incoming webhook - - } - > - - - - Message Appearance - - The following settings will influence how messages will show up in Slack.{' '} - openSlackMessagePreview(data)} openInNewContext> - Show Preview - - . - - - - - - - - - - -
-
-
-
-
+ Incoming Webhooks + {' '} + to post messages from Splunk into Slack channels. You can set a default webhook URL here, + which will be used for all alerts by default. Each alert can override and use a different + webhook URL that has different permissions or can send to a different Slack workspace. + + + + Configure Slack incoming webhook + + } + > + + + + + + +
+ Message Appearance + + The following settings will influence how messages will show up in Slack.{' '} + openSlackMessagePreview(data)} openInNewContext> + Show Preview + + . + + + + + + + + + + +
+
+
+
+
+
); }