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/07 20:09:47 UTC

(allura) 02/02: [#8613] Implement field level encryption for user preferences email address

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

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

commit e13496f8d2ecad3bca5059832f761b31303e7116
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Tue Jul 7 20:08:57 2026 +0000

    [#8613] Implement field level encryption for user preferences email address
---
 Allura/allura/model/auth.py            | 26 +++-----------------------
 Allura/allura/tests/model/test_auth.py |  8 ++++----
 2 files changed, 7 insertions(+), 27 deletions(-)

diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 1ee866005..7cf8618d7 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -38,7 +38,8 @@
 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, MapperExtension
+from ming.encryption import NestedEncryptedProperty
+from ming.odm import session, state
 from ming.odm import FieldProperty, RelationProperty, ForeignIdProperty, DecryptedProperty, DecryptedListProperty
 from ming.odm.declarative import MappedClass
 from ming.odm.odmsession import ThreadLocalODMSession
@@ -266,15 +267,6 @@ def get_decrypted(self, instance):
         return super().__get__(instance, type(instance))
 
 
-class UserPreferencesMapperExtension(MapperExtension):
-
-    def before_insert(self, obj, state, sess):
-        obj.sync_preference_email_address_encrypted()
-
-    def before_update(self, obj, state, sess):
-        obj.sync_preference_email_address_encrypted()
-
-
 class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     SALT_LEN = 8
 
@@ -283,7 +275,6 @@ class __mongometa__:
         session = main_orm_session
         indexes = ['tool_data.AuthPasswordReset.hash']
         unique_indexes = ['username']
-        extensions = [UserPreferencesMapperExtension]
         custom_indexes = [
             dict(fields=('tool_data.phone_verification.number_hash',), sparse=True),
         ]
@@ -309,9 +300,8 @@ class __mongometa__:
     pending = FieldProperty(bool, if_missing=False)
 
     # Don't use these directly, use get/set_pref() instead
-    preferences = FieldProperty(dict(
+    preferences = NestedEncryptedProperty(dict(
         results_per_page=int,
-        email_address=str,
         email_address_encrypted=S.Binary,
         email_format=str,
         disable_user_messages=bool,
@@ -799,16 +789,6 @@ def set_tool_data(self, tool, **kw):
         d.update(kw)
         state(self).soil()
 
-    @classmethod
-    def encrypt_preference_email_address(cls, email_address):
-        return cls.encr(email_address)
-
-    def sync_preference_email_address_encrypted(self):
-        preferences = dict(self.preferences or {})
-        preferences['email_address_encrypted'] = type(self).encrypt_preference_email_address(
-            preferences.get('email_address'))
-        self.preferences = preferences
-
     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 9ac8a0966..46816fc15 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -467,7 +467,7 @@ def test_email_addresses_encrypted_is_synced_on_update(self):
         assert 'email_addresses' not in state(user).document
         assert state(user).document['email_addresses_encrypted'] == expected_encrypted
 
-    def test_preference_email_address_encrypted_is_populated_on_creation(self):
+    def test_preference_email_address_is_stored_encrypted_on_creation(self):
         user = M.User(
             username='preference-email-address-encrypted-create-test',
             preferences={'email_address': 'primary@example.com'})
@@ -475,11 +475,11 @@ def test_preference_email_address_encrypted_is_populated_on_creation(self):
 
         raw_preferences = state(user).document['preferences']
         assert user.get_pref('email_address') == 'primary@example.com'
-        assert raw_preferences['email_address'] == 'primary@example.com'
+        assert 'email_address' not in raw_preferences
         assert raw_preferences['email_address_encrypted'] == M.User.encr('primary@example.com')
         assert M.User.decr(raw_preferences['email_address_encrypted']) == 'primary@example.com'
 
-    def test_preference_email_address_encrypted_is_synced_on_update(self):
+    def test_preference_email_address_updates_encrypted_field(self):
         user = M.User(
             username='preference-email-address-encrypted-update-test',
             preferences={'email_address': 'old@example.com'})
@@ -490,7 +490,7 @@ def test_preference_email_address_encrypted_is_synced_on_update(self):
 
         raw_preferences = state(user).document['preferences']
         assert user.get_pref('email_address') == 'new@example.com'
-        assert raw_preferences['email_address'] == 'new@example.com'
+        assert 'email_address' not in raw_preferences
         assert raw_preferences['email_address_encrypted'] == M.User.encr('new@example.com')
         assert M.User.decr(raw_preferences['email_address_encrypted']) == 'new@example.com'