FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects
Commit 53667662 authored by Robin Goodall's avatar Robin Goodall :speech_balloon:
Browse files

Merge branch 'audit-logging' into 'main'

chore: audit log successful and failed logins and lockout deletions

Closes #35

See merge request !105
parents 9a02908e dfb715d5
No related branches found
No related tags found
1 merge request!105chore: audit log successful and failed logins and lockout deletions
Pipeline #703482 passed
......@@ -100,6 +100,7 @@ class TokenRequestSerializer(serializers.Serializer):
identity_key=identity_key,
date_of_birth=date_of_birth,
lockout_until=lockout.lockout_until,
category="audit",
)
raise InvalidGrantError("Account locked out")
......@@ -129,6 +130,7 @@ class TokenRequestSerializer(serializers.Serializer):
last_name=data.get("last_name"),
date_of_birth=data["date_of_birth"],
name_match=name_match,
category="audit",
)
raise InvalidGrantError("Ambiguous token request")
except Account.DoesNotExist:
......@@ -159,6 +161,7 @@ class TokenRequestSerializer(serializers.Serializer):
last_name=data.get("last_name"),
date_of_birth=data.get("date_of_birth"),
name_match=name_match,
category="audit",
)
raise InvalidGrantError("No matching account")
......@@ -168,6 +171,16 @@ class TokenRequestSerializer(serializers.Serializer):
# Set account in validated data
data["account"] = account
logger.info(
"Successful authentication",
crsid=data.get("crsid"),
last_name=data.get("last_name"),
date_of_birth=data.get("date_of_birth"),
name_match=name_match,
account=account.crsid,
category="audit",
)
return data
......
......@@ -135,7 +135,7 @@ def test_exponential_backoff_lockout(identity_field):
@pytest.mark.parametrize("identity_field", ["crsid", "last_name"])
@freeze_time("2025-01-01 00:00:00")
def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client):
def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client, caplog):
account = AccountFactory(account_identifier=True)
identifier = account.account_identifier.first()
identity_value = account.crsid if identity_field == "crsid" else identifier.last_name
......@@ -164,11 +164,15 @@ def test_lockout_clear_from_api_endpoint_flow(identity_field, api_client):
# Confirm account is locked out
assert Lockout.objects.filter(**lockout_kwargs).exists()
caplog.clear()
response = api_client.post(
reverse("knox_login"),
data=urlencode(data),
content_type="application/x-www-form-urlencoded",
)
# Confirm that the audit log entry was made
assert "Account locked out" in caplog.text
assert "'category': 'audit'" in caplog.text
assert response.status_code == 400
......
......@@ -83,9 +83,12 @@ def test_unsupported_media_type_multipart(client, valid_session_grant_body, pars
@pytest.mark.parametrize("valid_session_grant_body", ["crsid", "last_name"], indirect=True)
def test_valid_session_grant(client, valid_session_grant_body):
def test_valid_session_grant(client, valid_session_grant_body, caplog):
"""A valid session grant request body succeeds"""
assert_is_valid_login(post_login(client, valid_session_grant_body))
# Confirm that the audit log entry was made
assert "Successful authentication" in caplog.text
assert "'category': 'audit'" in caplog.text
@pytest.mark.parametrize("valid_session_grant_body", ["crsid"], indirect=True)
......@@ -176,7 +179,7 @@ def test_missing_field(client, request, valid_session_grant_body):
assert_is_error(post_login(client, missing_field_body), "invalid_request")
def test_invalid_grant(faker, client):
def test_invalid_grant(faker, client, caplog):
"""Providing credentials that do not exist"""
assert_is_error(
post_login(
......@@ -190,6 +193,9 @@ def test_invalid_grant(faker, client):
),
"invalid_grant",
)
# Confirm that the audit log entry was made
assert "No matching user" in caplog.text
assert "'category': 'audit'" in caplog.text
@pytest.mark.parametrize("valid_session_grant_body", ["crsid", "last_name"], indirect=True)
......
import structlog
from drf_spectacular.utils import OpenApiResponse, extend_schema
from knox.views import LoginView as KnoxLoginView
from rest_framework import (
......@@ -21,6 +22,8 @@ from authentication.serializers import (
TokenResponseSerializer,
)
logger = structlog.get_logger(__name__)
class LoginView(KnoxLoginView):
throttle_classes = ()
......@@ -177,3 +180,14 @@ class LockoutViewSet(
queryset = Lockout.objects.all()
serializer_class = LockoutSerializer
versioning_class = None
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
logger.info(
"Lockout removed",
identity_key=instance.identity_key,
date_of_birth=instance.date_of_birth,
category="audit",
)
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment