Skip to content

Commit

Permalink
🔧 Add missing Django CORS headers settings
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenbal committed Apr 26, 2024
1 parent e0f99dd commit 6cd51f7
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 4 deletions.
14 changes: 14 additions & 0 deletions src/openklant/accounts/tests/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import factory


class UserFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"user-{n}")
password = factory.PostGenerationMethodCall("set_password", "secret")

class Meta:
model = "accounts.User"


class SuperUserFactory(UserFactory):
is_staff = True
is_superuser = True
40 changes: 36 additions & 4 deletions src/openklant/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.urls import reverse_lazy

import sentry_sdk
from corsheaders.defaults import default_headers as default_cors_headers
from log_outgoing_requests.formatters import HttpFormatter

from .api import * # noqa
Expand Down Expand Up @@ -227,7 +228,14 @@
# LOGGING
#
LOG_STDOUT = config("LOG_STDOUT", default=False)
LOG_REQUESTS = config("LOG_REQUESTS", default=True)
LOG_LEVEL = config("LOG_LEVEL", default="WARNING")
LOG_QUERIES = config("LOG_QUERIES", default=False)
LOG_REQUESTS = config("LOG_REQUESTS", default=False)
if LOG_QUERIES and not DEBUG:
warnings.warn(
"Requested LOG_QUERIES=1 but DEBUG is false, no query logs will be emited.",
RuntimeWarning,
)

LOGGING_DIR = os.path.join(BASE_DIR, "log")

Expand All @@ -243,6 +251,7 @@
"performance": {
"format": "%(asctime)s %(process)d | %(thread)d | %(message)s",
},
"db": {"format": "%(asctime)s | %(message)s"},
"outgoing_requests": {"()": HttpFormatter},
},
"filters": {
Expand All @@ -263,6 +272,11 @@
"class": "logging.StreamHandler",
"formatter": "timestamped",
},
"console_db": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "db",
},
"django": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
Expand Down Expand Up @@ -300,12 +314,12 @@
"loggers": {
"openklant": {
"handlers": ["project"] if not LOG_STDOUT else ["console"],
"level": "INFO",
"level": LOG_LEVEL,
"propagate": True,
},
"django.request": {
"handlers": ["django"] if not LOG_STDOUT else ["console"],
"level": "ERROR",
"level": LOG_LEVEL,
"propagate": True,
},
"django.template": {
Expand All @@ -315,7 +329,7 @@
},
"mozilla_django_oidc": {
"handlers": ["project"],
"level": "DEBUG",
"level": LOG_LEVEL,
},
"log_outgoing_requests": {
"handlers": (
Expand All @@ -326,6 +340,11 @@
"level": "DEBUG",
"propagate": True,
},
"django.db.backends": {
"handlers": ["console_db"] if LOG_QUERIES else [],
"level": "DEBUG",
"propagate": False,
},
},
}

Expand Down Expand Up @@ -456,6 +475,19 @@
"REMOTE_ADDR",
)

#
# DJANGO-CORS-MIDDLEWARE
#
CORS_ALLOW_ALL_ORIGINS = config("CORS_ALLOW_ALL_ORIGINS", default=False)
CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", split=True, default=[])
CORS_ALLOWED_ORIGIN_REGEXES = config(
"CORS_ALLOWED_ORIGIN_REGEXES", split=True, default=[]
)
# Authorization is included in default_cors_headers
CORS_ALLOW_HEADERS = list(default_cors_headers) + config(
"CORS_EXTRA_ALLOW_HEADERS", split=True, default=[]
)

#
# RAVEN/SENTRY - error monitoring
#
Expand Down
156 changes: 156 additions & 0 deletions src/openklant/tests/test_cors_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Documentation on CORS spec, see MDN
# https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
from unittest.mock import patch

from django.test import override_settings
from django.urls import path

from rest_framework import views
from rest_framework.response import Response
from rest_framework.test import APITestCase

from openklant.accounts.tests.factories import SuperUserFactory


class View(views.APIView):
def get(self, request, *args, **kwargs):
return Response({"ok": True})

post = get


urlpatterns = [path("cors", View.as_view())]


class CorsMixin:
def setUp(self):
super().setUp()
mocker = patch(
"openklant.utils.middleware.get_version_mapping",
return_value={"/": "1.0.0"},
)
mocker.start()
self.addCleanup(mocker.stop)


@override_settings(ROOT_URLCONF="openklant.tests.test_cors_configuration")
class DefaultCORSConfigurationTests(CorsMixin, APITestCase):
"""
Test the default CORS settings.
"""

def test_preflight_request(self):
"""
Test the most basic preflight request.
"""
response = self.client.options(
"/cors",
HTTP_ACCESS_CONTROL_REQUEST_METHOD="POST",
HTTP_ACCESS_CONTROL_REQUEST_HEADERS="origin, x-requested-with",
HTTP_ORIGIN="https://evil.com",
)

self.assertNotIn("Access-Control-Allow-Origin", response)
self.assertNotIn("Access-Control-Allow-Credentials", response)

def test_credentialed_request(self):
user = SuperUserFactory.create(password="secret")
self.client.force_login(user=user)

response = self.client.get(
"/cors",
HTTP_ORIGIN="https://evil.com",
)

self.assertNotIn("Access-Control-Allow-Origin", response)
self.assertNotIn("Access-Control-Allow-Credentials", response)


@override_settings(
ROOT_URLCONF="openklant.tests.test_cors_configuration",
CORS_ALLOW_ALL_ORIGINS=True,
CORS_ALLOW_CREDENTIALS=False,
)
class CORSEnabledWithoutCredentialsTests(CorsMixin, APITestCase):
"""
Test the default CORS settings.
"""

def test_preflight_request(self):
"""
Test the most basic preflight request.
"""
response = self.client.options(
"/cors",
HTTP_ACCESS_CONTROL_REQUEST_METHOD="POST",
HTTP_ACCESS_CONTROL_REQUEST_HEADERS="origin, x-requested-with",
HTTP_ORIGIN="https://evil.com",
)

# wildcard "*" prevents browsers from sending credentials - this is good
self.assertNotEqual(response["Access-Control-Allow-Origin"], "https://evil.com")
self.assertEqual(response["Access-Control-Allow-Origin"], "*")
self.assertNotIn("Access-Control-Allow-Credentials", response)

def test_simple_request(self):
response = self.client.get(
"/cors",
HTTP_ORIGIN="https://evil.com",
)

# wildcard "*" prevents browsers from sending credentials - this is good
self.assertNotEqual(response["Access-Control-Allow-Origin"], "https://evil.com")
self.assertEqual(response["Access-Control-Allow-Origin"], "*")
self.assertNotIn("Access-Control-Allow-Credentials", response)

def test_credentialed_request(self):
user = SuperUserFactory.create(password="secret")
self.client.force_login(user=user)

response = self.client.get(
"/cors",
HTTP_ORIGIN="https://evil.com",
)

# wildcard "*" prevents browsers from sending credentials - this is good
self.assertNotEqual(response["Access-Control-Allow-Origin"], "https://evil.com")
self.assertEqual(response["Access-Control-Allow-Origin"], "*")
self.assertNotIn("Access-Control-Allow-Credentials", response)


@override_settings(
ROOT_URLCONF="openklant.tests.test_cors_configuration",
CORS_ALLOW_ALL_ORIGINS=True,
CORS_ALLOW_CREDENTIALS=False,
)
class CORSEnabledWithAuthHeaderTests(CorsMixin, APITestCase):
def test_preflight_request(self):
"""
Test a pre-flight request for requests including the HTTP Authorization header.
The inclusion of htis header makes it a not-simple request, see
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests
"""
response = self.client.options(
"/cors",
HTTP_ACCESS_CONTROL_REQUEST_METHOD="GET",
HTTP_ACCESS_CONTROL_REQUEST_HEADERS="origin, x-requested-with, authorization",
HTTP_ORIGIN="https://evil.com",
)

self.assertEqual(response["Access-Control-Allow-Origin"], "*")
self.assertIn(
"authorization",
response["Access-Control-Allow-Headers"].lower(),
)
self.assertNotIn("Access-Control-Allow-Credentials", response)

def test_credentialed_request(self):
response = self.client.get(
"/cors",
HTTP_ORIGIN="http://localhost:3000",
HTTP_AUTHORIZATION="Bearer foobarbaz",
)

self.assertEqual(response["Access-Control-Allow-Origin"], "*")
self.assertNotIn("Access-Control-Allow-Credentials", response)

0 comments on commit 6cd51f7

Please sign in to comment.