refactor: change approach of masking fields

This commit is contained in:
Ejaaz Khan 2025-08-18 23:38:18 +05:30
parent c0aa39ee9a
commit c2544f9096
8 changed files with 56 additions and 109 deletions

View file

@ -52,24 +52,12 @@ def get_meta(doctype, cached=True) -> "FormMeta":
def mask_protected_fields(meta):
if (
frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_setup_wizard
):
return meta
for df in meta.fields:
if df.mask and not meta.has_permlevel_access_to(
fieldname=df.fieldname, df=df, permission_type="mask"
):
# store orignal fieldtype and change fieldtype to Data
df.read_only = 1
df.mask_readonly = 1
df.set("old_fieldtype", df.get("old_fieldtype") or df.fieldtype)
if df.fieldtype != "Data":
df.fieldtype = "Data"
return meta

View file

@ -376,8 +376,7 @@ def export_query():
form_params = get_form_params()
form_params["limit_page_length"] = None
# remove as_list param because key is needed for data masking
# form_params["as_list"] = True
form_params["as_list"] = True
doctype = form_params.pop("doctype")
if isinstance(form_params["fields"], list):
form_params["fields"].append("owner")
@ -421,10 +420,6 @@ def export_query():
data = [[_("Sr"), *labels]]
processed_data = []
# convert ret to a list of lists if it is a list of dicts
if isinstance(ret, list) and ret and isinstance(ret[0], dict):
ret = [[value for value in row.values()] for row in ret]
if frappe.local.lang == "en" or not translate_values:
data.extend([i + 1, *list(row)] for i, row in enumerate(ret))
elif translate_values:

View file

@ -231,24 +231,36 @@ class DatabaseQuery:
if not masked_fields:
return result
for row in result:
for field in masked_fields:
if field.fieldname in row:
val = row[field.fieldname]
if not val:
continue
if self.as_list:
masked_result = []
field_index_map = {}
for idx, field in enumerate(self.fields):
# handle aliases (e.g. `tabSI`.`posting_date` as posting_date)
if " as " in field.lower():
alias = field.split(" as ")[1].strip(" '")
field_index_map[alias] = idx
else:
# extract last part after `.`
col = field.split(".")[-1].strip("`")
field_index_map[col] = idx
# if as_list then we don't have field names in the result so we need to mask by position
for row in result:
row = list(row) # convert tuple to list mutable
for field in masked_fields:
if field.fieldname in field_index_map:
idx = field_index_map[field.fieldname]
val = row[idx]
row[idx] = mask_field_value(field, val)
masked_result.append(tuple(row)) # convert back to tuple
result = masked_result
else:
for row in result:
for field in masked_fields:
if field.fieldname in row:
val = row[field.fieldname]
row[field.fieldname] = mask_field_value(field, val)
if field.old_fieldtype == "Data" and field.options == "Phone":
row[field.fieldname] = val[:3] + "XXXXXX"
elif field.old_fieldtype == "Data" and field.options == "Email":
email = val.split("@")
row[field.fieldname] = "XXXXXX@" + email[1]
elif field.old_fieldtype == "Date":
row[field.fieldname] = "XX-XX-XXXX"
elif field.old_fieldtype == "Time":
row[field.fieldname] = "XX:XX"
else:
row[field.fieldname] = "XXXXXXXX"
return result
def get_masked_fields(self):
@ -660,15 +672,6 @@ from {tables}
)
)
# get_permitted_field = get_permitted_fields(
# doctype=self.doctype,
# parenttype=self.parent_doctype,
# permission_type=self.permission_map.get(self.doctype),
# ignore_virtual=True,
# )
print(self.doctype, self.permission_map.get(self.doctype), "permitted_fields \n\n\n\n")
# print(get_permitted_field, "get_permitted_field \n\n\n\n")
permitted_child_table_fields = {}
for i, field in enumerate(self.fields):
@ -1239,6 +1242,23 @@ from {tables}
update_user_settings(self.doctype, user_settings)
def mask_field_value(field, val):
if not val:
return val
if field.fieldtype == "Data" and field.options == "Phone":
return val[:3] + "XXXXXX"
elif field.fieldtype == "Data" and field.options == "Email":
email = val.split("@")
return "XXXXXX@" + email[1] if len(email) > 1 else "XXXXXX"
elif field.fieldtype == "Date":
return "XX-XX-XXXX"
elif field.fieldtype == "Time":
return "XX:XX"
else:
return "XXXXXXXX"
def cast_name(column: str) -> str:
"""Casts name field to varchar for postgres

View file

@ -273,70 +273,16 @@ class Document(BaseDocument):
return self
def mask_field_value(self, field):
# TODO: use this method to mask value and remove other code
already_masked = False
if field.fieldtype == "Data" and field.options == "Phone":
already_masked = True
self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX")
if field.fieldtype == "Data" and field.options == "Email":
already_masked = True
email = self.get(field.fieldname)
if email:
email = email.split("@")
self.set(field.fieldname, "XXXXXXXX" + "@" + email[1])
if field.fieldtype == "Date":
already_masked = True
date = self.get(field.fieldname)
if date:
self.set(field.fieldname, "XX-XX-XXXX")
if field.fieldtype == "Time":
already_masked = True
date = self.get(field.fieldname)
if date:
self.set(field.fieldname, "XX:XX")
if not already_masked:
self.set(field.fieldname, "XXXXXXXX")
def mask_fields(self):
from frappe.model.db_query import mask_field_value
mask_fields = frappe.get_meta(self.doctype).get_masked_fields()
if not mask_fields:
return
for field in mask_fields:
already_masked = False
# if field type is Data and option is Phone the mask all value except last 3
if field.old_fieldtype == "Data" and field.options == "Phone":
already_masked = True
self.set(field.fieldname, self.get(field.fieldname)[0:3] + "XXXXXXX")
if field.old_fieldtype == "Data" and field.options == "Email":
already_masked = True
email = self.get(field.fieldname)
if email:
email = email.split("@")
self.set(field.fieldname, "XXXXXXXX" + "@" + email[1])
if field.old_fieldtype == "Date":
already_masked = True
date = self.get(field.fieldname)
if date:
self.set(field.fieldname, "XX-XX-XXXX")
if field.old_fieldtype == "Time":
already_masked = True
date = self.get(field.fieldname)
if date:
self.set(field.fieldname, "XX:XXXX")
if not already_masked:
self.set(field.fieldname, "XXXXXXXX")
val = self.get(field.fieldname)
self.set(field.fieldname, mask_field_value(field, val))
def load_children_from_db(self):
is_doctype = self.doctype == "DocType"
@ -981,7 +927,6 @@ class Document(BaseDocument):
return
# check for child tables
print(high_permlevel_fields, "high_permlevel_fields \n\n\n")
for df in self.meta.get_table_fields():
high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields()
if high_permlevel_fields:

View file

@ -88,14 +88,13 @@ def get_meta(doctype: "str | DocType", cached: bool = True) -> "_Meta":
meta = Meta(doctype)
key = f"doctype_meta::{meta.name}"
frappe.client_cache.set_value(key, meta)
if meta.name not in meta.special_doctypes:
from frappe.desk.form.meta import mask_protected_fields
meta = mask_protected_fields(meta)
key = f"doctype_meta::{meta.name}"
frappe.client_cache.set_value(key, meta)
return meta

View file

@ -9,7 +9,7 @@ frappe.ui.form.Control = class BaseControl {
make() {
this.make_wrapper();
this.$wrapper
.attr("data-fieldtype", this.df?.old_fieldtype || this.df.fieldtype)
.attr("data-fieldtype", this.df.fieldtype)
.attr("data-fieldname", this.df.fieldname);
this.wrapper = this.$wrapper.get(0);
this.wrapper.fieldobj = this; // reference for event handlers

View file

@ -413,7 +413,7 @@ frappe.form.get_formatter = function (fieldtype) {
};
frappe.format = function (value, df, options, doc) {
if (!df) df = { fieldtype: "Data" };
if (!df || df?.mask_readonly) df = { fieldtype: "Data" };
if (df.fieldname == "_user_tags") df = { ...df, fieldtype: "Tag" };
var fieldtype = df.fieldtype || "Data";

View file

@ -248,7 +248,7 @@ frappe.ui.form.Layout = class Layout {
init_field(df, parent, render = false) {
if (df.mask && df.mask_readonly) {
if (df.fieldtype !== "Data") {
df.original_fieldtype = df.fieldtype;
df.read_only = 1;
df.fieldtype = "Data";
}
}