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/04/24 20:41:34 UTC

(allura) branch cc/11102-part2 created (now 8f05a758f)

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

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


      at 8f05a758f Remove dead ProjectUserSelect code

This branch includes the following new commits:

     new e23b6731b [#11102] Implement field level encryption for User's display_name_field
     new 8f05a758f Remove dead ProjectUserSelect code

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(allura) 02/02: Remove dead ProjectUserSelect code

Posted by cc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 8f05a758f53a38b12bc30ebf4473a9959e6686c6
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Fri Apr 24 12:37:36 2026 -0600

    Remove dead ProjectUserSelect code
---
 Allura/allura/controllers/project.py               | 22 ------------
 Allura/allura/lib/widgets/form_fields.py           | 39 ----------------------
 .../templates/widgets/project_user_select.html     | 21 ------------
 Allura/allura/tests/functional/test_home.py        | 19 -----------
 .../forgetracker/tests/functional/test_root.py     |  2 +-
 5 files changed, 1 insertion(+), 102 deletions(-)

diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index cf521e59c..23d0d1e74 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -42,7 +42,6 @@
 from allura.controllers.feed import FeedArgs, FeedController
 from allura.controllers.rest import nbhd_lookup_first_path
 from allura.lib.security import require_access
-from allura.lib.security import RoleCache
 from allura.lib.widgets import forms as ff
 from allura.lib.widgets import form_fields as ffw
 from allura.lib.widgets import project_list as plw
@@ -448,27 +447,6 @@ def user_icon(self, **kw):
                 default_image_url = g.forge_static('images/user.png')
             redirect(default_image_url)
 
-    @expose('json:')
-    def user_search(self, term='', **kw):
-        if len(term) < 3:
-            raise exc.HTTPBadRequest('"term" param must be at least length 3')
-        named_roles = RoleCache(
-            g.credentials,
-            g.credentials.project_roles(project_id=c.project.root_project._id).named)
-        users = M.User.query.find({
-            '_id': {'$in': named_roles.userids_that_reach},
-            'display_name': re.compile(r'(?i)%s' % re.escape(term)),
-            'disabled': False,
-            'pending': False,
-        }).sort('username').limit(10).all()
-        return dict(
-            users=[
-                dict(
-                    label='{} ({})'.format(u.get_pref('display_name'), u.username),
-                    value=u.username,
-                    id=u.username)
-                for u in users])
-
     @expose('json:')
     def users(self, **kw):
         users = c.project.users()
diff --git a/Allura/allura/lib/widgets/form_fields.py b/Allura/allura/lib/widgets/form_fields.py
index 95d04311e..c4fa3cd29 100644
--- a/Allura/allura/lib/widgets/form_fields.py
+++ b/Allura/allura/lib/widgets/form_fields.py
@@ -102,45 +102,6 @@ def resources(self):
         ''' % dict(url=c.app.url))
 
 
-class ProjectUserSelect(ew.InputField):
-    template = 'jinja:allura:templates/widgets/project_user_select.html'
-    defaults = dict(
-        ew.InputField.defaults,
-        name=None,
-        value=None,
-        show_label=True,
-        className=None)
-
-    def __init__(self, **kw):
-        super().__init__(**kw)
-        if not isinstance(self.value, list):
-            self.value = [self.value]
-
-    def from_python(self, value, state=None):
-        return value
-
-    def resources(self):
-        yield from super().resources()
-        yield ew.JSLink('allura/js/jquery-ui.min.js', location='body_top_js')
-        yield ew.CSSLink('css/autocomplete.css')  # customized in [6b78ed] so we can't just use jquery-ui.min.css
-        yield onready('''
-          $('input.project_user_select').autocomplete({
-            source: function (request, response) {
-              $.ajax({
-                url: "%suser_search",
-                dataType: "json",
-                data: {
-                  term: request.term
-                },
-                success: function (data) {
-                  response(data.users);
-                }
-              });
-            },
-            minLength: 2
-          });''' % c.project.url())
-
-
 class ProjectUserCombo(ew.SingleSelectField):
     template = 'jinja:allura:templates/widgets/project_user_combo.html'
 
diff --git a/Allura/allura/templates/widgets/project_user_select.html b/Allura/allura/templates/widgets/project_user_select.html
deleted file mode 100644
index 23c434703..000000000
--- a/Allura/allura/templates/widgets/project_user_select.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{#-
-       Licensed to the Apache Software Foundation (ASF) under one
-       or more contributor license agreements.  See the NOTICE file
-       distributed with this work for additional information
-       regarding copyright ownership.  The ASF licenses this file
-       to you under the Apache License, Version 2.0 (the
-       "License"); you may not use this file except in compliance
-       with the License.  You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-       Unless required by applicable law or agreed to in writing,
-       software distributed under the License is distributed on an
-       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-       KIND, either express or implied.  See the License for the
-       specific language governing permissions and limitations
-       under the License.
--#}
-<div>
-  <input type="text" id="{{id}}" name="{{name}}" class="ui-autocomplete-input project_user_select{{className and ' %s' % className or ''}}" value="{{value and value.username or value or ''}}"/>
-</div>
diff --git a/Allura/allura/tests/functional/test_home.py b/Allura/allura/tests/functional/test_home.py
index b98c5ba7d..ec3769b59 100644
--- a/Allura/allura/tests/functional/test_home.py
+++ b/Allura/allura/tests/functional/test_home.py
@@ -163,25 +163,6 @@ def test_user_icon(self):
         r = self.app.get('/u/test-admin/user_icon')
         assert r.content_type == 'image/png'
 
-    def test_user_search(self):
-        r = self.app.get('/p/test/user_search?term=test', status=200)
-        j = json.loads(r.text)
-        assert j['users'][0]['id'].startswith('test')
-
-    def test_user_search_for_disabled_user(self):
-        user = M.User.by_username('test-admin')
-        user.disabled = True
-        ThreadLocalODMSession.flush_all()
-        r = self.app.get('/p/test/user_search?term=test', status=200)
-        j = json.loads(r.text)
-        assert j == {'users': []}
-
-    def test_user_search_noparam(self):
-        self.app.get('/p/test/user_search', status=400)
-
-    def test_user_search_shortparam(self):
-        self.app.get('/p/test/user_search?term=ad', status=400)
-
     def test_users(self):
         r = self.app.get('/p/test/users', status=200)
         j = json.loads(r.text)
diff --git a/ForgeTracker/forgetracker/tests/functional/test_root.py b/ForgeTracker/forgetracker/tests/functional/test_root.py
index fcbb68199..6e123943f 100644
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -270,7 +270,7 @@ def test_deleted_ticket_visibility(self):
 
 class TestFunctionalController(TrackerTestController):
     def test_bad_ticket_number(self):
-        self.app.get('/bugs/input.project_user_select', status=404)
+        self.app.get('/bugs/not-a-ticket', status=404)
 
     def test_invalid_ticket(self):
         self.app.get('/bugs/2/', status=404)


(allura) 01/02: [#11102] Implement field level encryption for User's display_name_field

Posted by cc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit e23b6731be1b38e52e57f33eb73aee3e329f66f6
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Fri Apr 24 11:23:07 2026 -0600

    [#11102] 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 216d516e2..b6d37afe9 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -1846,10 +1846,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 b1bfda837..0678d5776 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -38,7 +38,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
@@ -233,8 +233,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
@@ -248,6 +248,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
@@ -292,7 +299,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(
@@ -587,6 +594,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])