Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spike Django Axes #873

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions common/jinja2/common/locked_out.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "layouts/layout.jinja" %}

{% set page_title = "Locked out" %}

{% block content %}
<h1 class="govuk-heading-xl">{{ page_title }}</h1>
<p class="govuk-body">You have been locked out of your account due to too many incorrect password
submissions. Please try again in 15 minutes
</p>
{% endblock %}
65 changes: 58 additions & 7 deletions common/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re

import pytest
from bs4 import BeautifulSoup
from django.conf import settings
from django.http import HttpRequest
from django.urls import reverse

from common.tests import factories
Expand All @@ -25,26 +27,59 @@ def test_index_displays_workbasket_action_form(valid_user_client):
assert "Search the tariff" in page.select("label")[4].text


def test_index_displays_logout_buttons_correctly_SSO_off_logged_in(valid_user_client):
settings.SSO_ENABLED = False
response = valid_user_client.get(reverse("home"))
def test_index_displays_restricted_workbasket_action_form_invalid_user(
client,
disable_sso,
):
response = client.get(reverse("home"))

assert response.status_code == 200

page = BeautifulSoup(str(response.content), "html.parser")
assert "Search the tariff" in page.select("label")[0].text


def test_index_displays_auth_buttons_SSO_off(client, valid_user, disable_sso):
# Make sure login button is rendered when SSO is off
response = client.get(reverse("home"))
page = BeautifulSoup(str(response.content), "html.parser")
assert page.find_all("a", {"href": "/login"})

# Make sure logout button is rendered when SSO is off
user = factories.UserFactory.create(
username="GregPasty",
password="GottaHaveTwelveCharactersNow",
)
HttpRequest()
client.post(
reverse("login"),
{"username": user.username, "passwords": user.password},
)
assert user.is_authenticated
assert 0
response = client.get(reverse("home"))
page = BeautifulSoup(str(response.content), "html.parser")
assert page.find_all("a", {"href": "/logout"})


def test_index_displays_logout_buttons_correctly_SSO_off_logged_in(
valid_user_client,
disable_sso,
):
response = valid_user_client.get(reverse("home"))
assert response.status_code == 200
page = BeautifulSoup(str(response.content), "html.parser")
assert page.find_all("a", {"href": "/logout"})


def test_index_redirects_to_login_page_logged_out_SSO_off(client):
settings.SSO_ENABLED = False
def test_index_redirects_to_login_page_logged_out_SSO_off(client, disable_sso):
response = client.get(reverse("home"))

assert response.status_code == 302
response.url.startswith(reverse("admin:login"))


def test_index_displays_login_buttons_correctly_SSO_on(valid_user_client):
settings.SSO_ENABLED = True
response = valid_user_client.get(reverse("home"))

assert response.status_code == 200
Expand All @@ -54,6 +89,22 @@ def test_index_displays_login_buttons_correctly_SSO_on(valid_user_client):
assert not page.find_all("a", {"href": "/login"})


def test_login_displays_lockout_page(client, disable_sso):
login_url = reverse("login")
form_data = {
"username": "wrong username",
"password": "wrong password",
}
for x in range(4):
client.post(login_url, form_data)

response = client.post(login_url, form_data)
assert response.status_code == 403
page = BeautifulSoup(str(response.content), "html.parser")
assert page.find("h1").text == "Locked out"
assert page.find_all(string=re.compile("You have been locked out of your account"))


@pytest.mark.parametrize(
("data", "response_url"),
(
Expand Down
21 changes: 20 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def policy_group(db) -> Group:

@pytest.fixture
def valid_user(db, policy_group):
user = factories.UserFactory.create()
user = factories.UserFactory.create(password="GottaHaveTwelveCharactersNow")
policy_group.user_set.add(user)
return user

Expand Down Expand Up @@ -1540,3 +1540,22 @@ def quotas_json():
def mock_aioresponse():
with aioresponses() as m:
yield m


@pytest.fixture(autouse=False)
def disable_sso(settings):
settings.SSO_ENABLED = False
settings.INSTALLED_APPS.pop(settings.INSTALLED_APPS.index("authbroker_client"))
settings.MIDDLEWARE.pop(
settings.MIDDLEWARE.index(
"authbroker_client.middleware.ProtectAllViewsMiddleware",
),
)
settings.LOGIN_URL = "/login"
settings.AUTHBROKER_CLIENT_ID = None
settings.AUTHBROKER_CLIENT_SECRET = None
settings.AUTHENTICATION_BACKENDS.pop(
settings.AUTHENTICATION_BACKENDS.index(
"authbroker_client.backends.AuthbrokerBackend",
),
)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ crispy-forms-gds @ git+https://github.com/uktrade/crispy-forms-gds.git@b50168d0e
defusedxml==0.7.*
dj-database-url==0.5.0
django==3.2.18
django-axes==5.40.1
django-crispy-forms==1.12.0
django-dotenv==1.4.2
drf-extra-fields==3.0.2
Expand Down
14 changes: 13 additions & 1 deletion settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"exporter.apps.ExporterConfig",
"crispy_forms",
"crispy_forms_gds",
"axes",
]

APPS_THAT_MUST_COME_LAST = ["django.forms"]
Expand All @@ -144,6 +145,7 @@
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"common.models.utils.TransactionMiddleware",
"csp.middleware.CSPMiddleware",
"axes.middleware.AxesMiddleware",
]
if SSO_ENABLED:
MIDDLEWARE += [
Expand Down Expand Up @@ -226,7 +228,11 @@
AUTHBROKER_CLIENT_ID = os.environ.get("AUTHBROKER_CLIENT_ID")
AUTHBROKER_CLIENT_SECRET = os.environ.get("AUTHBROKER_CLIENT_SECRET")

AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"]
AUTHENTICATION_BACKENDS = [
# Axes must be at the top
"axes.backends.AxesStandaloneBackend",
"django.contrib.auth.backends.ModelBackend",
]
if SSO_ENABLED:
AUTHENTICATION_BACKENDS += [
"authbroker_client.backends.AuthbrokerBackend",
Expand Down Expand Up @@ -659,3 +665,9 @@
BASE_SERVICE_URL = "https://" + VCAP_APPLICATION["application_uris"][0]
else:
BASE_SERVICE_URL = os.environ.get("BASE_SERVICE_URL")


AXES_ENABLED = True
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 0.05
AXES_LOCKOUT_TEMPLATE = "/common/locked_out.jinja"
7 changes: 6 additions & 1 deletion settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
# Enable Django debug toolbar
if is_truthy(os.environ.get("ENABLE_DJANGO_DEBUG_TOOLBAR")):
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
INSTALLED_APPS.extend(["debug_toolbar", "whitenoise.runserver_nostatic"])
INSTALLED_APPS.extend(
[
"debug_toolbar",
"whitenoise.runserver_nostatic",
],
)
DEBUG_TOOLBAR_PANELS = [
"debug_toolbar.panels.versions.VersionsPanel",
"debug_toolbar.panels.timer.TimerPanel",
Expand Down