Skip to content
Merged
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
4 changes: 2 additions & 2 deletions codeforlife/user/fixtures/legacy.json
Original file line number Diff line number Diff line change
Expand Up @@ -986,10 +986,10 @@
"pk": 20,
"fields": {
"_first_name_enc": "ZmFrZV9lbmM6kEW6JpB05mjWAPE/xrxMrbFNoyD6ZBNVl+YCZI/q3+LRROg=",
"_first_name_hash": "1dbd289e95b4925fc85eff99d7e879b2033f60182e05872f8665ff271d0d337c",
"_first_name_hash": "",
"_last_name_enc": "ZmFrZV9lbmM6gwxaGy9/aDQWDEVUwUverJ6wmk3ohaTkvF5GYq6koNg=",
"_username_enc": "ZmFrZV9lbmM6kUOy1ae7Y1ElSiAiDXnKIZxPkOs1saBSjmH+UCEThmkOR7lKe9Nlmza/K+mW1Slr0UutzlVaqMLU3yWT",
"_username_hash": "daa37b0878d74480285044dccc67750ad597b99719dc7323462cbe16e0b0271d",
"_username_hash": "",
"date_joined": "2026-02-04T16:02:37.614Z",
"dek": "ZmFrZV9lbmM6Ct68dRt74lesvjRcTVtlbUlPC8xt85anig+MaarMMZAUvcFmkJmno57KkQUR8S5/o5ElV9Bpn3RDpxeS",
"groups": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from django.db import migrations

from ...models.fields import EncryptedTextField, Sha256Field

user_migrations = [
# Username
migrations.RemoveField(
Expand Down
146 changes: 146 additions & 0 deletions codeforlife/user/migrations/0007_empty_hash_when_inactive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import typing as t

from django.db import migrations
from django.db.migrations.state import StateApps
from django.db.models import CheckConstraint, Model, Q


def set_hash_fields_to_empty(apps: StateApps, _):
"""
Set hash fields to empty for inactive users, classes, schools, and school
teacher invitations.
"""

User: t.Type[Model] = apps.get_model("user", "User")
Class: t.Type[Model] = apps.get_model("user", "Class")
School: t.Type[Model] = apps.get_model("user", "School")
SchoolTeacherInvitation: t.Type[Model] = apps.get_model(
"user", "SchoolTeacherInvitation"
)

is_not_active = Q(is_active=False)

User.objects.filter(is_not_active).update(
_username_hash="", _email_hash="", _first_name_hash=""
)

Class.objects.filter(is_not_active).update(_name_enc=b"", _name_hash="")

School.objects.filter(is_not_active).update(_name_hash="")

SchoolTeacherInvitation.objects.filter(is_not_active).update(
_invited_teacher_first_name_enc=b"",
_invited_teacher_last_name_enc=b"",
_invited_teacher_email_enc=b"",
_token_hash="",
)


class Migration(migrations.Migration):

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("user", "0006_client_side_encryption_part_4"),
]

operations = [
migrations.RunPython(set_hash_fields_to_empty),
migrations.AddConstraint(
model_name="class",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_name_enc", b""), _connector="OR"
),
name="klass__name_enc_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="class",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_name_hash", ""), _connector="OR"
),
name="klass__name_hash_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="school",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_name_hash", ""), _connector="OR"
),
name="school__name_hash_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="schoolteacherinvitation",
constraint=CheckConstraint(
condition=Q(
("is_active", True),
("_invited_teacher_first_name_enc", b""),
_connector="OR",
),
name="school_teacher_invitation__invited_teacher_first_name_enc_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="schoolteacherinvitation",
constraint=CheckConstraint(
condition=Q(
("is_active", True),
("_invited_teacher_last_name_enc", b""),
_connector="OR",
),
name="school_teacher_invitation__invited_teacher_last_name_enc_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="schoolteacherinvitation",
constraint=CheckConstraint(
condition=Q(
("is_active", True),
("_invited_teacher_email_enc", b""),
_connector="OR",
),
name="school_teacher_invitation__invited_teacher_email_enc_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="schoolteacherinvitation",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_token_hash", ""), _connector="OR"
),
name="school_teacher_invitation__token_hash_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="user",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_email_hash", ""), _connector="OR"
),
name="user__email_hash_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="user",
constraint=CheckConstraint(
condition=Q(
("is_active", True), ("_username_hash", ""), _connector="OR"
),
name="user__username_hash_non_empty_when_inactive",
),
),
migrations.AddConstraint(
model_name="user",
constraint=CheckConstraint(
condition=Q(
("is_active", True),
("_first_name_hash", ""),
_connector="OR",
),
name="user__first_name_hash_non_empty_when_inactive",
),
),
]
12 changes: 11 additions & 1 deletion codeforlife/user/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ def get_requests_message(self):
def anonymise(self):
"""Anonymise the class."""
self.is_active = False
self.save()
self._name_enc = b""
self._name_hash = ""
self.save(update_fields=["is_active", "_name_enc", "_name_hash"])

# Remove independent students' requests to join this class
# pylint: disable-next=no-member
Expand All @@ -248,6 +250,14 @@ class Meta(TypedModelMeta):
condition=~models.Q(_access_code_hash=""),
name="unique_access_code_hash_non_empty",
),
models.CheckConstraint(
condition=models.Q(is_active=True) | models.Q(_name_enc=b""),
name="klass__name_enc_non_empty_when_inactive",
),
models.CheckConstraint(
condition=models.Q(is_active=True) | models.Q(_name_hash=""),
name="klass__name_hash_non_empty_when_inactive",
),
]

@property
Expand Down
54 changes: 50 additions & 4 deletions codeforlife/user/models/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,43 @@ class Meta:
fields=["_token_hash"],
name="unique_token_hash_non_empty",
),
models.CheckConstraint(
condition=(
models.Q(is_active=True)
| models.Q(_invited_teacher_first_name_enc=b"")
),
name=(
"school_teacher_invitation__"
"invited_teacher_first_name_enc_non_empty_when_inactive"
),
),
models.CheckConstraint(
condition=(
models.Q(is_active=True)
| models.Q(_invited_teacher_last_name_enc=b"")
),
name=(
"school_teacher_invitation__"
"invited_teacher_last_name_enc_non_empty_when_inactive"
),
),
models.CheckConstraint(
condition=(
models.Q(is_active=True)
| models.Q(_invited_teacher_email_enc=b"")
),
name=(
"school_teacher_invitation__"
"invited_teacher_email_enc_non_empty_when_inactive"
),
),
models.CheckConstraint(
condition=models.Q(is_active=True) | models.Q(_token_hash=""),
name=(
"school_teacher_invitation__"
"token_hash_non_empty_when_inactive"
),
),
]

@property
Expand All @@ -455,11 +492,20 @@ def __str__(self):

def anonymise(self):
"""Anonymise the invitation."""
self.invited_teacher_first_name = ""
self.invited_teacher_last_name = ""
self.invited_teacher_email = ""
self._invited_teacher_first_name_enc = b""
self._invited_teacher_last_name_enc = b""
self._invited_teacher_email_enc = b""
self._token_hash = ""
self.is_active = False
self.save()
self.save(
update_fields=[
"_invited_teacher_first_name_enc",
"_invited_teacher_last_name_enc",
"_invited_teacher_email_enc",
"_token_hash",
"is_active",
]
)

@property
def dek_aead(self):
Expand Down
7 changes: 6 additions & 1 deletion codeforlife/user/models/school.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ class Meta(TypedModelMeta):
fields=["_name_hash"],
name="unique_name_hash_non_empty",
),
models.CheckConstraint(
condition=models.Q(is_active=True) | models.Q(_name_hash=""),
name="school__name_hash_non_empty_when_inactive",
),
]

def __str__(self):
Expand Down Expand Up @@ -168,4 +172,5 @@ def anonymise(self):
"""Anonymize the school."""
self.dek = None
self.is_active = False
self.save(update_fields=["dek", "is_active"])
self._name_hash = ""
self.save(update_fields=["dek", "is_active", "_name_hash"])
29 changes: 28 additions & 1 deletion codeforlife/user/models/user/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ class Meta(TypedModelMeta):
fields=["_username_hash"],
name="unique_username_hash_non_empty",
),
models.CheckConstraint(
condition=models.Q(is_active=True) | models.Q(_email_hash=""),
name="user__email_hash_non_empty_when_inactive",
),
models.CheckConstraint(
condition=(
models.Q(is_active=True) | models.Q(_username_hash="")
),
name="user__username_hash_non_empty_when_inactive",
),
models.CheckConstraint(
condition=(
models.Q(is_active=True) | models.Q(_first_name_hash="")
),
name="user__first_name_hash_non_empty_when_inactive",
),
]

# TODO: remove in new schema
Expand Down Expand Up @@ -427,7 +443,18 @@ def anonymize(self):
"""Anonymize the user."""
self.dek = None
self.is_active = False
self.save(update_fields=["dek", "is_active"])
self._username_hash = ""
self._email_hash = ""
self._first_name_hash = ""
self.save(
update_fields=[
"dek",
"is_active",
"_username_hash",
"_email_hash",
"_first_name_hash",
]
)

# self.userprofile.google_refresh_token = None
# self.userprofile.google_sub = None
Expand Down
Loading