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/05/11 20:11:36 UTC
(allura) 02/04: [#8604] Implement field level encryption for User's display_name_field
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch cc/11102-part2a
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 02e3d1d530527e867225805e01e0a19277c35fbe
Author: Carlos Cruz <ca...@slashdotmedia.com>
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 6cd02d63c..21abe14d9 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -1850,10 +1850,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 1f44966e5..dcd2d151e 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
-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
@@ -234,8 +234,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
@@ -249,6 +249,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
@@ -293,7 +300,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(
@@ -597,6 +604,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 2496c8ab1..4244feada 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -404,6 +404,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])