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/06/17 19:59:37 UTC

(allura) branch cc/8618-part1 created (now 0703100c7)

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

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


      at 0703100c7 [#8610] Add support for list fields to field conversion script

This branch includes the following new commits:

     new de898ffd9 [#8610] Add encrypted field for Users email addresses
     new 0703100c7 [#8610] Add support for list fields to field conversion script

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) 01/02: [#8610] Add encrypted field for Users email addresses

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/8618-part1
in repository https://gitbox.apache.org/repos/asf/allura.git

commit de898ffd94494b9734d88d1e87396c8895190905
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Tue Jun 16 12:56:24 2026 -0600

    [#8610] Add encrypted field for Users email addresses
---
 Allura/allura/model/auth.py            | 20 ++++++++++++++++++-
 Allura/allura/tests/model/test_auth.py | 35 ++++++++++++++++++++++++++++++++++
 2 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 05322ca82..8bf930cae 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -38,7 +38,7 @@
 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 session, state, MapperExtension
 from ming.odm import FieldProperty, RelationProperty, ForeignIdProperty, DecryptedProperty
 from ming.odm.declarative import MappedClass
 from ming.odm.odmsession import ThreadLocalODMSession
@@ -264,6 +264,15 @@ def __set__(self, instance, value):
         instance.display_name_encrypted = type(instance).encr(state(instance).document[self.name])
 
 
+class UserEmailAddressesMapperExtension(MapperExtension):
+
+    def before_insert(self, obj, state, sess):
+        obj.sync_email_addresses_encrypted()
+
+    def before_update(self, obj, state, sess):
+        obj.sync_email_addresses_encrypted()
+
+
 class User(MappedClass, ActivityNode, ActivityObject, SearchIndexable):
     SALT_LEN = 8
 
@@ -272,6 +281,7 @@ class __mongometa__:
         session = main_orm_session
         indexes = ['tool_data.AuthPasswordReset.hash']
         unique_indexes = ['username']
+        extensions = [UserEmailAddressesMapperExtension]
         custom_indexes = [
             dict(fields=('tool_data.phone_verification.number_hash',), sparse=True),
         ]
@@ -284,6 +294,7 @@ class __mongometa__:
     sfx_userid = FieldProperty(S.Deprecated)
     username = FieldProperty(str)
     email_addresses = FieldProperty([str])
+    email_addresses_encrypted = FieldProperty([S.Binary], if_missing=[])
     password = FieldProperty(str)  # hashed
     last_password_updated = FieldProperty(datetime)  # to access, use AuthProvider's get_last_password_updated
     password_algorithm = FieldProperty(str)
@@ -781,6 +792,13 @@ def set_tool_data(self, tool, **kw):
         d.update(kw)
         state(self).soil()
 
+    @classmethod
+    def encrypt_email_addresses(cls, email_addresses):
+        return [cls.encr(addr) if addr is not None else None for addr in email_addresses or []]
+
+    def sync_email_addresses_encrypted(self):
+        self.email_addresses_encrypted = type(self).encrypt_email_addresses(self.email_addresses)
+
     def address_object(self, addr):
         return EmailAddress.get(email=addr, claimed_by_user_id=self._id)
 
diff --git a/Allura/allura/tests/model/test_auth.py b/Allura/allura/tests/model/test_auth.py
index a035f4466..bad3e1e86 100644
--- a/Allura/allura/tests/model/test_auth.py
+++ b/Allura/allura/tests/model/test_auth.py
@@ -428,6 +428,41 @@ def test_display_name_encrypted_is_populated_on_creation(self):
         assert M.User.decr(user.display_name_encrypted) == 'Encrypted Display Name'
         assert user.__dict__['__ming__'].state.document.display_name == 'Encrypted Display Name'
 
+    def test_email_addresses_encrypted_is_populated_on_creation(self):
+        user = M.User(
+            username='email-addresses-encrypted-create-test',
+            email_addresses=['primary@example.com', None, 'secondary@example.com'])
+        ThreadLocalODMSession.flush_all()
+
+        expected_encrypted = [
+            M.User.encr('primary@example.com'),
+            None,
+            M.User.encr('secondary@example.com'),
+        ]
+        assert user.email_addresses == ['primary@example.com', None, 'secondary@example.com']
+        assert user.email_addresses_encrypted == expected_encrypted
+        assert state(user).document['email_addresses'] == ['primary@example.com', None, 'secondary@example.com']
+        assert state(user).document['email_addresses_encrypted'] == expected_encrypted
+
+    def test_email_addresses_encrypted_is_synced_on_update(self):
+        user = M.User(
+            username='email-addresses-encrypted-update-test',
+            email_addresses=['keep@example.com', 'remove@example.com'])
+        ThreadLocalODMSession.flush_all()
+
+        user.email_addresses.append('add@example.com')
+        user.email_addresses.remove('remove@example.com')
+        ThreadLocalODMSession.flush_all()
+
+        expected_encrypted = [
+            M.User.encr('keep@example.com'),
+            M.User.encr('add@example.com'),
+        ]
+        assert user.email_addresses == ['keep@example.com', 'add@example.com']
+        assert user.email_addresses_encrypted == expected_encrypted
+        assert state(user).document['email_addresses'] == ['keep@example.com', 'add@example.com']
+        assert state(user).document['email_addresses_encrypted'] == expected_encrypted
+
     def test_set_display_name_pref_updates_encrypted_field_and_cache(self):
         user = M.User(
             username='display-name-cache-test',


(allura) 02/02: [#8610] Add support for list fields to field conversion script

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/8618-part1
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 0703100c7fb6cbdaecbaed02ef7052642da10efd
Author: Carlos Cruz <ca...@slashdotmedia.com>
AuthorDate: Wed Jun 17 11:24:53 2026 -0600

    [#8610] Add support for list fields to field conversion script
---
 scripts/convert_encrypted_field.py | 24 ++++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/scripts/convert_encrypted_field.py b/scripts/convert_encrypted_field.py
index 78ce8155c..e99c25a77 100644
--- a/scripts/convert_encrypted_field.py
+++ b/scripts/convert_encrypted_field.py
@@ -80,6 +80,19 @@ def _get_nested_value(rec: dict, field_name: str):
     return value
 
 
+def _is_encrypted_list_schema(field_schema) -> bool:
+    return (
+        isinstance(field_schema, schema.Array)
+        and isinstance(field_schema.field_type, schema.Binary)
+    )
+
+
+def _encrypt_field_value(Model: type[MappedClass], value, field_schema):
+    if _is_encrypted_list_schema(field_schema):
+        return [Model.encr(v) if v is not None else None for v in value or []]
+    return Model.encr(value)
+
+
 def main(class_name: str, plain_field_name: str,
          *, remove_unencrypted: bool = False, redo_all: bool = False, limit: int | None = None):
     """
@@ -103,7 +116,7 @@ def main(class_name: str, plain_field_name: str,
         # model schema, but we can still create it with raw MongoDB updates
         encr_schema = None
     if encr_schema is not None:
-        assert isinstance(encr_schema, schema.Binary)
+        assert isinstance(encr_schema, schema.Binary) or _is_encrypted_list_schema(encr_schema)
     elif remove_unencrypted:
         raise AssertionError(
             f'Cannot use --remove-unencrypted because {encrypted_field_name!r} '
@@ -111,7 +124,10 @@ def main(class_name: str, plain_field_name: str,
     if remove_unencrypted:
         if '.' not in plain_field_name:
             plain_prop = Model.__dict__[plain_field_name]  # getattr() better but needs Ming fix released
-            assert isinstance(plain_prop, DecryptedProperty)
+            if _is_encrypted_list_schema(encr_schema):
+                assert hasattr(plain_prop, 'encrypted_field')
+            else:
+                assert isinstance(plain_prop, DecryptedProperty)
             assert plain_prop.encrypted_field == encrypted_field_name
 
     # TODO: figure out how it works with inheritance
@@ -132,7 +148,7 @@ def main(class_name: str, plain_field_name: str,
         bulk_update_result = Model.query.update(
             query,
             {
-                '$set': {encrypted_field_name: Model.encr(bulk_val)},
+                '$set': {encrypted_field_name: _encrypt_field_value(Model, bulk_val, encr_schema)},
             },
             multi=True,
         )
@@ -159,7 +175,7 @@ def main(class_name: str, plain_field_name: str,
 
         for rec_doc in docs:
             val = _get_nested_value(rec_doc, plain_field_name)
-            encr_val = Model.encr(val)
+            encr_val = _encrypt_field_value(Model, val, encr_schema)
             raw_collection.update_one({'_id': rec_doc['_id']}, {
                 '$set': {encrypted_field_name: encr_val},
             })