From 9d615f7f12c7eebf1d5eb9f1c41d95cfdc02c37a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Fri, 11 Oct 2019 09:45:36 +0530 Subject: [PATCH] fix(security): use restricted python --- frappe/core/doctype/report/report.json | 979 ++++-------------- frappe/core/doctype/report/report.py | 42 +- frappe/core/doctype/report/test_report.py | 3 + .../doctype/server_script/server_script.json | 33 +- .../doctype/server_script/server_script.py | 11 +- .../server_script/server_script_utils.py | 14 +- .../server_script/test_server_script.py | 85 +- frappe/desk/query_report.py | 43 +- frappe/tests/test_safe_exec.py | 11 + frappe/utils/jinja.py | 2 +- .../utils/{safe_globals.py => safe_exec.py} | 37 +- requirements.txt | 1 + 12 files changed, 383 insertions(+), 878 deletions(-) create mode 100644 frappe/tests/test_safe_exec.py rename frappe/utils/{safe_globals.py => safe_exec.py} (76%) diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index 339b6c5d4c..40d2417a56 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -1,777 +1,204 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:report_name", - "beta": 0, - "creation": "2013-03-09 15:45:57", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 1 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "ref_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Ref DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "reference_report", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Report", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "is_standard", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Is Standard", - "length": 0, - "no_copy": 0, - "options": "No\nYes", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "module", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Module", - "length": 0, - "no_copy": 0, - "options": "Module Def", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "add_total_row", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Add Total Row", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "report_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Report Type", - "length": 0, - "no_copy": 0, - "options": "Report Builder\nQuery Report\nScript Report\nCustom Report", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "icon", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Icon", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "color", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Color", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.is_standard == \"No\"", - "fetch_if_empty": 0, - "fieldname": "letter_head", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Letter Head", - "length": 0, - "no_copy": 0, - "options": "Letter Head", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.report_type==\"Query Report\"", - "fetch_if_empty": 0, - "fieldname": "query", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Query", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", - "fetch_if_empty": 0, - "fieldname": "javascript", - "fieldtype": "Code", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Javascript", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", - "fetch_if_empty": 0, - "fieldname": "json", - "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "JSON", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "permission_rules", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.is_standard == 'Yes'", - "fetch_if_empty": 0, - "fieldname": "roles", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Roles", - "length": 0, - "no_copy": 0, - "options": "Has Role", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "disable_prepared_report", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disable Prepared Report", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "prepared_report", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Prepared Report", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "icon": "", - "idx": 1, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-12 15:53:14.194591", - "modified_by": "Administrator", - "module": "Core", - "name": "Report", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Report Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 - } \ No newline at end of file + "autoname": "field:report_name", + "creation": "2013-03-09 15:45:57", + "doctype": "DocType", + "document_type": "System", + "engine": "InnoDB", + "field_order": [ + "report_name", + "ref_doctype", + "reference_report", + "is_standard", + "module", + "column_break_4", + "report_type", + "letter_head", + "add_total_row", + "disabled", + "disable_prepared_report", + "prepared_report", + "section_break_6", + "query", + "javascript", + "report_script", + "json", + "permission_rules", + "roles" + ], + "fields": [ + { + "fieldname": "report_name", + "fieldtype": "Data", + "label": "Report Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Ref DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "reference_report", + "fieldtype": "Data", + "label": "Reference Report" + }, + { + "fieldname": "is_standard", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Is Standard", + "options": "No\nYes", + "reqd": 1 + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" + }, + { + "default": "0", + "fieldname": "add_total_row", + "fieldtype": "Check", + "label": "Add Total Row" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "report_type", + "fieldtype": "Select", + "label": "Report Type", + "options": "Report Builder\nQuery Report\nScript Report\nCustom Report", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "eval: doc.is_standard == \"No\"", + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.report_type==\"Query Report\"", + "fieldname": "query", + "fieldtype": "Code", + "label": "Query" + }, + { + "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", + "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", + "fieldname": "javascript", + "fieldtype": "Code", + "label": "Javascript" + }, + { + "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", + "fieldname": "json", + "fieldtype": "Code", + "label": "JSON", + "read_only": 1 + }, + { + "fieldname": "permission_rules", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.is_standard == 'Yes'", + "fieldname": "roles", + "fieldtype": "Table", + "label": "Roles", + "options": "Has Role" + }, + { + "default": "0", + "fieldname": "disable_prepared_report", + "fieldtype": "Check", + "label": "Disable Prepared Report" + }, + { + "default": "0", + "fieldname": "prepared_report", + "fieldtype": "Check", + "hidden": 1, + "label": "Prepared Report", + "read_only": 1 + }, + { + "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", + "description": "output in the form of `data = [columns, result]`", + "fieldname": "report_script", + "fieldtype": "Code", + "label": "Script" + } + ], + "idx": 1, + "modified": "2019-10-09 15:43:08.577610", + "modified_by": "Administrator", + "module": "Core", + "name": "Report", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Report Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All" + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index df62f94bcb..6e07c1a650 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -import json +import json, datetime from frappe import _ import frappe.desk.query_report from frappe.utils import cint @@ -27,16 +27,15 @@ class Report(Document): if frappe.session.user=="Administrator" and getattr(frappe.local.conf, 'developer_mode',0)==1: self.is_standard = "Yes" - if self.is_standard == "No" and frappe.db.get_value("Report", self.name, "is_standard") == "Yes": - frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) + if self.is_standard == "No": + frappe.only_for('Script Manager') + + if frappe.db.get_value("Report", self.name, "is_standard") == "Yes": + frappe.throw(_("Cannot edit a standard report. Please duplicate and create a new report")) if self.is_standard == "Yes" and frappe.session.user!="Administrator": frappe.throw(_("Only Administrator can save a standard report. Please rename and save.")) - if self.report_type in ("Query Report", "Script Report") \ - and frappe.session.user!="Administrator": - frappe.throw(_("Only Administrator allowed to create Query / Script Reports")) - if self.report_type == "Report Builder": self.update_report_json() @@ -92,6 +91,35 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def execute_script_report(self, filters): + threshold = 30 + res = [] + + start_time = datetime.datetime.now() + # The JOB + if self.is_standard == 'Yes': + module = self.module or frappe.db.get_value("DocType", self.ref_doctype, "module") + method_name = get_report_module_dotted_path(module, report.name) + ".execute" + res = frappe.get_attr(method_name)(frappe._dict(filters)) + else: + if not frappe.conf.server_script_enabled: + raise ServerScriptNotEnabled + loc = {"filters": frappe._dict(filters), 'data':[]} + exec(self.report_script, globals(), loc) + res = loc['data'] + + end_time = datetime.datetime.now() + + execution_time = (end_time - start_time).seconds + + if execution_time > threshold and not self.prepared_report: + self.db_set('prepared_report', 1) + + frappe.cache().hset('report_execution_time', self.name, execution_time) + + return res + + def get_data(self, filters=None, limit=None, user=None, as_dict=False): columns = [] out = [] diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 258734743f..ad21675ee5 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -68,3 +68,6 @@ class TestReport(unittest.TestCase): self.assertEqual(columns[1].get('label'), 'User Type') self.assertTrue('Administrator' in [d[0] for d in data]) frappe.delete_doc('Report', 'User Activity Report Without Sort') + + def test_non_standard_script_report(self): + pass diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index ed72eba030..82fff31394 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -6,11 +6,13 @@ "engine": "InnoDB", "field_order": [ "script_type", + "disabled", + "column_break_3", "reference_doctype", "doctype_event", - "frequency", "api_method", "allow_guest", + "section_break_8", "script" ], "fields": [ @@ -33,7 +35,7 @@ "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, - "label": "Reference DocType", + "label": "Reference Document Type", "options": "DocType" }, { @@ -41,14 +43,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nBefore Cancel\nBefore Delete" - }, - { - "depends_on": "eval:doc.script_type==='Scheduled Job'", - "fieldname": "frequency", - "fieldtype": "Select", - "label": "Frequency", - "options": "Hourly\nDaily\nWeekly\nMonthly" + "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete" }, { "depends_on": "eval:doc.script_type==='API'", @@ -62,9 +57,23 @@ "fieldname": "allow_guest", "fieldtype": "Check", "label": "Allow Guest" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" } ], - "modified": "2019-10-04 15:51:57.995877", + "modified": "2019-10-09 15:08:40.085059", "modified_by": "Administrator", "module": "Core", "name": "Server Script", @@ -78,7 +87,7 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Script Manager", "share": 1, "write": 1 } diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 26650dab99..50d64b2180 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -6,13 +6,13 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe.utils.safe_globals import get_safe_globals +from frappe.utils.safe_exec import safe_exec class ServerScriptNotEnabled(frappe.PermissionError): pass class ServerScript(Document): def validate(self): - frappe.only_for('System Manager') + frappe.only_for('Script Manager') def on_update(self): frappe.cache().delete_value('server_script_map') @@ -23,14 +23,13 @@ class ServerScript(Document): if self.script_type == 'API': if frappe.session.user == 'Guest' and not self.allow_guest: raise frappe.PermissionError - exec(self.script, globals(), None) + safe_exec(self.script) else: raise frappe.DoesNotExistError def execute_doc(self, doc): if not frappe.conf.server_script_enabled: raise ServerScriptNotEnabled - context = doc.as_dict() - context.doc = doc - exec(self.script, globals(), context) + context = dict(doc = doc) + safe_exec(self.script, None, context) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 27894f9b6a..2bece465dd 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -6,8 +6,11 @@ EVENT_MAP = { 'validate': 'Before Save', 'on_update': 'After Save', 'before_submit': 'Before Submit', + 'on_submit': 'After Submit', 'before_cancel': 'Before Cancel', - 'before_delete': 'Before Delete' + 'on_cancel': 'After Cancel', + 'on_trash': 'Before Delete', + 'after_delete': 'After Delete', } def run_server_script_api(method): @@ -23,9 +26,10 @@ def run_server_script_for_doc_event(doc, event): if frappe.flags.in_install: return - script_name = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) - if script_name: - frappe.get_doc('Server Script', script_name).execute_doc(doc) + scripts = get_server_script_map().get(doc.doctype, {}).get(EVENT_MAP[event], None) + if scripts: + for script_name in scripts: + frappe.get_doc('Server Script', script_name).execute_doc(doc) def get_server_script_map(): script_map = frappe.cache().get_value('server_script_map') @@ -34,7 +38,7 @@ def get_server_script_map(): for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event', 'api_method', 'script_type')): if script.script_type == 'DocType Event': - script_map.setdefault(script.reference_doctype, {})[script.doctype_event] = script.name + script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: script_map.setdefault('_api', {})[script.api_method] = script.name frappe.cache().set_value('server_script_map', script_map) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 1c5e0ddf6d..287b18d7fa 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -9,46 +9,67 @@ import requests from frappe.utils import get_site_url from frappe.core.doctype.server_script.server_script_utils import get_server_script_map +scripts = [ + dict( + name='test_todo', + script_type = 'DocType Event', + doctype_event = 'Before Insert', + reference_doctype = 'ToDo', + script = ''' +if "test" in doc.description: + doc.status = 'Closed' +''' + ), + dict( + name='test_todo_validate', + script_type = 'DocType Event', + doctype_event = 'Before Insert', + reference_doctype = 'ToDo', + script = ''' +if "validate" in doc.description: + raise frappe.ValidationError +''' + ), + dict( + name='test_api', + script_type = 'API', + api_method = 'test_server_script', + allow_guest = 1, + script = ''' +frappe.response['message'] = 'hello' +''' + ) +] class TestServerScript(unittest.TestCase): + @classmethod + def setUpClass(cls): + frappe.db.commit() + frappe.db.sql('truncate `tabServer Script`') + frappe.get_doc('User', 'Administrator').add_roles('Script Manager') + for script in scripts: + script_doc = frappe.get_doc(doctype ='Server Script') + script_doc.update(script) + script_doc.insert() + + frappe.db.commit() + + # @classmethod + # def tearDownClass(cls): + # frappe.db.sql('truncate `tabServer Script`') + def setUp(self): frappe.cache().delete_value('server_script_map') def test_doctype_event(self): - script = get_server_script() - script.script_type = 'DocType Event' - script.script = 'frappe.flags._ping = True' - script.reference_doctype = 'ToDo' - script.doctype_event = 'Before Save' - script.save() - frappe.db.commit() + todo = frappe.get_doc(dict(doctype='ToDo', description='hello')).insert() + self.assertEqual(todo.status, 'Open') - frappe.flags._ping = False - frappe.get_doc(dict(doctype='ToDo', description='test todo')).insert() - self.assertTrue(frappe.flags._ping) + todo = frappe.get_doc(dict(doctype='ToDo', description='test todo')).insert() + self.assertEqual(todo.status, 'Closed') + + self.assertRaises(frappe.ValidationError, frappe.get_doc(dict(doctype='ToDo', description='validate me')).insert) def test_api(self): - script = get_server_script() - script.script_type = 'API' - script.api_method = 'test_server_script' - script.allow_guest = 1 - script.script = 'frappe.response["message"] = "hello"' - script.save() - frappe.db.commit() - response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script") self.assertEqual(response.status_code, 200) self.assertEqual("hello", response.json()["message"]) - - -def get_server_script(): - if frappe.db.exists('Server Script', 'Test Server Script'): - return frappe.get_doc('Server Script', 'Test Server Script') - else: - script = frappe.get_doc(dict( - doctype = 'Server Script', - name = 'Test Server Script', - script = '# nothing', - script_type = 'DocType Event' - )).insert() - - return script diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 5d1829abb2..192290c127 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -import os, json, datetime +import os, json from frappe import _ from frappe.modules import scrub, get_module_path @@ -63,38 +63,21 @@ def generate_report_result(report, filters=None, user=None): result = [list(t) for t in frappe.db.sql(report.query, filters)] columns = [cstr(c[0]) for c in frappe.db.get_description()] - else: - module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module") - if report.is_standard == "Yes": - method_name = get_report_module_dotted_path(module, report.name) + ".execute" - threshold = 30 - res = [] - start_time = datetime.datetime.now() - # The JOB - res = frappe.get_attr(method_name)(frappe._dict(filters)) + elif report.report_type == 'Script Report': + res = report.execute_script_report(filters) - end_time = datetime.datetime.now() + columns, result = res[0], res[1] + if len(res) > 2: + message = res[2] + if len(res) > 3: + chart = res[3] + if len(res) > 4: + data_to_be_printed = res[4] - execution_time = (end_time - start_time).seconds - - if execution_time > threshold and not report.prepared_report: - report.db_set('prepared_report', 1) - - frappe.cache().hset('report_execution_time', report.name, execution_time) - - columns, result = res[0], res[1] - if len(res) > 2: - message = res[2] - if len(res) > 3: - chart = res[3] - if len(res) > 4: - data_to_be_printed = res[4] - - - if report.custom_columns: - columns = json.loads(report.custom_columns) - result = add_data_to_custom_columns(columns, result) + if report.custom_columns: + columns = json.loads(report.custom_columns) + result = add_data_to_custom_columns(columns, result) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py new file mode 100644 index 0000000000..b868b4fcb8 --- /dev/null +++ b/frappe/tests/test_safe_exec.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +import unittest +import frappe +from frappe.utils.safe_exec import safe_exec + +class TestSafeExec(unittest.TestCase): + def test_import_fails(self): + self.assertRaises(ImportError, safe_exec, 'import os') + + def test_internal_attributes(self): + self.assertRaises(SyntaxError, safe_exec, '().__class__.__call__') \ No newline at end of file diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index d4c8ddc50e..befb9336fa 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals def get_jenv(): import frappe - from frappe.utils.safe_globals import get_safe_globals + from frappe.utils.safe_exec import get_safe_globals if not getattr(frappe.local, 'jenv', None): from jinja2 import DebugUndefined diff --git a/frappe/utils/safe_globals.py b/frappe/utils/safe_exec.py similarity index 76% rename from frappe/utils/safe_globals.py rename to frappe/utils/safe_exec.py index 0decf110f4..89acd14d2f 100644 --- a/frappe/utils/safe_globals.py +++ b/frappe/utils/safe_exec.py @@ -1,7 +1,12 @@ -import os, json +import os, json, inspect import mimetypes from html2text import html2text +from RestrictedPython import compile_restricted, safe_globals + +def safe_exec(script, _globals=None, _locals=None): + if not _globals: _globals = get_safe_globals() + exec(compile_restricted(script), _globals, _locals) def get_safe_globals(): import frappe @@ -11,6 +16,7 @@ def get_safe_globals(): from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub from frappe.www.printview import get_visible_columns + import frappe.exceptions datautils = {} if frappe.db: @@ -18,14 +24,7 @@ def get_safe_globals(): else: date_format = 'yyyy-mm-dd' - for key, obj in frappe.utils.data.__dict__.items(): - if key.startswith("_"): - # ignore - continue - - if hasattr(obj, "__call__"): - # only allow functions - datautils[key] = obj + add_module_properties(frappe.utils.data, datautils, lambda obj: hasattr(obj, "__call__")) if "_" in getattr(frappe.local, 'form_dict', {}): del frappe.local.form_dict["_"] @@ -77,6 +76,8 @@ def get_safe_globals(): dev_server = 1 if os.environ.get('DEV_SERVER', False) else 0 ) + add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)) + if not frappe.flags.in_setup_help: out.get_visible_columns = get_visible_columns out.frappe.date_format = date_format @@ -87,4 +88,22 @@ def get_safe_globals(): escape = frappe.db.escape, ) + if frappe.response: + out.frappe.response = frappe.response + + out.update(safe_globals) + + # default writer allows write access + out._write_ = lambda obj: obj + return out + +def add_module_properties(module, data, filter_method): + for key, obj in module.__dict__.items(): + if key.startswith("_"): + # ignore + continue + + if filter_method(obj): + # only allow functions + data[key] = obj diff --git a/requirements.txt b/requirements.txt index 5e56468c0e..84788c863e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -65,3 +65,4 @@ Pygments==2.2.0 frontmatter PyYAML==3.13 xlrd +RestrictedPython==5.0 \ No newline at end of file