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])
