Run tests behind logged in pages (#162)

* Redirect admin login to Login.gov

* Add logged in scanning to CI

* Fix bug in LOGIN_URL

* Fix linter and tests

* Address PR feedback

* Try quotes
This commit is contained in:
Seamus Johnston 2022-10-11 16:26:11 +00:00 committed by GitHub
parent f130ffc9a8
commit 9b008d6363
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 101 additions and 11 deletions

View file

@ -35,6 +35,17 @@ jobs:
name: security-check-output name: security-check-output
path: ./src/output.txt path: ./src/output.txt
backdoor-check:
name: Ensure custom mods are contained
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v3
- name: MockUserLogin should not be in settings.MIDDLEWARE
run: "! grep -rwn * --exclude-dir=node_modules -e registrar.tests.common.MockUserLogin"
working-directory: ./src
owasp-scan: owasp-scan:
name: OWASP security scan name: OWASP security scan
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -42,6 +53,15 @@ jobs:
steps: steps:
- name: Check out - name: Check out
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Disable Login
# by adding MockUserLogin to settings.MIDDLEWARE
run: |
perl -pi \
-e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
src/registrar/config/settings.py
working-directory: ./src
- name: OWASP scan - name: OWASP scan
run: docker compose run owasp run: docker compose run owasp
working-directory: ./src working-directory: ./src

View file

@ -35,6 +35,14 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - 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 - name: Accessibility Scan
working-directory: ./src working-directory: ./src
# leverage the docker compose setup that we already have for local development # leverage the docker compose setup that we already have for local development

View file

@ -68,7 +68,17 @@ Linters:
docker-compose exec app ./manage.py lint docker-compose exec app ./manage.py lint
``` ```
## Accessibility Scanning ### Testing behind logged in pages
To test behind logged in pages with external tools, like `pa11y-ci` or `OWASP Zap`, add
```
"registrar.tests.common.MockUserLogin"
```
to MIDDLEWARE in settings.py. **Remove it when you are finished testing.**
### Accessibility Scanning
The tool `pa11y-ci` is used to scan pages for compliance with a set of The tool `pa11y-ci` is used to scan pages for compliance with a set of
accessibility rules. The scan runs as part of our CI setup (see accessibility rules. The scan runs as part of our CI setup (see
@ -82,6 +92,17 @@ docker-compose run pa11y npm run pa11y-ci
The URLs that `pa11y-ci` will scan are configured in `src/.pa11yci`. When new The URLs that `pa11y-ci` will scan are configured in `src/.pa11yci`. When new
views and pages are added, their URLs should also be added to that file. views and pages are added, their URLs should also be added to that file.
### Security Scanning
The tool OWASP Zap is used for scanning the codebase for compliance with
security rules. The scan runs as part of our CI setup (see
`.github/workflows/test.yaml`) but it can also be run locally. To run locally,
type
```shell
docker-compose run owasp
```
## USWDS and styling ## USWDS and styling
We use the U.S. Web Design System (USWDS) for building and styling our applications. Additionally, we utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package the static assets. We use the U.S. Web Design System (USWDS) for building and styling our applications. Additionally, we utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package the static assets.
When you run `docker-compose up` the `node` service in the container will begin to watch for changes in the `registrar/assets` folder, and will recompile once any changes are made. When you run `docker-compose up` the `node` service in the container will begin to watch for changes in the `registrar/assets` folder, and will recompile once any changes are made.

View file

@ -1,6 +1,7 @@
{ {
"urls": [ "urls": [
"http://app:8080/", "http://app:8080/",
"http://app:8080/health/" "http://app:8080/health/",
"http://app:8080/whoami/"
] ]
} }

View file

@ -33,7 +33,7 @@ class ViewsTest(TestCase):
# mock # mock
mock_client.create_authn_request.side_effect = self.say_hi mock_client.create_authn_request.side_effect = self.say_hi
# test # test
response = self.client.get(reverse("openid"), {"next": callback_url}) response = self.client.get(reverse("login"), {"next": callback_url})
# assert # assert
session = mock_client.create_authn_request.call_args[0][0] session = mock_client.create_authn_request.call_args[0][0]
self.assertEqual(session["next"], callback_url) self.assertEqual(session["next"], callback_url)
@ -45,7 +45,7 @@ class ViewsTest(TestCase):
mock_client.create_authn_request.side_effect = Exception("Test") mock_client.create_authn_request.side_effect = Exception("Test")
# test # test
with less_console_noise(): with less_console_noise():
response = self.client.get(reverse("openid")) response = self.client.get(reverse("login"))
# assert # assert
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html") self.assertTemplateUsed(response, "500.html")

View file

@ -5,7 +5,7 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("login/", views.openid, name="openid"), path("login/", views.openid, name="login"),
path("callback/login/", views.login_callback, name="openid_login_callback"), path("callback/login/", views.login_callback, name="openid_login_callback"),
path("logout/", views.logout, name="logout"), path("logout/", views.logout, name="logout"),
path("callback/logout/", views.logout_callback, name="openid_logout_callback"), path("callback/logout/", views.logout_callback, name="openid_logout_callback"),

View file

@ -403,7 +403,7 @@ AUTHENTICATION_BACKENDS = [
# this is where unauthenticated requests are redirected when using # this is where unauthenticated requests are redirected when using
# the login_required() decorator, LoginRequiredMixin, or AccessMixin # the login_required() decorator, LoginRequiredMixin, or AccessMixin
LOGIN_URL = "openid/login" LOGIN_URL = "/openid/login"
# where to go after logging out # where to go after logging out
LOGOUT_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "home"

View file

@ -4,20 +4,35 @@ For more information see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/ https://docs.djangoproject.com/en/4.0/topics/http/urls/
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView
from registrar.views import health, index, profile, whoami from registrar.views import health, index, profile, whoami
urlpatterns = [ urlpatterns = [
path("", index.index, name="home"), path("", index.index, name="home"),
path("whoami", whoami.whoami, name="whoami"), path("whoami/", whoami.whoami, name="whoami"),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("health/", health.health), path("health/", health.health),
path("edit_profile/", profile.edit_profile, name="edit-profile"), path("edit_profile/", profile.edit_profile, name="edit-profile"),
path("openid/", include("djangooidc.urls")), 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 # 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 # 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 # was actually True. Instead, let's add these URLs any time we are able to

View file

@ -12,7 +12,7 @@
<p>{% translate "Authorization failed." %}</p> <p>{% translate "Authorization failed." %}</p>
{% endif %} {% endif %}
<p><a href="{% url 'openid' %}"> <p><a href="{% url 'login' %}">
{% translate "Would you like to try logging in again?" %} {% translate "Would you like to try logging in again?" %}
</a></p> </a></p>

View file

@ -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

View file

@ -18,9 +18,9 @@ class TestViews(TestCase):
def test_whoami_page_no_user(self): def test_whoami_page_no_user(self):
"""Whoami page not accessible without a logged-in user.""" """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.assertEqual(response.status_code, 302)
self.assertIn("?next=/whoami", response.headers["Location"]) self.assertIn("?next=/whoami/", response.headers["Location"])
class LoggedInTests(TestCase): class LoggedInTests(TestCase):
@ -36,7 +36,7 @@ class LoggedInTests(TestCase):
def test_whoami_page(self): def test_whoami_page(self):
"""User information appears on the whoami page.""" """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.first_name)
self.assertContains(response, self.user.last_name) self.assertContains(response, self.user.last_name)
self.assertContains(response, self.user.email) self.assertContains(response, self.user.email)