This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch cc/8618-part1
in repository https://gitbox.apache.org/repos/asf/allura.git

commit de898ffd94494b9734d88d1e87396c8895190905
Author: Carlos Cruz <[email protected]>
AuthorDate: Tue Jun 16 12:56:24 2026 -0600

    [#8610] Add encrypted field for Users email addresses
---
 Allura/allura/model/auth.py            | 20 ++++++++++++++++++-
 Allura/allura/tests/model/test_auth.py | 35 ++++++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 05322ca82..8bf930cae 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -38,7 +38,7 @@
 from tg import tmpl_context as c, app_globals as g
 from tg import request
 from ming import schema as S
-from ming.odm import session, state
+from ming.odm import session, state, MapperExtension
 from ming.odm import FieldProperty, RelationProperty, ForeignIdProperty, 
DecryptedProperty
 from ming.odm.declarative import MappedClass
 from ming.odm.odmsession import ThreadLocalODMSession
@@ -264,6 +264,15 @@ def __set__(self, instance, value):
         instance.display_name_encrypted = 
type(instance).encr(state(instance).document[self.name])
 
 
+class UserEmailAddressesMapperExtension(MapperExtension):
+
+    def before_insert(self, obj, state, sess):
+        obj.sync_email_addresses_encrypted()
+
+    def before_update(self, obj, state, sess):
+        obj.sync_email_addresses_encrypted()
+
+
 class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     SALT_LEN = 8
 
@@ -272,6 +281,7 @@ class __mongometa__:
         session = main_orm_session
         indexes = ['tool_data.AuthPasswordReset.hash']
         unique_indexes = ['username']
+        extensions = [UserEmailAddressesMapperExtension]
         custom_indexes = [
             dict(fields=('tool_data.phone_verification.number_hash',), 
sparse=True),
         ]
@@ -284,6 +294,7 @@ class __mongometa__:
     sfx_userid = FieldProperty(S.Deprecated)
     username = FieldProperty(str)
     email_addresses = FieldProperty([str])
+    email_addresses_encrypted = FieldProperty([S.Binary], if_missing=[])
     password = FieldProperty(str)  # hashed
     last_password_updated = FieldProperty(datetime)  # to access, use 
AuthProvider's get_last_password_updated
     password_algorithm = FieldProperty(str)
@@ -781,6 +792,13 @@ def set_tool_data(self, tool, **kw):
         d.update(kw)
         state(self).soil()
 
+    @classmethod
+    def encrypt_email_addresses(cls, email_addresses):
+        return [cls.encr(addr) if addr is not None else None for addr in 
email_addresses or []]
+
+    def sync_email_addresses_encrypted(self):
+        self.email_addresses_encrypted = 
type(self).encrypt_email_addresses(self.email_addresses)
+
     def address_object(self, addr):
         return EmailAddress.get(email=addr, claimed_by_user_id=self._id)
 
diff --git a/Allura/allura/tests/model/test_auth.py 
b/Allura/allura/tests/model/test_auth.py
index a035f4466..bad3e1e86 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -428,6 +428,41 @@ def 
test_display_name_encrypted_is_populated_on_creation(self):
         assert M.User.decr(user.display_name_encrypted) == 'Encrypted Display 
Name'
         assert user.__dict__['__ming__'].state.document.display_name == 
'Encrypted Display Name'
 
+    def test_email_addresses_encrypted_is_populated_on_creation(self):
+        user = M.User(
+            username='email-addresses-encrypted-create-test',
+            email_addresses=['[email protected]', None, 
'[email protected]'])
+        ThreadLocalODMSession.flush_all()
+
+        expected_encrypted = [
+            M.User.encr('[email protected]'),
+            None,
+            M.User.encr('[email protected]'),
+        ]
+        assert user.email_addresses == ['[email protected]', None, 
'[email protected]']
+        assert user.email_addresses_encrypted == expected_encrypted
+        assert state(user).document['email_addresses'] == 
['[email protected]', None, '[email protected]']
+        assert state(user).document['email_addresses_encrypted'] == 
expected_encrypted
+
+    def test_email_addresses_encrypted_is_synced_on_update(self):
+        user = M.User(
+            username='email-addresses-encrypted-update-test',
+            email_addresses=['[email protected]', '[email protected]'])
+        ThreadLocalODMSession.flush_all()
+
+        user.email_addresses.append('[email protected]')
+        user.email_addresses.remove('[email protected]')
+        ThreadLocalODMSession.flush_all()
+
+        expected_encrypted = [
+            M.User.encr('[email protected]'),
+            M.User.encr('[email protected]'),
+        ]
+        assert user.email_addresses == ['[email protected]', '[email protected]']
+        assert user.email_addresses_encrypted == expected_encrypted
+        assert state(user).document['email_addresses'] == ['[email protected]', 
'[email protected]']
+        assert state(user).document['email_addresses_encrypted'] == 
expected_encrypted
+
     def test_set_display_name_pref_updates_encrypted_field_and_cache(self):
         user = M.User(
             username='display-name-cache-test',

Reply via email to