FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • uis/devops/iam/activate-account/api
1 result
Show changes
Commits on Source (4)
# Changelog
## [1.2.0](https://gitlab.developers.cam.ac.uk/uis/devops/iam/activate-account/api/compare/1.1.0...1.2.0) (2025-02-25)
### Features
* audit log successful and failed logins and lockout deletions ([dfb715d](https://gitlab.developers.cam.ac.uk/uis/devops/iam/activate-account/api/commit/dfb715d5c39d6006c3bdc21cc906a646e0b92df7))
## [1.1.0](https://gitlab.developers.cam.ac.uk/uis/devops/iam/activate-account/api/compare/1.0.0...1.1.0) (2025-02-19)
### Features
......
......@@ -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)
[tool.poetry]
name = "activate_account"
version = "1.1.0"
version = "1.2.0"
description = ""
authors = [ ]
readme = "README.md"
......