Browse Source

Add org level throttling for events. Not actually used yet

David Burke 4 years ago
parent
commit
84ce76ff5e

+ 4 - 4
event_store/serializers.py

@@ -47,7 +47,7 @@ class StoreDefaultSerializer(serializers.Serializer):
                             frame["in_app"] = False
         return exception
 
-    def create(self, project, data):
+    def create(self, project_id: int, data):
         eventtype = self.get_eventtype()
         metadata = eventtype.get_metadata(data)
         title = eventtype.get_title(metadata)
@@ -63,7 +63,7 @@ class StoreDefaultSerializer(serializers.Serializer):
             issue, _ = Issue.objects.get_or_create(
                 title=title,
                 culprit=culprit,
-                project=project,
+                project_id=project_id,
                 type=self.type,
                 defaults={"metadata": metadata},
             )
@@ -110,7 +110,7 @@ class StoreCSPReportSerializer(serializers.Serializer):
         # This is done to support the hyphen
         self.fields.update({"csp-report": serializers.JSONField()})
 
-    def create(self, project, data):
+    def create(self, project_id: int, data):
         csp = data["csp-report"]
         title = self.get_title(csp)
         culprit = self.get_culprit(csp)
@@ -124,7 +124,7 @@ class StoreCSPReportSerializer(serializers.Serializer):
         issue, _ = Issue.objects.get_or_create(
             title=title,
             culprit=culprit,
-            project=project,
+            project_id=project_id,
             type=EventType.CSP,
             defaults={"metadata": metadata},
         )

+ 16 - 0
event_store/tests.py

@@ -55,3 +55,19 @@ class EventStoreTestCase(APITestCase):
         self.client.post(self.url, data, format="json")
         issue.refresh_from_db()
         self.assertEqual(issue.status, EventStatus.UNRESOLVED)
+
+    def test_performance(self):
+        with open("event_store/test_data/py_hi_event.json") as json_file:
+            data = json.load(json_file)
+        with self.assertNumQueries(8):
+            res = self.client.post(self.url, data, format="json")
+        self.assertEqual(res.status_code, 200)
+
+    def test_throttle_organization(self):
+        organization = self.project.organization
+        organization.is_accepting_events = False
+        organization.save()
+        with open("event_store/test_data/py_hi_event.json") as json_file:
+            data = json.load(json_file)
+        res = self.client.post(self.url, data, format="json")
+        self.assertEqual(res.status_code, 429)

+ 5 - 1
event_store/urls.py

@@ -1,7 +1,11 @@
 from django.urls import path
-from .views import EventStoreAPIView, CSPStoreAPIView
+from django.conf import settings
+from .views import EventStoreAPIView, CSPStoreAPIView, test_event_view
 
 urlpatterns = [
     path("<int:id>/store/", EventStoreAPIView.as_view(), name="event_store"),
     path("<int:id>/security/", CSPStoreAPIView.as_view(), name="csp_store"),
 ]
+
+if settings.DEBUG:
+    urlpatterns += [path("event-test/", test_event_view)]

+ 37 - 4
event_store/views.py

@@ -1,6 +1,11 @@
 import json
+import uuid
+import string
+import random
 from django.core.exceptions import SuspiciousOperation
 from django.conf import settings
+from django.http import HttpResponse
+from django.test import RequestFactory
 from rest_framework import permissions, exceptions
 from rest_framework.negotiation import BaseContentNegotiation
 from rest_framework.response import Response
@@ -33,6 +38,27 @@ class IgnoreClientContentNegotiation(BaseContentNegotiation):
         return (renderers[0], renderers[0].media_type)
 
 
+def test_event_view(request):
+    """
+    This view is used only to test event store performance
+    It requires DEBUG to be True
+    """
+    factory = RequestFactory()
+    request = request = factory.get(
+        "/api/6/store/?sentry_key=244703e8083f4b16988c376ea46e9a08"
+    )
+    with open("event_store/test_data/py_hi_event.json") as json_file:
+        data = json.load(json_file)
+    data["event_id"] = uuid.uuid4()
+    data["message"] = "".join(
+        random.choices(string.ascii_uppercase + string.digits, k=8)
+    )
+    request.data = data
+    EventStoreAPIView().post(request, id=6)
+
+    return HttpResponse("<html><body></body></html>")
+
+
 class EventStoreAPIView(APIView):
     permission_classes = [permissions.AllowAny]
     authentication_classes = []
@@ -51,14 +77,21 @@ class EventStoreAPIView(APIView):
         if settings.EVENT_STORE_DEBUG:
             print(json.dumps(request.data))
         sentry_key = EventStoreAPIView.auth_from_request(request)
-        project = Project.objects.filter(
-            id=kwargs.get("id"), projectkey__public_key=sentry_key
-        ).first()
+        project = (
+            Project.objects.filter(
+                id=kwargs.get("id"), projectkey__public_key=sentry_key
+            )
+            .select_related("organization")
+            .only("id", "organization__is_accepting_events")
+            .first()
+        )
+        if not project.organization.is_accepting_events:
+            raise exceptions.Throttled(detail="event rejected due to rate limit")
         if not project:
             raise exceptions.PermissionDenied()
         serializer = self.get_serializer_class(request.data)(data=request.data)
         if serializer.is_valid():
-            event = serializer.create(project, serializer.data)
+            event = serializer.create(project.id, serializer.data)
             return Response({"id": event.event_id_hex})
         # TODO {"error": "Invalid api key"}, CSP type, valid json but no type at all
         return Response()

+ 18 - 0
organizations_ext/migrations/0002_organization_is_accepting_events.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.0.6 on 2020-05-15 19:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('organizations_ext', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='organization',
+            name='is_accepting_events',
+            field=models.BooleanField(default=True),
+        ),
+    ]

+ 3 - 0
organizations_ext/models.py

@@ -26,6 +26,9 @@ class Organization(SharedBaseModel, OrganizationBase):
         unique=True,
         help_text=_("The name in all lowercase, suitable for URL identification"),
     )
+    is_accepting_events = models.BooleanField(
+        default=True, help_text="Used for throttling at org level"
+    )
 
     def add_user(self, user, role=OrganizationUserRole.MEMBER):
         """