Преглед изворни кода

Merge pull request #15407 from resilient-tech/server-scripts

feat: `frappe.enqueue` and `frappe.call` for Server Scripts
version-14
mergify[bot] пре 3 година
committed by GitHub
родитељ
комит
1b28c3b4ed
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
4 измењених фајлова са 101 додато и 18 уклоњено
  1. +0
    -7
      frappe/core/doctype/server_script/server_script_utils.py
  2. +17
    -3
      frappe/handler.py
  3. +24
    -1
      frappe/tests/test_safe_exec.py
  4. +60
    -7
      frappe/utils/safe_exec.py

+ 0
- 7
frappe/core/doctype/server_script/server_script_utils.py Прегледај датотеку

@@ -19,13 +19,6 @@ EVENT_MAP = {
'on_update_after_submit': 'After Save (Submitted Document)' 'on_update_after_submit': 'After Save (Submitted Document)'
} }


def run_server_script_api(method):
# called via handler, execute an API script
script_name = get_server_script_map().get('_api', {}).get(method)
if script_name:
frappe.get_doc('Server Script', script_name).execute_method()
return True

def run_server_script_for_doc_event(doc, event): def run_server_script_for_doc_event(doc, event):
# run document event method # run document event method
if not event in EVENT_MAP: if not event in EVENT_MAP:


+ 17
- 3
frappe/handler.py Прегледај датотеку

@@ -12,7 +12,7 @@ from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response from frappe.utils.csvutils import build_csv_response
from frappe.utils.image import optimize_image from frappe.utils.image import optimize_image
from mimetypes import guess_type from mimetypes import guess_type
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map




ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
@@ -49,8 +49,9 @@ def execute_cmd(cmd, from_async=False):
break break


# via server script # via server script
if run_server_script_api(cmd):
return None
server_script = get_server_script_map().get('_api', {}).get(cmd)
if server_script:
return run_server_script(server_script)


try: try:
method = get_attr(cmd) method = get_attr(cmd)
@@ -66,7 +67,20 @@ def execute_cmd(cmd, from_async=False):


return frappe.call(method, **frappe.form_dict) return frappe.call(method, **frappe.form_dict)



def run_server_script(server_script):
response = frappe.get_doc('Server Script', server_script).execute_method()

# some server scripts return output using flags (empty dict by default),
# while others directly modify frappe.response
# return flags if not empty dict (this overwrites frappe.response.message)
if response != {}:
return response

def is_valid_http_method(method): def is_valid_http_method(method):
if frappe.flags.in_safe_exec:
return

http_method = frappe.local.request.method http_method = frappe.local.request.method


if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]: if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]:


+ 24
- 1
frappe/tests/test_safe_exec.py Прегледај датотеку

@@ -31,4 +31,27 @@ class TestSafeExec(unittest.TestCase):
self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"]) self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"])


def test_safe_query_builder(self): def test_safe_query_builder(self):
self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''')
self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''')

def test_call(self):
# call non whitelisted method
self.assertRaises(
frappe.PermissionError,
safe_exec,
"""frappe.call("frappe.get_user")"""
)

# call whitelisted method
safe_exec("""frappe.call("ping")""")


def test_enqueue(self):
# enqueue non whitelisted method
self.assertRaises(
frappe.PermissionError,
safe_exec,
"""frappe.enqueue("frappe.get_user", now=True)"""
)

# enqueue whitelisted method
safe_exec("""frappe.enqueue("ping", now=True)""")

+ 60
- 7
frappe/utils/safe_exec.py Прегледај датотеку

@@ -14,11 +14,12 @@ import frappe.integrations.utils
import frappe.utils import frappe.utils
import frappe.utils.data import frappe.utils.data
from frappe import _ from frappe import _
from frappe.handler import execute_cmd
from frappe.frappeclient import FrappeClient from frappe.frappeclient import FrappeClient
from frappe.modules import scrub from frappe.modules import scrub
from frappe.website.utils import get_next_link, get_shade, get_toc from frappe.website.utils import get_next_link, get_shade, get_toc
from frappe.www.printview import get_visible_columns from frappe.www.printview import get_visible_columns
from frappe.utils.background_jobs import enqueue, get_jobs


class ServerScriptNotEnabled(frappe.PermissionError): class ServerScriptNotEnabled(frappe.PermissionError):
pass pass
@@ -74,7 +75,9 @@ def get_safe_globals():


add_data_utils(datautils) add_data_utils(datautils)


if "_" in getattr(frappe.local, 'form_dict', {}):
form_dict = getattr(frappe.local, 'form_dict', frappe._dict())

if "_" in form_dict:
del frappe.local.form_dict["_"] del frappe.local.form_dict["_"]


user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest"
@@ -89,14 +92,16 @@ def get_safe_globals():
dict=dict, dict=dict,
log=frappe.log, log=frappe.log,
_dict=frappe._dict, _dict=frappe._dict,
args=form_dict,
frappe=NamespaceDict( frappe=NamespaceDict(
call=call_whitelisted_function,
flags=frappe._dict(), flags=frappe._dict(),
format=frappe.format_value, format=frappe.format_value,
format_value=frappe.format_value, format_value=frappe.format_value,
date_format=date_format, date_format=date_format,
time_format=time_format, time_format=time_format,
format_date=frappe.utils.data.global_date_format, format_date=frappe.utils.data.global_date_format,
form_dict=getattr(frappe.local, 'form_dict', {}),
form_dict=form_dict,
bold=frappe.bold, bold=frappe.bold,
copy_doc=frappe.copy_doc, copy_doc=frappe.copy_doc,
errprint=frappe.errprint, errprint=frappe.errprint,
@@ -132,6 +137,7 @@ def get_safe_globals():
make_post_request=frappe.integrations.utils.make_post_request, make_post_request=frappe.integrations.utils.make_post_request,
socketio_port=frappe.conf.socketio_port, socketio_port=frappe.conf.socketio_port,
get_hooks=get_hooks, get_hooks=get_hooks,
enqueue=safe_enqueue,
sanitize_html=frappe.utils.sanitize_html, sanitize_html=frappe.utils.sanitize_html,
log_error=frappe.log_error log_error=frappe.log_error
), ),
@@ -147,7 +153,8 @@ def get_safe_globals():
guess_mimetype=mimetypes.guess_type, guess_mimetype=mimetypes.guess_type,
html2text=html2text, html2text=html2text,
dev_server=1 if frappe._dev_server else 0, dev_server=1 if frappe._dev_server else 0,
run_script=run_script
run_script=run_script,
is_job_queued=is_job_queued,
) )


add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)) add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
@@ -190,6 +197,55 @@ def get_safe_globals():


return out return out


def is_job_queued(job_name, queue="default"):
'''
:param job_name: used to identify a queued job, usually dotted path to function
:param queue: should be either long, default or short
'''

site = frappe.local.site
queued_jobs = get_jobs(site=site, queue=queue, key='job_name').get(site)
return queued_jobs and job_name in queued_jobs

def safe_enqueue(function, **kwargs):
'''
Enqueue function to be executed using a background worker
Accepts frappe.enqueue params like job_name, queue, timeout, etc.
in addition to params to be passed to function

:param function: whitelised function or API Method set in Server Script
'''

return enqueue(
'frappe.utils.safe_exec.call_whitelisted_function',
function=function,
**kwargs
)

def call_whitelisted_function(function, **kwargs):
'''Executes a whitelisted function or Server Script of type API'''

return call_with_form_dict(lambda: execute_cmd(function), kwargs)

def run_script(script, **kwargs):
'''run another server script'''

return call_with_form_dict(
lambda: frappe.get_doc('Server Script', script).execute_method(),
kwargs
)

def call_with_form_dict(function, kwargs):
# temporarily update form_dict, to use inside below call
form_dict = getattr(frappe.local, 'form_dict', frappe._dict())
if kwargs:
frappe.local.form_dict = form_dict.copy().update(kwargs)

try:
return function()
finally:
frappe.local.form_dict = form_dict

def get_python_builtins(): def get_python_builtins():
return { return {
'abs': abs, 'abs': abs,
@@ -221,9 +277,6 @@ def read_sql(query, *args, **kwargs):
raise frappe.PermissionError('Only SELECT SQL allowed in scripting') raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
return frappe.db.sql(query, *args, **kwargs) return frappe.db.sql(query, *args, **kwargs)


def run_script(script):
'''run another server script'''
return frappe.get_doc('Server Script', script).execute_method()


def _getitem(obj, key): def _getitem(obj, key):
# guard function for RestrictedPython # guard function for RestrictedPython


Loading…
Откажи
Сачувај