From 2c96697c764597f176e9fd193fd0c471fa687bd5 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 18 Dec 2025 10:21:00 +0530 Subject: [PATCH 1/6] feat(custom app): add custom permissions hook --- frappe/model/db_query.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 683adb1fe6..ebdd3977f7 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1155,6 +1155,31 @@ from {tables} if condition := script.get_permission_query_conditions(self.user): conditions.append(condition) + if hasattr(self, "tables") and len(self.tables) > 1: + """(Custom): Applying User Permissions on linked child tables (for report view)""" + user_permissions = frappe.permissions.get_user_permissions(self.user) + for table_name in self.tables: + # skip parent table since already permissions are handled (look only for child tables) + if table_name == f"`tab{self.doctype}`": + continue + child_doctype = table_name.strip("`").replace("tab", "", 1) + child_meta = frappe.get_meta(child_doctype) + for field in child_meta.get_link_fields(): + if field.options in user_permissions: + allowed = [frappe.db.escape(p.doc) for p in user_permissions[field.options]] + conditions.append(f"{table_name}.{field.fieldname} IN ({', '.join(allowed)})") + else: + linked_meta = frappe.get_meta(field.options) + for nested_field in linked_meta.get_link_fields(): + if nested_field.options in user_permissions: + allowed = [ + frappe.db.escape(p.doc) for p in user_permissions[nested_field.options] + ] + conditions.append(f"""{table_name}.{field.fieldname} IN ( + SELECT name FROM `tab{field.options}` + WHERE {nested_field.fieldname} IN ({", ".join(allowed)}) + )""") + return " and ".join(conditions) if conditions else "" def set_order_by(self, args): From 6929f5e7a943cdfdb68fcd4b7ed6b62583181ac0 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Tue, 13 Jan 2026 12:13:37 +0530 Subject: [PATCH 2/6] feat(permissions): parse child tables to be used in server scripts --- .../doctype/server_script/server_script.py | 9 +++-- frappe/model/db_query.py | 36 ++++++------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 70d4270a04..aef07227c6 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -206,7 +206,7 @@ class ServerScript(Document): safe_exec(self.script, script_filename=self.name) - def get_permission_query_conditions(self, user: str) -> list[str]: + def get_permission_query_conditions(self, user: str, active_child_tables=None) -> list[str]: """Specific to Permission Query Server Scripts. Args: @@ -215,7 +215,12 @@ class ServerScript(Document): Return: list: Return list of conditions defined by rules in self.script. """ - locals = {"user": user, "conditions": ""} + locals = { + "user": user, + "conditions": "", + "active_child_tables": active_child_tables + or [], # add 'active_child_tables' to the locals dictionary + } safe_exec(self.script, None, locals, script_filename=self.name) if locals["conditions"]: return locals["conditions"] diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ebdd3977f7..a32cf1215b 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1150,36 +1150,20 @@ from {tables} if c := frappe.call(frappe.get_attr(method), self.user, doctype=self.doctype): conditions.append(c) + active_child_tables = [] + if hasattr(self, "tables") and len(self.tables) > 1: # only if query has multiple tables involved + for table_name in self.tables: + # skip parent table (user_permissions are already applied) + if table_name != f"`tab{self.doctype}`": + active_child_tables.append(table_name) # track child tables + if permission_script_name := get_server_script_map().get("permission_query", {}).get(self.doctype): script = frappe.get_doc("Server Script", permission_script_name) - if condition := script.get_permission_query_conditions(self.user): + if condition := script.get_permission_query_conditions( + self.user, active_child_tables=active_child_tables + ): # parse tracked child tables conditions.append(condition) - if hasattr(self, "tables") and len(self.tables) > 1: - """(Custom): Applying User Permissions on linked child tables (for report view)""" - user_permissions = frappe.permissions.get_user_permissions(self.user) - for table_name in self.tables: - # skip parent table since already permissions are handled (look only for child tables) - if table_name == f"`tab{self.doctype}`": - continue - child_doctype = table_name.strip("`").replace("tab", "", 1) - child_meta = frappe.get_meta(child_doctype) - for field in child_meta.get_link_fields(): - if field.options in user_permissions: - allowed = [frappe.db.escape(p.doc) for p in user_permissions[field.options]] - conditions.append(f"{table_name}.{field.fieldname} IN ({', '.join(allowed)})") - else: - linked_meta = frappe.get_meta(field.options) - for nested_field in linked_meta.get_link_fields(): - if nested_field.options in user_permissions: - allowed = [ - frappe.db.escape(p.doc) for p in user_permissions[nested_field.options] - ] - conditions.append(f"""{table_name}.{field.fieldname} IN ( - SELECT name FROM `tab{field.options}` - WHERE {nested_field.fieldname} IN ({", ".join(allowed)}) - )""") - return " and ".join(conditions) if conditions else "" def set_order_by(self, args): From 3774a68093b32abb5c196c4bc92a1f7a89c5cd82 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Tue, 27 Jan 2026 13:51:03 +0530 Subject: [PATCH 3/6] refactor: get rid of noise and add docstring --- frappe/core/doctype/server_script/server_script.py | 4 ++-- frappe/model/db_query.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index aef07227c6..cfdf0ab2f1 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -211,6 +211,7 @@ class ServerScript(Document): Args: user (str): Take user email to execute script and return list of conditions. + active_child_tables (list, optional): A list of child table names involved in the current SQL query. Return: list: Return list of conditions defined by rules in self.script. @@ -218,8 +219,7 @@ class ServerScript(Document): locals = { "user": user, "conditions": "", - "active_child_tables": active_child_tables - or [], # add 'active_child_tables' to the locals dictionary + "active_child_tables": active_child_tables or [], } safe_exec(self.script, None, locals, script_filename=self.name) if locals["conditions"]: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index a32cf1215b..c67d6a2aa0 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1151,17 +1151,16 @@ from {tables} conditions.append(c) active_child_tables = [] - if hasattr(self, "tables") and len(self.tables) > 1: # only if query has multiple tables involved + if len(self.tables) > 1: # only if query has multiple tables involved for table_name in self.tables: - # skip parent table (user_permissions are already applied) if table_name != f"`tab{self.doctype}`": - active_child_tables.append(table_name) # track child tables + active_child_tables.append(table_name) if permission_script_name := get_server_script_map().get("permission_query", {}).get(self.doctype): script = frappe.get_doc("Server Script", permission_script_name) if condition := script.get_permission_query_conditions( self.user, active_child_tables=active_child_tables - ): # parse tracked child tables + ): conditions.append(condition) return " and ".join(conditions) if conditions else "" From edd15715b682a80ab42fdb44b6ecd242fddd51e5 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Tue, 27 Jan 2026 13:58:32 +0530 Subject: [PATCH 4/6] feat(query): parse child tables via query file too --- frappe/database/query.py | 22 +++++++++++++++++++++- frappe/model/db_query.py | 4 +++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/frappe/database/query.py b/frappe/database/query.py index 73ffc6a25d..1fa09380f3 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -1450,6 +1450,16 @@ class Engine: self.query = self.query.where(where_condition) + def get_queried_tables(self) -> list[str]: + """Extract all table names involved in the current query.""" + tables = [] + for table in self.query._from: + tables.append(table.get_sql()) + + for join in self.query._joins: + tables.append(join.item.get_sql()) + return list(set(tables)) + def get_permission_query_conditions(self) -> list["RawCriterion"]: """Add permission query conditions from hooks and server scripts""" from frappe.core.doctype.server_script.server_script_utils import get_server_script_map @@ -1462,6 +1472,14 @@ class Engine: if c := frappe.call(frappe.get_attr(method), self.user, doctype=self.permission_doctype): conditions.append(RawCriterion(f"({c})")) + active_child_tables = [] + current_tables = self.get_queried_tables() + if len(current_tables) > 1: + main_table_name = f"tab{self.doctype}" + for table_name in current_tables: + if table_name != main_table_name: + active_child_tables.append(table_name) + # Get conditions from server scripts if ( permission_script_name := get_server_script_map() @@ -1469,7 +1487,9 @@ class Engine: .get(self.permission_doctype) ): script = frappe.get_doc("Server Script", permission_script_name) - if condition := script.get_permission_query_conditions(self.user): + if condition := script.get_permission_query_conditions( + self.user, active_child_tables=active_child_tables + ): conditions.append(RawCriterion(f"({condition})")) return conditions diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index c67d6a2aa0..4b07ce0b26 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1152,8 +1152,10 @@ from {tables} active_child_tables = [] if len(self.tables) > 1: # only if query has multiple tables involved + main_table_name = f"tab{self.doctype}" for table_name in self.tables: - if table_name != f"`tab{self.doctype}`": + clean_name = table_name.replace("`", "").replace('"', "") + if clean_name != main_table_name: active_child_tables.append(table_name) if permission_script_name := get_server_script_map().get("permission_query", {}).get(self.doctype): From 7485f1367d50c1953b24e732fa0c6e23bde55158 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Fri, 30 Jan 2026 23:45:03 +0530 Subject: [PATCH 5/6] refactor: parse in db_query as is parsed in query to maintain parity --- frappe/model/db_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 4b07ce0b26..7829b6799c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1156,7 +1156,7 @@ from {tables} for table_name in self.tables: clean_name = table_name.replace("`", "").replace('"', "") if clean_name != main_table_name: - active_child_tables.append(table_name) + active_child_tables.append(clean_name) if permission_script_name := get_server_script_map().get("permission_query", {}).get(self.doctype): script = frappe.get_doc("Server Script", permission_script_name) From 4d898d56deaaaac6bd53592154bbd1dae4a8c893 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Sat, 31 Jan 2026 00:06:11 +0530 Subject: [PATCH 6/6] refactor(test): update test based on new changes to get_permission_query_conditions --- frappe/tests/test_db_query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 5110671578..aca38c394b 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -990,11 +990,14 @@ class TestDBQuery(IntegrationTestCase): from frappe.desk.doctype.dashboard_settings.dashboard_settings import ( create_dashboard_settings, ) + from frappe.model.db_query import DatabaseQuery self.doctype = "Dashboard Settings" self.user = "test'5@example.com" - permission_query_conditions = DatabaseQuery.get_permission_query_conditions(self) + db_query = DatabaseQuery(self.doctype, user=self.user) + + permission_query_conditions = db_query.get_permission_query_conditions() create_dashboard_settings(self.user)