Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 6dd1f48

Browse files
Add email hash field to simplify GDPR deletions
1 parent 94c227a commit 6dd1f48

File tree

6 files changed

+115
-1
lines changed

6 files changed

+115
-1
lines changed

‎docs/customizing.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ API documentation
119119

120120
.. autoattribute:: mailauth.contrib.user.models.AbstractEmailUser.email
121121
:noindex:
122+
.. autoattribute:: mailauth.contrib.user.models.AbstractEmailUser.email_hash
123+
:noindex:
122124
.. autoattribute:: mailauth.contrib.user.models.AbstractEmailUser.session_salt
123125
:noindex:
124126

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from django.db import migrations, models
2+
3+
try:
4+
from django.contrib.postgres.fields import CIEmailField
5+
except ImportError:
6+
CIEmailField = models.EmailField
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("mailauth_user", "0004_auto_20200812_0722"),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name="emailuser",
18+
name="email_hash",
19+
field=models.TextField(db_index=True, null=True),
20+
),
21+
migrations.AlterField(
22+
model_name="emailuser",
23+
name="email",
24+
field=CIEmailField(
25+
blank=True,
26+
db_index=True,
27+
max_length=254,
28+
null=True,
29+
unique=True,
30+
verbose_name="email address",
31+
),
32+
),
33+
migrations.RunSQL(
34+
"UPDATE mailauth_user_emailuser SET email_hash = md5(email)",
35+
"UPDATE mailauth_user_emailuser SET email_hash = NULL",
36+
),
37+
migrations.AlterField(
38+
model_name="emailuser",
39+
name="email_hash",
40+
field=models.TextField(db_index=True, default=0, max_length=16),
41+
),
42+
]

‎mailauth/contrib/user/models.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import hashlib
2+
13
from django.conf import settings
24
from django.contrib.auth.base_user import BaseUserManager
35
from django.contrib.auth.models import AbstractUser
@@ -18,6 +20,7 @@ def _create_user(self, email, **extra_fields):
1820
"""Create and save a user with the given email."""
1921
email = self.normalize_email(email)
2022
user = self.model(email=email, **extra_fields)
23+
user.email_hash = hashlib.md5(email.lower().encode()).hexdigest()
2124
user.save(using=self._db)
2225
return user
2326

@@ -50,9 +53,24 @@ class AbstractEmailUser(AbstractUser):
5053
username = None
5154
password = None
5255

53-
email = CIEmailField(_("email address"), unique=True, db_index=True)
56+
email = CIEmailField(
57+
_("email address"), blank=True, null=True, unique=True, db_index=True
58+
)
5459
"""Unique and case insensitive to serve as a better username."""
5560

61+
email_hash = models.TextField(db_index=True, editable=False)
62+
"""
63+
A hash of the email address, to erase the email address without loosing the user.
64+
65+
GDPR may require us to erase the email address, yet we still want to be able to
66+
retain non-personal information for statistical or other legal purposes. We may
67+
also want to be able to authenticate users without ever storing the email address.
68+
69+
A hash, being non-reversible in nature, means that the personal information is
70+
not stored and you may only processes personal information during runtime for
71+
the explicit purpose of authenticating the user.
72+
"""
73+
5674
session_salt = models.CharField(
5775
max_length=12,
5876
editable=False,
@@ -68,6 +86,19 @@ def has_usable_password(self):
6886
class Meta(AbstractUser.Meta):
6987
abstract = True
7088

89+
def __init__(self, *args, **kwargs):
90+
super().__init__(*args, **kwargs)
91+
self._email = self.email
92+
93+
def save(self, *args, **kwargs):
94+
if self.email and ((self._email != self.email) or not self.pk):
95+
self.email_hash = hashlib.md5(self.email.lower().encode()).digest()
96+
try:
97+
kwargs["update_fields"] = {*kwargs.pop("update_fields"), "email_hash"}
98+
except KeyError:
99+
pass
100+
super().save(*args, **kwargs)
101+
71102
def _legacy_get_session_auth_hash(self):
72103
# RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
73104
key_salt = "mailauth.contrib.user.models.EmailUserManager.get_session_auth_hash"

‎mailauth/forms.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import urllib
23

34
from django import forms
@@ -104,6 +105,10 @@ def __init__(self, request, *args, **kwargs):
104105
self.fields[self.field_name] = field
105106

106107
def get_users(self, email=None):
108+
if self.field_name == "email" and hasattr(get_user_model(), "email_hash"):
109+
email_hash = hashlib.md5(email.lower().encode()).hexdigest()
110+
return get_user_model().objects.filter(email_hash=email_hash).iterator()
111+
107112
if connection.vendor == "postgresql":
108113
query = {self.field_name: email}
109114
else:

‎tests/contrib/auth/test_models.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ def test_password_field(self):
6060
with pytest.raises(FieldDoesNotExist):
6161
user.password
6262

63+
def test_init(self):
64+
user = EmailUser(email="spiderman@avengers.com")
65+
assert user._email == "spiderman@avengers.com"
66+
user.email = "ironman@avengers.com"
67+
assert user._email == "spiderman@avengers.com"
68+
69+
@pytest.mark.django_db
70+
def test_save(self):
71+
user = EmailUser(email="spiderman@avengers.com")
72+
assert user._email == "spiderman@avengers.com"
73+
user.save()
74+
assert (
75+
user.email_hash
76+
== b"\xac\xbf \xaf,\xa5\xf6\xe3\xe6\xb5\xe0\x88\xd8\xe9\x96\x81"
77+
)
78+
79+
@pytest.mark.django_db
80+
def test_save__update_fields(self):
81+
user = EmailUser(email="spiderman@avengers.com")
82+
assert user._email == "spiderman@avengers.com"
83+
user.save()
84+
assert (
85+
user.email_hash
86+
== b"\xac\xbf \xaf,\xa5\xf6\xe3\xe6\xb5\xe0\x88\xd8\xe9\x96\x81"
87+
)
88+
user.email = "ironman@avengers.com"
89+
user.save(update_fields=["email"])
90+
assert user.email_hash == b"7\x12\x0b\x80V\xf5\xdeUi\xcb(v\xac\xf9\xf5 "
91+
6392

6493
class TestEmailUserManager:
6594
def test_create_user(self, db):

‎tests/test_forms.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ def test_get_users(self, db, user):
3838
assert list(EmailLoginForm(request=None).get_users("spiderman@avengers.com"))
3939
assert list(EmailLoginForm(request=None).get_users("SpiderMan@Avengers.com"))
4040
assert not list(EmailLoginForm(request=None).get_users("SpiderMan@dc.com"))
41+
42+
def test_get_users__no_hash(self, db, user):
43+
form = EmailLoginForm(request=None)
44+
form.field_name = "pk"
45+
assert list(form.get_users(user.pk))

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /