slack: support slack-gov.com (#10270)
Some checks are pending
EOL CI / EOL Sanity (Ⓐ2.16) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py2.7) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.11) (push) Waiting to run
EOL CI / EOL Units (Ⓐ2.16+py3.6) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+alpine3+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+fedora38+py:azp/posix/3/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/1/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/2/) (push) Waiting to run
EOL CI / EOL I (Ⓐ2.16+opensuse15+py:azp/posix/3/) (push) Waiting to run
nox / Run extra sanity tests (push) Waiting to run

* slack: support slack-gov.com

Allow the slack module to work with GovSlack, hosted at https://slack-gov.com/

This re-uses the existing `domain` option so that users can set it to
`slack-gov.com` to use GovSlack. To maintain backwards compatibility,
any setting of `domain` for WebAPI tokens that is not `slack.com` or
`slack-gov.com` is ignored.

* fixup

* cleanup

* fix pep8

* clean up docs and better function name

* document default value

* try to fix yaml, not sure what is wrong

* Update plugins/modules/slack.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/slack.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/slack.py

Co-authored-by: Felix Fontein <felix@fontein.de>

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Wade Simmons 2025-06-25 02:08:12 -04:00 committed by GitHub
parent dd53a2cee0
commit 1ed0f329bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 40 additions and 15 deletions

View file

@ -32,8 +32,9 @@ options:
domain: domain:
type: str type: str
description: description:
- Slack (sub)domain for your environment without protocol. (For example V(example.slack.com).) In Ansible 1.8 and beyond, - "When using new format 'Webhook token' and WebAPI tokens: this can be V(slack.com) or V(slack-gov.com) and is ignored otherwise."
this is deprecated and may be ignored. See token documentation for information. - "When using old format 'Webhook token': Slack (sub)domain for your environment without protocol. (For example V(example.slack.com).)
in Ansible 1.8 and beyond, this is deprecated and may be ignored. See token documentation for information."
token: token:
type: str type: str
description: description:
@ -41,9 +42,10 @@ options:
depending on what method you use. depending on what method you use.
- 'Webhook token: Prior to Ansible 1.8, a token looked like V(3Ffe373sfhRE6y42Fg3rvf4GlK). In Ansible 1.8 and above, - 'Webhook token: Prior to Ansible 1.8, a token looked like V(3Ffe373sfhRE6y42Fg3rvf4GlK). In Ansible 1.8 and above,
Ansible adapts to the new slack API where tokens look like V(G922VJP24/D921DW937/3Ffe373sfhRE6y42Fg3rvf4GlK). If tokens Ansible adapts to the new slack API where tokens look like V(G922VJP24/D921DW937/3Ffe373sfhRE6y42Fg3rvf4GlK). If tokens
are in the new format then slack will ignore any value of domain. If the token is in the old format the domain is are in the new format then slack will ignore any value of domain except V(slack.com) or V(slack-gov.com). If the token
required. Ansible has no control of when slack will get rid of the old API. When slack does that the old format will is in the old format the domain is required. Ansible has no control of when slack will get rid of the old API. When slack
stop working. ** Please keep in mind the tokens are not the API tokens but are the webhook tokens. In slack these does that the old format will stop working.
** Please keep in mind the tokens are not the API tokens but are the webhook tokens. In slack these
are found in the webhook URL which are obtained under the apps and integrations. The incoming webhooks can be added are found in the webhook URL which are obtained under the apps and integrations. The incoming webhooks can be added
in that area. In some cases this may be locked by your Slack admin and you must request access. It is there that the in that area. In some cases this may be locked by your Slack admin and you must request access. It is there that the
incoming webhooks can be added. The key is on the end of the URL given to you in that section.' incoming webhooks can be added. The key is on the end of the URL given to you in that section.'
@ -267,10 +269,10 @@ from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s' OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s'
SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s' SLACK_INCOMING_WEBHOOK = 'https://hooks.%s/services/%s'
SLACK_POSTMESSAGE_WEBAPI = 'https://slack.com/api/chat.postMessage' SLACK_POSTMESSAGE_WEBAPI = 'https://%s/api/chat.postMessage'
SLACK_UPDATEMESSAGE_WEBAPI = 'https://slack.com/api/chat.update' SLACK_UPDATEMESSAGE_WEBAPI = 'https://%s/api/chat.update'
SLACK_CONVERSATIONS_HISTORY_WEBAPI = 'https://slack.com/api/conversations.history' SLACK_CONVERSATIONS_HISTORY_WEBAPI = 'https://%s/api/conversations.history'
# Escaping quotes and apostrophes to avoid ending string prematurely in ansible call. # Escaping quotes and apostrophes to avoid ending string prematurely in ansible call.
# We do not escape other characters used as Slack metacharacters (e.g. &, <, >). # We do not escape other characters used as Slack metacharacters (e.g. &, <, >).
@ -372,7 +374,11 @@ def build_payload_for_slack(text, channel, thread_id, username, icon_url, icon_e
return payload return payload
def get_slack_message(module, token, channel, ts): def validate_slack_domain(domain):
return (domain if domain in ('slack.com', 'slack-gov.com') else 'slack.com')
def get_slack_message(module, domain, token, channel, ts):
headers = { headers = {
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json', 'Accept': 'application/json',
@ -384,7 +390,8 @@ def get_slack_message(module, token, channel, ts):
'limit': 1, 'limit': 1,
'inclusive': 'true', 'inclusive': 'true',
}) })
url = SLACK_CONVERSATIONS_HISTORY_WEBAPI + '?' + qs domain = validate_slack_domain(domain)
url = (SLACK_CONVERSATIONS_HISTORY_WEBAPI % domain) + '?' + qs
response, info = fetch_url(module=module, url=url, headers=headers, method='GET') response, info = fetch_url(module=module, url=url, headers=headers, method='GET')
if info['status'] != 200: if info['status'] != 200:
module.fail_json(msg="failed to get slack message") module.fail_json(msg="failed to get slack message")
@ -402,9 +409,11 @@ def do_notify_slack(module, domain, token, payload):
use_webapi = False use_webapi = False
if token.count('/') >= 2: if token.count('/') >= 2:
# New style webhook token # New style webhook token
slack_uri = SLACK_INCOMING_WEBHOOK % token domain = validate_slack_domain(domain)
slack_uri = SLACK_INCOMING_WEBHOOK % (domain, token)
elif re.match(r'^xox[abp]-\S+$', token): elif re.match(r'^xox[abp]-\S+$', token):
slack_uri = SLACK_UPDATEMESSAGE_WEBAPI if 'ts' in payload else SLACK_POSTMESSAGE_WEBAPI domain = validate_slack_domain(domain)
slack_uri = (SLACK_UPDATEMESSAGE_WEBAPI if 'ts' in payload else SLACK_POSTMESSAGE_WEBAPI) % domain
use_webapi = True use_webapi = True
else: else:
if not domain: if not domain:
@ -426,7 +435,7 @@ def do_notify_slack(module, domain, token, payload):
if use_webapi: if use_webapi:
obscured_incoming_webhook = slack_uri obscured_incoming_webhook = slack_uri
else: else:
obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % '[obscured]' obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % (domain, '[obscured]')
module.fail_json(msg=" failed to send %s to %s: %s" % (data, obscured_incoming_webhook, info['msg'])) module.fail_json(msg=" failed to send %s to %s: %s" % (data, obscured_incoming_webhook, info['msg']))
# each API requires different handling # each API requires different handling
@ -494,7 +503,7 @@ def main():
# if updating an existing message, we can check if there's anything to update # if updating an existing message, we can check if there's anything to update
if message_id is not None: if message_id is not None:
changed = False changed = False
msg = get_slack_message(module, token, channel, message_id) msg = get_slack_message(module, domain, token, channel, message_id)
for key in ('icon_url', 'icon_emoji', 'link_names', 'color', 'attachments', 'blocks'): for key in ('icon_url', 'icon_emoji', 'link_names', 'color', 'attachments', 'blocks'):
if msg.get(key) != module.params.get(key): if msg.get(key) != module.params.get(key):
changed = True changed = True

View file

@ -103,6 +103,22 @@ class TestSlackModule(ModuleTestCase):
self.assertTrue(fetch_url_mock.call_count, 1) self.assertTrue(fetch_url_mock.call_count, 1)
self.assertEqual(fetch_url_mock.call_args[1]['url'], "https://slack.com/api/chat.postMessage") self.assertEqual(fetch_url_mock.call_args[1]['url'], "https://slack.com/api/chat.postMessage")
def test_govslack_message(self):
with set_module_args({
'token': 'xoxa-123456789abcdef',
'domain': 'slack-gov.com',
'msg': 'test with ts'
}):
with patch.object(slack, "fetch_url") as fetch_url_mock:
mock_response = Mock()
mock_response.read.return_value = '{"fake":"data"}'
fetch_url_mock.return_value = (mock_response, {"status": 200})
with self.assertRaises(AnsibleExitJson):
self.module.main()
self.assertTrue(fetch_url_mock.call_count, 1)
self.assertEqual(fetch_url_mock.call_args[1]['url'], "https://slack-gov.com/api/chat.postMessage")
def test_edit_message(self): def test_edit_message(self):
with set_module_args({ with set_module_args({
'token': 'xoxa-123456789abcdef', 'token': 'xoxa-123456789abcdef',