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

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

commit 2d845f178dff233012c71b0ec3381a5ce9f15da8
Author: Carlos Cruz <[email protected]>
AuthorDate: Fri Apr 24 11:23:07 2026 -0600

    [#8604] Implement field level encryption for User's display_name_field
---
 Allura/allura/lib/plugin.py            |  7 +++----
 Allura/allura/model/auth.py            | 17 +++++++++++++----
 Allura/allura/tests/model/test_auth.py | 31 +++++++++++++++++++++++++++++++
 3 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index ac21d66df..2e4e59956 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -1851,10 +1851,9 @@ def get_pref(self, user, pref_name):
         if pref_name in user.preferences:
             pref_value = user.preferences[pref_name]
         elif pref_name == 'display_name':
-            # get the value directly from ming's internals, bypassing
-            # FieldPropertyDisplayName which always calls back to this get_pref
-            # method (infinite recursion)
-            pref_value = user.__dict__['__ming__'].state.document.display_name
+            # bypass FieldPropertyDisplayName.__get__, which always calls
+            # back to this get_pref method (infinite recursion)
+            pref_value = type(user).display_name.get_decrypted(user)
         else:
             pref_value = getattr(user, pref_name)
 
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 1dfce31f9..59ee9fe18 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -39,7 +39,7 @@
 from tg import request
 from ming import schema as S
 from ming.odm import session, state, MapperExtension
-from ming.odm import FieldProperty, RelationProperty, ForeignIdProperty
+from ming.odm import DecryptedProperty, FieldProperty, RelationProperty, 
ForeignIdProperty
 from ming.odm.declarative import MappedClass
 from ming.odm.odmsession import ThreadLocalODMSession
 from ming.utils import LazyProperty
@@ -250,8 +250,8 @@ def get_next_uid(cls):
         return g.next_uid
 
 
-class FieldPropertyDisplayName(FieldProperty):
-    # display_name is mongo field but only for preference storage
+class FieldPropertyDisplayName(DecryptedProperty):
+    # display_name is stored encrypted in mongo but only for preference storage
     # force all requests for this field to use the get_pref mechanism
     # Cache it per user, since it may be re-used several times in a request
     # and non-local preferences (ldap, database, etc) can be relatively 
expensive
@@ -265,6 +265,13 @@ def __get__(self, instance, cls=None):
             display_name = instance._cache_display_name = 
instance.get_pref('display_name')
         return display_name
 
+    def __set__(self, instance, value):
+        instance.__dict__.pop('_cache_display_name', None)
+        super().__set__(instance, value)
+
+    def get_decrypted(self, instance):
+        return super().__get__(instance, type(instance))
+
 
 class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     SALT_LEN = 8
@@ -309,7 +316,7 @@ class __mongometa__:
     ))
     # Additional top-level fields can/should be accessed with get/set_pref also
     # Not sure why we didn't put them within the 'preferences' dictionary :(
-    display_name: str = FieldPropertyDisplayName(str)
+    display_name: str = FieldPropertyDisplayName(str, 'display_name_encrypted')
     display_name_encrypted = FieldProperty(S.Binary, if_missing=None)
     # Personal data
     sex = FieldProperty(
@@ -616,6 +623,8 @@ def get_pref(self, pref_name):
 
     def set_pref(self, pref_name, pref_value):
         ret = plugin.UserPreferencesProvider.get().set_pref(self, pref_name, 
pref_value)
+        if pref_name == 'display_name':
+            self.__dict__.pop('_cache_display_name', None)
         state(self).soil()  # to make sure it gets reindexed in solr (even if 
pref is not part on the User ming model)
         return ret
 
diff --git a/Allura/allura/tests/model/test_auth.py 
b/Allura/allura/tests/model/test_auth.py
index 839b9912e..837013066 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -412,6 +412,37 @@ def test_user_index(self):
         # provided bby auth provider
         assert 'user_registration_date_dt' in idx
 
+    def test_display_name_is_stored_encrypted(self):
+        user = M.User(username='encrypted-display-name-test',
+                      display_name='Encrypted Display Name')
+        ThreadLocalODMSession.flush_all()
+
+        assert user.display_name == 'Encrypted Display Name'
+        assert user.get_pref('display_name') == 'Encrypted Display Name'
+        assert user.display_name_encrypted
+        assert user.display_name_encrypted != 'Encrypted Display Name'
+        assert 'display_name' not in user.__dict__['__ming__'].state.document
+
+        user = M.User.query.get(
+            display_name_encrypted=M.User.encr('Encrypted Display Name'))
+        assert user.username == 'encrypted-display-name-test'
+
+    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')
+        ThreadLocalODMSession.flush_all()
+        original_encrypted_display_name = user.display_name_encrypted
+
+        assert user.display_name == 'Original Display Name'
+        assert user._cache_display_name == 'Original Display Name'
+
+        user.set_pref('display_name', 'Updated Display Name')
+
+        assert not hasattr(user, '_cache_display_name')
+        assert user.display_name_encrypted != original_encrypted_display_name
+        assert user.display_name == 'Updated Display Name'
+        assert user.get_pref('display_name') == 'Updated Display Name'
+
     def test_user_index_none_values(self):
         c.user.email_addresses = [None]
         c.user.set_pref('telnumbers', [None])

Reply via email to