Add rate limiting for Pushover notifications

This commit is contained in:
Pēteris Caune 2021-01-28 14:07:39 +02:00
parent ae976a38b6
commit c2bb4b31b5
No known key found for this signature in database
GPG Key ID: E28D7679E9A9EDE2
5 changed files with 86 additions and 36 deletions

View File

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Change Zulip onboarding, ask for the zuliprc file (#202)
- Add a section in Docs about running self-hosted instances
- Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute)
## Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications

View File

@ -931,6 +931,13 @@ class TokenBucket(models.Model):
# 6 messages for a single recipient per minute:
return TokenBucket.authorize(value, 6, 60)
@staticmethod
def authorize_pushover(user_key):
salted_encoded = (user_key + settings.SECRET_KEY).encode()
value = "po-%s" % hashlib.sha1(salted_encoded).hexdigest()
# 6 messages for a single user key per minute:
return TokenBucket.authorize(value, 6, 60)
@staticmethod
def authorize_sudo_code(user):
value = "sudo-%d" % user.id

View File

@ -2,7 +2,7 @@
from datetime import timedelta as td
import json
from unittest.mock import patch, Mock
from unittest.mock import patch
from django.core import mail
from django.utils.timezone import now
@ -492,33 +492,6 @@ class NotifyTestCase(BaseTestCase):
n = Notification.objects.first()
self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')
@patch("hc.api.transports.requests.request")
def test_pushover(self, mock_post):
self._setup_data("po", "123|0")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("DOWN", payload["title"])
@patch("hc.api.transports.requests.request")
def test_pushover_up_priority(self, mock_post):
self._setup_data("po", "123|0|2", status="up")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("UP", payload["title"])
self.assertEqual(payload["priority"], 2)
self.assertIn("retry", payload)
self.assertIn("expire", payload)
@patch("hc.api.transports.requests.request")
def test_victorops(self, mock_post):
self._setup_data("victorops", "123")

View File

@ -0,0 +1,65 @@
# coding: utf-8
from datetime import timedelta as td
from unittest.mock import patch
from django.test.utils import override_settings
from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification, TokenBucket
from hc.test import BaseTestCase
class NotifyTestCase(BaseTestCase):
def _setup_data(self, value, status="down", email_verified=True):
self.check = Check(project=self.project)
self.check.status = status
self.check.last_ping = now() - td(minutes=61)
self.check.save()
self.channel = Channel(project=self.project)
self.channel.kind = "po"
self.channel.value = value
self.channel.email_verified = email_verified
self.channel.save()
self.channel.checks.add(self.check)
@patch("hc.api.transports.requests.request")
def test_pushover(self, mock_post):
self._setup_data("123|0")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("DOWN", payload["title"])
@patch("hc.api.transports.requests.request")
def test_pushover_up_priority(self, mock_post):
self._setup_data("123|0|2", status="up")
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
assert Notification.objects.count() == 1
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertIn("UP", payload["title"])
self.assertEqual(payload["priority"], 2)
self.assertIn("retry", payload)
self.assertIn("expire", payload)
@override_settings(SECRET_KEY="test-secret")
@patch("hc.api.transports.requests.request")
def test_it_obeys_rate_limit(self, mock_post):
self._setup_data("123|0")
# "c0ca..." is sha1("123test-secret")
obj = TokenBucket(value="po-c0ca2a9774952af32cabf86453f69e442c4ed0eb")
obj.tokens = 0
obj.save()
self.channel.notify(self.check)
n = Notification.objects.first()
self.assertEqual(n.error, "Rate limit exceeded")

View File

@ -358,20 +358,24 @@ class Pushover(HttpTransport):
URL = "https://api.pushover.net/1/messages.json"
def notify(self, check):
others = self.checks().filter(status="down").exclude(code=check.code)
# list() executes the query, to avoid DB access while
# rendering a template
ctx = {"check": check, "down_checks": list(others)}
text = tmpl("pushover_message.html", **ctx)
title = tmpl("pushover_title.html", **ctx)
pieces = self.channel.value.split("|")
user_key, prio = pieces[0], pieces[1]
# The third element, if present, is the priority for "up" events
if len(pieces) == 3 and check.status == "up":
prio = pieces[2]
from hc.api.models import TokenBucket
if not TokenBucket.authorize_pushover(user_key):
return "Rate limit exceeded"
others = self.checks().filter(status="down").exclude(code=check.code)
# list() executes the query, to avoid DB access while
# rendering a template
ctx = {"check": check, "down_checks": list(others)}
text = tmpl("pushover_message.html", **ctx)
title = tmpl("pushover_title.html", **ctx)
payload = {
"token": settings.PUSHOVER_API_TOKEN,
"user": user_key,