fix: restrict child table access if user has only "select" on parent

This commit is contained in:
Faris Ansari 2025-05-02 19:08:00 +05:30
parent a9fb29fb8e
commit 63afc0601b
2 changed files with 64 additions and 3 deletions

View file

@ -391,8 +391,8 @@ class Engine:
if not fields:
return []
# Handle direct pypika Field or Function objects
if isinstance(fields, Field | AggregateFunction):
# return if fields is already a pypika Term
if isinstance(fields, Term):
return [fields]
initial_field_list = []
@ -545,17 +545,23 @@ class Engine:
def apply_field_permissions(self):
"""Filter the list of fields based on permlevel."""
allowed_fields = []
parent_permission_type = self.get_permission_type(self.doctype)
permitted_fields_set = set(
get_permitted_fields(
doctype=self.doctype,
parenttype=self.parent_doctype,
permission_type=self.get_permission_type(self.doctype),
permission_type=parent_permission_type,
ignore_virtual=True,
)
)
for field in self.fields:
if isinstance(field, ChildTableField):
if parent_permission_type == "select":
# Skip child table fields if parent permission is only 'select'
continue
# Cache permitted fields for child doctypes if accessed multiple times
permitted_child_fields_set = set(
get_permitted_fields(
@ -573,6 +579,10 @@ class Engine:
if field.link_fieldname in permitted_fields_set:
allowed_fields.append(field)
elif isinstance(field, ChildQuery):
if parent_permission_type == "select":
# Skip child queries if parent permission is only 'select'
continue
# Cache permitted fields for the child doctype of the query
permitted_child_fields_set = set(
get_permitted_fields(

View file

@ -2,6 +2,7 @@ import itertools
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.permissions import add_permission, update_permission_property
from frappe.query_builder import Field
from frappe.query_builder.functions import Abs, Count, Ifnull, Max, Now, Timestamp
from frappe.tests import IntegrationTestCase
@ -734,6 +735,56 @@ class TestQuery(IntegrationTestCase):
test_post.delete()
def test_child_table_access_with_select_permission(self):
"""Test that child table fields are inaccessible if user only has select perm on parent."""
test_role = "Select Note Test Role"
test_user_email = "test2@example.com" # Use existing test user
test_note_title = "Child Select Test Note"
# Cleanup
frappe.set_user("Administrator")
test_user = frappe.get_doc("User", test_user_email)
test_user.remove_roles(test_role)
frappe.delete_doc("Role", test_role, ignore_missing=True, force=True)
frappe.delete_doc("Note", {"title": test_note_title}, ignore_missing=True, force=True)
# Setup Role with 'select' on Note and 'read' on Note Seen By
frappe.get_doc({"doctype": "Role", "role_name": test_role}).insert(ignore_if_duplicate=True)
# Grant select on Note, read on Note Seen By
add_permission("Note", test_role, 0, ptype="select")
add_permission("Note Seen By", test_role, 0, ptype="read")
# Ensure no read permission on Note for this role by explicitly setting it to 0
update_permission_property("Note", test_role, 0, "read", 0, validate=False)
test_user.add_roles(test_role)
note = frappe.get_doc(
doctype="Note", title=test_note_title, public=1, seen_by=[{"user": "Administrator"}]
).insert(ignore_permissions=True)
frappe.set_user(test_user_email)
query = frappe.qb.get_query(
"Note",
filters={"name": note.name},
fields=["name", {"seen_by": ["user"]}],
ignore_permissions=False,
)
result = query.run(as_dict=True)
self.assertEqual(len(result), 1, "Should find the note record")
self.assertIn("name", result[0], "Parent field 'name' should be accessible")
self.assertNotIn(
"seen_by",
result[0],
"Child table field 'seen_by' should NOT be accessible with only 'select' on parent",
)
# Cleanup
frappe.set_user("Administrator")
note.delete(ignore_permissions=True)
test_user.remove_roles(test_role)
frappe.delete_doc("Role", test_role, force=True, ignore_permissions=True)
def test_nested_permission(self):
"""Test permission on nested doctypes"""
frappe.set_user("Administrator")