diff --git a/frappe/database/query.py b/frappe/database/query.py index 5020d9cdb8..6a6da98439 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -19,6 +19,7 @@ from frappe.database.utils import ( get_doctype_name, get_doctype_sort_info, ) +from frappe.model import CORE_DOCTYPES as PERMITTED_CORE_DOCTYPES from frappe.model import OPTIONAL_FIELDS, get_permitted_fields from frappe.model.base_document import DOCTYPES_FOR_DOCTYPE from frappe.model.document import Document @@ -1040,8 +1041,8 @@ class Engine: # for select permission on parent doctype, allow all permlevel 0 fields in filters cache_key = (doctype, None, "_filterable_select") if cache_key not in self.permitted_fields_cache: - if doctype in CORE_DOCTYPES: - # core doctypes have no restrictions - return all valid columns + if doctype in PERMITTED_CORE_DOCTYPES: + # no restrictions - return all valid columns self.permitted_fields_cache[cache_key] = set(meta.get_valid_columns()) else: permlevel_0_fields = set(meta.default_fields) | OPTIONAL_FIELDS diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 9965df0a03..8fba2c7667 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -224,6 +224,7 @@ def get_permitted_fields( meta = frappe.get_meta(doctype) valid_columns = meta.get_valid_columns() + # note: any change here should also be made in _get_filterable_fields in query.py if doctype in CORE_DOCTYPES: return valid_columns diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 86b4114b99..ccbdb2dcd2 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -1037,6 +1037,40 @@ class TestQuery(IntegrationTestCase): self.assertEqual(len(result), 1, "Should find the note when filtering by permlevel 0 field") self.assertEqual(result[0]["name"], note.name) + def test_core_doctype_filterable_fields_with_select_permission(self): + """Core doctypes like User should allow filtering by any field when the user + only has select permission. Regression test for #37923.""" + test_role = "CoreSelectTestRole" + test_user_email = "test2@example.com" + + 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.get_doc({"doctype": "Role", "role_name": test_role}).insert(ignore_if_duplicate=True) + add_permission("User", test_role, 0, ptype="select") + update_permission_property("User", test_role, 0, "read", 0, validate=False) + test_user.add_roles(test_role) + + def cleanup(): + frappe.set_user("Administrator") + test_user.remove_roles(test_role) + frappe.delete_doc("Role", test_role, ignore_missing=True, force=True) + + self.addCleanup(cleanup) + + frappe.set_user(test_user_email) + + # filter by user_type and enabled — the exact filters used by search_link for assignment + result = frappe.qb.get_query( + "User", + filters={"user_type": "System User", "enabled": 1}, + fields=["name"], + ignore_permissions=False, + ).run(as_dict=True) + self.assertTrue(len(result) > 0, "Should be able to filter User by user_type and enabled") + def test_nested_permission(self): """Test permission on nested doctypes""" frappe.set_user("Administrator")