authentication.py 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. from typing import Literal
  2. from django.conf import settings
  3. from django.core.cache import cache
  4. from django.http import HttpRequest
  5. from ninja.errors import AuthenticationError, HttpError, ValidationError
  6. from glitchtip.api.exceptions import ThrottleException
  7. from projects.models import Project
  8. from sentry.utils.auth import parse_auth_header
  9. from .constants import EVENT_BLOCK_CACHE_KEY
  10. def auth_from_request(request: HttpRequest):
  11. # Accept both sentry or glitchtip prefix.
  12. for k in request.GET.keys():
  13. if k in ["sentry_key", "glitchtip_key"]:
  14. return request.GET[k]
  15. if auth_header := request.META.get(
  16. "HTTP_X_SENTRY_AUTH", request.META.get("HTTP_AUTHORIZATION")
  17. ):
  18. result = parse_auth_header(auth_header)
  19. return result.get("sentry_key", result.get("glitchtip_key"))
  20. raise AuthenticationError("Unable to find authentication information")
  21. # One letter codes to save cache memory and map to various event rejection type exceptions
  22. REJECTION_MAP: dict[Literal["v", "t"], Exception] = {
  23. "v": ValidationError([{"message": "Invalid DSN"}]),
  24. "t": ThrottleException(),
  25. }
  26. REJECTION_WAIT = 30
  27. async def get_project(request: HttpRequest):
  28. """
  29. Return the valid and accepting events project based on a request.
  30. Throttle unwanted requests using cache to mitigate repeat attempts
  31. """
  32. if not request.resolver_match:
  33. raise ValidationError([{"message": "Invalid project ID"}])
  34. project_id: int = request.resolver_match.captured_kwargs.get("project_id")
  35. sentry_key = auth_from_request(request)
  36. # block cache check should be right before database call
  37. block_cache_key = EVENT_BLOCK_CACHE_KEY + str(project_id)
  38. if block_value := cache.get(block_cache_key):
  39. # Repeat the original message until cache expires
  40. raise REJECTION_MAP[block_value]
  41. project = (
  42. await Project.objects.filter(
  43. id=project_id,
  44. projectkey__public_key=sentry_key,
  45. )
  46. .select_related("organization")
  47. .only(
  48. "id",
  49. "organization__is_accepting_events",
  50. )
  51. .afirst()
  52. )
  53. if not project:
  54. cache.set(block_cache_key, "v", REJECTION_WAIT)
  55. raise REJECTION_MAP["v"]
  56. if not project.organization.is_accepting_events:
  57. cache.set(block_cache_key, "t", REJECTION_WAIT)
  58. raise REJECTION_MAP["t"]
  59. return project
  60. async def event_auth(request: HttpRequest):
  61. if settings.MAINTENANCE_EVENT_FREEZE:
  62. raise HttpError(
  63. 503, "Events are not currently being accepted due to maintenance."
  64. )
  65. return await get_project(request)