You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by cc...@apache.org on 2026/07/24 15:02:27 UTC
(allura) 01/03: [#8614] Add backing encrypted fields for User and UserLoginDetails fields
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch cc/8614-part1
in repository https://gitbox.apache.org/repos/asf/allura.git
commit c48e50764e2c946bb47224d91b7e3c0e9b9f658e
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Mon Jul 20 12:19:07 2026 -0600
[#8614] Add backing encrypted fields for User and UserLoginDetails fields
---
Allura/allura/model/auth.py | 48 ++++++++++++++++++-
Allura/allura/tests/model/test_auth.py | 88 ++++++++++++++++++++++++++++++++++
2 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 1ee866005..43c69ac45 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -275,6 +275,15 @@ def before_update(self, obj, state, sess):
obj.sync_preference_email_address_encrypted()
+class UserFieldEncryptionMapperExtension(MapperExtension):
+
+ def before_insert(self, obj, state, sess):
+ obj.sync_personal_data_encrypted()
+
+ def before_update(self, obj, state, sess):
+ obj.sync_personal_data_encrypted()
+
+
class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
SALT_LEN = 8
@@ -283,7 +292,7 @@ class __mongometa__:
session = main_orm_session
indexes = ['tool_data.AuthPasswordReset.hash']
unique_indexes = ['username']
- extensions = [UserPreferencesMapperExtension]
+ extensions = [UserPreferencesMapperExtension, UserFieldEncryptionMapperExtension]
custom_indexes = [
dict(fields=('tool_data.phone_verification.number_hash',), sparse=True),
]
@@ -342,8 +351,13 @@ class __mongometa__:
end_date=S.DateTime)])
# Additional contacts
- socialnetworks = FieldProperty([dict(socialnetwork=str, accounturl=str)])
+ socialnetworks = FieldProperty([dict(
+ socialnetwork=str,
+ accounturl=str,
+ accounturl_encrypted=S.Binary,
+ )])
telnumbers = FieldProperty([str])
+ telnumbers_encrypted = FieldProperty([S.Binary], if_missing=[])
webpages = FieldProperty([str])
# Skills list
@@ -357,11 +371,27 @@ class __mongometa__:
last_access = FieldProperty(dict(
login_date=S.DateTime,
login_ip=str,
+ login_ip_encrypted=S.Binary,
login_ua=str,
session_date=S.DateTime,
session_ip=str,
+ session_ip_encrypted=S.Binary,
session_ua=str))
+ def sync_personal_data_encrypted(self):
+ last_access = dict(self.last_access or {})
+ for field_name in ('login_ip', 'session_ip'):
+ value = last_access.get(field_name)
+ last_access[f'{field_name}_encrypted'] = self.encr(value)
+ self.last_access = last_access
+
+ socialnetworks = [dict(socialnetwork) for socialnetwork in self.socialnetworks or []]
+ for socialnetwork in socialnetworks:
+ socialnetwork['accounturl_encrypted'] = self.encr(socialnetwork.get('accounturl'))
+ self.socialnetworks = socialnetworks
+
+ self.telnumbers_encrypted = [self.encr(telnumber) for telnumber in self.telnumbers or []]
+
def __repr__(self):
try:
display_name = self.display_name
@@ -1209,6 +1239,15 @@ def comment_user(cls, by, message, *args, **kwargs):
return cls.log_user(message, *args, **kwargs)
+class UserLoginDetailsEncryptionMapperExtension(MapperExtension):
+
+ def before_insert(self, obj, state, sess):
+ obj.sync_ip_encrypted()
+
+ def before_update(self, obj, state, sess):
+ obj.sync_ip_encrypted()
+
+
class UserLoginDetails(MappedClass):
"""
Store unique entries for users' previous login details. And other trusted actions like successful password reset
@@ -1222,15 +1261,20 @@ class __mongometa__:
indexes = ['user_id']
unique_indexes = [('user_id', 'ip', 'ua'), # DuplicateKeyError checked in add_login_detail
]
+ extensions = [UserLoginDetailsEncryptionMapperExtension]
query: Query[UserLoginDetails]
_id = FieldProperty(S.ObjectId)
user_id: ObjectId = AlluraUserProperty(required=True)
ip = FieldProperty(str)
+ ip_encrypted = FieldProperty(S.Binary, if_missing=None)
ua = FieldProperty(str)
extra = FieldProperty({
str: S.Anything
})
user = RelationProperty('User')
+
+ def sync_ip_encrypted(self):
+ self.ip_encrypted = self.encr(self.ip)
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index 9ac8a0966..c27ef0b3e 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -494,6 +494,94 @@ def test_preference_email_address_encrypted_is_synced_on_update(self):
assert raw_preferences['email_address_encrypted'] == M.User.encr('new@example.com')
assert M.User.decr(raw_preferences['email_address_encrypted']) == 'new@example.com'
+ def test_personal_data_encrypted_fields_are_populated_on_creation(self):
+ user = M.User(
+ username='personal-data-encrypted-create-test',
+ last_access={
+ 'login_ip': '192.0.2.1',
+ 'session_ip': '198.51.100.1',
+ },
+ socialnetworks=[
+ {'socialnetwork': 'Mastodon', 'accounturl': 'https://example.com/@user'},
+ {'socialnetwork': 'Other', 'accounturl': None},
+ ],
+ telnumbers=['+1-555-0100', None],
+ )
+ ThreadLocalODMSession.flush_all()
+
+ document = state(user).document
+ assert document['last_access']['login_ip'] == '192.0.2.1'
+ assert document['last_access']['login_ip_encrypted'] == M.User.encr('192.0.2.1')
+ assert document['last_access']['session_ip'] == '198.51.100.1'
+ assert document['last_access']['session_ip_encrypted'] == M.User.encr('198.51.100.1')
+ assert document['socialnetworks'][0]['accounturl'] == 'https://example.com/@user'
+ assert document['socialnetworks'][0]['accounturl_encrypted'] == M.User.encr(
+ 'https://example.com/@user')
+ assert document['socialnetworks'][1]['accounturl_encrypted'] is None
+ assert document['telnumbers'] == ['+1-555-0100', None]
+ assert user.telnumbers_encrypted == [M.User.encr('+1-555-0100'), None]
+
+ def test_personal_data_encrypted_fields_are_synced_on_update(self):
+ user = M.User(
+ username='personal-data-encrypted-update-test',
+ last_access={
+ 'login_ip': '192.0.2.2',
+ 'session_ip': '198.51.100.2',
+ },
+ socialnetworks=[
+ {'socialnetwork': 'Mastodon', 'accounturl': 'https://old.example/@user'},
+ {'socialnetwork': 'Other', 'accounturl': 'https://remove.example/user'},
+ ],
+ telnumbers=['+1-555-0101', '+1-555-0102'],
+ )
+ ThreadLocalODMSession.flush_all()
+
+ assert user.telnumbers_encrypted == [
+ M.User.encr('+1-555-0101'),
+ M.User.encr('+1-555-0102'),
+ ]
+ user.last_access['login_ip'] = '203.0.113.2'
+ user.last_access['session_ip'] = None
+ user.socialnetworks[0]['accounturl'] = 'https://new.example/@user'
+ user.socialnetworks.pop()
+ user.telnumbers.remove('+1-555-0101')
+ user.telnumbers.append('+1-555-0103')
+ ThreadLocalODMSession.flush_all()
+
+ document = state(user).document
+ assert document['last_access']['login_ip'] == '203.0.113.2'
+ assert document['last_access']['login_ip_encrypted'] == M.User.encr('203.0.113.2')
+ assert document['last_access']['session_ip'] is None
+ assert document['last_access']['session_ip_encrypted'] is None
+ assert len(document['socialnetworks']) == 1
+ assert document['socialnetworks'][0]['accounturl'] == 'https://new.example/@user'
+ assert document['socialnetworks'][0]['accounturl_encrypted'] == M.User.encr(
+ 'https://new.example/@user')
+ assert document['telnumbers'] == ['+1-555-0102', '+1-555-0103']
+ assert user.telnumbers_encrypted == [
+ M.User.encr('+1-555-0102'),
+ M.User.encr('+1-555-0103'),
+ ]
+
+ def test_user_login_details_ip_encrypted_is_synced(self):
+ detail = M.UserLoginDetails(
+ user_id=c.user._id,
+ ip='192.0.2.3',
+ ua='PersonalDataEncryptionTest/1.0',
+ )
+ session(detail).flush(detail)
+
+ assert detail.ip == '192.0.2.3'
+ assert detail.ip_encrypted == M.UserLoginDetails.encr('192.0.2.3')
+ assert state(detail).document['ip'] == '192.0.2.3'
+
+ detail.ip = '203.0.113.3'
+ session(detail).flush(detail)
+
+ assert detail.ip == '203.0.113.3'
+ assert detail.ip_encrypted == M.UserLoginDetails.encr('203.0.113.3')
+ assert state(detail).document['ip'] == '203.0.113.3'
+
def test_set_display_name_pref_updates_encrypted_field_and_cache(self):
user = M.User(username='display-name-cache-test',
display_name='Original Display Name')