diff --git a/authentication/serializers.py b/authentication/serializers.py index f90a5cd42e7b0b878919ad07735084a3e1464bba..2190aebd1111dbef8f78115be49bba0fe68bf164 100644 --- a/authentication/serializers.py +++ b/authentication/serializers.py @@ -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 diff --git a/authentication/tests/test_lockout.py b/authentication/tests/test_lockout.py index ad9eb59204d04b7bb7cc85df394fc3d23735639e..2ef60c607f38f79d00bebbf6f34e12f7408143b3 100644 --- a/authentication/tests/test_lockout.py +++ b/authentication/tests/test_lockout.py @@ -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 diff --git a/authentication/tests/test_login_view.py b/authentication/tests/test_login_view.py index d953bc68ad24923b1b5d369f9aef22f2923618bb..55e533333716cc5e69358a6bef082ae11d03e47f 100644 --- a/authentication/tests/test_login_view.py +++ b/authentication/tests/test_login_view.py @@ -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) diff --git a/authentication/views.py b/authentication/views.py index 61cc1aa61a3cce1af7e6385ce731adcc176e3f11..d05fd87b79aa751ca8292dfe12cdd8b36e4b26e2 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -1,3 +1,4 @@ +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)