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 cef27607f836d284b3dc2cd00f3945a7e903d995 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 | 19 ++++++++++++++----- Allura/allura/tests/model/test_auth.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 9 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 f63100179..399cd3f5e 100644 --- a/Allura/allura/model/auth.py +++ b/Allura/allura/model/auth.py @@ -38,8 +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 -from ming.odm import FieldProperty, RelationProperty, ForeignIdProperty, DecryptedProperty +from ming.odm import session, state, MapperExtension +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 @@ -243,8 +243,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 @@ -258,6 +258,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 @@ -302,7 +309,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( @@ -609,6 +616,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 54d25b828..2a570c1c3 100644 --- a/Allura/allura/tests/model/test_auth.py +++ b/Allura/allura/tests/model/test_auth.py @@ -415,6 +415,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])
