Repository: cisagov/manage.get.gov

Issue:
Create a method of disabling login required during testing and development
⚠ Any method of disabling login creates a "backdoor". ⚠ 

- [ ] document in an ADR security considerations and how risks have been mitigated


Discussion:
this will be important for automated accessibility testing
In Django unit tests, can be accomplished with `force_login`. 

For pa11y, the suggestion in their FAQ https://github.com/pa11y/pa11y/blob/master/TROUBLESHOOTING.md#how-can-pa11y-log-in-if-my-site-has-a-log-in-form is to use actions to fill a login form and then continue. That could be combined with either a test fixture that includes the test user or a prior call to `manage.py createsuperuser`.
10/6:

- Working on this, running into some blockers - will work through in engineering huddle

Resolving patch:
diff --git a/src/djangooidc/urls.py b/src/djangooidc/urls.py
--- a/src/djangooidc/urls.py
+++ b/src/djangooidc/urls.py
@@ -5,7 +5,7 @@
 from . import views
 
 urlpatterns = [
-    path("login/", views.openid, name="openid"),
+    path("login/", views.openid, name="login"),
     path("callback/login/", views.login_callback, name="openid_login_callback"),
     path("logout/", views.logout, name="logout"),
     path("callback/logout/", views.logout_callback, name="openid_logout_callback"),
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -403,7 +403,7 @@
 
 # this is where unauthenticated requests are redirected when using
 # the login_required() decorator, LoginRequiredMixin, or AccessMixin
-LOGIN_URL = "openid/login"
+LOGIN_URL = "/openid/login"
 
 # where to go after logging out
 LOGOUT_REDIRECT_URL = "home"
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -4,20 +4,35 @@
     https://docs.djangoproject.com/en/4.0/topics/http/urls/
 """
 
+from django.conf import settings
 from django.contrib import admin
 from django.urls import include, path
+from django.views.generic import RedirectView
 
 from registrar.views import health, index, profile, whoami
 
 urlpatterns = [
     path("", index.index, name="home"),
-    path("whoami", whoami.whoami, name="whoami"),
+    path("whoami/", whoami.whoami, name="whoami"),
     path("admin/", admin.site.urls),
     path("health/", health.health),
     path("edit_profile/", profile.edit_profile, name="edit-profile"),
     path("openid/", include("djangooidc.urls")),
 ]
 
+if not settings.DEBUG:
+    urlpatterns += [
+        # redirect to login.gov
+        path(
+            "admin/login/", RedirectView.as_view(pattern_name="login", permanent=False)
+        ),
+        # redirect to login.gov
+        path(
+            "admin/logout/",
+            RedirectView.as_view(pattern_name="logout", permanent=False),
+        ),
+    ]
+
 # we normally would guard these with `if settings.DEBUG` but tests run with
 # DEBUG = False even when these apps have been loaded because settings.DEBUG
 # was actually True. Instead, let's add these URLs any time we are able to


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -35,6 +35,14 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
+      - name: Disable Login
+        working-directory: ./src
+        # by adding MockUserLogin to settings.MIDDLEWARE
+        run: |
+          perl -pi \
+          -e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
+          src/registrar/config/settings.py
+
       - name: Accessibility Scan
         working-directory: ./src
         # leverage the docker compose setup that we already have for local development
diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py
--- a/src/djangooidc/tests/test_views.py
+++ b/src/djangooidc/tests/test_views.py
@@ -33,7 +33,7 @@ def test_openid_sets_next(self, mock_client):
         # mock
         mock_client.create_authn_request.side_effect = self.say_hi
         # test
-        response = self.client.get(reverse("openid"), {"next": callback_url})
+        response = self.client.get(reverse("login"), {"next": callback_url})
         # assert
         session = mock_client.create_authn_request.call_args[0][0]
         self.assertEqual(session["next"], callback_url)
@@ -45,7 +45,7 @@ def test_openid_raises(self, mock_client):
         mock_client.create_authn_request.side_effect = Exception("Test")
         # test
         with less_console_noise():
-            response = self.client.get(reverse("openid"))
+            response = self.client.get(reverse("login"))
         # assert
         self.assertEqual(response.status_code, 500)
         self.assertTemplateUsed(response, "500.html")
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+from django.contrib.auth import get_user_model, login
+
+
+class MockUserLogin:
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        if request.user.is_anonymous:
+            user = None
+            UserModel = get_user_model()
+            username = "Testy"
+            args = {
+                UserModel.USERNAME_FIELD: username,
+            }
+            user = UserModel.objects.get_or_create(**args)
+            user.is_staff = True
+            user.is_superuser = True
+            user.save()
+            backend = settings.AUTHENTICATION_BACKENDS[-1]
+            login(request, user, backend=backend)
+
+        response = self.get_response(request)
+        return response
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -18,9 +18,9 @@ def test_home_page(self):
 
     def test_whoami_page_no_user(self):
         """Whoami page not accessible without a logged-in user."""
-        response = self.client.get("/whoami")
+        response = self.client.get("/whoami/")
         self.assertEqual(response.status_code, 302)
-        self.assertIn("?next=/whoami", response.headers["Location"])
+        self.assertIn("?next=/whoami/", response.headers["Location"])
 
 
 class LoggedInTests(TestCase):
@@ -36,7 +36,7 @@ def setUp(self):
 
     def test_whoami_page(self):
         """User information appears on the whoami page."""
-        response = self.client.get("/whoami")
+        response = self.client.get("/whoami/")
         self.assertContains(response, self.user.first_name)
         self.assertContains(response, self.user.last_name)
         self.assertContains(response, self.user.email)