mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-21 13:00:13 -06:00
31
bugsink/authentication.py
Normal file
31
bugsink/authentication.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework import exceptions
|
||||
from bsmain.models import AuthToken
|
||||
|
||||
|
||||
class BearerTokenAuthentication(BaseAuthentication):
|
||||
"""
|
||||
Accepts: Authorization: Bearer <40-hex>
|
||||
Returns (AnonymousUser, AuthToken) on success; leaves request.user anonymous.
|
||||
"""
|
||||
keyword = "Bearer"
|
||||
|
||||
def authenticate(self, request):
|
||||
header = request.headers.get("Authorization")
|
||||
if not header or not header.startswith(f"{self.keyword} "):
|
||||
return None
|
||||
|
||||
raw = header[len(self.keyword) + 1:].strip()
|
||||
if len(raw) != 40 or any(c not in "0123456789abcdef" for c in raw):
|
||||
raise exceptions.AuthenticationFailed("Invalid Bearer token.")
|
||||
|
||||
token_obj = AuthToken.objects.filter(token=raw).first()
|
||||
if not token_obj:
|
||||
raise exceptions.AuthenticationFailed("Invalid Bearer token.")
|
||||
|
||||
return (AnonymousUser(), token_obj)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
# tells DRF what to send in WWW-Authenticate on 401 responses, hinting the required auth scheme
|
||||
return self.keyword
|
||||
10
bugsink/permissions.py
Normal file
10
bugsink/permissions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from bsmain.models import AuthToken
|
||||
|
||||
|
||||
class IsGlobalAuthenticated(BasePermission):
|
||||
"""Allows access only to authenticated users with a valid (global) AuthToken."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return isinstance(request.auth, AuthToken)
|
||||
@@ -73,6 +73,13 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # from the tutorial
|
||||
'PAGE_SIZE': 10,
|
||||
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"bugsink.authentication.BearerTokenAuthentication",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"bugsink.permissions.IsGlobalAuthenticated",
|
||||
],
|
||||
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
],
|
||||
|
||||
24
bugsink/test_api.py
Normal file
24
bugsink/test_api.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import unittest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from bsmain.models import AuthToken
|
||||
|
||||
|
||||
class BearerAuthRouterTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_ok_on_event_list(self):
|
||||
tok = AuthToken.objects.create()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {tok.token}")
|
||||
resp = self.client.get(reverse("api:event-list"))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_missing_on_event_list(self):
|
||||
resp = self.client.get(reverse("api:event-list"))
|
||||
self.assertIn(resp.status_code, (401, 403))
|
||||
|
||||
def test_invalid_on_event_list(self):
|
||||
self.client.credentials(HTTP_AUTHORIZATION="Bearer " + "a" * 40)
|
||||
resp = self.client.get(reverse("api:event-list"))
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
@@ -60,7 +60,7 @@ urlpatterns = [
|
||||
# many user-related views are directly exposed above (/accounts/), the rest is here:
|
||||
path("users/", include("users.urls")),
|
||||
|
||||
path("api/canonical/0/", include(api_router.urls)),
|
||||
path("api/canonical/0/", include((api_router.urls, "api"), namespace="api")),
|
||||
|
||||
# these are sentry-cli endpoint for uploading; they're unrelated to e.g. the ingestion API.
|
||||
# the /api/0/ is just a hard prefix (for the ingest API, that position indicates the project id, but here it's just
|
||||
|
||||
Reference in New Issue
Block a user