authentication.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. from typing import Literal, Optional
  2. from uuid import UUID
  3. from django.conf import settings
  4. from django.core.cache import cache
  5. from django.http import HttpRequest
  6. from ninja.errors import AuthenticationError, HttpError, ValidationError
  7. from apps.projects.models import Project
  8. from glitchtip.api.exceptions import ThrottleException
  9. from sentry.utils.auth import parse_auth_header
  10. from .constants import EVENT_BLOCK_CACHE_KEY
  11. class EventAuthHttpRequest(HttpRequest):
  12. """Django HttpRequest that is known to be authenticated by a project DSN"""
  13. auth: Project
  14. def auth_from_request(request: HttpRequest):
  15. """
  16. Get DSN (sentry_key) from request header
  17. Accept both sentry or glitchtip prefix
  18. Do not read request body when possible. This may result in uncompression which is slow.
  19. """
  20. for k in request.GET.keys():
  21. if k in ["sentry_key", "glitchtip_key"]:
  22. return request.GET[k]
  23. if auth_header := request.META.get(
  24. "HTTP_X_SENTRY_AUTH", request.META.get("HTTP_AUTHORIZATION")
  25. ):
  26. result = parse_auth_header(auth_header)
  27. return result.get("sentry_key", result.get("glitchtip_key"))
  28. raise AuthenticationError("Unable to find authentication information")
  29. # One letter codes to save cache memory and map to various event rejection type exceptions
  30. REJECTION_MAP: dict[Literal["v", "t"], Exception] = {
  31. "v": AuthenticationError([{"message": "Invalid DSN"}]),
  32. "t": ThrottleException(),
  33. }
  34. REJECTION_WAIT = 30
  35. async def get_project(request: HttpRequest) -> Optional[Project]:
  36. """
  37. Return the valid and accepting events project based on a request.
  38. Throttle unwanted requests using cache to mitigate repeat attempts
  39. """
  40. if not request.resolver_match:
  41. raise ValidationError([{"message": "Invalid project ID"}])
  42. project_id: int = request.resolver_match.captured_kwargs.get("project_id")
  43. try:
  44. sentry_key = UUID(auth_from_request(request))
  45. except ValueError as err:
  46. raise ValidationError(
  47. [{"message": "dsn key badly formed hexadecimal UUID string"}]
  48. ) from err
  49. # block cache check should be right before database call
  50. block_cache_key = EVENT_BLOCK_CACHE_KEY + str(project_id)
  51. if block_value := cache.get(block_cache_key):
  52. # Repeat the original message until cache expires
  53. raise REJECTION_MAP[block_value]
  54. project = (
  55. await Project.objects.filter(
  56. id=project_id,
  57. projectkey__public_key=sentry_key,
  58. )
  59. .select_related("organization")
  60. .only(
  61. "id",
  62. "scrub_ip_addresses",
  63. "organization_id",
  64. "organization__is_accepting_events",
  65. "organization__scrub_ip_addresses",
  66. )
  67. .afirst()
  68. )
  69. if not project:
  70. cache.set(block_cache_key, "v", REJECTION_WAIT)
  71. raise REJECTION_MAP["v"]
  72. if not project.organization.is_accepting_events:
  73. cache.set(block_cache_key, "t", REJECTION_WAIT)
  74. raise REJECTION_MAP["t"]
  75. return project
  76. async def event_auth(request: HttpRequest) -> Optional[Project]:
  77. """
  78. Event Ingest authentication means validating the DSN (sentry_key).
  79. Throttling is also handled here.
  80. It does not include user authentication.
  81. """
  82. if settings.MAINTENANCE_EVENT_FREEZE:
  83. raise HttpError(
  84. 503, "Events are not currently being accepted due to maintenance."
  85. )
  86. return await get_project(request)