Browse Source

Merge pull request #12720 from resilient-tech/whitelist-methods

feat: frappe.whitelist for doc methods
version-14
mergify[bot] 4 years ago
committed by GitHub
parent
commit
b51f13c291
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 183 additions and 141 deletions
  1. +23
    -2
      frappe/__init__.py
  2. +2
    -0
      frappe/automation/doctype/auto_repeat/auto_repeat.py
  3. +1
    -0
      frappe/core/doctype/data_import/data_import.py
  4. +2
    -1
      frappe/core/doctype/report/report.py
  5. +3
    -0
      frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py
  6. +3
    -0
      frappe/custom/doctype/customize_form/customize_form.py
  7. +1
    -0
      frappe/data_migration/doctype/data_migration_run/data_migration_run.py
  8. +0
    -81
      frappe/desk/form/run_method.py
  9. +2
    -3
      frappe/desk/search.py
  10. +1
    -0
      frappe/email/doctype/newsletter/newsletter.py
  11. +75
    -39
      frappe/handler.py
  12. +1
    -0
      frappe/integrations/doctype/connected_app/connected_app.py
  13. +1
    -0
      frappe/integrations/doctype/social_login_key/social_login_key.py
  14. +10
    -10
      frappe/model/document.py
  15. +1
    -1
      frappe/public/js/frappe/form/controls/button.js
  16. +1
    -1
      frappe/public/js/frappe/request.js
  17. +1
    -0
      frappe/social/doctype/energy_point_log/energy_point_log.py
  18. +1
    -2
      frappe/tests/test_api.py
  19. +49
    -1
      frappe/tests/test_client.py
  20. +1
    -0
      frappe/website/doctype/blog_post/blog_post.py
  21. +1
    -0
      frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
  22. +1
    -0
      frappe/website/doctype/portal_settings/portal_settings.py
  23. +2
    -0
      frappe/website/doctype/website_theme/website_theme.py

+ 23
- 2
frappe/__init__.py View File

@@ -555,8 +555,15 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):

def innerfn(fn):
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
whitelisted.append(fn)

# get function from the unbound / bound method
# this is needed because functions can be compared, but not methods
method = None
if hasattr(fn, '__func__'):
method = fn
fn = method.__func__

whitelisted.append(fn)
allowed_http_methods_for_whitelisted_func[fn] = methods

if allow_guest:
@@ -565,10 +572,24 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
if xss_safe:
xss_safe_methods.append(fn)

return fn
return method or fn

return innerfn

def is_whitelisted(method):
from frappe.utils import sanitize_html

is_guest = session['user'] == 'Guest'
if method not in whitelisted or is_guest and method not in guest_methods:
throw(_("Not permitted"), PermissionError)

if is_guest and method not in xss_safe_methods:
# strictly sanitize form_dict
# escapes html characters like <> except for predefined tags like a, b, ul etc.
for key, value in form_dict.items():
if isinstance(value, string_types):
form_dict[key] = sanitize_html(value)

def read_only():
def innfn(fn):
def wrapper_fn(*args, **kwargs):


+ 2
- 0
frappe/automation/doctype/auto_repeat/auto_repeat.py View File

@@ -118,6 +118,7 @@ class AutoRepeat(Document):
def is_completed(self):
return self.end_date and getdate(self.end_date) < getdate(today())

@frappe.whitelist()
def get_auto_repeat_schedule(self):
schedule_details = []
start_date = getdate(self.start_date)
@@ -328,6 +329,7 @@ class AutoRepeat(Document):
make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
subject=subject, content=message, attachments=attachments, send_email=1)

@frappe.whitelist()
def fetch_linked_contacts(self):
if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])


+ 1
- 0
frappe/core/doctype/data_import/data_import.py View File

@@ -38,6 +38,7 @@ class DataImport(Document):
return
validate_google_sheets_url(self.google_sheets_url)

@frappe.whitelist()
def get_preview_from_template(self, import_file=None, google_sheets_url=None):
if import_file:
self.import_file = import_file


+ 2
- 1
frappe/core/doctype/report/report.py View File

@@ -58,6 +58,7 @@ class Report(Document):
def get_columns(self):
return [d.as_dict(no_default_fields = True) for d in self.columns]

@frappe.whitelist()
def set_doctype_roles(self):
if not self.get('roles') and self.is_standard == 'No':
meta = frappe.get_meta(self.ref_doctype)
@@ -304,7 +305,7 @@ class Report(Document):

return data

@Document.whitelist
@frappe.whitelist()
def toggle_disable(self, disable):
self.db_set("disabled", cint(disable))



+ 3
- 0
frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py View File

@@ -8,6 +8,7 @@ from frappe.core.doctype.report.report import is_prepared_report_disabled
from frappe.model.document import Document

class RolePermissionforPageandReport(Document):
@frappe.whitelist()
def set_report_page_data(self):
self.set_custom_roles()
self.check_prepared_report_disabled()
@@ -35,12 +36,14 @@ class RolePermissionforPageandReport(Document):
doc = frappe.get_doc(doctype, docname)
return doc.roles

@frappe.whitelist()
def reset_roles(self):
roles = self.get_standard_roles()
self.set('roles', roles)
self.update_custom_roles()
self.update_disable_prepared_report()

@frappe.whitelist()
def update_report_page_data(self):
self.update_custom_roles()
self.update_disable_prepared_report()


+ 3
- 0
frappe/custom/doctype/customize_form/customize_form.py View File

@@ -24,6 +24,7 @@ class CustomizeForm(Document):
frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
frappe.db.sql("delete from `tabCustomize Form Field`")

@frappe.whitelist()
def fetch_to_customize(self):
self.clear_existing_doc()
if not self.doc_type:
@@ -133,6 +134,7 @@ class CustomizeForm(Document):
self.doc_type = doc_type
self.name = "Customize Form"

@frappe.whitelist()
def save_customization(self):
if not self.doc_type:
return
@@ -448,6 +450,7 @@ class CustomizeForm(Document):

self.flags.update_db = True

@frappe.whitelist()
def reset_to_defaults(self):
if not self.doc_type:
return


+ 1
- 0
frappe/data_migration/doctype/data_migration_run/data_migration_run.py View File

@@ -10,6 +10,7 @@ from frappe.utils import cstr
from frappe.data_migration.doctype.data_migration_mapping.data_migration_mapping import get_source_value

class DataMigrationRun(Document):
@frappe.whitelist()
def run(self):
self.begin()
if self.total_pages > 0:


+ 0
- 81
frappe/desk/form/run_method.py View File

@@ -1,81 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import json, inspect
import frappe
from frappe import _
from frappe.utils import cint
from six import text_type, string_types

@frappe.whitelist()
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run controller method - old style"""
if not args: args = arg or ""

if dt: # not called from a doctype (from a page)
if not dn: dn = dt # single
doc = frappe.get_doc(dt, dn)

else:
doc = frappe.get_doc(json.loads(docs))
doc._original_modified = doc.modified
doc.check_if_latest()

if not doc.has_permission("read"):
frappe.msgprint(_("Not permitted"), raise_exception = True)

if doc:
try:
args = json.loads(args)
except ValueError:
args = args

try:
fnargs, varargs, varkw, defaults = inspect.getargspec(getattr(doc, method))
except ValueError:
fnargs = inspect.getfullargspec(getattr(doc, method)).args
varargs = inspect.getfullargspec(getattr(doc, method)).varargs
varkw = inspect.getfullargspec(getattr(doc, method)).varkw
defaults = inspect.getfullargspec(getattr(doc, method)).defaults

if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
r = doc.run_method(method)

elif "args" in fnargs or not isinstance(args, dict):
r = doc.run_method(method, args)

else:
r = doc.run_method(method, **args)

if r:
#build output as csv
if cint(frappe.form_dict.get('as_csv')):
make_csv_output(r, doc.doctype)
else:
frappe.response['message'] = r

frappe.response.docs.append(doc)

def make_csv_output(res, dt):
"""send method response as downloadable CSV file"""
import frappe

from six import StringIO
import csv

f = StringIO()
writer = csv.writer(f)
for r in res:
row = []
for v in r:
if isinstance(v, string_types):
v = v.encode("utf-8")
row.append(v)
writer.writerow(row)

f.seek(0)

frappe.response['result'] = text_type(f.read(), 'utf-8')
frappe.response['type'] = 'csv'
frappe.response['doctype'] = dt.replace(' ','')

+ 2
- 3
frappe/desk/search.py View File

@@ -6,8 +6,7 @@ from __future__ import unicode_literals
import frappe, json
from frappe.utils import cstr, unique, cint
from frappe.permissions import has_permission
from frappe.handler import is_whitelisted
from frappe import _
from frappe import _, is_whitelisted
from six import string_types
import re
import wrapt
@@ -221,4 +220,4 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']):
return []

return fn(**kwargs)
return fn(**kwargs)

+ 1
- 0
frappe/email/doctype/newsletter/newsletter.py View File

@@ -29,6 +29,7 @@ class Newsletter(WebsiteGenerator):
self.queue_all(test_email=True)
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))

@frappe.whitelist()
def send_emails(self):
"""send emails to leads and customers"""
if self.email_sent:


+ 75
- 39
frappe/handler.py View File

@@ -2,17 +2,19 @@
# MIT License. See license.txt

from __future__ import unicode_literals

from werkzeug.wrappers import Response

import frappe
from frappe import _
import frappe.utils
import frappe.sessions
import frappe.desk.form.run_method
from frappe.utils.response import build_response
from frappe.api import validate_auth
from frappe.utils import cint
from frappe.api import validate_auth
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
from werkzeug.wrappers import Response
from six import string_types


ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -64,8 +66,9 @@ def execute_cmd(cmd, from_async=False):
if from_async:
method = method.queue

is_whitelisted(method)
is_valid_http_method(method)
if method != run_doc_method:
is_whitelisted(method)
is_valid_http_method(method)

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

@@ -73,33 +76,15 @@ def is_valid_http_method(method):
http_method = frappe.local.request.method

if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]:
frappe.throw(_("Not permitted"), frappe.PermissionError)

def is_whitelisted(method):
# check if whitelisted
if frappe.session['user'] == 'Guest':
if (method not in frappe.guest_methods):
frappe.throw(_("Not permitted"), frappe.PermissionError)
throw_permission_error()

if method not in frappe.xss_safe_methods:
# strictly sanitize form_dict
# escapes html characters like <> except for predefined tags like a, b, ul etc.
for key, value in frappe.form_dict.items():
if isinstance(value, string_types):
frappe.form_dict[key] = frappe.utils.sanitize_html(value)

else:
if not method in frappe.whitelisted:
frappe.throw(_("Not permitted"), frappe.PermissionError)
def throw_permission_error():
frappe.throw(_("Not permitted"), frappe.PermissionError)

@frappe.whitelist(allow_guest=True)
def version():
return frappe.__version__

@frappe.whitelist()
def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None):
frappe.desk.form.run_method.runserverobj(method, docs=docs, dt=dt, dn=dn, arg=arg, args=args)

@frappe.whitelist(allow_guest=True)
def logout():
frappe.local.login_manager.logout()
@@ -112,15 +97,6 @@ def web_logout():
frappe.respond_as_web_page(_("Logged Out"), _("You have been successfully logged out"),
indicator_color='green')

@frappe.whitelist(allow_guest=True)
def run_custom_method(doctype, name, custom_method):
"""cmd=run_custom_method&doctype={doctype}&name={name}&custom_method={custom_method}"""
doc = frappe.get_doc(doctype, name)
if getattr(doc, custom_method, frappe._dict()).is_whitelisted:
frappe.call(getattr(doc, custom_method), **frappe.local.form_dict)
else:
frappe.throw(_("Not permitted"), frappe.PermissionError)

@frappe.whitelist()
def uploadfile():
ret = None
@@ -222,6 +198,66 @@ def get_attr(cmd):
frappe.log("method:" + cmd)
return method

@frappe.whitelist(allow_guest = True)
@frappe.whitelist(allow_guest=True)
def ping():
return "pong"


def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method"""
import json
import inspect

if not args:
args = arg or ""

if dt: # not called from a doctype (from a page)
if not dn:
dn = dt # single
doc = frappe.get_doc(dt, dn)

else:
doc = frappe.get_doc(json.loads(docs))
doc._original_modified = doc.modified
doc.check_if_latest()

if not doc or not doc.has_permission("read"):
throw_permission_error()

try:
args = json.loads(args)
except ValueError:
args = args

method_obj = getattr(doc, method)
fn = getattr(method_obj, '__func__', method_obj)
is_whitelisted(fn)
is_valid_http_method(fn)

try:
fnargs = inspect.getargspec(method_obj)[0]
except ValueError:
fnargs = inspect.getfullargspec(method_obj).args

if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
response = doc.run_method(method)

elif "args" in fnargs or not isinstance(args, dict):
response = doc.run_method(method, args)

else:
response = doc.run_method(method, **args)

frappe.response.docs.append(doc)
if not response:
return

# build output as csv
if cint(frappe.form_dict.get('as_csv')):
build_csv_response(response, doc.doctype.replace(' ', ''))
return

frappe.response['message'] = response

# for backwards compatibility
runserverobj = run_doc_method

+ 1
- 0
frappe/integrations/doctype/connected_app/connected_app.py View File

@@ -44,6 +44,7 @@ class ConnectedApp(Document):
scope=self.get_scopes()
)

@frappe.whitelist()
def initiate_web_application_flow(self, user=None, success_uri=None):
"""Return an authorization URL for the user. Save state in Token Cache."""
user = user or frappe.session.user


+ 1
- 0
frappe/integrations/doctype/social_login_key/social_login_key.py View File

@@ -49,6 +49,7 @@ class SocialLoginKey(Document):
icon_file = icon_map[self.provider_name]
self.icon = '/assets/frappe/icons/social/{0}'.format(icon_file)

@frappe.whitelist()
def get_social_login_provider(self, provider, initialize=False):
providers = {}



+ 10
- 10
frappe/model/document.py View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
import time
from frappe import _, msgprint
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.naming import set_new_name
@@ -126,10 +126,10 @@ class Document(BaseDocument):
raise ValueError('Illegal arguments')

@staticmethod
def whitelist(f):
def whitelist(fn):
"""Decorator: Whitelist method to be called remotely via REST API."""
f.whitelisted = True
return f
frappe.whitelist()(fn)
return fn

def reload(self):
"""Reload document from database"""
@@ -1148,12 +1148,12 @@ class Document(BaseDocument):

return composer

def is_whitelisted(self, method):
fn = getattr(self, method, None)
if not fn:
raise NotFound("Method {0} not found".format(method))
elif not getattr(fn, "whitelisted", False):
raise Forbidden("Method {0} not whitelisted".format(method))
def is_whitelisted(self, method_name):
method = getattr(self, method_name, None)
if not method:
raise NotFound("Method {0} not found".format(method_name))
is_whitelisted(getattr(method, '__func__', method))

def validate_value(self, fieldname, condition, val2, doc=None, raise_exception=None):
"""Check that value of fieldname should be 'condition' val2


+ 1
- 1
frappe/public/js/frappe/form/controls/button.js View File

@@ -34,7 +34,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
var me = this;
if(this.frm && this.frm.docname) {
frappe.call({
method: "runserverobj",
method: "run_doc_method",
args: {'docs': this.frm.doc, 'method': this.df.options },
btn: this.$input,
callback: function(r) {


+ 1
- 1
frappe/public/js/frappe/request.js View File

@@ -55,7 +55,7 @@ frappe.call = function(opts) {
args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method;
} else if(opts.doc) {
$.extend(args, {
cmd: "runserverobj",
cmd: "run_doc_method",
docs: frappe.get_doc(opts.doc.doctype, opts.doc.name),
method: opts.method,
args: opts.args,


+ 1
- 0
frappe/social/doctype/energy_point_log/energy_point_log.py View File

@@ -52,6 +52,7 @@ class EnergyPointLog(Document):
reference_log.reverted = 0
reference_log.save()

@frappe.whitelist()
def revert(self, reason, ignore_permissions=False):
if not ignore_permissions:
frappe.only_for('System Manager')


+ 1
- 2
frappe/tests/test_api.py View File

@@ -143,8 +143,7 @@ class TestAPI(unittest.TestCase):
self.assertFalse(frappe.db.get_value('Note', {'title': 'delete'}))

def test_auth_via_api_key_secret(self):

# generate api ke and api secret for administrator
# generate API key and API secret for administrator
keys = generate_keys("Administrator")
frappe.db.commit()
generated_secret = frappe.utils.password.get_decrypted_password(


+ 49
- 1
frappe/tests/test_client.py View File

@@ -2,7 +2,9 @@

from __future__ import unicode_literals

import unittest, frappe
import unittest
import frappe


class TestClient(unittest.TestCase):
def test_set_value(self):
@@ -55,3 +57,49 @@ class TestClient(unittest.TestCase):
})

self.assertRaises(frappe.PermissionError, execute_cmd, 'frappe.client.save')

def test_run_doc_method(self):
from frappe.handler import execute_cmd

if not frappe.db.exists('Report', 'Test Run Doc Method'):
report = frappe.get_doc({
'doctype': 'Report',
'ref_doctype': 'User',
'report_name': 'Test Run Doc Method',
'report_type': 'Query Report',
'is_standard': 'No',
'roles': [
{'role': 'System Manager'}
]
}).insert()
else:
report = frappe.get_doc('Report', 'Test Run Doc Method')

frappe.local.request = frappe._dict()
frappe.local.request.method = 'GET'

# Whitelisted, works as expected
frappe.local.form_dict = frappe._dict({
'dt': report.doctype,
'dn': report.name,
'method': 'toggle_disable',
'cmd': 'run_doc_method',
'args': 0
})

execute_cmd(frappe.local.form_dict.cmd)

# Not whitelisted, throws permission error
frappe.local.form_dict = frappe._dict({
'dt': report.doctype,
'dn': report.name,
'method': 'create_report_py',
'cmd': 'run_doc_method',
'args': 0
})

self.assertRaises(
frappe.PermissionError,
execute_cmd,
frappe.local.form_dict.cmd
)

+ 1
- 0
frappe/website/doctype/blog_post/blog_post.py View File

@@ -18,6 +18,7 @@ class BlogPost(WebsiteGenerator):
order_by = "published_on desc"
)

@frappe.whitelist()
def make_route(self):
if not self.route:
return frappe.db.get_value('Blog Category', self.blog_category,


+ 1
- 0
frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py View File

@@ -101,6 +101,7 @@ class PersonalDataDeletionRequest(Document):
if self.status != "Pending Approval":
frappe.throw(_("This request has not yet been approved by the user."))

@frappe.whitelist()
def trigger_data_deletion(self):
"""Redact user data defined in current site's hooks under `user_data_fields`"""
self.validate_data_anonymization()


+ 1
- 0
frappe/website/doctype/portal_settings/portal_settings.py View File

@@ -19,6 +19,7 @@ class PortalSettings(Document):
self.append('menu', item)
return True

@frappe.whitelist()
def reset(self):
'''Restore defaults'''
self.menu = []


+ 2
- 0
frappe/website/doctype/website_theme/website_theme.py View File

@@ -98,6 +98,7 @@ class WebsiteTheme(Document):
else:
self.generate_bootstrap_theme()

@frappe.whitelist()
def set_as_default(self):
self.generate_bootstrap_theme()
self.save()
@@ -106,6 +107,7 @@ class WebsiteTheme(Document):
website_settings.ignore_validate = True
website_settings.save()

@frappe.whitelist()
def get_apps(self):
from frappe.utils.change_log import get_versions
apps = get_versions()


Loading…
Cancel
Save