Skip to content

Commit c7662c1

Browse files
committed
PLAT-1841 adding new fields to store encrypted data on backend
1 parent 4f8bf57 commit c7662c1

11 files changed

+201
-12
lines changed

poms/clients/filters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ class ClientsFilterSet(FilterSet):
1515
short_name = CharFilter()
1616
public_name = CharFilter()
1717
first_name = CharFilter()
18+
first_name_hash = CharFilter()
1819
last_name = CharFilter()
20+
last_name_hash = CharFilter()
1921
telephone = CharFilter()
22+
telephone_hash = CharFilter()
2023
email = CharFilter()
24+
email_hash = CharFilter()
2125
portfolios = CharFilter(field_name="portfolios__user_code", lookup_expr="icontains")
2226
client_secrets = CharFilter(
2327
field_name="client_secrets__user_code", lookup_expr="icontains"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 4.2.20 on 2025-05-22 14:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('clients', '0007_alter_clientsecret_unique_together'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='client',
15+
name='email',
16+
field=models.TextField(blank=True, help_text='Email address of client', null=True),
17+
),
18+
migrations.AlterField(
19+
model_name='client',
20+
name='first_name',
21+
field=models.TextField(blank=True, help_text='First name of client', null=True),
22+
),
23+
migrations.AlterField(
24+
model_name='client',
25+
name='last_name',
26+
field=models.TextField(blank=True, help_text='Last name of client', null=True),
27+
),
28+
migrations.AlterField(
29+
model_name='client',
30+
name='telephone',
31+
field=models.TextField(blank=True, help_text='Telephone number of client', null=True),
32+
),
33+
]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 4.2.20 on 2025-05-22 14:25
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('clients', '0008_alter_client_email_alter_client_first_name_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='client',
15+
name='email_hash',
16+
field=models.CharField(blank=True, help_text='SHA-256 hash of the email address', max_length=64, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='client',
20+
name='first_name_hash',
21+
field=models.CharField(blank=True, help_text='SHA-256 hash of the first name', max_length=64, null=True),
22+
),
23+
migrations.AddField(
24+
model_name='client',
25+
name='last_name_hash',
26+
field=models.CharField(blank=True, help_text='SHA-256 hash of the last name', max_length=64, null=True),
27+
),
28+
migrations.AddField(
29+
model_name='client',
30+
name='telephone_hash',
31+
field=models.CharField(blank=True, help_text='SHA-256 hash of the telephone number', max_length=64, null=True),
32+
),
33+
]

poms/clients/models.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,50 @@ class Client(NamedModel):
1212
verbose_name=gettext_lazy("master user"),
1313
on_delete=models.CASCADE,
1414
)
15-
first_name = models.CharField(
16-
max_length=255,
15+
first_name = models.TextField(
1716
blank=True,
1817
null=True,
1918
help_text="First name of client",
2019
)
21-
last_name = models.CharField(
22-
max_length=255,
20+
first_name_hash = models.CharField(
21+
max_length=64,
22+
blank=True,
23+
null=True,
24+
help_text="SHA-256 hash of the first name",
25+
)
26+
last_name = models.TextField(
2327
blank=True,
2428
null=True,
2529
help_text="Last name of client",
2630
)
27-
telephone = models.CharField(
28-
max_length=255,
31+
last_name_hash = models.CharField(
32+
max_length=64,
33+
blank=True,
34+
null=True,
35+
help_text="SHA-256 hash of the last name",
36+
)
37+
telephone = models.TextField(
2938
blank=True,
3039
null=True,
3140
help_text="Telephone number of client",
3241
)
33-
email = models.EmailField(
34-
max_length=255,
42+
telephone_hash = models.CharField(
43+
max_length=64,
44+
blank=True,
45+
null=True,
46+
help_text="SHA-256 hash of the telephone number",
47+
)
48+
email = models.TextField(
3549
blank=True,
3650
null=True,
3751
help_text="Email address of client",
3852
)
53+
email_hash = models.CharField(
54+
max_length=64,
55+
blank=True,
56+
null=True,
57+
help_text="SHA-256 hash of the email address",
58+
)
3959

4060
class Meta(NamedModel.Meta):
4161
verbose_name = gettext_lazy("client")

poms/clients/serializers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ class Meta:
4444
"short_name",
4545
"public_name",
4646
"first_name",
47+
"first_name_hash",
4748
"last_name",
49+
"last_name_hash",
4850
"telephone",
51+
"telephone_hash",
4952
"email",
53+
"email_hash",
5054
"notes",
5155
"portfolios",
5256
"portfolios_object",

poms/users/fields.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib.auth.models import User
22
from rest_framework import serializers
33
from rest_framework.fields import CurrentUserDefault
4+
import base64
45

56
from poms_app import settings
67

@@ -100,3 +101,18 @@ class RoleField(UserCodeOrPrimaryKeyRelatedField):
100101

101102
class AccessPolicyField(UserCodeOrPrimaryKeyRelatedField):
102103
queryset = AccessPolicy.objects.all()
104+
105+
106+
class Base64BinaryField(serializers.Field):
107+
def to_representation(self, value):
108+
if value is None:
109+
return None
110+
return base64.b64encode(value).decode('utf-8')
111+
112+
def to_internal_value(self, data):
113+
if data is None:
114+
return None
115+
try:
116+
return base64.b64decode(data)
117+
except Exception as e:
118+
raise serializers.ValidationError('Invalid Base64-encoded data.')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 4.2.20 on 2025-05-22 11:47
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('users', '0015_alter_ecosystemdefault_managers'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='member',
16+
name='public_key',
17+
field=models.TextField(blank=True, null=True, verbose_name='public key'),
18+
),
19+
migrations.CreateModel(
20+
name='MasterUserSymmetricKey',
21+
fields=[
22+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23+
('encrypted_key', models.BinaryField(help_text='Store AES key encrypted by member public key')),
24+
('master_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='symmetric_keys', to='users.masteruser')),
25+
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_keys', to='users.member')),
26+
],
27+
),
28+
]

poms/users/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,8 @@ class Member(FakeDeletableModel):
12401240
default="active",
12411241
)
12421242

1243+
public_key = models.TextField(null=True, blank=True, verbose_name="public key") # PEM format
1244+
12431245
@property
12441246
def data(self):
12451247
if not self.json_data:
@@ -1305,6 +1307,20 @@ def display_name(self):
13051307
return self.username
13061308

13071309

1310+
class MasterUserSymmetricKey(models.Model):
1311+
master_user = models.ForeignKey(
1312+
MasterUser,
1313+
on_delete=models.CASCADE,
1314+
related_name="symmetric_keys"
1315+
)
1316+
member = models.ForeignKey(
1317+
Member,
1318+
on_delete=models.CASCADE,
1319+
related_name="workspace_keys"
1320+
)
1321+
encrypted_key = models.BinaryField(help_text="Store AES key encrypted by member public key")
1322+
1323+
13081324
class OtpToken(models.Model):
13091325
user = models.ForeignKey(
13101326
settings.AUTH_USER_MODEL,

poms/users/serializers.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
HiddenMemberField,
4545
MasterUserField,
4646
MemberField,
47-
RoleField,
47+
RoleField, Base64BinaryField,
4848
)
4949
from poms.users.models import (
5050
TIMEZONE_CHOICES,
@@ -53,7 +53,7 @@
5353
Member,
5454
OtpToken,
5555
UsercodePrefix,
56-
UserProfile,
56+
UserProfile, MasterUserSymmetricKey,
5757
)
5858
from poms.users.utils import (
5959
get_master_user_from_context,
@@ -730,6 +730,18 @@ def update(self, instance, validated_data):
730730
return super().update(instance, validated_data)
731731

732732

733+
class MasterUserSymmetricKeySerializer(serializers.ModelSerializer):
734+
735+
member_object = MemberSerializer(many=True, read_only=True, source="member")
736+
737+
encrypted_key = Base64BinaryField()
738+
739+
class Meta:
740+
model = MasterUserSymmetricKey
741+
fields = ['id', 'master_user', 'member', 'encrypted_key', 'member_object']
742+
read_only_fields = ['id']
743+
744+
733745
class MemberViewSerializer(serializers.ModelSerializer):
734746
class Meta:
735747
model = Member

poms/users/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"ping",
1111
)
1212
router.register(r"user", users.UserViewSet, "user")
13+
router.register(r"symmetric-key", users.MasterUserSymmetricKeyViewSet, "symmetrickey")
1314
router.register(r"master-user", users.MasterUserViewSet, "masteruser")
1415
router.register( # Deprecated at all, no light-method needed
1516
r"master-user-light",

0 commit comments

Comments
 (0)