Add test that login is required for almost every URL

This commit is contained in:
Neil Martinsen-Burrell 2023-06-01 16:35:40 -05:00
parent 2e7644bdc3
commit 85a8b108de
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184

View file

@ -0,0 +1,158 @@
"""Test that almost all URLs require authentication.
This uses deep Django URLConf pattern magic and was shamelessly lifted from
https://github.com/18F/tock/blob/main/tock/tock/tests/test_url_auth.py
"""
from django.test import TestCase
from django.urls import reverse, URLPattern
from django.urls.resolvers import URLResolver
import registrar.config.urls
from .common import less_console_noise
# When a URLconf pattern contains named capture groups, we'll use this
# dictionary to retrieve a sample value for it, which will be included
# in the sample URLs we generate, when attempting to perform a GET
# request on the view.
SAMPLE_KWARGS = {
"app_label": "registrar",
"pk": "1",
"id": "1",
"content_type_id": "2",
"object_id": "3",
"domain": "whitehouse.gov",
}
# Our test suite will ignore some namespaces.
IGNORE_NAMESPACES = [
# The Django Debug Toolbar (DJDT) ends up in the URL config but it's always
# disabled in production, so don't worry about it.
"djdt"
]
# In general, we don't want to have any unnamed views, because that makes it
# impossible to generate sample URLs that point at them. We'll make exceptions
# for some namespaces that we don't have control over, though.
NAMESPACES_WITH_UNNAMED_VIEWS = ["admin", None]
def iter_patterns(urlconf, patterns=None, namespace=None):
"""
Iterate through all patterns in the given Django URLconf. Yields
`(viewname, route)` tuples, where `viewname` is the fully-qualified view name
(including its namespace, if any), and `route` is a regular expression that
corresponds to the part of the pattern that contains any capturing groups.
"""
if patterns is None:
patterns = urlconf.urlpatterns
for pattern in patterns:
# Resolve if it's a route or an include
if isinstance(pattern, URLPattern):
viewname = pattern.name
if viewname is None and namespace not in NAMESPACES_WITH_UNNAMED_VIEWS:
raise AssertionError(
f"namespace {namespace} cannot contain unnamed views"
)
if namespace and viewname is not None:
viewname = f"{namespace}:{viewname}"
yield (viewname, pattern.pattern)
elif isinstance(pattern, URLResolver):
if len(pattern.default_kwargs.keys()) > 0:
raise AssertionError("resolvers are not expected to have kwargs")
if pattern.namespace and namespace is not None:
raise AssertionError("nested namespaces are not currently supported")
if pattern.namespace in IGNORE_NAMESPACES:
continue
yield from iter_patterns(
urlconf, pattern.url_patterns, namespace or pattern.namespace
)
else:
raise AssertionError("unknown pattern class")
def iter_sample_urls(urlconf):
"""
Yields sample URLs for all entries in the given Django URLconf.
This gets pretty deep into the muck of RoutePattern
https://docs.djangoproject.com/en/2.1/_modules/django/urls/resolvers/
"""
for viewname, route in iter_patterns(urlconf):
if not viewname:
continue
if viewname == "auth_user_password_change":
print(route)
break
named_groups = route.regex.groupindex.keys()
kwargs = {}
args = ()
for kwarg in named_groups:
if kwarg not in SAMPLE_KWARGS:
raise AssertionError(
f'Sample value for {kwarg} in pattern "{route}" not found'
)
kwargs[kwarg] = SAMPLE_KWARGS[kwarg]
url = reverse(viewname, args=args, kwargs=kwargs)
yield (viewname, url)
class TestURLAuth(TestCase):
"""
Tests to ensure that most URLs in a Django URLconf are protected by
authentication.
"""
# We won't test that the following URLs are protected by auth.
# Note that the trailing slash is wobbly depending on how the URL was defined.
IGNORE_URLS = [
# These are the OIDC auth endpoints that always need
# to be public.
"/openid/login/",
"/openid/logout/",
"/openid/callback",
"/openid/callback/logout/",
]
def assertURLIsProtectedByAuth(self, url):
"""
Make a GET request to the given URL, and ensure that it either redirects
to login or denies access outright.
"""
try:
with less_console_noise():
response = self.client.get(url)
except Exception as e:
# It'll be helpful to provide information on what URL was being
# accessed at the time the exception occurred. Python 3 will
# also include a full traceback of the original exception, so
# we don't need to worry about hiding the original cause.
raise AssertionError(f'Accessing {url} raised "{e}"', e)
code = response.status_code
if code == 302:
redirect = response["location"]
self.assertRegex(
redirect,
r"^\/openid\/login",
f"GET {url} should redirect to login or deny access, but instead "
f"it redirects to {redirect}",
)
elif code == 401 or code == 403:
pass
else:
raise AssertionError(
f"GET {url} returned HTTP {code}, but should redirect to login or "
"deny access",
)
def test_login_required_all_urls(self):
"""All URLs redirect to the login view."""
for viewname, url in iter_sample_urls(registrar.config.urls):
if url not in self.IGNORE_URLS:
with self.subTest(viewname=viewname):
self.assertURLIsProtectedByAuth(url)