From d309343c6fa43b8a2a28fa615a534647a6c3e069 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 18 Apr 2021 18:56:45 +0530 Subject: [PATCH 1/4] feat(hooks): auth hooks hook for request authentication --- frappe/api.py | 33 +++++++++++++++------------------ frappe/utils/boilerplate.py | 7 +++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 6a09b795b0..3a1be2593e 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -149,24 +149,17 @@ def get_request_form_data(): return frappe.parse_json(data) def validate_auth(): - if frappe.get_request_header("Authorization") is None: - return - VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token'] VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title() authorization_header = frappe.get_request_header("Authorization", str()).split(" ") authorization_type = authorization_header[0].lower() - if len(authorization_header) == 1: - frappe.throw(_('Invalid Authorization headers, add a token with a prefix from one of the following: {0}.').format(VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationHeader) - - if authorization_type == "bearer": + if len(authorization_header) == 2: validate_oauth(authorization_header) - elif authorization_type in VALID_AUTH_PREFIX_TYPES: validate_auth_via_api_keys(authorization_header) - else: - frappe.throw(_('Invalid Authorization Type {0}, must be one of {1}.').format(authorization_type, VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationPrefix) + + validate_auth_via_hooks() def validate_oauth(authorization_header): @@ -192,14 +185,13 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict except AttributeError: - frappe.throw(_("Invalid Bearer token, please provide a valid access token with prefix 'Bearer'."), frappe.InvalidAuthorizationToken) - - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict + pass def validate_auth_via_api_keys(authorization_header): @@ -222,7 +214,7 @@ def validate_auth_via_api_keys(authorization_header): except binascii.Error: frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) except (AttributeError, TypeError, ValueError): - frappe.throw(_("Invalid token, please provide a valid token with prefix 'Basic' or 'Token'."), frappe.InvalidAuthorizationToken) + pass @@ -248,3 +240,8 @@ def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=Non if frappe.local.login_manager.user in ('', 'Guest'): frappe.set_user(user) frappe.local.form_dict = form_dict + + +def validate_auth_via_hooks(): + for auth_hook in frappe.get_hooks('auth_hooks', []): + frappe.get_attr(auth_hook)() diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index e59f579f75..ffcb64cff2 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -303,6 +303,13 @@ user_data_fields = [ }} ] +# Authentication and authorization +# -------------------------------- + +# auth_hooks = [ +# "{app_name}.auth.validate" +# ] + """ desktop_template = """# -*- coding: utf-8 -*- From 557235471ed3540f12d5b9ac883e682f9dbf3daa Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 19 Apr 2021 13:39:39 +0530 Subject: [PATCH 2/4] fix: remove unused variables --- frappe/api.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 3a1be2593e..59f14b54c8 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -1,12 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals import base64 import binascii import json - -from six.moves.urllib.parse import urlencode, urlparse +from urllib.parse import urlencode, urlparse import frappe import frappe.client @@ -14,6 +12,7 @@ import frappe.handler from frappe import _ from frappe.utils.response import build_response + def handle(): """ Handler for `/api` methods @@ -38,7 +37,6 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ - validate_auth() parts = frappe.request.path[1:].split("/",3) @@ -116,7 +114,7 @@ def handle(): frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields']) frappe.local.form_dict.setdefault('limit_page_length', 20) frappe.local.response.update({ - "data": frappe.call( + "data": frappe.call( frappe.client.get_list, doctype, **frappe.local.form_dict @@ -140,6 +138,7 @@ def handle(): return build_response("json") + def get_request_form_data(): if frappe.local.form_dict.data is None: data = frappe.safe_decode(frappe.local.request.get_data()) @@ -148,12 +147,9 @@ def get_request_form_data(): return frappe.parse_json(data) -def validate_auth(): - VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token'] - VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title() +def validate_auth(): authorization_header = frappe.get_request_header("Authorization", str()).split(" ") - authorization_type = authorization_header[0].lower() if len(authorization_header) == 2: validate_oauth(authorization_header) @@ -170,8 +166,8 @@ def validate_oauth(authorization_header): authorization_header (list of str): The 'Authorization' header containing the prefix and token """ - from frappe.oauth import get_url_delimiter from frappe.integrations.oauth2 import get_oauth_server + from frappe.oauth import get_url_delimiter form_dict = frappe.local.form_dict token = authorization_header[1] @@ -185,14 +181,14 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict except AttributeError: pass + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict + def validate_auth_via_api_keys(authorization_header): """ @@ -217,7 +213,6 @@ def validate_auth_via_api_keys(authorization_header): pass - def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): """frappe_authorization_source to provide api key and secret for a doctype apart from User""" doctype = frappe_authorization_source or 'User' From 8c74df6cc3b5bf4b1664b6ba76f38d9fc8d6b19f Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 19 Apr 2021 15:18:15 +0530 Subject: [PATCH 3/4] fix: duplicate validate_auth calls --- frappe/api.py | 8 ++++---- frappe/handler.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 59f14b54c8..4117c49333 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -181,13 +181,13 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict except AttributeError: pass - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict def validate_auth_via_api_keys(authorization_header): diff --git a/frappe/handler.py b/frappe/handler.py index 82c1ea65c6..1897abe019 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -24,7 +24,7 @@ ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/ def handle(): """handle request""" - validate_auth() + cmd = frappe.local.form_dict.cmd data = None From 1d0e72834d3f67ec84089190a32ddb28b53963e5 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 19 Apr 2021 15:54:16 +0530 Subject: [PATCH 4/4] fix: remove unused imports --- frappe/api.py | 5 +++-- frappe/app.py | 1 + frappe/handler.py | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 4117c49333..4a120f228a 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -37,8 +37,6 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ - validate_auth() - parts = frappe.request.path[1:].split("/",3) call = doctype = name = None @@ -149,6 +147,9 @@ def get_request_form_data(): def validate_auth(): + """ + Authenticate and sets user for the request. + """ authorization_header = frappe.get_request_header("Authorization", str()).split(" ") if len(authorization_header) == 2: diff --git a/frappe/app.py b/frappe/app.py index 607479ad52..c9e993a853 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -56,6 +56,7 @@ def application(request): frappe.recorder.record() frappe.monitor.start() frappe.rate_limiter.apply() + frappe.api.validate_auth() if request.method == "OPTIONS": response = Response() diff --git a/frappe/handler.py b/frappe/handler.py index 1897abe019..a38feb90fa 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -9,7 +9,6 @@ import frappe import frappe.utils import frappe.sessions 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