From f13ad875a1f7f214275476e97c47d3e1d9dad568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Tue, 25 Feb 2020 09:57:11 +0200 Subject: [PATCH] Project code in URL for the "Add Discord" page. cc: #336 --- hc/front/tests/test_add_discord.py | 49 +----------- hc/front/tests/test_add_discord_complete.py | 70 ++++++++++++++++ hc/front/urls.py | 3 +- hc/front/views.py | 88 +++++++++++++-------- templates/front/channels.html | 2 +- 5 files changed, 130 insertions(+), 82 deletions(-) create mode 100644 hc/front/tests/test_add_discord_complete.py diff --git a/hc/front/tests/test_add_discord.py b/hc/front/tests/test_add_discord.py index 214d0336..0776e31a 100644 --- a/hc/front/tests/test_add_discord.py +++ b/hc/front/tests/test_add_discord.py @@ -1,14 +1,12 @@ -import json - from django.test.utils import override_settings -from hc.api.models import Channel from hc.test import BaseTestCase -from mock import patch @override_settings(DISCORD_CLIENT_ID="t1", DISCORD_CLIENT_SECRET="s1") class AddDiscordTestCase(BaseTestCase): - url = "/integrations/add_discord/" + def setUp(self): + super(AddDiscordTestCase, self).setUp() + self.url = "/projects/%s/add_discord/" % self.project.code def test_instructions_work(self): self.client.login(username="alice@example.org", password="password") @@ -17,49 +15,10 @@ class AddDiscordTestCase(BaseTestCase): self.assertContains(r, "discordapp.com/api/oauth2/authorize") # There should now be a key in session - self.assertTrue("discord" in self.client.session) + self.assertTrue("add_discord" in self.client.session) @override_settings(DISCORD_CLIENT_ID=None) def test_it_requires_client_id(self): self.client.login(username="alice@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 404) - - @patch("hc.front.views.requests.post") - def test_it_handles_oauth_response(self, mock_post): - session = self.client.session - session["discord"] = "foo" - session.save() - - oauth_response = { - "access_token": "test-token", - "webhook": {"url": "foo", "id": "bar"}, - } - - mock_post.return_value.text = json.dumps(oauth_response) - mock_post.return_value.json.return_value = oauth_response - - url = self.url + "?code=12345678&state=foo" - - self.client.login(username="alice@example.org", password="password") - r = self.client.get(url, follow=True) - self.assertRedirects(r, "/integrations/") - self.assertContains(r, "The Discord integration has been added!") - - ch = Channel.objects.get() - self.assertEqual(ch.discord_webhook_url, "foo") - self.assertEqual(ch.project, self.project) - - # Session should now be clean - self.assertFalse("discord" in self.client.session) - - def test_it_avoids_csrf(self): - session = self.client.session - session["discord"] = "foo" - session.save() - - url = self.url + "?code=12345678&state=bar" - - self.client.login(username="alice@example.org", password="password") - r = self.client.get(url) - self.assertEqual(r.status_code, 400) diff --git a/hc/front/tests/test_add_discord_complete.py b/hc/front/tests/test_add_discord_complete.py new file mode 100644 index 00000000..fd80987f --- /dev/null +++ b/hc/front/tests/test_add_discord_complete.py @@ -0,0 +1,70 @@ +import json + +from django.test.utils import override_settings +from hc.api.models import Channel +from hc.test import BaseTestCase +from mock import patch + + +@override_settings(DISCORD_CLIENT_ID="t1", DISCORD_CLIENT_SECRET="s1") +class AddDiscordCompleteTestCase(BaseTestCase): + url = "/integrations/add_discord/" + + @patch("hc.front.views.requests.post") + def test_it_handles_oauth_response(self, mock_post): + session = self.client.session + session["add_discord"] = ("foo", str(self.project.code)) + session.save() + + oauth_response = { + "access_token": "test-token", + "webhook": {"url": "foo", "id": "bar"}, + } + + mock_post.return_value.text = json.dumps(oauth_response) + mock_post.return_value.json.return_value = oauth_response + + url = self.url + "?code=12345678&state=foo" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, self.channels_url) + self.assertContains(r, "The Discord integration has been added!") + + ch = Channel.objects.get() + self.assertEqual(ch.discord_webhook_url, "foo") + self.assertEqual(ch.project, self.project) + + # Session should now be clean + self.assertFalse("add_discord" in self.client.session) + + def test_it_avoids_csrf(self): + session = self.client.session + session["add_discord"] = ("foo", str(self.project.code)) + session.save() + + url = self.url + "?code=12345678&state=bar" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url) + self.assertEqual(r.status_code, 403) + + # Session should now be clean + self.assertFalse("add_discord" in self.client.session) + + def test_it_handles_access_denied(self): + session = self.client.session + session["add_discord"] = ("foo", str(self.project.code)) + session.save() + + url = self.url + "?error=access_denied" + + self.client.login(username="alice@example.org", password="password") + r = self.client.get(url, follow=True) + self.assertRedirects(r, self.channels_url) + self.assertContains(r, "Discord setup was cancelled.") + + self.assertEqual(Channel.objects.count(), 0) + + # Session should now be clean + self.assertFalse("add_discord" in self.client.session) diff --git a/hc/front/urls.py b/hc/front/urls.py index 7ef9b83c..28a30e9d 100644 --- a/hc/front/urls.py +++ b/hc/front/urls.py @@ -33,7 +33,7 @@ channel_urls = [ views.add_pushbullet_complete, name="hc-add-pushbullet-complete", ), - path("add_discord/", views.add_discord, name="hc-add-discord"), + path("add_discord/", views.add_discord_complete, name="hc-add-discord-complete"), path("add_pushover/", views.add_pushover, name="hc-add-pushover"), path("telegram/bot/", views.telegram_bot, name="hc-telegram-webhook"), path("add_telegram/", views.add_telegram, name="hc-add-telegram"), @@ -55,6 +55,7 @@ channel_urls = [ project_urls = [ path("add_apprise/", views.add_apprise, name="hc-add-apprise"), + path("add_discord/", views.add_discord, name="hc-add-discord"), path("add_email/", views.add_email, name="hc-add-email"), path("add_matrix/", views.add_matrix, name="hc-add-matrix"), path("add_mattermost/", views.add_mattermost, name="hc-add-mattermost"), diff --git a/hc/front/views.py b/hc/front/views.py index 933b7106..71548d65 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta as td import json import os +from secrets import token_urlsafe from urllib.parse import urlencode from croniter import croniter @@ -1108,7 +1109,7 @@ def add_pushbullet(request, code): project = _get_project_for_user(request, code) redirect_uri = settings.SITE_ROOT + reverse("hc-add-pushbullet-complete") - state = get_random_string() + state = token_urlsafe() authorize_url = "https://www.pushbullet.com/authorize?" + urlencode( { "client_id": settings.PUSHBULLET_CLIENT_ID, @@ -1140,7 +1141,7 @@ def add_pushbullet_complete(request): project = _get_project_for_user(request, code) if request.GET.get("error") == "access_denied": - messages.warning(request, "Pushbullet setup was cancelled") + messages.warning(request, "Pushbullet setup was cancelled.") return redirect("hc-p-channels", project.code) if request.GET.get("state") != state: @@ -1170,55 +1171,72 @@ def add_pushbullet_complete(request): @login_required -def add_discord(request): +def add_discord(request, code): if settings.DISCORD_CLIENT_ID is None: raise Http404("discord integration is not available") - redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord") - if "code" in request.GET: - code = _get_validated_code(request, "discord") - if code is None: - return HttpResponseBadRequest() - - result = requests.post( - "https://discordapp.com/api/oauth2/token", - { - "client_id": settings.DISCORD_CLIENT_ID, - "client_secret": settings.DISCORD_CLIENT_SECRET, - "code": code, - "grant_type": "authorization_code", - "redirect_uri": redirect_uri, - }, - ) - - doc = result.json() - if "access_token" in doc: - channel = Channel(kind="discord", project=request.project) - channel.user = request.project.owner - channel.value = result.text - channel.save() - channel.assign_all_checks() - messages.success(request, "The Discord integration has been added!") - else: - messages.warning(request, "Something went wrong") - - return redirect("hc-channels") - + project = _get_project_for_user(request, code) + redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord-complete") + state = token_urlsafe() auth_url = "https://discordapp.com/api/oauth2/authorize?" + urlencode( { "client_id": settings.DISCORD_CLIENT_ID, "scope": "webhook.incoming", "redirect_uri": redirect_uri, "response_type": "code", - "state": _prepare_state(request, "discord"), + "state": state, } ) - ctx = {"page": "channels", "project": request.project, "authorize_url": auth_url} + ctx = {"page": "channels", "project": project, "authorize_url": auth_url} + request.session["add_discord"] = (state, str(project.code)) return render(request, "integrations/add_discord.html", ctx) +@login_required +def add_discord_complete(request): + if settings.DISCORD_CLIENT_ID is None: + raise Http404("discord integration is not available") + + if "add_discord" not in request.session: + return HttpResponseForbidden() + + state, code = request.session.pop("add_discord") + project = _get_project_for_user(request, code) + + if request.GET.get("error") == "access_denied": + messages.warning(request, "Discord setup was cancelled.") + return redirect("hc-p-channels", project.code) + + if request.GET.get("state") != state: + return HttpResponseForbidden() + + redirect_uri = settings.SITE_ROOT + reverse("hc-add-discord-complete") + result = requests.post( + "https://discordapp.com/api/oauth2/token", + { + "client_id": settings.DISCORD_CLIENT_ID, + "client_secret": settings.DISCORD_CLIENT_SECRET, + "code": request.GET.get("code"), + "grant_type": "authorization_code", + "redirect_uri": redirect_uri, + }, + ) + + doc = result.json() + if "access_token" in doc: + channel = Channel(kind="discord", project=project) + channel.value = result.text + channel.save() + channel.assign_all_checks() + messages.success(request, "The Discord integration has been added!") + else: + messages.warning(request, "Something went wrong.") + + return redirect("hc-p-channels", project.code) + + def add_pushover(request): if ( settings.PUSHOVER_API_TOKEN is None diff --git a/templates/front/channels.html b/templates/front/channels.html index ffec7856..21faf5e1 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -207,7 +207,7 @@

Discord

Cross-platform voice and text chat app designed for gamers.

- Add Integration + Add Integration {% endif %}