You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2025/11/07 18:01:24 UTC

(allura) 03/04: script to convert encrypted fields

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

brondsem pushed a commit to branch db/explore_encryption
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 12a415bfdedd74abe79da07b3dd5ef66a7687ab0
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Fri Nov 7 12:33:00 2025 -0500

    script to convert encrypted fields
---
 scripts/convert_encrypted_field.py | 106 +++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/scripts/convert_encrypted_field.py b/scripts/convert_encrypted_field.py
new file mode 100644
index 000000000..49a982708
--- /dev/null
+++ b/scripts/convert_encrypted_field.py
@@ -0,0 +1,106 @@
+#  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.
+
+from __future__ import annotations
+
+from importlib import import_module
+
+import defopt
+
+from allura.lib.utils import chunked_find
+from ming import schema
+from ming.odm import session
+from ming.odm.declarative import MappedClass
+from ming.odm.property import FieldProperty, DecryptedProperty
+
+
+def main(class_name: str, plain_field_name: str,
+         *, remove_unencrypted: bool = False, redo_all: bool = False):
+    """
+    :param class_name: full class name, e.g. allura.model.user.User
+    :param plain_field_name: name of the unencrypted field, e.g. display_name
+    :param remove_unencrypted: WARNING only run this after your codebase is already on the latest code
+    :param redo_all: re-encrypt records that already have encrypted values (in case they changed since last run)
+    """
+    encrypted_field_name = f'{plain_field_name}_encrypted'
+
+    module_name, class_basename = class_name.rsplit('.', 1)
+    module = import_module(module_name)
+    Model: type[MappedClass] = getattr(module, class_basename)
+
+    # sanity checks that the fields are correct and ready
+    encr_prop = getattr(Model, encrypted_field_name)
+    assert isinstance(encr_prop, FieldProperty)
+    assert encr_prop.field.type == schema.Binary
+    if remove_unencrypted:
+        plain_prop = Model.__dict__[plain_field_name]  # getattr() better but needs Ming fix released
+        assert isinstance(plain_prop, DecryptedProperty)
+        assert plain_prop.encrypted_field == encrypted_field_name
+
+    # TODO: figure out how it works with inheritance
+
+    sess = session(Model)
+
+    # encrypt all records that need it.  Even for --remove-unencrypted, to make sure everything's converted first
+    bulk_update_values = ["", None]
+    for bulk_val in bulk_update_values:
+        bulk_update_result = Model.query.update(
+            {
+                plain_field_name: {'$exists': True, '$eq': bulk_val},  # $exists to avoid converting missing fields
+            },
+            {
+                '$set': {encrypted_field_name: Model.encr(bulk_val)},
+            },
+            multi=True,
+        )
+        print(f'Converted {bulk_update_result.modified_count} {class_name} records with {plain_field_name}={bulk_val!r}')
+        # bulk_update_result.matched_count has # matches, already handled if run multiple times
+
+    q = {
+        plain_field_name: {'$nin': bulk_update_values},
+    }
+    if not redo_all:
+        q[encrypted_field_name] = None
+    count = 0
+    for chunk in chunked_find(Model, q):
+        for rec in chunk:
+            val = rec[plain_field_name]
+            encr_val = Model.encr(val)
+            Model.query.update({'_id': rec['_id']}, {
+                '$set': {encrypted_field_name: encr_val},
+            })
+            count += 1
+            sess.expunge(rec)
+        print(f'Converted {count} so far...')
+    if not count:
+        print(f'Did not find any {class_name} records with {plain_field_name} values to encrypt')
+    else:
+        print(f'Encrypted {count} {class_name} records with {plain_field_name} values')
+
+    if remove_unencrypted:
+        remove_result = Model.query.update(
+            {},
+            {
+                "$unset": {plain_field_name: True}
+            },
+            multi=True,
+        )
+        print(f'Removed {remove_result.modified_count} {class_name} unencrypted {plain_field_name} values')
+
+
+if __name__ == "__main__":
+    defopt.run(main, no_negated_flags=True, short={})