From 6b104e2bf152eb25eef3c31c987ecadcd3d41dc0 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 26 Feb 2021 21:14:12 +0530 Subject: [PATCH] feat: Partial field value redaction Finds and replaces Full Name and User email ID from specified DocTypes Changes: * Option "partial" added in user_data_fields * If "redact_fields" aren't speciifed, "partial" mode is assumed * If "rename" is set, the respective docs are renamed with self.anon * "strict" if unset, is assumed to be False. In this case, a non conditional query is used to delete data. If "strict" is True, Personal Data Deletion Request will obey "filter_by" field value if defined else "owner" is used --- frappe/hooks.py | 6 +-- .../personal_data_deletion_request.py | 47 ++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index f386b79f46..8fb7903db7 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -293,8 +293,8 @@ after_migrate = ['frappe.website.doctype.website_theme.website_theme.after_migra otp_methods = ['OTP App','Email','SMS'] user_data_fields = [ - {"doctype": "Access Log", "strict": False}, - {"doctype": "Activity Log", "strict": False}, + {"doctype": "Access Log", "strict": True}, + {"doctype": "Activity Log", "strict": True}, {"doctype": "Comment"}, { "doctype": "Contact", @@ -354,7 +354,7 @@ user_data_fields = [ "email_signature", ], }, - {"doctype": "Version", "strict": False}, + {"doctype": "Version", "strict": True}, ] global_search_doctypes = { diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py index 15e98d3251..a1a7208249 100644 --- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py +++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py @@ -121,12 +121,58 @@ class PersonalDataDeletionRequest(Document): for doctype in self.full_match_privacy_docs: self.redact_full_match_data(doctype, email) + for doctype in self.partial_privacy_docs: + self.redact_partial_match_data(doctype) frappe.rename_doc("User", email, anon, force=True, show_alert=False) self.db_set("status", "Deleted") + def redact_partial_match_data(self, doctype): + match_fields = [] + editable_text_fields = { + "Small Text", + "Text", + "Text Editor", + "Code", + "HTML Editor", + "Markdown Editor", + "Long Text", + "Data", + } + + for df in frappe.get_meta(doctype["doctype"]).fields: + if df.fieldtype not in editable_text_fields: continue + match_fields += [ + f"`{df.fieldname}`= REPLACE(`{df.fieldname}`, %(name)s, 'REDACTED')", + f"`{df.fieldname}`= REPLACE(`{df.fieldname}`, %(email)s, '{self.anon}')", + ] + + update_predicate = f"SET {', '.join(match_fields)}" + where_predicate = "" if doctype.get("strict") else f"WHERE `{doctype.get('filter_by', 'owner')}` = %(email)s" + + frappe.db.sql( + f"UPDATE `tab{doctype['doctype']}` {update_predicate} {where_predicate}", + {"name": self.full_name, "email": self.email}, + debug=1 + ) + + if doctype.get("rename"): + def new_name(email, number): + email_user, domain = email.split("@") + return f"{email_user}-{number}@{domain}" + + for i, name in enumerate( + frappe.get_all( + doctype["doctype"], + filters={doctype.get("filter_by", "owner"): self.email}, + pluck="name", + ) + ): + frappe.rename_doc( + doctype["doctype"], name, new_name(self.anon, i + 1), force=True, show_alert=False + ) def redact_full_match_data(self, ref, email): """Replaces the entire field value by the values set in the anonymization_value_map""" @@ -190,7 +236,6 @@ class PersonalDataDeletionRequest(Document): ) if ref.get("rename") and doc["name"] != self.anon: - print(f'redact_doc: {ref["doctype"]} {doc["name"]} {self.anon}') frappe.rename_doc( ref["doctype"], doc["name"], self.anon, force=True, show_alert=False )