From 54dedd349c358b1aadba606797aaa853ef558e89 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 19:33:39 +0200 Subject: [PATCH 001/126] feat: connected app and token cache --- frappe/config/integrations.py | 5 + .../doctype/connected_app/__init__.py | 0 .../doctype/connected_app/connected_app.js | 34 +++ .../doctype/connected_app/connected_app.json | 151 ++++++++++++ .../doctype/connected_app/connected_app.py | 218 ++++++++++++++++++ .../connected_app/test_connected_app.js | 23 ++ .../connected_app/test_connected_app.py | 10 + .../doctype/token_cache/__init__.py | 0 .../doctype/token_cache/test_token_cache.js | 23 ++ .../doctype/token_cache/test_token_cache.py | 10 + .../doctype/token_cache/token_cache.js | 8 + .../doctype/token_cache/token_cache.json | 81 +++++++ .../doctype/token_cache/token_cache.py | 16 ++ 13 files changed, 579 insertions(+) create mode 100644 frappe/integrations/doctype/connected_app/__init__.py create mode 100644 frappe/integrations/doctype/connected_app/connected_app.js create mode 100644 frappe/integrations/doctype/connected_app/connected_app.json create mode 100644 frappe/integrations/doctype/connected_app/connected_app.py create mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.js create mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.py create mode 100644 frappe/integrations/doctype/token_cache/__init__.py create mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.js create mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.py create mode 100644 frappe/integrations/doctype/token_cache/token_cache.js create mode 100644 frappe/integrations/doctype/token_cache/token_cache.json create mode 100644 frappe/integrations/doctype/token_cache/token_cache.py diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index f41adc9ea4..6c1d3d55ae 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,6 +72,11 @@ def get_data(): "name": "OAuth Provider Settings", "description": _("Settings for OAuth Provider"), }, + { + "type": "doctype", + "name": "Connected App", + "description": _("Connected App"), + }, ] }, { diff --git a/frappe/integrations/doctype/connected_app/__init__.py b/frappe/integrations/doctype/connected_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js new file mode 100644 index 0000000000..8e239d277b --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -0,0 +1,34 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Connected App', { + refresh: frm => { + frm.add_custom_button(__("Get OpenID Configuration"), async () => { + if(!frm.doc.openid_configuration) { + frappe.msgprint(__('Please enter OpenID Configuration URL')); + } else { + try { + const response = await fetch(frm.doc.openid_configuration); + const oidc = await response.json(); + frm.set_value('authorization_endpoint', oidc.authorization_endpoint); + frm.set_value('token_endpoint', oidc.token_endpoint); + frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); + frm.set_value('introspection_endpoint', oidc.introspection_endpoint); + frm.set_value('revocation_endpoint', oidc.revocation_endpoint); + } catch(error) { + frappe.msgprint(__('Please check OpenID Configuration URL')); + } + } + }); + + frm.add_custom_button(__("Init"), async () => { + frappe.call({ + method: "initiate_auth_code_flow", + doc: frm.doc, + callback: function(r) { + console.log(r.message); + } + }) + }); + } +}); diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json new file mode 100644 index 0000000000..6ccce573db --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -0,0 +1,151 @@ +{ + "actions": [], + "autoname": "field:callback", + "beta": 1, + "creation": "2019-01-24 15:51:06.362222", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "provider_name", + "cb_00", + "openid_configuration", + "callback", + "sb_client_credentials_section", + "client_id", + "redirect_uri", + "cb_01", + "client_secret", + "sb_scope_section", + "scope", + "sb_endpoints_section", + "authorization_endpoint", + "token_endpoint", + "revocation_endpoint", + "cb_02", + "userinfo_endpoint", + "introspection_endpoint" + ], + "fields": [ + { + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "openid_configuration", + "fieldtype": "Data", + "label": "OpenID Configuration" + }, + { + "fieldname": "callback", + "fieldtype": "Data", + "label": "Callback", + "read_only": 1, + "unique": 1 + }, + { + "collapsible": 1, + "fieldname": "sb_client_credentials_section", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client Id" + }, + { + "fieldname": "redirect_uri", + "fieldtype": "Data", + "label": "Redirect URI", + "read_only": 1 + }, + { + "fieldname": "cb_01", + "fieldtype": "Column Break" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "collapsible": 1, + "fieldname": "sb_scope_section", + "fieldtype": "Section Break", + "label": "Scope" + }, + { + "fieldname": "scope", + "fieldtype": "Text", + "label": "Scope" + }, + { + "collapsible": 1, + "fieldname": "sb_endpoints_section", + "fieldtype": "Section Break", + "label": "Endpoints" + }, + { + "fieldname": "authorization_endpoint", + "fieldtype": "Data", + "label": "Authorization Endpoint" + }, + { + "fieldname": "token_endpoint", + "fieldtype": "Data", + "label": "Token Endpoint" + }, + { + "fieldname": "revocation_endpoint", + "fieldtype": "Data", + "label": "Revocation Endpoint" + }, + { + "fieldname": "cb_02", + "fieldtype": "Column Break" + }, + { + "fieldname": "userinfo_endpoint", + "fieldtype": "Data", + "label": "Userinfo Endpoint" + }, + { + "fieldname": "introspection_endpoint", + "fieldtype": "Data", + "label": "Introspection Endpoint" + } + ], + "links": [], + "modified": "2020-07-14 18:52:06.041273", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Connected App", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py new file mode 100644 index 0000000000..26e0a93c14 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import requests +import frappe +import base64 +from frappe import _ +from frappe.model.document import Document +from datetime import datetime, timedelta +from urllib.parse import urlencode +from six.moves.urllib.parse import unquote + + +class ConnectedApp(Document): + def autoname(self): + self.callback = frappe.scrub(self.provider_name) + + def validate(self): + self.redirect_uri = frappe.request.host_url + self.redirect_uri += 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.redirect_uri += self.callback + + def get_client_token(self): + try: + token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + token = self.retrieve_client_token() + + token = self.check_validity(token) + return token + + def get_params(self, **kwargs): + return { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope + }.update(kwargs) + + def retrieve_client_token(self): + client_secret = self.get_password('client_secret') + data = self.get_params(grant_type='client_credentials', client_secret=client_secret) + response = requests.post( + self.token_endpoint, + data=urlencode(data), + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + token = response.json() + return self.update_stored_client_token(token) + + def check_validity(self, token): + if(token.get('__islocal') or (not token.access_token)): + raise frappe.exceptions.DoesNotExistError + + expiry = token.modified + timedelta(seconds=token.expires_in) + if expiry > datetime.now(): + return token + + return self.refresh_token(token) + + def initiate_auth_code_flow(self, user=None, redirect_to=None): + if not redirect_to: + redirect_to = '/desk' + + if not user: + user = frappe.session.user + + uid = frappe.generate_hash() + payload = { + 'uid': uid, + 'redirect_to': redirect_to, + } + state = str_to_b64(json.dumps(payload)) + + try: + token = frappe.get_doc('Token Cache', self.name + '-' + user) + except frappe.exceptions.DoesNotExistError: + token = frappe.new_doc('Token Cache') + token.user = user + token.connected_app = self.name + + token.state = state + token.save() + frappe.db.commit() + + params = self.get_params(response_type='code', state=state.decode('utf-8')) + return self.authorization_endpoint + urlencode(params) + + def get_user_token(self, user=None, redirect_to=None): + if not user: + user = frappe.session.user + + try: + token = self.get_stored_user_token(user) + token = self.check_validity(token) + except frappe.exceptions.DoesNotExistError: + redirect = self.initiate_auth_code_flow(user, redirect_to) + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = redirect + return redirect + + return token + + def refresh_token(self, token): + data = self.get_params(grant_type='refresh_token', refresh_token=token.refresh_token) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(self.token_endpoint, data=urlencode(data), headers=headers) + new_token = response.json() + + # Revoke old token + data = urlencode({'token': token.get('access_token')}) + headers['Authorization'] = 'Bearer ' + new_token.get('access_token') + requests.post(self.revocation_endpoint, data=data, headers=headers) + + return self.update_stored_client_token(new_token) + + def get_stored_client_token(self): + return frappe.get_doc('Token Cache', self.name + '-user') + + def get_stored_user_token(self, user): + return frappe.get_doc('Token Cache', self.name + '-' + user) + + def update_stored_client_token(self, token_data): + try: + stored_token = frappe.get_doc('Token Cache', self.name + '-user') + except frappe.exceptions.DoesNotExistError: + stored_token = frappe.new_doc('Token Cache') + + stored_token.connected_app = self.name + stored_token.access_token = token_data.get('access_token') + stored_token.refresh_token = token_data.get('refresh_token') + stored_token.expires_in = token_data.get('expires_in') + stored_token.save(ignore_permissions=True) + frappe.db.commit() + + return frappe.get_doc('Token Cache', stored_token.name) + + +@frappe.whitelist(allow_guest=True) +def callback(code=None, state=None): + """Handle client's code.""" + if frappe.request.method != 'GET': + throw_error(_('Invalid Method')) + return + + if frappe.session.user == 'Guest': + throw_error(_('Please Sign In')) + return + + path = frappe.request.path[1:].split("/") + if len(path) == 4 and path[3]: + connected_app = path[3] + stored_state = frappe.get_all( + 'Token Cache', + filters={ + 'user': frappe.session.user, + 'connected_app': connected_app, + 'name': connected_app + '-' + frappe.session.user, + }, + limit=1 + ) + if not stored_state: + throw_error(_('State Not Found')) + return + + stored_state = frappe.get_doc('Token Cache', stored_state[0].name) + + payload = json.loads(b64_to_str(state)) + stored_payload = json.loads(b64_to_str(stored_state.state)) + + if payload.get('uid') != stored_payload.get('uid'): + throw_error(_('Invalid State')) + return + + try: + app = frappe.get_doc('Connected App', connected_app) + except frappe.exceptions.DoesNotExistError: + throw_error(_('Invalid App')) + return + + data = app.get_params(code=code, grant_type='authorization_code') + response = requests.post( + app.token_endpoint, + data=urlencode(data), + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + token = response.json() + stored_state.access_token = token.get('access_token') + stored_state.refresh_token = token.get('refresh_token') + stored_state.expires_in = token.get('expires_in') + stored_state.state = None + stored_state.save() + frappe.db.commit() + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = payload.get('redirect_to') + else: + throw_error(_('Invalid Parameter(s)')) + return + + +def throw_error(error): + """Set Response Status 400 and show error.""" + frappe.local.response['http_status_code'] = 400 + frappe.local.response['error'] = error + + +def str_to_b64(string): + """Return base64 encoded string.""" + return base64.b64encode(string.encode('utf-8')) + + +def b64_to_str(b64): + """Return base64 decoded string.""" + return base64.b64decode(b64).decode('utf-8') diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.js b/frappe/integrations/doctype/connected_app/test_connected_app.js new file mode 100644 index 0000000000..6db9056efc --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_connected_app.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Connected App", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Connected App + () => frappe.tests.make('Connected App', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py new file mode 100644 index 0000000000..5c92eddc73 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestConnectedApp(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/token_cache/__init__.py b/frappe/integrations/doctype/token_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.js b/frappe/integrations/doctype/token_cache/test_token_cache.js new file mode 100644 index 0000000000..ee52cd7465 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_token_cache.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Token Cache", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Token Cache + () => frappe.tests.make('Token Cache', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py new file mode 100644 index 0000000000..2c42e7f3b8 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestTokenCache(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js new file mode 100644 index 0000000000..dda742f469 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Token Cache', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json new file mode 100644 index 0000000000..536985c9dc --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "autoname": "format:{connected_app}-{user}", + "beta": 1, + "creation": "2019-01-24 16:56:55.631096", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "connected_app", + "access_token", + "refresh_token", + "expires_in", + "state" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "connected_app", + "fieldtype": "Link", + "label": "Connected App", + "options": "Connected App", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Data", + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "expires_in", + "fieldtype": "Int", + "label": "Expires In", + "read_only": 1 + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-07-14 18:52:25.452744", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Token Cache", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py new file mode 100644 index 0000000000..e40f207738 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class TokenCache(Document): + + def get_auth_header(self): + if self.access_token: + headers = {'Authorization': 'Bearer ' + self.access_token} + return headers + + raise frappe.exceptions.DoesNotExistError From 61fcceb043352d6aa70ea4baa2ba77bd54369768 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 19:46:51 +0200 Subject: [PATCH 002/126] fix: indentation --- frappe/config/integrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 6c1d3d55ae..cc467495ce 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,7 +72,7 @@ def get_data(): "name": "OAuth Provider Settings", "description": _("Settings for OAuth Provider"), }, - { + { "type": "doctype", "name": "Connected App", "description": _("Connected App"), From ff2412af48ab394049622fafd3690baa8e00c631 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 20:24:55 +0200 Subject: [PATCH 003/126] refactor --- .../doctype/connected_app/connected_app.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 26e0a93c14..352fbe6564 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -15,13 +15,13 @@ from six.moves.urllib.parse import unquote class ConnectedApp(Document): + def autoname(self): self.callback = frappe.scrub(self.provider_name) def validate(self): - self.redirect_uri = frappe.request.host_url - self.redirect_uri += 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' - self.redirect_uri += self.callback + callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.redirect_uri = frappe.request.host_url + callback_path + self.callback def get_client_token(self): try: @@ -61,21 +61,17 @@ class ConnectedApp(Document): return self.refresh_token(token) def initiate_auth_code_flow(self, user=None, redirect_to=None): - if not redirect_to: - redirect_to = '/desk' - - if not user: - user = frappe.session.user + redirect_to = redirect_to or '/desk' + user = user or frappe.session.user uid = frappe.generate_hash() - payload = { + state = str_to_b64(json.dumps({ 'uid': uid, 'redirect_to': redirect_to, - } - state = str_to_b64(json.dumps(payload)) + })) try: - token = frappe.get_doc('Token Cache', self.name + '-' + user) + token = self.get_stored_user_token(user) except frappe.exceptions.DoesNotExistError: token = frappe.new_doc('Token Cache') token.user = user @@ -89,8 +85,8 @@ class ConnectedApp(Document): return self.authorization_endpoint + urlencode(params) def get_user_token(self, user=None, redirect_to=None): - if not user: - user = frappe.session.user + redirect_to = redirect_to or '/desk' + user = user or frappe.session.user try: token = self.get_stored_user_token(user) @@ -124,7 +120,7 @@ class ConnectedApp(Document): def update_stored_client_token(self, token_data): try: - stored_token = frappe.get_doc('Token Cache', self.name + '-user') + stored_token = self.get_stored_client_token() except frappe.exceptions.DoesNotExistError: stored_token = frappe.new_doc('Token Cache') From 9be6204451a2810737ed967e8a69c6a22a2a9db4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 20:58:14 +0200 Subject: [PATCH 004/126] start to use requests_oauthlib --- .../doctype/connected_app/connected_app.py | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 352fbe6564..dc0e964bc7 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode from six.moves.urllib.parse import unquote +from requests_oauthlib import OAuth2Session class ConnectedApp(Document): @@ -23,6 +24,9 @@ class ConnectedApp(Document): callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' self.redirect_uri = frappe.request.host_url + callback_path + self.callback + def get_oauth2_session(self): + return OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope) + def get_client_token(self): try: token = self.get_stored_client_token() @@ -63,12 +67,8 @@ class ConnectedApp(Document): def initiate_auth_code_flow(self, user=None, redirect_to=None): redirect_to = redirect_to or '/desk' user = user or frappe.session.user - - uid = frappe.generate_hash() - state = str_to_b64(json.dumps({ - 'uid': uid, - 'redirect_to': redirect_to, - })) + oauth = self.get_oauth2_session() + authorization_url, state = oauth.authorization_url(self.authorization_endpoint) try: token = self.get_stored_user_token(user) @@ -81,8 +81,7 @@ class ConnectedApp(Document): token.save() frappe.db.commit() - params = self.get_params(response_type='code', state=state.decode('utf-8')) - return self.authorization_endpoint + urlencode(params) + return authorization_url def get_user_token(self, user=None, redirect_to=None): redirect_to = redirect_to or '/desk' @@ -148,25 +147,12 @@ def callback(code=None, state=None): path = frappe.request.path[1:].split("/") if len(path) == 4 and path[3]: connected_app = path[3] - stored_state = frappe.get_all( - 'Token Cache', - filters={ - 'user': frappe.session.user, - 'connected_app': connected_app, - 'name': connected_app + '-' + frappe.session.user, - }, - limit=1 - ) - if not stored_state: + token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) + if not token_cache: throw_error(_('State Not Found')) return - stored_state = frappe.get_doc('Token Cache', stored_state[0].name) - - payload = json.loads(b64_to_str(state)) - stored_payload = json.loads(b64_to_str(stored_state.state)) - - if payload.get('uid') != stored_payload.get('uid'): + if state != token_cache.state: throw_error(_('Invalid State')) return @@ -176,23 +162,18 @@ def callback(code=None, state=None): throw_error(_('Invalid App')) return - data = app.get_params(code=code, grant_type='authorization_code') - response = requests.post( - app.token_endpoint, - data=urlencode(data), - headers={'Content-Type': 'application/x-www-form-urlencoded'} - ) + oauth = app.get_oauth2_session() + token = oauth.fetch_token(app.token_endpoint, code=code) - token = response.json() - stored_state.access_token = token.get('access_token') - stored_state.refresh_token = token.get('refresh_token') - stored_state.expires_in = token.get('expires_in') - stored_state.state = None - stored_state.save() + token_cache.access_token = token.get('access_token') + token_cache.refresh_token = token.get('refresh_token') + token_cache.expires_in = token.get('expires_in') + token_cache.state = None + token_cache.save() frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = payload.get('redirect_to') + frappe.local.response["location"] = '/desk' else: throw_error(_('Invalid Parameter(s)')) return From 7eacdb307bb9851651b01610b224cd2b91dcc69e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 14:06:20 +0200 Subject: [PATCH 005/126] fix: redirect to provider --- frappe/integrations/doctype/connected_app/connected_app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 8e239d277b..d999300e38 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Connected App', { method: "initiate_auth_code_flow", doc: frm.doc, callback: function(r) { - console.log(r.message); + window.location.replace(r.message); } }) }); From 0cf5cb9a6759fe8b3811de6675328f69d9c373c4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:34:11 +0200 Subject: [PATCH 006/126] add Connected App to desk page --- .../integrations/desk_page/integrations/integrations.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json index 9201e223f8..d90ad60e63 100644 --- a/frappe/integrations/desk_page/integrations/integrations.json +++ b/frappe/integrations/desk_page/integrations/integrations.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Authentication", - "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n ,\n {\n \"description\": \"Connect to any OAuth Provider\",\n \"label\": \"Connected App\",\n \"name\": \"Connected App\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -34,11 +34,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "frapicon-dashboard", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Integrations", - "modified": "2020-04-01 11:24:40.751651", + "modified": "2020-07-15 20:10:14.074203", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", From 604b88be5848c8b93767c41bfa9ba0d8d22788d7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:38:50 +0200 Subject: [PATCH 007/126] OAuth scopes are a table. Also, show Token Cache in Dashboard of Connected App. --- .../doctype/connected_app/connected_app.json | 24 +++++++++------ .../doctype/oauth_scope/__init__.py | 0 .../doctype/oauth_scope/oauth_scope.json | 30 +++++++++++++++++++ .../doctype/oauth_scope/oauth_scope.py | 10 +++++++ .../doctype/token_cache/token_cache.json | 12 ++++++-- 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 frappe/integrations/doctype/oauth_scope/__init__.py create mode 100644 frappe/integrations/doctype/oauth_scope/oauth_scope.json create mode 100644 frappe/integrations/doctype/oauth_scope/oauth_scope.py diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 6ccce573db..3d83695621 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -18,7 +18,7 @@ "cb_01", "client_secret", "sb_scope_section", - "scope", + "scopes", "sb_endpoints_section", "authorization_endpoint", "token_endpoint", @@ -81,12 +81,7 @@ "collapsible": 1, "fieldname": "sb_scope_section", "fieldtype": "Section Break", - "label": "Scope" - }, - { - "fieldname": "scope", - "fieldtype": "Text", - "label": "Scope" + "label": "Scopes" }, { "collapsible": 1, @@ -122,10 +117,21 @@ "fieldname": "introspection_endpoint", "fieldtype": "Data", "label": "Introspection Endpoint" + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope" } ], - "links": [], - "modified": "2020-07-14 18:52:06.041273", + "links": [ + { + "link_doctype": "Token Cache", + "link_fieldname": "connected_app" + } + ], + "modified": "2020-07-15 22:10:07.122237", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/oauth_scope/__init__.py b/frappe/integrations/doctype/oauth_scope/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.json b/frappe/integrations/doctype/oauth_scope/oauth_scope.json new file mode 100644 index 0000000000..3a6e528999 --- /dev/null +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-07-15 22:08:14.616585", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "scope" + ], + "fields": [ + { + "fieldname": "scope", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Scope" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-15 22:15:18.930632", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Scope", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py new file mode 100644 index 0000000000..a5dfe7e1ce --- /dev/null +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class OAuthScope(Document): + pass diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 536985c9dc..b674018912 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -13,7 +13,8 @@ "access_token", "refresh_token", "expires_in", - "state" + "state", + "scopes" ], "fields": [ { @@ -53,10 +54,17 @@ "fieldtype": "Data", "label": "State", "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope", + "read_only": 1 } ], "links": [], - "modified": "2020-07-14 18:52:25.452744", + "modified": "2020-07-15 22:32:14.268178", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From fbdd86f38b76b5a6dc002567ef74495facbe38bc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:39:11 +0200 Subject: [PATCH 008/126] use small text to store long tokens --- frappe/integrations/doctype/token_cache/token_cache.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index b674018912..5a7e8f5d41 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -33,13 +33,13 @@ }, { "fieldname": "access_token", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Access Token", "read_only": 1 }, { "fieldname": "refresh_token", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Refresh Token", "read_only": 1 }, From 714073f0d40af3a4c13007e2250f4880d6eb78ce Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:39:31 +0200 Subject: [PATCH 009/126] open auth flow in a new tab --- frappe/integrations/doctype/connected_app/connected_app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index d999300e38..d4f1b4673e 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Connected App', { method: "initiate_auth_code_flow", doc: frm.doc, callback: function(r) { - window.location.replace(r.message); + window.open(r.message, '_blank'); } }) }); From 169f5f3ef20291681306929998a9bcaf352987c8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:40:49 +0200 Subject: [PATCH 010/126] refactor: connected app --- .../doctype/connected_app/connected_app.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index dc0e964bc7..b10d1615f0 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -14,6 +14,10 @@ from urllib.parse import urlencode from six.moves.urllib.parse import unquote from requests_oauthlib import OAuth2Session +if frappe.conf.developer_mode: + # Disable mandatory TLS in developer mode + import os + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): @@ -25,7 +29,11 @@ class ConnectedApp(Document): self.redirect_uri = frappe.request.host_url + callback_path + self.callback def get_oauth2_session(self): - return OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope) + return OAuth2Session( + self.client_id, + redirect_uri=self.redirect_uri, + scope=[scope.scope for scope in self.scopes] + ) def get_client_token(self): try: @@ -137,37 +145,44 @@ class ConnectedApp(Document): def callback(code=None, state=None): """Handle client's code.""" if frappe.request.method != 'GET': - throw_error(_('Invalid Method')) - return + frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - throw_error(_('Please Sign In')) - return + frappe.throw(_("Log in to access this page."), frappe.PermissionError) path = frappe.request.path[1:].split("/") if len(path) == 4 and path[3]: connected_app = path[3] token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) if not token_cache: - throw_error(_('State Not Found')) - return + frappe.throw(_('State Not Found')) if state != token_cache.state: - throw_error(_('Invalid State')) - return + frappe.throw(_('Invalid State')) try: app = frappe.get_doc('Connected App', connected_app) except frappe.exceptions.DoesNotExistError: - throw_error(_('Invalid App')) - return + frappe.throw(_('Invalid App')) + client_secret = app.get_password('client_secret') oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, code=code) + token = oauth.fetch_token( + app.token_endpoint, + code=code, + client_secret=client_secret + ) token_cache.access_token = token.get('access_token') token_cache.refresh_token = token.get('refresh_token') token_cache.expires_in = token.get('expires_in') + + scopes = token.get('scope') + if isinstance(scopes, str): + scopes = [scopes] + for scope in scopes: + token_cache.append('scopes', {'scope': scope}) + token_cache.state = None token_cache.save() frappe.db.commit() @@ -175,21 +190,4 @@ def callback(code=None, state=None): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = '/desk' else: - throw_error(_('Invalid Parameter(s)')) - return - - -def throw_error(error): - """Set Response Status 400 and show error.""" - frappe.local.response['http_status_code'] = 400 - frappe.local.response['error'] = error - - -def str_to_b64(string): - """Return base64 encoded string.""" - return base64.b64encode(string.encode('utf-8')) - - -def b64_to_str(b64): - """Return base64 decoded string.""" - return base64.b64decode(b64).decode('utf-8') + frappe.throw(_('Invalid Parameter(s)')) From 83dcc8a5836036b9e1d7e4afeb550fc56baddee7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:49:24 +0200 Subject: [PATCH 011/126] style: fix sider --- frappe/integrations/doctype/connected_app/connected_app.js | 6 +++--- frappe/integrations/doctype/connected_app/connected_app.py | 3 --- .../doctype/connected_app/test_connected_app.py | 2 +- frappe/integrations/doctype/token_cache/test_token_cache.py | 2 +- frappe/integrations/doctype/token_cache/token_cache.js | 4 ++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index d4f1b4673e..004f150b2a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Connected App', { refresh: frm => { frm.add_custom_button(__("Get OpenID Configuration"), async () => { - if(!frm.doc.openid_configuration) { + if (!frm.doc.openid_configuration) { frappe.msgprint(__('Please enter OpenID Configuration URL')); } else { try { @@ -15,7 +15,7 @@ frappe.ui.form.on('Connected App', { frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); frm.set_value('introspection_endpoint', oidc.introspection_endpoint); frm.set_value('revocation_endpoint', oidc.revocation_endpoint); - } catch(error) { + } catch (error) { frappe.msgprint(__('Please check OpenID Configuration URL')); } } @@ -28,7 +28,7 @@ frappe.ui.form.on('Connected App', { callback: function(r) { window.open(r.message, '_blank'); } - }) + }); }); } }); diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b10d1615f0..b669241344 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,15 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -import json import requests import frappe -import base64 from frappe import _ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode -from six.moves.urllib.parse import unquote from requests_oauthlib import OAuth2Session if frappe.conf.developer_mode: diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 5c92eddc73..bb04ca6677 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -3,7 +3,7 @@ # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest class TestConnectedApp(unittest.TestCase): diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index 2c42e7f3b8..aebac0b52f 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -3,7 +3,7 @@ # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest class TestTokenCache(unittest.TestCase): diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js index dda742f469..b7cac9b804 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.js +++ b/frappe/integrations/doctype/token_cache/token_cache.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Token Cache', { - refresh: function(frm) { + // refresh: function(frm) { - } + // } }); From bdcc98442fd4b90d357ed6ce75812a7b9fdc8c64 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 16:06:43 +0200 Subject: [PATCH 012/126] refactor: token cache updates itself --- .../doctype/connected_app/connected_app.py | 30 +++---------------- .../doctype/token_cache/token_cache.py | 19 ++++++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b669241344..448a6bc1eb 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -128,14 +128,7 @@ class ConnectedApp(Document): except frappe.exceptions.DoesNotExistError: stored_token = frappe.new_doc('Token Cache') - stored_token.connected_app = self.name - stored_token.access_token = token_data.get('access_token') - stored_token.refresh_token = token_data.get('refresh_token') - stored_token.expires_in = token_data.get('expires_in') - stored_token.save(ignore_permissions=True) - frappe.db.commit() - - return frappe.get_doc('Token Cache', stored_token.name) + return stored_token.update_data(token_data) @frappe.whitelist(allow_guest=True) @@ -162,27 +155,12 @@ def callback(code=None, state=None): except frappe.exceptions.DoesNotExistError: frappe.throw(_('Invalid App')) - client_secret = app.get_password('client_secret') oauth = app.get_oauth2_session() - token = oauth.fetch_token( - app.token_endpoint, + token = oauth.fetch_token(app.token_endpoint, code=code, - client_secret=client_secret + client_secret=app.get_password('client_secret') ) - - token_cache.access_token = token.get('access_token') - token_cache.refresh_token = token.get('refresh_token') - token_cache.expires_in = token.get('expires_in') - - scopes = token.get('scope') - if isinstance(scopes, str): - scopes = [scopes] - for scope in scopes: - token_cache.append('scopes', {'scope': scope}) - - token_cache.state = None - token_cache.save() - frappe.db.commit() + token_cache.update_data(token) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = '/desk' diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index e40f207738..aecc33b03e 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -14,3 +14,22 @@ class TokenCache(Document): return headers raise frappe.exceptions.DoesNotExistError + + def update_data(self, data): + self.access_token = data.get('access_token') + self.refresh_token = data.get('refresh_token') + self.expires_in = data.get('expires_in') + + existing_scopes = [scope.scope for scope in self.scopes] + new_scopes = data.get('scope') + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(' ') + scopes = set(existing_scopes) | set(new_scopes) + self.scopes = None + for scope in scopes: + self.append('scopes', {'scope': scope}) + + self.state = None + self.save() + + return self From 72496c0a49d7450a1a544631c505cbe007203f15 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 18:40:28 +0200 Subject: [PATCH 013/126] fix: save only new scopes --- .../doctype/token_cache/token_cache.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index aecc33b03e..93adfb50a7 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -20,14 +20,14 @@ class TokenCache(Document): self.refresh_token = data.get('refresh_token') self.expires_in = data.get('expires_in') - existing_scopes = [scope.scope for scope in self.scopes] new_scopes = data.get('scope') - if isinstance(new_scopes, str): - new_scopes = new_scopes.split(' ') - scopes = set(existing_scopes) | set(new_scopes) - self.scopes = None - for scope in scopes: - self.append('scopes', {'scope': scope}) + if new_scopes: + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(' ') + if isinstance(new_scopes, list): + self.scopes = None + for scope in new_scopes: + self.append('scopes', {'scope': scope}) self.state = None self.save() From 507659cd30b0f0b2c65fd52b876ae266f56e7c0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 19:48:48 +0200 Subject: [PATCH 014/126] refactor: flatten callback --- .../doctype/connected_app/connected_app.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 448a6bc1eb..23e4a00abd 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -141,28 +141,28 @@ def callback(code=None, state=None): frappe.throw(_("Log in to access this page."), frappe.PermissionError) path = frappe.request.path[1:].split("/") - if len(path) == 4 and path[3]: - connected_app = path[3] - token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) - if not token_cache: - frappe.throw(_('State Not Found')) - - if state != token_cache.state: - frappe.throw(_('Invalid State')) - - try: - app = frappe.get_doc('Connected App', connected_app) - except frappe.exceptions.DoesNotExistError: - frappe.throw(_('Invalid App')) - - oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, - code=code, - client_secret=app.get_password('client_secret') - ) - token_cache.update_data(token) - - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = '/desk' - else: + if len(path) != 4 or not path[3]: frappe.throw(_('Invalid Parameter(s)')) + + connected_app = path[3] + token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) + if not token_cache: + frappe.throw(_('State Not Found')) + + if state != token_cache.state: + frappe.throw(_('Invalid State')) + + try: + app = frappe.get_doc('Connected App', connected_app) + except frappe.exceptions.DoesNotExistError: + frappe.throw(_('Invalid App')) + + oauth = app.get_oauth2_session() + token = oauth.fetch_token(app.token_endpoint, + code=code, + client_secret=app.get_password('client_secret') + ) + token_cache.update_data(token) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = token_cache.get('success_uri') or '/desk' From cf560e43a07a04a11bc5a9c292a2a114ebf66591 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 19:59:34 +0200 Subject: [PATCH 015/126] feat: add fields success_uri, token_type --- .../doctype/token_cache/token_cache.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 5a7e8f5d41..5f1836e300 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -14,7 +14,9 @@ "refresh_token", "expires_in", "state", - "scopes" + "scopes", + "success_uri", + "token_type" ], "fields": [ { @@ -61,10 +63,20 @@ "label": "Scopes", "options": "OAuth Scope", "read_only": 1 + }, + { + "fieldname": "success_uri", + "fieldtype": "Data", + "label": "Success URI" + }, + { + "fieldname": "token_type", + "fieldtype": "Data", + "label": "Token Type" } ], "links": [], - "modified": "2020-07-15 22:32:14.268178", + "modified": "2020-07-17 19:14:46.132134", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From ddbe0fc0b5368eeab223d2d478d7f591735cd6c4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:03:12 +0200 Subject: [PATCH 016/126] feat: return token as json, calculate expiry --- .../doctype/token_cache/token_cache.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 93adfb50a7..2a8cecd7fb 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from datetime import datetime, timedelta from frappe.model.document import Document class TokenCache(Document): @@ -19,6 +20,7 @@ class TokenCache(Document): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') self.expires_in = data.get('expires_in') + self.token_type = data.get('token_type') new_scopes = data.get('scope') if new_scopes: @@ -33,3 +35,18 @@ class TokenCache(Document): self.save() return self + + def get_expires_in(self): + expiry_time = self.modified + datetime.timedelta(self.expires_in) + return (datetime.now() - expiry_time).total_seconds() + + def is_expired(self): + return self.get_expires_in() < 0 + + def get_json(self): + return { + 'access_token': self.access_token, + 'refresh_token': self.refresh_token, + 'expires_in': self.get_expires_in(), + 'token_type': self.token_type + } From b198c8a932c45e8d810be9407a9b71fbb27a5e6b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:04:41 +0200 Subject: [PATCH 017/126] refactor: connected app --- .../doctype/connected_app/connected_app.py | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 23e4a00abd..51f8fee513 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient if frappe.conf.developer_mode: # Disable mandatory TLS in developer mode @@ -29,48 +30,21 @@ class ConnectedApp(Document): return OAuth2Session( self.client_id, redirect_uri=self.redirect_uri, - scope=[scope.scope for scope in self.scopes] + scope=[row.scope for row in self.scopes] ) - def get_client_token(self): - try: - token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - token = self.retrieve_client_token() - - token = self.check_validity(token) - return token - - def get_params(self, **kwargs): - return { - 'client_id': self.client_id, - 'redirect_uri': self.redirect_uri, - 'scope': self.scope - }.update(kwargs) - - def retrieve_client_token(self): - client_secret = self.get_password('client_secret') - data = self.get_params(grant_type='client_credentials', client_secret=client_secret) - response = requests.post( - self.token_endpoint, - data=urlencode(data), - headers={'Content-Type': 'application/x-www-form-urlencoded'} - ) - token = response.json() - return self.update_stored_client_token(token) - def check_validity(self, token): if(token.get('__islocal') or (not token.access_token)): raise frappe.exceptions.DoesNotExistError - expiry = token.modified + timedelta(seconds=token.expires_in) - if expiry > datetime.now(): + if not token.is_expired(): return token return self.refresh_token(token) - def initiate_auth_code_flow(self, user=None, redirect_to=None): - redirect_to = redirect_to or '/desk' + def initiate_web_application_flow(self, user=None, success_uri=None): + """Return an authorization URL for the user. Save state in Token Cache.""" + success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session() authorization_url, state = oauth.authorization_url(self.authorization_endpoint) @@ -82,13 +56,14 @@ class ConnectedApp(Document): token.user = user token.connected_app = self.name + token.success_uri = success_uri token.state = state token.save() - frappe.db.commit() return authorization_url def get_user_token(self, user=None, redirect_to=None): + """Return an existing user token or initiate a Web Application Flow.""" redirect_to = redirect_to or '/desk' user = user or frappe.session.user @@ -96,7 +71,7 @@ class ConnectedApp(Document): token = self.get_stored_user_token(user) token = self.check_validity(token) except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_auth_code_flow(user, redirect_to) + redirect = self.initiate_web_application_flow(user, redirect_to) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = redirect return redirect @@ -104,10 +79,12 @@ class ConnectedApp(Document): return token def refresh_token(self, token): - data = self.get_params(grant_type='refresh_token', refresh_token=token.refresh_token) - headers = {'Content-Type': 'application/x-www-form-urlencoded'} - response = requests.post(self.token_endpoint, data=urlencode(data), headers=headers) - new_token = response.json() + oauth = self.get_oauth2_session() + new_token = oauth.refresh_token( + self.token_endpoint, + client_secret=self.get_password('client_secret'), + token=token.get_json() + ) # Revoke old token data = urlencode({'token': token.get('access_token')}) @@ -116,6 +93,28 @@ class ConnectedApp(Document): return self.update_stored_client_token(new_token) + def get_client_token(self): + """Return an existing client token or initiate a Backend Application Flow.""" + try: + token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + token = self.initiate_backend_application_flow() + + token = self.check_validity(token) + return token + + def initiate_backend_application_flow(self): + """Retrieve token without user interaction. Token is not user specific.""" + client = BackendApplicationClient(client_id=self.client_id) + oauth = OAuth2Session(client=client) + token = oauth.fetch_token( + token_url=self.token_endpoint, + client_id=self.client_id, + client_secret=self.get_password('client_secret') + ) + + return self.update_stored_client_token(token) + def get_stored_client_token(self): return frappe.get_doc('Token Cache', self.name + '-user') From e10851cbee3c348156eced950c35fd72411dad5e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:39:03 +0200 Subject: [PATCH 018/126] refactor: move mothods to token cache --- .../doctype/connected_app/connected_app.js | 2 +- .../doctype/connected_app/connected_app.py | 89 +++++++------------ .../doctype/token_cache/token_cache.json | 8 +- .../doctype/token_cache/token_cache.py | 33 +++++++ 4 files changed, 69 insertions(+), 63 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 004f150b2a..71ff23fcc3 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -23,7 +23,7 @@ frappe.ui.form.on('Connected App', { frm.add_custom_button(__("Init"), async () => { frappe.call({ - method: "initiate_auth_code_flow", + method: "initiate_web_application_flow", doc: frm.doc, callback: function(r) { window.open(r.message, '_blank'); diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 51f8fee513..4d987baada 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -33,15 +33,6 @@ class ConnectedApp(Document): scope=[row.scope for row in self.scopes] ) - def check_validity(self, token): - if(token.get('__islocal') or (not token.access_token)): - raise frappe.exceptions.DoesNotExistError - - if not token.is_expired(): - return token - - return self.refresh_token(token) - def initiate_web_application_flow(self, user=None, success_uri=None): """Return an authorization URL for the user. Save state in Token Cache.""" success_uri = success_uri or '/desk' @@ -62,47 +53,6 @@ class ConnectedApp(Document): return authorization_url - def get_user_token(self, user=None, redirect_to=None): - """Return an existing user token or initiate a Web Application Flow.""" - redirect_to = redirect_to or '/desk' - user = user or frappe.session.user - - try: - token = self.get_stored_user_token(user) - token = self.check_validity(token) - except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_web_application_flow(user, redirect_to) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = redirect - return redirect - - return token - - def refresh_token(self, token): - oauth = self.get_oauth2_session() - new_token = oauth.refresh_token( - self.token_endpoint, - client_secret=self.get_password('client_secret'), - token=token.get_json() - ) - - # Revoke old token - data = urlencode({'token': token.get('access_token')}) - headers['Authorization'] = 'Bearer ' + new_token.get('access_token') - requests.post(self.revocation_endpoint, data=data, headers=headers) - - return self.update_stored_client_token(new_token) - - def get_client_token(self): - """Return an existing client token or initiate a Backend Application Flow.""" - try: - token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - token = self.initiate_backend_application_flow() - - token = self.check_validity(token) - return token - def initiate_backend_application_flow(self): """Retrieve token without user interaction. Token is not user specific.""" client = BackendApplicationClient(client_id=self.client_id) @@ -113,7 +63,36 @@ class ConnectedApp(Document): client_secret=self.get_password('client_secret') ) - return self.update_stored_client_token(token) + try: + stored_token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + stored_token = frappe.new_doc('Token Cache') + + return stored_token.update_data(token) + + def get_user_token(self, user=None, success_uri=None): + """Return an existing user token or initiate a Web Application Flow.""" + user = user or frappe.session.user + + try: + token = self.get_stored_user_token(user) + token = token.check_validity() + except frappe.exceptions.DoesNotExistError: + redirect = self.initiate_web_application_flow(user, success_uri) + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = redirect + return redirect + + return token + + def get_client_token(self): + """Return an existing client token or initiate a Backend Application Flow.""" + try: + token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + token = self.initiate_backend_application_flow() + + return token.check_validity() def get_stored_client_token(self): return frappe.get_doc('Token Cache', self.name + '-user') @@ -121,14 +100,6 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - def update_stored_client_token(self, token_data): - try: - stored_token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - stored_token = frappe.new_doc('Token Cache') - - return stored_token.update_data(token_data) - @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 5f1836e300..52dd848e14 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -67,16 +67,18 @@ { "fieldname": "success_uri", "fieldtype": "Data", - "label": "Success URI" + "label": "Success URI", + "read_only": 1 }, { "fieldname": "token_type", "fieldtype": "Data", - "label": "Token Type" + "label": "Token Type", + "read_only": 1 } ], "links": [], - "modified": "2020-07-17 19:14:46.132134", + "modified": "2020-07-17 20:10:40.268067", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2a8cecd7fb..a7287360fc 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals import frappe +import requests +from urllib.parse import urlencode from datetime import datetime, timedelta from frappe.model.document import Document @@ -16,6 +18,37 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError + def check_validity(self): + if(self.get('__islocal') or (not self.access_token)): + raise frappe.exceptions.DoesNotExistError + + if not self.is_expired(): + return token + + return self.refresh_token(token) + + def refresh_token(self): + app = frappe.get_doc("Connected App", self.connected_app) + oauth = app.get_oauth2_session() + new_token = oauth.refresh_token( + app.token_endpoint, + client_secret=app.get_password('client_secret'), + token=self.get_json() + ) + + if new_token.get('access_token') and app.revocation_endpoint: + # Revoke old token + requests.post( + app.revocation_endpoint, + data=urlencode({'token': new_token.get('access_token')}), + headers={ + 'Authorization': 'Bearer ' + new_token.get('access_token'), + 'Content-Type': 'application/x-www-form-urlencoded' + } + ) + + return self.update_data(new_token) + def update_data(self, data): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') From 277b082d15976b59cd610efeea997146af8092d8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:30:23 +0200 Subject: [PATCH 019/126] fix(oauth provider): parse cookies correctly --- frappe/oauth.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/frappe/oauth.py b/frappe/oauth.py index 4dc50366be..122c806072 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -4,6 +4,7 @@ import pytz from frappe import _ from frappe.auth import LoginManager +from http import cookies from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant from oauthlib.oauth2 import RequestValidator @@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator): oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict['user_id']) + oac.user = unquote(cookie_dict['user_id'].value) oac.authorization_code = code['code'] oac.save(ignore_permissions=True) frappe.db.commit() def authenticate_client(self, request, *args, **kwargs): - - cookie_dict = get_cookie_dict_from_headers(request) - #Get ClientID in URL if request.client_id: oc = frappe.get_doc("OAuth Client", request.client_id) @@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator): except Exception as e: print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) - return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest")) + cookie_dict = get_cookie_dict_from_headers(request) + user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') @@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator): return True def get_cookie_dict_from_headers(r): + cookie = cookies.BaseCookie() if r.headers.get('Cookie'): - cookie = r.headers.get('Cookie') - cookie = cookie.split("; ") - cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} - return cookie_dict - else: - return {} + cookie.load(r.headers.get('Cookie')) + return cookie def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token From f6b206b7783840c757f41fb98c9a7fed3ec0e242 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:31:50 +0200 Subject: [PATCH 020/126] refactor: extract get_scopes() --- frappe/integrations/doctype/connected_app/connected_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 4d987baada..dcf8f9029e 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -30,7 +30,7 @@ class ConnectedApp(Document): return OAuth2Session( self.client_id, redirect_uri=self.redirect_uri, - scope=[row.scope for row in self.scopes] + scope=self.get_scopes() ) def initiate_web_application_flow(self, user=None, success_uri=None): @@ -100,6 +100,8 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) + def get_scopes(self): + return [row.scope for row in self.scopes] @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): From d9506be966bde717243113c9fd66ccfa5b4dda96 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:32:47 +0200 Subject: [PATCH 021/126] fix: BackendApplicationClient also likes scopes --- frappe/integrations/doctype/connected_app/connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index dcf8f9029e..07efc51e47 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -55,7 +55,7 @@ class ConnectedApp(Document): def initiate_backend_application_flow(self): """Retrieve token without user interaction. Token is not user specific.""" - client = BackendApplicationClient(client_id=self.client_id) + client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) oauth = OAuth2Session(client=client) token = oauth.fetch_token( token_url=self.token_endpoint, From 7c22f42d9bce7b074c66d6e0110703b2bea86cb8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:33:46 +0200 Subject: [PATCH 022/126] fix: include client_id when fetching token --- .../integrations/doctype/connected_app/connected_app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 07efc51e47..08e34f2aaa 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -59,8 +59,8 @@ class ConnectedApp(Document): oauth = OAuth2Session(client=client) token = oauth.fetch_token( token_url=self.token_endpoint, - client_id=self.client_id, - client_secret=self.get_password('client_secret') + client_secret=self.get_password('client_secret'), + include_client_id=True ) try: @@ -99,7 +99,7 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - + def get_scopes(self): return [row.scope for row in self.scopes] @@ -132,7 +132,8 @@ def callback(code=None, state=None): oauth = app.get_oauth2_session() token = oauth.fetch_token(app.token_endpoint, code=code, - client_secret=app.get_password('client_secret') + client_secret=app.get_password('client_secret'), + include_client_id=True ) token_cache.update_data(token) From cdf1b597dbede9223be0c7d079db0b2786dd933d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:49:23 +0200 Subject: [PATCH 023/126] fix: sider --- frappe/integrations/doctype/connected_app/connected_app.py | 3 --- frappe/integrations/doctype/token_cache/token_cache.py | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 08e34f2aaa..d6de4c77f9 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,12 +3,9 @@ # For license information, please see license.txt from __future__ import unicode_literals -import requests import frappe from frappe import _ from frappe.model.document import Document -from datetime import datetime, timedelta -from urllib.parse import urlencode from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index a7287360fc..accd72f794 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -23,9 +23,9 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError if not self.is_expired(): - return token + return self - return self.refresh_token(token) + return self.refresh_token() def refresh_token(self): app = frappe.get_doc("Connected App", self.connected_app) @@ -70,7 +70,7 @@ class TokenCache(Document): return self def get_expires_in(self): - expiry_time = self.modified + datetime.timedelta(self.expires_in) + expiry_time = self.modified + timedelta(self.expires_in) return (datetime.now() - expiry_time).total_seconds() def is_expired(self): From 6a482d152759d73ee1aef3525f2b6c496548207b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 21:51:18 +0200 Subject: [PATCH 024/126] fix(database): allow passwords up to 20000 characters / 60000 bytes (#11039) --- frappe/database/mariadb/database.py | 4 ++-- frappe/database/mariadb/framework_mariadb.sql | 2 +- frappe/database/postgres/database.py | 4 ++-- frappe/database/postgres/framework_postgres.sql | 2 +- frappe/utils/password.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 4ec89c126d..3cbb2e4f0e 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -46,7 +46,7 @@ class MariaDBDatabase(Database): 'Data': ('varchar', self.VARCHAR_LEN), 'Link': ('varchar', self.VARCHAR_LEN), 'Dynamic Link': ('varchar', self.VARCHAR_LEN), - 'Password': ('varchar', self.VARCHAR_LEN), + 'Password': ('text', ''), 'Select': ('varchar', self.VARCHAR_LEN), 'Rating': ('int', '1'), 'Read Only': ('varchar', self.VARCHAR_LEN), @@ -186,7 +186,7 @@ class MariaDBDatabase(Database): `doctype` VARCHAR(140) NOT NULL, `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, - `password` VARCHAR(255) NOT NULL, + `password` TEXT NOT NULL, `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""") diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index af537e0612..1e3749e030 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -277,7 +277,7 @@ CREATE TABLE `__Auth` ( `doctype` VARCHAR(140) NOT NULL, `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, - `password` VARCHAR(255) NOT NULL, + `password` TEXT NOT NULL, `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index e348916705..3d997864e4 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -51,7 +51,7 @@ class PostgresDatabase(Database): 'Data': ('varchar', self.VARCHAR_LEN), 'Link': ('varchar', self.VARCHAR_LEN), 'Dynamic Link': ('varchar', self.VARCHAR_LEN), - 'Password': ('varchar', self.VARCHAR_LEN), + 'Password': ('text', ''), 'Select': ('varchar', self.VARCHAR_LEN), 'Rating': ('smallint', None), 'Read Only': ('varchar', self.VARCHAR_LEN), @@ -179,7 +179,7 @@ class PostgresDatabase(Database): "doctype" VARCHAR(140) NOT NULL, "name" VARCHAR(255) NOT NULL, "fieldname" VARCHAR(140) NOT NULL, - "password" VARCHAR(255) NOT NULL, + "password" TEXT NOT NULL, "encrypted" INT NOT NULL DEFAULT 0, PRIMARY KEY ("doctype", "name", "fieldname") )""") diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 8f77ed6230..a946a7ee5c 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -281,7 +281,7 @@ CREATE TABLE "__Auth" ( "doctype" VARCHAR(140) NOT NULL, "name" VARCHAR(255) NOT NULL, "fieldname" VARCHAR(140) NOT NULL, - "password" VARCHAR(255) NOT NULL, + "password" TEXT NOT NULL, "encrypted" int NOT NULL DEFAULT 0, PRIMARY KEY ("doctype", "name", "fieldname") ); diff --git a/frappe/utils/password.py b/frappe/utils/password.py index b939607b19..f2d75b87bb 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -131,9 +131,9 @@ def create_auth_table(): frappe.db.create_auth_table() def encrypt(pwd): - if len(pwd) > 127: - # encrypting > 127 chars will lead to truncation - frappe.throw(_('Password cannot be more than 127 characters long')) + if len(pwd) > 20000: + # https://github.com/frappe/frappe/issues/11039 + frappe.throw(_('Password cannot be more than 20000 characters long')) cipher_suite = Fernet(encode(get_encryption_key())) cipher_text = cstr(cipher_suite.encrypt(encode(pwd))) From ca6fce4598e2da9e9e2db3057c23974693ff3c1e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 22:01:39 +0200 Subject: [PATCH 025/126] feat: store tokens as password --- frappe/integrations/doctype/token_cache/token_cache.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 52dd848e14..91758a7332 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -35,13 +35,13 @@ }, { "fieldname": "access_token", - "fieldtype": "Small Text", + "fieldtype": "Password", "label": "Access Token", "read_only": 1 }, { "fieldname": "refresh_token", - "fieldtype": "Small Text", + "fieldtype": "Password", "label": "Refresh Token", "read_only": 1 }, @@ -78,7 +78,7 @@ } ], "links": [], - "modified": "2020-07-17 20:10:40.268067", + "modified": "2020-07-21 00:32:56.225300", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From 1bdcc5d9dbdc0f2b99bddec566ee0e7cde1214a9 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 21 Jul 2020 20:05:53 +0200 Subject: [PATCH 026/126] feat(database): patch for long passswords --- frappe/patches.txt | 1 + frappe/patches/v13_0/increase_password_length.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 frappe/patches/v13_0/increase_password_length.py diff --git a/frappe/patches.txt b/frappe/patches.txt index f8c767f5a3..a8acc8732f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -295,3 +295,4 @@ frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts +frappe.patches.v13_0.increase_password_length diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py new file mode 100644 index 0000000000..7a053345eb --- /dev/null +++ b/frappe/patches/v13_0/increase_password_length.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + if frappe.db.db_type == "postgres": + frappe.db.sql("""ALTER TABLE "__Auth" ALTER COLUMN "password" TYPE TEXT""") + else: + frappe.db.sql("""ALTER TABLE `__Auth` MODIFY `password` TEXT NOT NULL""") From 78debff5b983c052de4c73e3825d0f3816dc9f6f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:21:32 +0200 Subject: [PATCH 027/126] fat: add field base_url --- .../integrations/doctype/connected_app/connected_app.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 3d83695621..8fca1b620f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -9,6 +9,7 @@ "engine": "InnoDB", "field_order": [ "provider_name", + "base_url", "cb_00", "openid_configuration", "callback", @@ -123,6 +124,11 @@ "fieldtype": "Table", "label": "Scopes", "options": "OAuth Scope" + }, + { + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" } ], "links": [ @@ -131,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-07-15 22:10:07.122237", + "modified": "2020-08-13 11:45:02.983854", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From fe9f8cb295781c9497218f940c39fd4252e7d38b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:35:29 +0200 Subject: [PATCH 028/126] fix: add docstrings --- .../doctype/connected_app/connected_app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index d6de4c77f9..7b210bb9f5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -15,6 +15,9 @@ if frappe.conf.developer_mode: os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): + """Connect to a remote oAuth Server. Retrieve and store user's access token + in a Token Cache. + """ def autoname(self): self.callback = frappe.scrub(self.provider_name) @@ -100,9 +103,15 @@ class ConnectedApp(Document): def get_scopes(self): return [row.scope for row in self.scopes] + @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): - """Handle client's code.""" + """Handle client's code. + + Called during the oauthorization flow by the remote oAuth2 server to + transmit a code that can be used by the local server to obtain an access + token. + """ if frappe.request.method != 'GET': frappe.throw(_('Invalid Method')) From 27b9010c08589be42caa914bc148dcbb11088666 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:38:44 +0200 Subject: [PATCH 029/126] fix: redirect to desk#workspace instead of desk#desktop --- frappe/utils/oauth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index 969623c369..6ac5a4fdcd 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -305,7 +305,7 @@ def redirect_post_login(desk_user, redirect_to=None): frappe.local.response["type"] = "redirect" if not redirect_to: - # the #desktop is added to prevent a facebook redirect bug - redirect_to = "/desk#desktop" if desk_user else "/me" + # the #workspace is added to prevent a facebook redirect bug + redirect_to = "/desk#workspace" if desk_user else "/me" frappe.local.response["location"] = redirect_to From 4a53f12cdedad67284cbba1d630c75af013346da Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 16:24:52 +0200 Subject: [PATCH 030/126] fix: remove client_id from test_records Because it will be auto-generated and overwritten anyways. --- frappe/integrations/doctype/oauth_client/test_records.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json index cff06457c5..11e6338a87 100644 --- a/frappe/integrations/doctype/oauth_client/test_records.json +++ b/frappe/integrations/doctype/oauth_client/test_records.json @@ -1,7 +1,6 @@ [ { - "app_name": "_Test OAuth Client", - "client_id": "test_client_id", + "app_name": "_Test OAuth Client", "client_secret": "test_client_secret", "default_redirect_uri": "http://localhost", "docstatus": 0, From f017cfa12d8c9383f5188f0c1d69b2704a814ee8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 19:35:52 +0200 Subject: [PATCH 031/126] refactor(oauth2): better name and docstring --- frappe/integrations/oauth2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c8dfc52c95..51665325be 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -14,7 +14,8 @@ def get_oauth_server(): return frappe.local.oauth_server -def get_urlparams_from_kwargs(param_kwargs): +def clean_urlparams(param_kwargs): + """Remove 'data' and 'cmd' keys, if present.""" arguments = param_kwargs if arguments.get("data"): arguments.pop("data") @@ -50,7 +51,7 @@ def approve(*args, **kwargs): def authorize(*args, **kwargs): #Fetch provider URL from settings oauth_settings = get_oauth_settings() - params = get_urlparams_from_kwargs(kwargs) + params = clean_urlparams(kwargs) request_url = urlparse(frappe.request.url) success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" From 3f7b8f828228d67b6271983697ee669683e34104 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 19:36:44 +0200 Subject: [PATCH 032/126] feat: method to create social login key for tests --- .../social_login_key/test_social_login_key.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index 58bd48d64a..a1390b39b0 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -22,3 +22,16 @@ def make_social_login_key(**kwargs): kwargs["provider_name"] = "Test OAuth2 Provider" doc = frappe.get_doc(kwargs) return doc + +def create_or_update_social_login_key(): + # used in other tests (connected app, oauth20) + try: + social_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + social_login_key = frappe.new_doc("Social Login Key") + social_login_key.get_social_login_provider("Frappe", initialize=True) + social_login_key.base_url = frappe.get_site_config().host_name or "http://localhost:8000" + social_login_key.enable_social_login = 0 + social_login_key.save() + frappe.db.commit() + return social_login_key From d669b67473775bb1cfc5d5449617aea6a9248d65 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:55:38 +0200 Subject: [PATCH 033/126] fix: add fallback url for tests --- .../doctype/connected_app/connected_app.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 7b210bb9f5..9e3e31d1eb 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient +from urllib.parse import urljoin if frappe.conf.developer_mode: # Disable mandatory TLS in developer mode @@ -23,8 +24,14 @@ class ConnectedApp(Document): self.callback = frappe.scrub(self.provider_name) def validate(self): - callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' - self.redirect_uri = frappe.request.host_url + callback_path + self.callback + try: + base_url = frappe.request.host_url + except RuntimeError: + # for tests + base_url = frappe.get_site_config().host_name or 'http://localhost:8000' + + callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.callback + self.redirect_uri = urljoin(base_url, callback_path) def get_oauth2_session(self): return OAuth2Session( @@ -50,6 +57,7 @@ class ConnectedApp(Document): token.success_uri = success_uri token.state = state token.save() + frappe.db.commit() return authorization_url From 41eb1ae5f8a872628e5e85201b049457dba36131 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:56:32 +0200 Subject: [PATCH 034/126] fix: commit after saving token --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index accd72f794..3cb213e762 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -66,7 +66,7 @@ class TokenCache(Document): self.state = None self.save() - + frappe.db.commit() return self def get_expires_in(self): From 39440027bb9dda07d43e4cfdcdb243d50e14a6a0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:57:08 +0200 Subject: [PATCH 035/126] fix: add a useless link so that test record gets created --- .../doctype/connected_app/connected_app.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 8fca1b620f..6ab3f004e0 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -26,7 +26,8 @@ "revocation_endpoint", "cb_02", "userinfo_endpoint", - "introspection_endpoint" + "introspection_endpoint", + "useless_link" ], "fields": [ { @@ -129,6 +130,12 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" + }, + { + "fieldname": "useless_link", + "fieldtype": "Link", + "label": "Useless Link", + "options": "User" } ], "links": [ @@ -137,7 +144,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-08-13 11:45:02.983854", + "modified": "2020-08-13 18:55:35.554769", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 58a66b0cdcd8f5bfb2f080918b90ff239622ee12 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:57:51 +0200 Subject: [PATCH 036/126] tests: add test record --- .../doctype/connected_app/test_records.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 frappe/integrations/doctype/connected_app/test_records.json diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json new file mode 100644 index 0000000000..f9ba219f54 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_records.json @@ -0,0 +1,15 @@ +[ + { + "doctype": "Connected App", + "provider_name": "frappe", + "base_url": "http://localhost:8000", + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "redirect_uri": "http://localhost:8000/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/frappe", + "scopes": [ + { + "scope": "all" + } + ] + } +] From 5e787eb56f652e71a07149279d7bfd488ae83132 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 21:11:05 +0200 Subject: [PATCH 037/126] tests: web application fow against frappe --- .../connected_app/test_connected_app.py | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index bb04ca6677..9cd5bf5d22 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -3,8 +3,65 @@ # See license.txt from __future__ import unicode_literals -# import frappe import unittest +import requests +import frappe +from requests.auth import HTTPBasicAuth +from urllib.parse import urljoin, urlparse, parse_qs +from frappe.test_runner import make_test_records +from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key +from .connected_app import callback + +test_dependencies = ['OAuth Client', 'User', 'Connected App'] class TestConnectedApp(unittest.TestCase): - pass + + def setUp(self): + """Set up a Connected App that connects to our own oAuth provider. + + Frappe comes with it's own oAuth2 provider that we can test against. The + client credentials can be obtained from an "OAuth Client". All depends + on "Social Login Key" so we create one as well. + + The redirect URIs from "Connected App" and "OAuth Client" have to match. + Frappe's "Authorization URL" and "Access Token URL" (actually they're + just endpoints) are stored in "Social Login Key" so we get them from + there. + """ + connected_app = frappe.get_doc('Connected App', 'frappe') + social_login_key = create_or_update_social_login_key() + self.base_url = social_login_key.get('base_url') + + oauth_client_name = frappe.get_all('OAuth Client', fields=['name'])[0] + oauth_client = frappe.get_doc('OAuth Client', oauth_client_name['name']) + oauth_client.redirect_uris = connected_app.get('redirect_uri') + oauth_client.default_redirect_uri = connected_app.get('redirect_uri') + oauth_client.save() + frappe.db.commit() + + connected_app.client_id = oauth_client.get('client_id') + connected_app.client_secret = oauth_client.get('client_secret') + connected_app.authorization_endpoint = urljoin(self.base_url, social_login_key.get('authorize_url')) + connected_app.token_endpoint = urljoin(self.base_url, social_login_key.get('access_token_url')) + self.app = connected_app.save() + self.user_name = 'test@example.com' + self.user_password = 'Eastern_43A1W' + + def test_web_application_flow(self): + """Simulate a logged in user who opens the authorization URL.""" + session = requests.Session() + session.post(urljoin(self.base_url, '/api/method/login'), data={ + 'usr': self.user_name, + 'pwd': self.user_password + }) + authorization_url = self.app.initiate_web_application_flow(user=self.user_name) + + auth_response = session.get(authorization_url) + self.assertEqual(auth_response.status_code, 200) + + callback_response = session.get(auth_response.url) + self.assertEqual(callback_response.status_code, 200) + + token_cache = frappe.get_doc('Token Cache', self.app.name + '-' + self.user_name) + token = token_cache.get_password('access_token') + self.assertNotEqual(token, None) From af40088fbe95ee3edbc83ab708cbda253dc73d42 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 16 Sep 2020 11:17:14 +0200 Subject: [PATCH 038/126] fix: delete old js test --- .../doctype/token_cache/test_token_cache.js | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.js diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.js b/frappe/integrations/doctype/token_cache/test_token_cache.js deleted file mode 100644 index ee52cd7465..0000000000 --- a/frappe/integrations/doctype/token_cache/test_token_cache.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Token Cache", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Token Cache - () => frappe.tests.make('Token Cache', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); From b9260cdac157b2754871592f5376ee98913d4b11 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 16 Sep 2020 11:17:33 +0200 Subject: [PATCH 039/126] fix: delete old js test --- .../connected_app/test_connected_app.js | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.js diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.js b/frappe/integrations/doctype/connected_app/test_connected_app.js deleted file mode 100644 index 6db9056efc..0000000000 --- a/frappe/integrations/doctype/connected_app/test_connected_app.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Connected App", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Connected App - () => frappe.tests.make('Connected App', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); From f23fe9a9ec1838baf3bafd2ddd842e1ab1d99649 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 18:52:45 +0200 Subject: [PATCH 040/126] fix: style --- .../doctype/connected_app/connected_app.py | 16 ++++++++-------- .../doctype/connected_app/test_connected_app.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 9e3e31d1eb..57ff238471 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -87,8 +87,8 @@ class ConnectedApp(Document): token = token.check_validity() except frappe.exceptions.DoesNotExistError: redirect = self.initiate_web_application_flow(user, success_uri) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = redirect + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect return redirect return token @@ -107,7 +107,7 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - + def get_scopes(self): return [row.scope for row in self.scopes] @@ -116,7 +116,7 @@ class ConnectedApp(Document): def callback(code=None, state=None): """Handle client's code. - Called during the oauthorization flow by the remote oAuth2 server to + Called during the oauthorization flow by the remote oAuth2 server to transmit a code that can be used by the local server to obtain an access token. """ @@ -124,9 +124,9 @@ def callback(code=None, state=None): frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - frappe.throw(_("Log in to access this page."), frappe.PermissionError) + frappe.throw(_('Log in to access this page.'), frappe.PermissionError) - path = frappe.request.path[1:].split("/") + path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: frappe.throw(_('Invalid Parameter(s)')) @@ -151,5 +151,5 @@ def callback(code=None, state=None): ) token_cache.update_data(token) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = token_cache.get('success_uri') or '/desk' + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = token_cache.get('success_uri') or '/desk' diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 9cd5bf5d22..566c91768c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -18,7 +18,7 @@ class TestConnectedApp(unittest.TestCase): def setUp(self): """Set up a Connected App that connects to our own oAuth provider. - + Frappe comes with it's own oAuth2 provider that we can test against. The client credentials can be obtained from an "OAuth Client". All depends on "Social Login Key" so we create one as well. From 0bcaa9eea7339da6fc5f36304c65ecca514469e8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 18:53:22 +0200 Subject: [PATCH 041/126] fix: remove useless link --- .../doctype/connected_app/connected_app.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 6ab3f004e0..500f1df5b5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -26,8 +26,7 @@ "revocation_endpoint", "cb_02", "userinfo_endpoint", - "introspection_endpoint", - "useless_link" + "introspection_endpoint" ], "fields": [ { @@ -130,12 +129,6 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" - }, - { - "fieldname": "useless_link", - "fieldtype": "Link", - "label": "Useless Link", - "options": "User" } ], "links": [ @@ -144,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-08-13 18:55:35.554769", + "modified": "2020-09-27 18:23:15.001617", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 565c2177263c768c760fb20b9c7fac72373f417d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:35:59 +0200 Subject: [PATCH 042/126] refactor: rename Endpoint to URI --- .../doctype/connected_app/connected_app.json | 62 +++++++++---------- .../doctype/connected_app/connected_app.py | 4 +- .../connected_app/test_connected_app.py | 4 +- .../doctype/token_cache/token_cache.py | 6 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 500f1df5b5..59479e34a4 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -21,12 +21,12 @@ "sb_scope_section", "scopes", "sb_endpoints_section", - "authorization_endpoint", - "token_endpoint", - "revocation_endpoint", + "authorization_uri", + "token_uri", + "revocation_uri", "cb_02", - "userinfo_endpoint", - "introspection_endpoint" + "userinfo_uri", + "introspection_uri" ], "fields": [ { @@ -90,35 +90,10 @@ "fieldtype": "Section Break", "label": "Endpoints" }, - { - "fieldname": "authorization_endpoint", - "fieldtype": "Data", - "label": "Authorization Endpoint" - }, - { - "fieldname": "token_endpoint", - "fieldtype": "Data", - "label": "Token Endpoint" - }, - { - "fieldname": "revocation_endpoint", - "fieldtype": "Data", - "label": "Revocation Endpoint" - }, { "fieldname": "cb_02", "fieldtype": "Column Break" }, - { - "fieldname": "userinfo_endpoint", - "fieldtype": "Data", - "label": "Userinfo Endpoint" - }, - { - "fieldname": "introspection_endpoint", - "fieldtype": "Data", - "label": "Introspection Endpoint" - }, { "fieldname": "scopes", "fieldtype": "Table", @@ -129,6 +104,31 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" + }, + { + "fieldname": "authorization_uri", + "fieldtype": "Data", + "label": "Authorization URI" + }, + { + "fieldname": "token_uri", + "fieldtype": "Data", + "label": "Token URI" + }, + { + "fieldname": "revocation_uri", + "fieldtype": "Data", + "label": "Revocation URI" + }, + { + "fieldname": "userinfo_uri", + "fieldtype": "Data", + "label": "Userinfo URI" + }, + { + "fieldname": "introspection_uri", + "fieldtype": "Data", + "label": "Introspection URI" } ], "links": [ @@ -137,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 18:23:15.001617", + "modified": "2020-09-27 19:16:31.039086", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 57ff238471..293e8d478f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -45,7 +45,7 @@ class ConnectedApp(Document): success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session() - authorization_url, state = oauth.authorization_url(self.authorization_endpoint) + authorization_url, state = oauth.authorization_url(self.authorization_uri) try: token = self.get_stored_user_token(user) @@ -66,7 +66,7 @@ class ConnectedApp(Document): client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) oauth = OAuth2Session(client=client) token = oauth.fetch_token( - token_url=self.token_endpoint, + token_url=self.token_uri, client_secret=self.get_password('client_secret'), include_client_id=True ) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 566c91768c..488d537bba 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -41,8 +41,8 @@ class TestConnectedApp(unittest.TestCase): connected_app.client_id = oauth_client.get('client_id') connected_app.client_secret = oauth_client.get('client_secret') - connected_app.authorization_endpoint = urljoin(self.base_url, social_login_key.get('authorize_url')) - connected_app.token_endpoint = urljoin(self.base_url, social_login_key.get('access_token_url')) + connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) + connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) self.app = connected_app.save() self.user_name = 'test@example.com' self.user_password = 'Eastern_43A1W' diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 3cb213e762..8a10ddbd04 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -31,15 +31,15 @@ class TokenCache(Document): app = frappe.get_doc("Connected App", self.connected_app) oauth = app.get_oauth2_session() new_token = oauth.refresh_token( - app.token_endpoint, + app.token_uri, client_secret=app.get_password('client_secret'), token=self.get_json() ) - if new_token.get('access_token') and app.revocation_endpoint: + if new_token.get('access_token') and app.revocation_uri: # Revoke old token requests.post( - app.revocation_endpoint, + app.revocation_uri, data=urlencode({'token': new_token.get('access_token')}), headers={ 'Authorization': 'Bearer ' + new_token.get('access_token'), From b4bfcc734e5a1e2c32e88c43314f926757939fb2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:42:27 +0200 Subject: [PATCH 043/126] fix: remove 'callback' field --- .../doctype/connected_app/connected_app.json | 11 +---------- .../doctype/connected_app/connected_app.py | 5 +---- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 59479e34a4..0a07c927d4 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -1,6 +1,5 @@ { "actions": [], - "autoname": "field:callback", "beta": 1, "creation": "2019-01-24 15:51:06.362222", "doctype": "DocType", @@ -12,7 +11,6 @@ "base_url", "cb_00", "openid_configuration", - "callback", "sb_client_credentials_section", "client_id", "redirect_uri", @@ -45,13 +43,6 @@ "fieldtype": "Data", "label": "OpenID Configuration" }, - { - "fieldname": "callback", - "fieldtype": "Data", - "label": "Callback", - "read_only": 1, - "unique": 1 - }, { "collapsible": 1, "fieldname": "sb_client_credentials_section", @@ -137,7 +128,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 19:16:31.039086", + "modified": "2020-09-27 19:29:17.835067", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 293e8d478f..b734af2014 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -20,9 +20,6 @@ class ConnectedApp(Document): in a Token Cache. """ - def autoname(self): - self.callback = frappe.scrub(self.provider_name) - def validate(self): try: base_url = frappe.request.host_url @@ -30,7 +27,7 @@ class ConnectedApp(Document): # for tests base_url = frappe.get_site_config().host_name or 'http://localhost:8000' - callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.callback + callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) def get_oauth2_session(self): From 0f40d254e2a27028e361424d15f8aca4ec8df422 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:43:05 +0200 Subject: [PATCH 044/126] test: remove unused imports --- .../doctype/connected_app/test_connected_app.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 488d537bba..adb6a372f8 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -5,12 +5,10 @@ from __future__ import unicode_literals import unittest import requests +from urllib.parse import urljoin + import frappe -from requests.auth import HTTPBasicAuth -from urllib.parse import urljoin, urlparse, parse_qs -from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -from .connected_app import callback test_dependencies = ['OAuth Client', 'User', 'Connected App'] From 89c056998e6a19a104a4a518c47d20cbfeb4e1eb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:49:22 +0200 Subject: [PATCH 045/126] fix(Token Cache): use get_password --- frappe/integrations/doctype/token_cache/token_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 8a10ddbd04..cba8fad017 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -78,8 +78,8 @@ class TokenCache(Document): def get_json(self): return { - 'access_token': self.access_token, - 'refresh_token': self.refresh_token, + 'access_token': self.get_password('access_token'), + 'refresh_token': self.get_password('refresh_token'), 'expires_in': self.get_expires_in(), 'token_type': self.token_type } From 72cd67b9bd4a9a156b0f9adb126bf1bbbfb30615 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:55:40 +0200 Subject: [PATCH 046/126] fix: remove TokenCache.refresh_token() Will be handled by requests_oauthlib via ConnectedApp.get_oauth2_session(user). --- .../doctype/connected_app/connected_app.py | 14 +++++++-- .../doctype/token_cache/token_cache.py | 31 ------------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b734af2014..1cbca201a1 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -30,9 +30,19 @@ class ConnectedApp(Document): callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) - def get_oauth2_session(self): + def get_oauth2_session(self, user=None): + token = None + token_updater = None + if user: + token_cache = self.get_user_token(user) + token = token_cache.get_json() + token_updater = token_cache.update_data + return OAuth2Session( - self.client_id, + client_id=self.client_id, + token=token, + token_updater=token_updater, + auto_refresh_url=self.token_uri, redirect_uri=self.redirect_uri, scope=self.get_scopes() ) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index cba8fad017..2d6239921d 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -18,37 +18,6 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError - def check_validity(self): - if(self.get('__islocal') or (not self.access_token)): - raise frappe.exceptions.DoesNotExistError - - if not self.is_expired(): - return self - - return self.refresh_token() - - def refresh_token(self): - app = frappe.get_doc("Connected App", self.connected_app) - oauth = app.get_oauth2_session() - new_token = oauth.refresh_token( - app.token_uri, - client_secret=app.get_password('client_secret'), - token=self.get_json() - ) - - if new_token.get('access_token') and app.revocation_uri: - # Revoke old token - requests.post( - app.revocation_uri, - data=urlencode({'token': new_token.get('access_token')}), - headers={ - 'Authorization': 'Bearer ' + new_token.get('access_token'), - 'Content-Type': 'application/x-www-form-urlencoded' - } - ) - - return self.update_data(new_token) - def update_data(self, data): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') From 0d1a76ac4d75b95acf123666a048b012ebb2b46a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 20:03:35 +0200 Subject: [PATCH 047/126] test: authenticated session, backend flow --- .../doctype/connected_app/test_connected_app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index adb6a372f8..246afb878d 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -60,6 +60,15 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = frappe.get_doc('Token Cache', self.app.name + '-' + self.user_name) + token_cache = self.app.get_stored_user_token(self.user_name) + token = token_cache.get_password('access_token') + self.assertNotEqual(token, None) + + oauth2_session = self.app.get_oauth2_session(self.user_name) + resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) + self.assertEqual(resp.json().get('message'), self.user_name) + + def test_backend_application_flow(self): + token_cache = self.app.initiate_backend_application_flow() token = token_cache.get_password('access_token') self.assertNotEqual(token, None) From d1341bc1100b2b95f2cc60396744bf3dea4f1e45 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 20:05:13 +0200 Subject: [PATCH 048/126] fix: remove base_url use case not supported by requests, so not needed anymore --- .../integrations/doctype/connected_app/connected_app.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 0a07c927d4..9e0f361ec5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -8,7 +8,6 @@ "engine": "InnoDB", "field_order": [ "provider_name", - "base_url", "cb_00", "openid_configuration", "sb_client_credentials_section", @@ -91,11 +90,6 @@ "label": "Scopes", "options": "OAuth Scope" }, - { - "fieldname": "base_url", - "fieldtype": "Data", - "label": "Base URL" - }, { "fieldname": "authorization_uri", "fieldtype": "Data", @@ -128,7 +122,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 19:29:17.835067", + "modified": "2020-09-27 20:04:02.303982", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 60cb523439067e84cccb2118ac73ea26815f4c7a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 10:59:49 +0200 Subject: [PATCH 049/126] fix: get_auth_header --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2d6239921d..12f473addc 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -13,7 +13,7 @@ class TokenCache(Document): def get_auth_header(self): if self.access_token: - headers = {'Authorization': 'Bearer ' + self.access_token} + headers = {'Authorization': 'Bearer ' + self.get_password('access_token')} return headers raise frappe.exceptions.DoesNotExistError From ea0ceae42cf19e47e31d12d6fec5a609a6f89835 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:01:16 +0200 Subject: [PATCH 050/126] fix: validate token_type, use cstr --- .../doctype/token_cache/token_cache.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 12f473addc..2d3b3f9b4d 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -3,10 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe -import requests -from urllib.parse import urlencode from datetime import datetime, timedelta + +import frappe +from frappe import _ +from frappe.utils import cstr from frappe.model.document import Document class TokenCache(Document): @@ -19,10 +20,16 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError def update_data(self, data): - self.access_token = data.get('access_token') - self.refresh_token = data.get('refresh_token') - self.expires_in = data.get('expires_in') - self.token_type = data.get('token_type') + token_type = cstr(data.get('token_type', '')).lower() + if token_type not in ['bearer', 'mac']: + frappe.throw(_('Received an invalid token type.')) + # 'Bearer' or 'MAC' + token_type = token_type.title() if token_type == 'bearer' else token_type.upper() + + self.token_type = token_type + self.access_token = cstr(data.get('access_token', '')) + self.refresh_token = cstr(data.get('refresh_token', '')) + self.expires_in = cstr(data.get('expires_in', '')) new_scopes = data.get('scope') if new_scopes: From 20bc3f3a95c92f0b1464771ad433506800c53a8e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:01:40 +0200 Subject: [PATCH 051/126] test: Token Cache --- .../doctype/token_cache/test_records.json | 18 +++++++++++ .../doctype/token_cache/test_token_cache.py | 32 +++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 frappe/integrations/doctype/token_cache/test_records.json diff --git a/frappe/integrations/doctype/token_cache/test_records.json b/frappe/integrations/doctype/token_cache/test_records.json new file mode 100644 index 0000000000..05840221a6 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_records.json @@ -0,0 +1,18 @@ +[ + { + "doctype": "Token Cache", + "user": "test@example.com", + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "Bearer", + "expires_in": 1000, + "scopes": [ + { + "scope": "all" + }, + { + "scope": "openid" + } + ] + } +] \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index aebac0b52f..be393b0b60 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -3,8 +3,36 @@ # See license.txt from __future__ import unicode_literals -# import frappe import unittest +import frappe + +test_dependencies = ['User', 'Connected App', 'Token Cache'] class TestTokenCache(unittest.TestCase): - pass + + def setup(self): + token_cache_list = frappe.get_list('Token Cache') + connected_app_list = frappe.get_list('Connected App') + self.token_cache = frappe.get_doc('Token Cache', token_cache_list[0].name) + self.token_cache.update({'connected_app': connected_app_list[0].name}) + + def test_get_auth_header(self): + self.token_cache.get_auth_header() + + def test_update_data(self): + self.token_cache.update_data({ + 'access_token': 'new-access-token', + 'refresh_token': 'new-refresh-token', + 'token_type': 'bearer', + 'expires_in': 2000, + 'scope': 'new scope' + }) + + def test_get_expires_in(self): + self.token_cache.get_expires_in() + + def test_is_expired(self): + self.token_cache.is_expired() + + def get_json(self): + self.token_cache.get_json() From 86280638dd8e551896b10faaea79088a189e8088 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:18:19 +0200 Subject: [PATCH 052/126] fix: remove backend application flow --- .../doctype/connected_app/connected_app.py | 29 ------------------- .../connected_app/test_connected_app.py | 4 --- 2 files changed, 33 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 1cbca201a1..5e6e659e3d 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -68,23 +68,6 @@ class ConnectedApp(Document): return authorization_url - def initiate_backend_application_flow(self): - """Retrieve token without user interaction. Token is not user specific.""" - client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) - oauth = OAuth2Session(client=client) - token = oauth.fetch_token( - token_url=self.token_uri, - client_secret=self.get_password('client_secret'), - include_client_id=True - ) - - try: - stored_token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - stored_token = frappe.new_doc('Token Cache') - - return stored_token.update_data(token) - def get_user_token(self, user=None, success_uri=None): """Return an existing user token or initiate a Web Application Flow.""" user = user or frappe.session.user @@ -100,18 +83,6 @@ class ConnectedApp(Document): return token - def get_client_token(self): - """Return an existing client token or initiate a Backend Application Flow.""" - try: - token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - token = self.initiate_backend_application_flow() - - return token.check_validity() - - def get_stored_client_token(self): - return frappe.get_doc('Token Cache', self.name + '-user') - def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 246afb878d..26e66a374c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -68,7 +68,3 @@ class TestConnectedApp(unittest.TestCase): resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) - def test_backend_application_flow(self): - token_cache = self.app.initiate_backend_application_flow() - token = token_cache.get_password('access_token') - self.assertNotEqual(token, None) From df9dfd5a53eb1228937d280c330b6ac292ef5ccb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:43:59 +0200 Subject: [PATCH 053/126] fix: Token Cache permissions --- .../doctype/token_cache/token_cache.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 91758a7332..95f19daa08 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -78,23 +78,22 @@ } ], "links": [], - "modified": "2020-07-21 00:32:56.225300", + "modified": "2020-10-18 15:22:48.991735", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", "owner": "Administrator", "permissions": [ { - "create": 1, "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 + "role": "System Manager" + }, + { + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All" } ], "sort_field": "modified", From 3e75224bd45b64ba6a7c9be714d6fb82a1bea513 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:44:33 +0200 Subject: [PATCH 054/126] feat: add Token Cache to User dashboard --- frappe/core/doctype/user/user.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 2073f41fdd..7d91e8cfe0 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -642,10 +642,15 @@ "group": "Activity", "link_doctype": "ToDo", "link_fieldname": "owner" + }, + { + "group": "Integrations", + "link_doctype": "Token Cache", + "link_fieldname": "user" } ], "max_attachments": 5, - "modified": "2020-08-26 19:48:49.677800", + "modified": "2020-10-18 15:18:53.126800", "modified_by": "Administrator", "module": "Core", "name": "User", From 284ee3c4c3e0ce74b7efe2ead93da7d55f1fd2d0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:46:38 +0200 Subject: [PATCH 055/126] feat: allow role "All" to view Connected Apps --- .../integrations/doctype/connected_app/connected_app.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 9e0f361ec5..014b3b11f5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -51,6 +51,7 @@ { "fieldname": "client_id", "fieldtype": "Data", + "in_list_view": 1, "label": "Client Id" }, { @@ -122,7 +123,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 20:04:02.303982", + "modified": "2020-10-18 16:10:13.051678", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", @@ -139,6 +140,10 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "All" } ], "sort_field": "modified", From b79f24aac82506c5a94c3c8402935ec79b95a1d0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:47:29 +0200 Subject: [PATCH 056/126] fix: connected app fix errors from previous refactor --- frappe/integrations/doctype/connected_app/connected_app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 5e6e659e3d..f9764ba652 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -74,7 +74,6 @@ class ConnectedApp(Document): try: token = self.get_stored_user_token(user) - token = token.check_validity() except frappe.exceptions.DoesNotExistError: redirect = self.initiate_web_application_flow(user, success_uri) frappe.local.response['type'] = 'redirect' @@ -122,7 +121,7 @@ def callback(code=None, state=None): frappe.throw(_('Invalid App')) oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, + token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), include_client_id=True From 03c1d8dc772cdd21ab4bc7d1e7f686670c7dfb62 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:48:07 +0200 Subject: [PATCH 057/126] fix: token cache has to ignore permissions --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2d3b3f9b4d..f713a9e49c 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -41,7 +41,7 @@ class TokenCache(Document): self.append('scopes', {'scope': scope}) self.state = None - self.save() + self.save(ignore_permissions=True) frappe.db.commit() return self From 83b85cbdbd696f5ce2ba9123a63f720cd44a54cd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:48:31 +0200 Subject: [PATCH 058/126] test: fix connected app --- .../connected_app/test_connected_app.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 26e66a374c..f5bc463766 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -10,7 +10,7 @@ from urllib.parse import urljoin import frappe from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -test_dependencies = ['OAuth Client', 'User', 'Connected App'] +test_dependencies = ['Connected App', 'OAuth Client', 'User'] class TestConnectedApp(unittest.TestCase): @@ -26,24 +26,29 @@ class TestConnectedApp(unittest.TestCase): just endpoints) are stored in "Social Login Key" so we get them from there. """ - connected_app = frappe.get_doc('Connected App', 'frappe') + self.user_name = 'test@example.com' + self.user_password = 'Eastern_43A1W' + + connected_app = frappe.get_last_doc('Connected App') + redirect_uri = connected_app.get('redirect_uri') + + web_application_client = frappe.get_last_doc('OAuth Client') + web_application_client.update({ + 'redirect_uris': redirect_uri, + 'default_redirect_uri': redirect_uri + }) + web_application_client.save() + social_login_key = create_or_update_social_login_key() self.base_url = social_login_key.get('base_url') - oauth_client_name = frappe.get_all('OAuth Client', fields=['name'])[0] - oauth_client = frappe.get_doc('OAuth Client', oauth_client_name['name']) - oauth_client.redirect_uris = connected_app.get('redirect_uri') - oauth_client.default_redirect_uri = connected_app.get('redirect_uri') - oauth_client.save() - frappe.db.commit() - - connected_app.client_id = oauth_client.get('client_id') - connected_app.client_secret = oauth_client.get('client_secret') connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) - self.app = connected_app.save() - self.user_name = 'test@example.com' - self.user_password = 'Eastern_43A1W' + connected_app.client_id = web_application_client.get('client_id') + connected_app.client_secret = web_application_client.get('client_secret') + self.connected_app = connected_app.save() + + frappe.db.commit() def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" @@ -52,7 +57,7 @@ class TestConnectedApp(unittest.TestCase): 'usr': self.user_name, 'pwd': self.user_password }) - authorization_url = self.app.initiate_web_application_flow(user=self.user_name) + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) auth_response = session.get(authorization_url) self.assertEqual(auth_response.status_code, 200) @@ -60,11 +65,10 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.app.get_stored_user_token(self.user_name) + token_cache = self.connected_app.get_stored_user_token(self.user_name) token = token_cache.get_password('access_token') self.assertNotEqual(token, None) - oauth2_session = self.app.get_oauth2_session(self.user_name) + oauth2_session = self.connected_app.get_oauth2_session(self.user_name) resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) - From b6674eccffa63bb1b3ad00ff5973b22af738a399 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:53:02 +0200 Subject: [PATCH 059/126] fix: remove unused import of BackendApplicationClient --- frappe/integrations/doctype/connected_app/connected_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index f9764ba652..85b1e06169 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -7,7 +7,6 @@ import frappe from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -from oauthlib.oauth2 import BackendApplicationClient from urllib.parse import urljoin if frappe.conf.developer_mode: From 10423b2fc2f2c12037175c3a841635683808adfa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:19:56 +0200 Subject: [PATCH 060/126] fix: TokenCache.get_expires_in() --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index f713a9e49c..6c513c3734 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -46,7 +46,7 @@ class TokenCache(Document): return self def get_expires_in(self): - expiry_time = self.modified + timedelta(self.expires_in) + expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in) return (datetime.now() - expiry_time).total_seconds() def is_expired(self): From 3297356e175e5d5719d3917300fbcde8bd7dea60 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:20:18 +0200 Subject: [PATCH 061/126] test: fix TestTokenCache.setUp() --- .../integrations/doctype/token_cache/test_token_cache.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index be393b0b60..73c9f38fce 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -10,11 +10,10 @@ test_dependencies = ['User', 'Connected App', 'Token Cache'] class TestTokenCache(unittest.TestCase): - def setup(self): - token_cache_list = frappe.get_list('Token Cache') - connected_app_list = frappe.get_list('Connected App') - self.token_cache = frappe.get_doc('Token Cache', token_cache_list[0].name) - self.token_cache.update({'connected_app': connected_app_list[0].name}) + def setUp(self): + self.token_cache = frappe.get_last_doc('Token Cache') + self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name}) + self.token_cache.save() def test_get_auth_header(self): self.token_cache.get_auth_header() From ab22f75b48f4dc074567f3ccb9f23377d677cfc4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:31:14 +0200 Subject: [PATCH 062/126] fix: cint for expires_in, docstring --- frappe/integrations/doctype/token_cache/token_cache.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 6c513c3734..fba73f0b0f 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, cint from frappe.model.document import Document class TokenCache(Document): @@ -20,6 +20,12 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError def update_data(self, data): + """ + Store data returned by authorization flow. + + Params: + data - Dict with access_token, refresh_token, expires_in and scope. + """ token_type = cstr(data.get('token_type', '')).lower() if token_type not in ['bearer', 'mac']: frappe.throw(_('Received an invalid token type.')) @@ -29,7 +35,7 @@ class TokenCache(Document): self.token_type = token_type self.access_token = cstr(data.get('access_token', '')) self.refresh_token = cstr(data.get('refresh_token', '')) - self.expires_in = cstr(data.get('expires_in', '')) + self.expires_in = cint(data.get('expires_in', 0)) new_scopes = data.get('scope') if new_scopes: From bf4fd67dbf434920bc87af3d4dfdad167284d378 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:34:43 +0200 Subject: [PATCH 063/126] fix: allow insecure transport in tests --- frappe/integrations/doctype/connected_app/connected_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 85b1e06169..5da1564692 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -9,8 +9,8 @@ from frappe.model.document import Document from requests_oauthlib import OAuth2Session from urllib.parse import urljoin -if frappe.conf.developer_mode: - # Disable mandatory TLS in developer mode +if frappe.conf.developer_mode or frappe.flags.in_test: + # Disable mandatory TLS in developer mode and tests import os os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' From 33813fda881639e6fcaa05de48d08e777fde5cff Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 28 Oct 2020 14:11:50 +0530 Subject: [PATCH 064/126] fix: Connected App fix Get OpenID Configuration button fix permission error on token save --- .../doctype/connected_app/connected_app.js | 10 +++++----- .../doctype/connected_app/connected_app.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 71ff23fcc3..c78abe28be 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -10,11 +10,11 @@ frappe.ui.form.on('Connected App', { try { const response = await fetch(frm.doc.openid_configuration); const oidc = await response.json(); - frm.set_value('authorization_endpoint', oidc.authorization_endpoint); - frm.set_value('token_endpoint', oidc.token_endpoint); - frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); - frm.set_value('introspection_endpoint', oidc.introspection_endpoint); - frm.set_value('revocation_endpoint', oidc.revocation_endpoint); + frm.set_value('authorization_uri', oidc.authorization_endpoint); + frm.set_value('token_uri', oidc.token_endpoint); + frm.set_value('userinfo_uri', oidc.userinfo_endpoint); + frm.set_value('introspection_uri', oidc.introspection_endpoint); + frm.set_value('revocation_uri', oidc.revocation_endpoint); } catch (error) { frappe.msgprint(__('Please check OpenID Configuration URL')); } diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 5da1564692..737292b4f8 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -62,7 +62,7 @@ class ConnectedApp(Document): token.success_uri = success_uri token.state = state - token.save() + token.save(ignore_permissions=True) frappe.db.commit() return authorization_url From a582b1c36463e5cc3a8efefc329cfbbb1b3208e5 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 12 Nov 2020 19:43:21 +0100 Subject: [PATCH 065/126] fix: allow get_oauth2_session without user arg --- .../doctype/connected_app/connected_app.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 737292b4f8..14b4d4a1cf 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -29,10 +29,12 @@ class ConnectedApp(Document): callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) - def get_oauth2_session(self, user=None): + def get_oauth2_session(self, user=None, init=False): token = None token_updater = None - if user: + + if not init: + user = user or frappe.session.user token_cache = self.get_user_token(user) token = token_cache.get_json() token_updater = token_cache.update_data @@ -50,7 +52,7 @@ class ConnectedApp(Document): """Return an authorization URL for the user. Save state in Token Cache.""" success_uri = success_uri or '/desk' user = user or frappe.session.user - oauth = self.get_oauth2_session() + oauth = self.get_oauth2_session(init=True) authorization_url, state = oauth.authorization_url(self.authorization_uri) try: @@ -119,7 +121,7 @@ def callback(code=None, state=None): except frappe.exceptions.DoesNotExistError: frappe.throw(_('Invalid App')) - oauth = app.get_oauth2_session() + oauth = app.get_oauth2_session(init=True) token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), From 631f4fab7c6934467921936a172cd643ae6b53fa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:16:47 +0100 Subject: [PATCH 066/126] fix: show "Connect to {provider}" if doc is not new --- .../doctype/connected_app/connected_app.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index c78abe28be..700e630a6a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Connected App', { refresh: frm => { - frm.add_custom_button(__("Get OpenID Configuration"), async () => { + frm.add_custom_button(__('Get OpenID Configuration'), async () => { if (!frm.doc.openid_configuration) { frappe.msgprint(__('Please enter OpenID Configuration URL')); } else { @@ -21,14 +21,16 @@ frappe.ui.form.on('Connected App', { } }); - frm.add_custom_button(__("Init"), async () => { - frappe.call({ - method: "initiate_web_application_flow", - doc: frm.doc, - callback: function(r) { - window.open(r.message, '_blank'); - } + if (!frm.is_new()) { + frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => { + frappe.call({ + method: 'initiate_web_application_flow', + doc: frm.doc, + callback: function(r) { + window.open(r.message, '_blank'); + } + }); }); - }); + } } }); From 931a39c998c5ac00e2ce89227e194d4116e4c95c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:17:34 +0100 Subject: [PATCH 067/126] faet: display provider name in token cache --- .../integrations/doctype/token_cache/token_cache.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 95f19daa08..c016405031 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -10,6 +10,7 @@ "field_order": [ "user", "connected_app", + "provider_name", "access_token", "refresh_token", "expires_in", @@ -75,10 +76,17 @@ "fieldtype": "Data", "label": "Token Type", "read_only": 1 + }, + { + "fetch_from": "connected_app.provider_name", + "fieldname": "provider_name", + "fieldtype": "Data", + "label": "Provider Name", + "read_only": 1 } ], "links": [], - "modified": "2020-10-18 15:22:48.991735", + "modified": "2020-11-13 13:35:53.714352", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From 956e407c5f17248aeeba26be308645768a995832 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 18:07:46 +0100 Subject: [PATCH 068/126] feat: toggle client credential section --- frappe/integrations/doctype/connected_app/connected_app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 700e630a6a..4d20f65559 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -32,5 +32,7 @@ frappe.ui.form.on('Connected App', { }); }); } + + frm.toggle_display('sb_client_credentials_section', !frm.is_new()); } }); From 74a51aa1776b917f0acc2d9cf63f9f1d570d58b1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 18:08:36 +0100 Subject: [PATCH 069/126] fix: default if password is empty --- frappe/integrations/doctype/token_cache/token_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index fba73f0b0f..7cac58fae0 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -60,8 +60,8 @@ class TokenCache(Document): def get_json(self): return { - 'access_token': self.get_password('access_token'), - 'refresh_token': self.get_password('refresh_token'), + 'access_token': self.get_password('access_token', ''), + 'refresh_token': self.get_password('refresh_token', ''), 'expires_in': self.get_expires_in(), 'token_type': self.token_type } From 63de77477779dd49a59a60ab3d2f9216365956b8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:33:29 +0100 Subject: [PATCH 070/126] feat: allow extra parameters --- .../doctype/connected_app/connected_app.json | 17 ++++++++- .../doctype/connected_app/connected_app.py | 10 ++++- .../doctype/query_parameters/__init__.py | 0 .../query_parameters/query_parameters.json | 37 +++++++++++++++++++ .../query_parameters/query_parameters.py | 10 +++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 frappe/integrations/doctype/query_parameters/__init__.py create mode 100644 frappe/integrations/doctype/query_parameters/query_parameters.json create mode 100644 frappe/integrations/doctype/query_parameters/query_parameters.py diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 014b3b11f5..e5dbb0472a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -23,7 +23,9 @@ "revocation_uri", "cb_02", "userinfo_uri", - "introspection_uri" + "introspection_uri", + "section_break_18", + "query_parameters" ], "fields": [ { @@ -115,6 +117,17 @@ "fieldname": "introspection_uri", "fieldtype": "Data", "label": "Introspection URI" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Extra Parameters" + }, + { + "fieldname": "query_parameters", + "fieldtype": "Table", + "label": "Query Parameters", + "options": "Query Parameters" } ], "links": [ @@ -123,7 +136,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-10-18 16:10:13.051678", + "modified": "2020-11-16 16:29:50.277405", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 14b4d4a1cf..a281d9d850 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -53,7 +53,8 @@ class ConnectedApp(Document): success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session(init=True) - authorization_url, state = oauth.authorization_url(self.authorization_uri) + query_params = self.get_query_params() + authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) try: token = self.get_stored_user_token(user) @@ -89,6 +90,9 @@ class ConnectedApp(Document): def get_scopes(self): return [row.scope for row in self.scopes] + def get_query_params(self): + return {param.key: param.value for param in self.query_parameters} + @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): @@ -122,10 +126,12 @@ def callback(code=None, state=None): frappe.throw(_('Invalid App')) oauth = app.get_oauth2_session(init=True) + query_params = app.get_query_params() token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), - include_client_id=True + include_client_id=True, + **query_params ) token_cache.update_data(token) diff --git a/frappe/integrations/doctype/query_parameters/__init__.py b/frappe/integrations/doctype/query_parameters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.json b/frappe/integrations/doctype/query_parameters/query_parameters.json new file mode 100644 index 0000000000..de31c28df7 --- /dev/null +++ b/frappe/integrations/doctype/query_parameters/query_parameters.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-11-16 14:54:37.226914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "key", + "value" + ], + "fields": [ + { + "fieldname": "key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Key", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-16 15:18:35.887149", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Query Parameters", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py new file mode 100644 index 0000000000..bfb8eae0b6 --- /dev/null +++ b/frappe/integrations/doctype/query_parameters/query_parameters.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class QueryParameters(Document): + pass From 645cacce30418cc89cf5ae5ceada6b3b10463603 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:36:03 +0100 Subject: [PATCH 071/126] fix: redirect back to connected app by default (instead of to desk) --- frappe/integrations/doctype/connected_app/connected_app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index a281d9d850..146caf8657 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -50,7 +50,6 @@ class ConnectedApp(Document): def initiate_web_application_flow(self, user=None, success_uri=None): """Return an authorization URL for the user. Save state in Token Cache.""" - success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session(init=True) query_params = self.get_query_params() @@ -136,4 +135,4 @@ def callback(code=None, state=None): token_cache.update_data(token) frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = token_cache.get('success_uri') or '/desk' + frappe.local.response['location'] = token_cache.get('success_uri') or app.get_url() From da0ea7c225197b1d45c5283b63de8d223a30d700 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:37:06 +0100 Subject: [PATCH 072/126] refactor: frappe.db.exists instead of try/except --- .../doctype/connected_app/connected_app.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 146caf8657..9254aa7631 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,11 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals +from urllib.parse import urljoin + import frappe from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -from urllib.parse import urljoin if frappe.conf.developer_mode or frappe.flags.in_test: # Disable mandatory TLS in developer mode and tests @@ -54,17 +55,16 @@ class ConnectedApp(Document): oauth = self.get_oauth2_session(init=True) query_params = self.get_query_params() authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) + token_cache = self.get_token_cache(user) - try: - token = self.get_stored_user_token(user) - except frappe.exceptions.DoesNotExistError: - token = frappe.new_doc('Token Cache') - token.user = user - token.connected_app = self.name + if not token_cache: + token_cache = frappe.new_doc('Token Cache') + token_cache.user = user + token_cache.connected_app = self.name - token.success_uri = success_uri - token.state = state - token.save(ignore_permissions=True) + token_cache.success_uri = success_uri + token_cache.state = state + token_cache.save(ignore_permissions=True) frappe.db.commit() return authorization_url @@ -72,19 +72,24 @@ class ConnectedApp(Document): def get_user_token(self, user=None, success_uri=None): """Return an existing user token or initiate a Web Application Flow.""" user = user or frappe.session.user + token_cache = self.get_token_cache(user) - try: - token = self.get_stored_user_token(user) - except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_web_application_flow(user, success_uri) - frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = redirect - return redirect + if token_cache: + return token_cache - return token + redirect = self.initiate_web_application_flow(user, success_uri) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect + return redirect - def get_stored_user_token(self, user): - return frappe.get_doc('Token Cache', self.name + '-' + user) + def get_token_cache(self, user): + token_cache = None + token_cache_name = self.name + '-' + user + + if frappe.db.exists('Token Cache', token_cache_name): + token_cache = frappe.get_doc('Token Cache', token_cache_name) + + return token_cache def get_scopes(self): return [row.scope for row in self.scopes] From e3e8e1dca222c23778c28ea6a3fc8081b7d5612d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:41:16 +0100 Subject: [PATCH 073/126] test: fix method name --- frappe/integrations/doctype/connected_app/test_connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index f5bc463766..4d8acb9b59 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -65,7 +65,7 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.connected_app.get_stored_user_token(self.user_name) + token_cache = self.connected_app.get_token_cache(self.user_name) token = token_cache.get_password('access_token') self.assertNotEqual(token, None) From 5270643bcb285c8ecdc8834e3c4c7240afb7ad75 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 Nov 2020 11:53:57 +0100 Subject: [PATCH 074/126] fix: description in config --- frappe/config/integrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index deba1945f3..672c0c4acc 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -80,7 +80,7 @@ def get_data(): { "type": "doctype", "name": "Connected App", - "description": _("Connected App"), + "description": _("Connect to any OAuth Provider"), }, ] }, From c5767b818facb2e3b9e61a8cede7c7653dca5a67 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 1 Dec 2020 18:04:12 +0530 Subject: [PATCH 075/126] feat: setup record for background color while setting up website theme --- frappe/patches/v13_0/website_theme_custom_scss.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py index 0035283428..a5f08324e8 100644 --- a/frappe/patches/v13_0/website_theme_custom_scss.py +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -2,9 +2,23 @@ import frappe def execute(): frappe.reload_doctype('Website Theme') + frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app') + frappe.reload_doc('website', 'doctype', 'color') + for theme in frappe.get_all('Website Theme'): doc = frappe.get_doc('Website Theme', theme.name) if not doc.get('custom_scss') and doc.theme_scss: # move old theme to new theme doc.custom_scss = doc.theme_scss + + if doc.background_color: + setup_color_record(doc.background_color) + doc.save() + +def setup_color_record(color): + frappe.get_doc({ + "doctype": "Color", + "__newname": color, + "color": color, + }).save() From 46edda896e2f9756ab2d85e599d0bdff33a8e7c6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 4 Dec 2020 17:44:36 +0100 Subject: [PATCH 076/126] fix: improve translation pattern --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 340f4f8772..184d5bde73 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +pattern = re.compile(r"_{1,2}\(([\"'`]{1,3})(?P((?!\1)[\s\S])*)\1(\s*,\s*context\s*=\s*([\"']{1,3})(?P((?!\5)[\s\S])*)\5){0,1}(\s*,\s*([\s\S])*\s*(,\s*([\"'`])(?P((?!\11).)*)\11)*){0,1}\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From a4e11583ddff106fff66451d062271374966e0b6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 10 Dec 2020 11:56:31 +0100 Subject: [PATCH 077/126] fix: no multiline source or context, allow null --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 184d5bde73..863175a028 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(([\"'`]{1,3})(?P((?!\1)[\s\S])*)\1(\s*,\s*context\s*=\s*([\"']{1,3})(?P((?!\5)[\s\S])*)\5){0,1}(\s*,\s*([\s\S])*\s*(,\s*([\"'`])(?P((?!\11).)*)\11)*){0,1}\)") +pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From c7824f6211a0c7ca429c63f77e9f8b8737236ade Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 18 Dec 2020 15:59:18 +0530 Subject: [PATCH 078/126] fix: allow images from links in print formats --- frappe/templates/print_formats/standard_macros.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 0d904bb59c..9a14b860ff 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -6,7 +6,10 @@ {%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} {{ render_text_field(df, doc) }} {%- elif df.fieldtype in ("Image", "Attach Image", "Attach") - and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%} + and ( + (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") + or doc[df.fieldname].startswith("http") + ) -%} {{ render_image(df, doc) }} {%- elif df.fieldtype=="Geolocation" -%} {{ render_geolocation(df, doc) }} @@ -123,15 +126,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% include doc.print_templates[df.fieldname] %} {% elif df.fieldtype=="Check" %} - {% elif df.fieldtype=="Image" %} + {% elif df.fieldtype in ("Image", "Attach Image") %} {% elif df.fieldtype=="Signature" %} - {% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname] - and frappe.utils.is_image(doc[df.fieldname]) %} + {% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %} {% elif df.fieldtype=="HTML" %} From 9f6fc4618feb85adf8c7ec7bb56c2b1494598fa0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Dec 2020 17:25:16 +0100 Subject: [PATCH 079/126] fix: base_url --- .../doctype/connected_app/connected_app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 9254aa7631..64ec6d11c8 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -21,11 +21,13 @@ class ConnectedApp(Document): """ def validate(self): - try: - base_url = frappe.request.host_url - except RuntimeError: - # for tests - base_url = frappe.get_site_config().host_name or 'http://localhost:8000' + if not frappe.flags.in_test: + try: + base_url = frappe.request.host_url + except RuntimeError: + base_url = frappe.utils.get_url() + else: + base_url = 'http://localhost:8000' callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) From bf8b40aceceb88c2aac619fa9efe65922355a01e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Dec 2020 17:25:26 +0100 Subject: [PATCH 080/126] fix: test records --- frappe/integrations/doctype/connected_app/test_records.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json index f9ba219f54..4d19369248 100644 --- a/frappe/integrations/doctype/connected_app/test_records.json +++ b/frappe/integrations/doctype/connected_app/test_records.json @@ -2,10 +2,8 @@ { "doctype": "Connected App", "provider_name": "frappe", - "base_url": "http://localhost:8000", "client_id": "test_client_id", "client_secret": "test_client_secret", - "redirect_uri": "http://localhost:8000/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/frappe", "scopes": [ { "scope": "all" From 301ed4c7e322fb1de93a197ee3423992d8719435 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 18:35:26 +0100 Subject: [PATCH 081/126] fix: rely on frappe.utils.get_url --- .../integrations/doctype/connected_app/connected_app.py | 9 +-------- .../doctype/social_login_key/test_social_login_key.py | 3 ++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 64ec6d11c8..a26f93f676 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -21,14 +21,7 @@ class ConnectedApp(Document): """ def validate(self): - if not frappe.flags.in_test: - try: - base_url = frappe.request.host_url - except RuntimeError: - base_url = frappe.utils.get_url() - else: - base_url = 'http://localhost:8000' - + base_url = frappe.utils.get_url() callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index a1390b39b0..e0b99ad391 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -30,8 +30,9 @@ def create_or_update_social_login_key(): except frappe.DoesNotExistError: social_login_key = frappe.new_doc("Social Login Key") social_login_key.get_social_login_provider("Frappe", initialize=True) - social_login_key.base_url = frappe.get_site_config().host_name or "http://localhost:8000" + social_login_key.base_url = frappe.utils.get_url() social_login_key.enable_social_login = 0 social_login_key.save() frappe.db.commit() + return social_login_key From bf97382002e07e4e568e4a4c509c0f97f56feedc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 23 Dec 2020 13:09:53 +0530 Subject: [PATCH 082/126] fix: row removed in child table not syncing when it has a dependency field --- .../doctype/event_producer/event_producer.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index d8a6a55510..e43b4d131c 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -295,7 +295,7 @@ def set_update(update, producer_site): if data.changed: local_doc.update(data.changed) if data.removed: - update_row_removed(local_doc, data.removed) + local_doc = update_row_removed(local_doc, data.removed) if data.row_changed: update_row_changed(local_doc, data.row_changed) if data.added: @@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed): for tablename, rownames in iteritems(removed): table = local_doc.get_table_field_doctype(tablename) for row in rownames: - frappe.db.delete(table, row) + table_rows = local_doc.get(tablename) + child_table_row = get_child_table_row(table_rows, row) + table_rows.remove(child_table_row) + local_doc.set(tablename, table_rows) + return local_doc + + +def get_child_table_row(table_rows, row): + for entry in table_rows: + if entry.get('name') == row: + return entry def update_row_changed(local_doc, changed): From 5b8294f92b3c05f39442f6a85af0e6156f66dad6 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 21 Dec 2020 15:16:34 +0530 Subject: [PATCH 083/126] fix: throw error if name already exists --- frappe/model/rename_doc.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 35fbf94dc6..2de1c51ea1 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -21,8 +21,15 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge) if old_title and new_title and not old_title == new_title: - frappe.db.set_value(doctype, docname, title_field, new_title) - frappe.msgprint(_('Saved'), alert=True, indicator='green') + try: + frappe.db.set_value(doctype, docname, title_field, new_title) + frappe.msgprint(_('Saved'), alert=True, indicator='green') + except Exception as e: + if frappe.db.is_duplicate_entry(e): + frappe.msgprint(_("{0} {1} already exists").format( + doctype, frappe.bold(docname)), title=_("Duplicate Name"), indicator="red" + ) + raise frappe.DuplicateEntryError(doctype, docname, e) return docname From 9a1d8ae6b20d587d2884e2b29a9abe5dbee5b05d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 25 Dec 2020 15:23:43 +0100 Subject: [PATCH 084/126] feat: redirect to Guest to login --- frappe/integrations/doctype/connected_app/connected_app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index a26f93f676..92b3977585 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from urllib.parse import urljoin +from urllib.parse import urlencode import frappe from frappe import _ @@ -105,7 +106,9 @@ def callback(code=None, state=None): frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - frappe.throw(_('Log in to access this page.'), frappe.PermissionError) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url}) + return path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: From f06961e001dd322f01f09caa6b551bf7b67c47db Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 25 Dec 2020 15:28:34 +0100 Subject: [PATCH 085/126] refactor: useless try except, better error messages --- .../doctype/connected_app/connected_app.py | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 92b3977585..0711be697f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -103,7 +103,7 @@ def callback(code=None, state=None): token. """ if frappe.request.method != 'GET': - frappe.throw(_('Invalid Method')) + frappe.throw(_('Invalid request method: {}').format(frappe.request.method)) if frappe.session.user == 'Guest': frappe.local.response['type'] = 'redirect' @@ -112,30 +112,23 @@ def callback(code=None, state=None): path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: - frappe.throw(_('Invalid Parameter(s)')) + frappe.throw(_('Invalid Parameters.')) - connected_app = path[3] - token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) - if not token_cache: - frappe.throw(_('State Not Found')) + connected_app = frappe.get_doc('Connected App', path[3]) + token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user) if state != token_cache.state: - frappe.throw(_('Invalid State')) + frappe.throw(_('Invalid state.')) - try: - app = frappe.get_doc('Connected App', connected_app) - except frappe.exceptions.DoesNotExistError: - frappe.throw(_('Invalid App')) - - oauth = app.get_oauth2_session(init=True) - query_params = app.get_query_params() - token = oauth.fetch_token(app.token_uri, + oauth_session = connected_app.get_oauth2_session(init=True) + query_params = connected_app.get_query_params() + token = oauth_session.fetch_token(connected_app.token_uri, code=code, - client_secret=app.get_password('client_secret'), + client_secret=connected_app.get_password('client_secret'), include_client_id=True, **query_params ) token_cache.update_data(token) frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = token_cache.get('success_uri') or app.get_url() + frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url() From 1d975f07e7b52ab3e8022023675f32759840d0bf Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Dec 2020 17:26:57 +0100 Subject: [PATCH 086/126] fix: allow insecure transport in travis --- frappe/integrations/doctype/connected_app/connected_app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 0711be697f..ec08f8e4be 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -2,7 +2,7 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals +import os from urllib.parse import urljoin from urllib.parse import urlencode @@ -11,9 +11,8 @@ from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -if frappe.conf.developer_mode or frappe.flags.in_test: +if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)): # Disable mandatory TLS in developer mode and tests - import os os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): From 4df9a203f0019dfd1faacaf1be5c47cb5b2eb073 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Dec 2020 14:14:29 +0530 Subject: [PATCH 087/126] feat: fetch email account signature in email dialog --- frappe/public/js/frappe/views/communication.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 29b21242af..fe14ad4793 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -464,6 +464,7 @@ frappe.views.CommunicationComposer = Class.extend({ }, send_action: function() { + debugger; var me = this; var btn = me.dialog.get_primary_btn(); @@ -625,10 +626,19 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - setup_earlier_reply: function() { + get_default_outgoing_email_account_signature: function() { + return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature'); + }, + + setup_earlier_reply: async function() { let fields = this.dialog.fields_dict; let signature = frappe.boot.user.email_signature || ""; + if (!signature) { + const res = await this.get_default_outgoing_email_account_signature(); + signature = res.message.signature; + } + if(!frappe.utils.is_html(signature)) { signature = signature.replace(/\n/g, "
"); } From ce0b243d9394fe25a23adab36776b294f76eaa20 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 29 Dec 2020 12:07:32 +0100 Subject: [PATCH 088/126] chore: flake8 allow long line --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 863175a028..9220c8b605 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") +pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") # noqa: E501 words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From a498751d8866e5dd9aaf165227b439926ab8b449 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 31 Dec 2020 16:42:19 +0530 Subject: [PATCH 089/126] fix: secondary button in dialog for website --- frappe/website/js/bootstrap-4.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/website/js/bootstrap-4.js b/frappe/website/js/bootstrap-4.js index dbe837b101..da720eedaf 100644 --- a/frappe/website/js/bootstrap-4.js +++ b/frappe/website/js/bootstrap-4.js @@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) { return false; }); -frappe.get_modal = function(title, content) { +frappe.get_modal = function (title, content) { return $( ` From 51d8046da15b6539f9badecdff506ccb32eee26b Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Mon, 4 Jan 2021 15:30:28 +0530 Subject: [PATCH 090/126] fix: use frappe.throw Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/model/rename_doc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 2de1c51ea1..15e044cc38 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -26,10 +26,11 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne frappe.msgprint(_('Saved'), alert=True, indicator='green') except Exception as e: if frappe.db.is_duplicate_entry(e): - frappe.msgprint(_("{0} {1} already exists").format( - doctype, frappe.bold(docname)), title=_("Duplicate Name"), indicator="red" + frappe.throw( + _("{0} {1} already exists").format(doctype, frappe.bold(docname)), + title=_("Duplicate Name"), + exc=frappe.DuplicateEntryError ) - raise frappe.DuplicateEntryError(doctype, docname, e) return docname From 1591cee1457b51d1d58765abd92afbd435b5cfac Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 6 Jan 2021 17:08:57 +0530 Subject: [PATCH 091/126] fix: remove debugger --- frappe/public/js/frappe/views/communication.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index fe14ad4793..e974cd52ed 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -464,7 +464,6 @@ frappe.views.CommunicationComposer = Class.extend({ }, send_action: function() { - debugger; var me = this; var btn = me.dialog.get_primary_btn(); @@ -719,4 +718,3 @@ frappe.views.CommunicationComposer = Class.extend({ return text.replace(/\n{3,}/g, '\n\n'); } }); - From aa2360e589368d58b909fc9f49ad9dd028990da4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 7 Jan 2021 14:46:32 +0530 Subject: [PATCH 092/126] fix: cannot refresh grid_row --- frappe/public/js/frappe/form/script_helpers.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js index 83ba191d4d..a0caae33e5 100644 --- a/frappe/public/js/frappe/form/script_helpers.js +++ b/frappe/public/js/frappe/form/script_helpers.js @@ -18,15 +18,17 @@ window.refresh_field = function(n, docname, table_field) { if (n && typeof n==='string' && table_field){ var grid = cur_frm.fields_dict[table_field].grid, - field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}); + field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}), + grid_row = grid.grid_rows_by_docname[docname]; + if (field && field.length){ field = field[0]; var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname); $.extend(field, meta); - if (docname){ - cur_frm.fields_dict[table_field].grid.grid_rows_by_docname[docname].refresh_field(n); + if (grid_row){ + grid_row.refresh_field(n); } else { - cur_frm.fields_dict[table_field].grid.refresh(); + grid.refresh(); } } } else if(cur_frm) { From 4806dcff32fa4f3187c05877659741bb7b67179c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 11 Jan 2021 14:26:05 +0530 Subject: [PATCH 093/126] fix: sider issues --- frappe/public/js/frappe/form/script_helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js index a0caae33e5..0465624975 100644 --- a/frappe/public/js/frappe/form/script_helpers.js +++ b/frappe/public/js/frappe/form/script_helpers.js @@ -16,16 +16,16 @@ window.refresh_field = function(n, docname, table_field) { if(typeof n==typeof []) refresh_many(n, docname, table_field); - if (n && typeof n==='string' && table_field){ + if (n && typeof n==='string' && table_field) { var grid = cur_frm.fields_dict[table_field].grid, field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}), grid_row = grid.grid_rows_by_docname[docname]; - if (field && field.length){ + if (field && field.length) { field = field[0]; var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname); $.extend(field, meta); - if (grid_row){ + if (grid_row) { grid_row.refresh_field(n); } else { grid.refresh(); From d2d905be140647d404f089e9abb97bcc55a1c97e Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 11 Jan 2021 14:48:33 +0530 Subject: [PATCH 094/126] fix: grid row index no longer dependant on doc index --- frappe/public/js/frappe/form/grid_row.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index ec9cee9c39..466032dbef 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -568,13 +568,15 @@ export default class GridRow { this.wrapper.removeClass("grid-row-open"); } open_prev() { - if(this.grid.grid_rows[this.doc.idx-2]) { - this.grid.grid_rows[this.doc.idx-2].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index - 1]) { + this.grid.grid_rows[row_index - 1].toggle_view(true); } } open_next() { - if(this.grid.grid_rows[this.doc.idx]) { - this.grid.grid_rows[this.doc.idx].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index + 1]) { + this.grid.grid_rows[row_index + 1].toggle_view(true); } else { this.grid.add_new_row(null, null, true); } From 0964f07ee47aad676236977fc7a2d5473c8a8ef4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 12 Jan 2021 09:25:52 +0530 Subject: [PATCH 095/126] fix: Auto Repeat JSON file not updated --- .../doctype/auto_repeat/auto_repeat.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 5ff4cbeead..74965346fd 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -23,7 +23,7 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", - "section_break_12", + "section_break_16", "repeat_on_days", "notification", "notify_by_email", @@ -198,20 +198,20 @@ "label": "Repeat on Days", "options": "Auto Repeat Day" }, - { - "depends_on": "eval:doc.frequency==='Weekly';", - "fieldname": "section_break_12", - "fieldtype": "Section Break" - }, { "default": "0", "fieldname": "submit_on_creation", "fieldtype": "Check", "label": "Submit on Creation" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_16", + "fieldtype": "Section Break" } ], "links": [], - "modified": "2020-12-10 10:43:13.449172", + "modified": "2021-01-12 09:24:49.719611", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", From 4f5002251de9b2f33feffe985a41586915ba1e99 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 Jan 2021 13:29:33 +0530 Subject: [PATCH 096/126] chore: hide 'did not cancel' message if exception is raised --- frappe/desk/form/save.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 5219a98cbd..50f9c984e4 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -42,7 +42,8 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat except Exception: frappe.errprint(frappe.utils.get_traceback()) - frappe.msgprint(frappe._("Did not cancel")) + if len(frappe.get_message_log()) == 0: + frappe.msgprint(frappe._("Did not cancel")) raise def send_updated_docs(doc): From 58eeefe993ba1e161639fb2b563cef921ffa44e9 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Tue, 12 Jan 2021 15:29:37 +0530 Subject: [PATCH 097/126] fix(email): error object is not json parseable --- frappe/email/doctype/email_account/email_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 343141c66d..ca4dbb83e2 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -210,7 +210,7 @@ class EmailAccount(Document): elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): self.throw_invalid_credentials_exception() else: - frappe.throw(e) + frappe.throw(cstr(e)) except socket.error: if in_receive: From a85842b6d115f1d7705c8c67a4b313f77a6e4d36 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 Jan 2021 16:02:15 +0530 Subject: [PATCH 098/126] fix: remove unwanted message --- frappe/desk/form/save.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 50f9c984e4..da43b14fce 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -42,8 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat except Exception: frappe.errprint(frappe.utils.get_traceback()) - if len(frappe.get_message_log()) == 0: - frappe.msgprint(frappe._("Did not cancel")) raise def send_updated_docs(doc): From 38d349d31fa13918af390cded85e749f8d34ea86 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 12 Jan 2021 23:09:42 +0530 Subject: [PATCH 099/126] fix: Center align hero title --- frappe/public/scss/page-builder.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index 24dbca3e21..1803e52cf7 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -29,11 +29,11 @@ } .hero.align-center { - h1, .hero-subtitle, .hero-buttons { + h1, .hero-title, .hero-subtitle, .hero-buttons { text-align: center; } - .hero-subtitle { + .hero-title, .hero-subtitle { margin-left: auto; margin-right: auto; } From 2efe5e9cdefc7c75c6eb304ed64fa9e5b7df38a2 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Wed, 13 Jan 2021 14:55:04 +0530 Subject: [PATCH 100/126] fix(desk): Correctly format and render Link fields This seems to be broken after https://github.com/frappe/frappe/pull/12063 https://github.com/saurabh6790/frappe/commit/00cc7df05e53f682a64a53b1479dd3140164a202 --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 2b8956653b..be3f10fd0c 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -130,7 +130,7 @@ frappe.form.formatters = { } else if(docfield && doctype) { if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) { return ` ${__(options && options.label || value)}`; From 64f3887dce25da4d93d9bc2b0eccc4f04a744ce2 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Wed, 13 Jan 2021 16:00:23 +0530 Subject: [PATCH 101/126] fix: html download of auto download report broken --- .../doctype/auto_email_report/auto_email_report.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 539f6c9db8..de27fafee3 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -81,7 +81,7 @@ class AutoEmailReport(Document): if self.format == 'HTML': columns, data = make_links(columns, data) - + columns = update_field_types(columns) return self.get_html_table(columns, data) elif self.format == 'XLSX': @@ -236,5 +236,14 @@ def make_links(columns, data): elif col.fieldtype == "Dynamic Link": if col.options and row.get(col.fieldname) and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) + elif col.fieldtype == "Currency": + row[col.fieldname] = frappe.format_value(row[col.fieldname], col) return columns, data + +def update_field_types(columns): + for col in columns: + if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency": + col.fieldtype = "Data" + col.options = "" + return columns \ No newline at end of file From 8ac66bbb37b784c95fc5b25223a3ba613e9accf7 Mon Sep 17 00:00:00 2001 From: UrvashiKishnani <41088003+UrvashiKishnani@users.noreply.github.com> Date: Wed, 13 Jan 2021 17:52:08 +0400 Subject: [PATCH 102/126] fix: removed keyboard smash (#12186) --- frappe/core/doctype/data_import/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 7880648b6f..dde3dfaee9 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -751,7 +751,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "message": _("{0} is a mandatory field asdadsf").format(id_field.label), + "message": _("{0} is a mandatory field").format(id_field.label), } ) return From 26e40a9d68910a59b6313b25b83660cf6d10b921 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 13 Jan 2021 19:24:24 +0530 Subject: [PATCH 103/126] fix: data.non_standard_fieldnames is None (#12174) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/model/meta.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 53fcadce42..88ed1a7e78 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -484,6 +484,8 @@ class Meta(Document): if not data.transactions: # init groups data.transactions = [] + + if not data.non_standard_fieldnames: data.non_standard_fieldnames = {} for link in dashboard_links: From f619ef4a5b856666582e8ab7254e8f7d3fcf9652 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 13 Jan 2021 19:32:31 +0530 Subject: [PATCH 104/126] fix: Skip "Attach" from auto image render Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- frappe/templates/print_formats/standard_macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 9a14b860ff..7a0dce7f5e 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -5,7 +5,7 @@
{{ frappe.render_template(df.options, {"doc": doc}) or "" }}
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} {{ render_text_field(df, doc) }} - {%- elif df.fieldtype in ("Image", "Attach Image", "Attach") + {%- elif df.fieldtype in ("Image", "Attach Image") and ( (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") or doc[df.fieldname].startswith("http") From 00896f36fefd2d464c069252b7869ab8c9c0bff3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 13 Jan 2021 20:46:27 +0530 Subject: [PATCH 105/126] revert: "fix: improve translation pattern" (#12205) --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 9220c8b605..340f4f8772 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") # noqa: E501 +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From f23d88be5e0209cb7e4cf60aed1ca217f1f3065a Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 15 Jan 2021 13:48:53 +0530 Subject: [PATCH 106/126] fix: Remove whitespace from testimonial text --- frappe/website/web_template/testimonial/testimonial.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/website/web_template/testimonial/testimonial.html b/frappe/website/web_template/testimonial/testimonial.html index b656d3b03d..f860abbae6 100644 --- a/frappe/website/web_template/testimonial/testimonial.html +++ b/frappe/website/web_template/testimonial/testimonial.html @@ -5,9 +5,7 @@ {% endif %}
- - {{ content }} - + “{{ content }}”
{{ name }} From cd693d5a17e5e90668f53ca8e4caccab417c9a45 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Mon, 18 Jan 2021 17:45:14 +0530 Subject: [PATCH 107/126] fix: hide theme url --- frappe/website/doctype/website_theme/website_theme.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index 78c3c696e9..ee4b33d854 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -65,8 +65,10 @@ }, { "fieldname": "theme_url", - "fieldtype": "Read Only", - "label": "Theme URL" + "fieldtype": "Data", + "hidden": 1, + "label": "Theme URL", + "read_only": 1 }, { "collapsible": 1, @@ -179,7 +181,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-24 11:42:33.867840", + "modified": "2021-01-18 17:43:39.804765", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", From 962fff15c4a350362edeedabb5cf2a452972f045 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 18 Jan 2021 20:49:09 +0530 Subject: [PATCH 108/126] Link Field Validation doesn't use Filter criteria defined for link field In link field if we enter invalid i.e not available record it will clear field but if we enter valid record that is not available in filter but available in db than it will accept that record --- frappe/public/js/frappe/form/controls/link.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 4c0fe39f60..019ea5ca58 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -455,6 +455,11 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } + // check if value exist in the filtered dropdown values + if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ + value = "" + } + return frappe.call({ method:'frappe.desk.form.utils.validate_link', type: "GET", From ebcb8438de691eb085528912253a0b17ee03dee9 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 18 Jan 2021 21:45:07 +0530 Subject: [PATCH 109/126] fix: semantic commit and sider issues --- frappe/public/js/frappe/form/controls/link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 019ea5ca58..6ac10c8534 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,8 +456,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ - value = "" + if (this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)) { + value = ""; } return frappe.call({ From b9f2f5bc22cdafd735011ab109b3fe376cbef29e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:41:11 +0100 Subject: [PATCH 110/126] test: fix connected app --- .../connected_app/test_connected_app.py | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 4d8acb9b59..af5a5d8e3c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -8,9 +8,50 @@ import requests from urllib.parse import urljoin import frappe +from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -test_dependencies = ['Connected App', 'OAuth Client', 'User'] + +def get_user(usr, pwd): + user = frappe.new_doc('User') + user.email = usr + user.enabled = 1 + user.first_name = "_Test" + user.new_password = pwd + user.roles = [] + user.append('roles', { + 'doctype': 'Has Role', + 'parentfield': 'roles', + 'role': 'System Manager' + }) + user.insert() + + return user + + +def get_connected_app(): + doctype = 'Connected App' + connected_app = frappe.new_doc(doctype) + connected_app.provider_name = 'frappe' + connected_app.scopes = [] + connected_app.append('scopes', {'scope': 'all'}) + connected_app.insert() + + return connected_app + + +def get_oauth_client(): + oauth_client = frappe.new_doc('OAuth Client') + oauth_client.app_name = '_Test Connected App' + oauth_client.redirect_uris = 'to be replaced' + oauth_client.default_redirect_uri = 'to be replaced' + oauth_client.grant_type = 'Authorization Code' + oauth_client.response_type = 'Code' + oauth_client.skip_authorization = 1 + oauth_client.insert() + + return oauth_client + class TestConnectedApp(unittest.TestCase): @@ -26,37 +67,47 @@ class TestConnectedApp(unittest.TestCase): just endpoints) are stored in "Social Login Key" so we get them from there. """ - self.user_name = 'test@example.com' + self.user_name = 'test-connected-app@example.com' self.user_password = 'Eastern_43A1W' - connected_app = frappe.get_last_doc('Connected App') - redirect_uri = connected_app.get('redirect_uri') - - web_application_client = frappe.get_last_doc('OAuth Client') - web_application_client.update({ - 'redirect_uris': redirect_uri, - 'default_redirect_uri': redirect_uri - }) - web_application_client.save() - + self.user = get_user(self.user_name, self.user_password) + self.connected_app = get_connected_app() + self.oauth_client = get_oauth_client() social_login_key = create_or_update_social_login_key() self.base_url = social_login_key.get('base_url') - connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) - connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) - connected_app.client_id = web_application_client.get('client_id') - connected_app.client_secret = web_application_client.get('client_secret') - self.connected_app = connected_app.save() + frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + redirect_uri = self.connected_app.get('redirect_uri') + self.oauth_client.update({ + 'redirect_uris': redirect_uri, + 'default_redirect_uri': redirect_uri + }) + self.oauth_client.save() + + self.connected_app.update({ + 'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')), + 'client_id': self.oauth_client.get('client_id'), + 'client_secret': self.oauth_client.get('client_secret'), + 'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url')) + }) + self.connected_app.save() frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" session = requests.Session() - session.post(urljoin(self.base_url, '/api/method/login'), data={ + login_response = session.post(urljoin(self.base_url, '/api/method/login'), data={ 'usr': self.user_name, 'pwd': self.user_password }) + self.assertEqual(login_response.status_code, 200) + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) auth_response = session.get(authorization_url) @@ -65,10 +116,39 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.connected_app.get_token_cache(self.user_name) - token = token_cache.get_password('access_token') + self.token_cache = self.connected_app.get_token_cache(self.user_name) + token = self.token_cache.get_password('access_token') self.assertNotEqual(token, None) oauth2_session = self.connected_app.get_oauth2_session(self.user_name) resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) + + def tearDown(self): + def delete_if_exists(attribute): + doc = getattr(self, attribute, None) + if doc: + doc.delete() + + delete_if_exists('token_cache') + delete_if_exists('connected_app') + + if getattr(self, 'oauth_client', None): + tokens = frappe.get_all('OAuth Bearer Token', filters={ + 'client': self.oauth_client.name + }) + for token in tokens: + doc = frappe.get_doc('OAuth Bearer Token', token.name) + doc.delete() + + codes = frappe.get_all('OAuth Authorization Code', filters={ + 'client': self.oauth_client.name + }) + for code in codes: + doc = frappe.get_doc('OAuth Authorization Code', code.name) + doc.delete() + + delete_if_exists('user') + delete_if_exists('oauth_client') + + frappe.db.commit() From aa9a8b2f1b3f319ef943260f06d4befc606c1546 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:46:55 +0100 Subject: [PATCH 111/126] fix: remove unused import --- frappe/integrations/doctype/connected_app/test_connected_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index af5a5d8e3c..e2c229f26c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -8,7 +8,6 @@ import requests from urllib.parse import urljoin import frappe -from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key From 7fd4a114a17707b0349696f0d1de43842deb6764 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:53:35 +0100 Subject: [PATCH 112/126] test: use get for login --- frappe/integrations/doctype/connected_app/test_connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index e2c229f26c..324600d76e 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -101,7 +101,7 @@ class TestConnectedApp(unittest.TestCase): def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" session = requests.Session() - login_response = session.post(urljoin(self.base_url, '/api/method/login'), data={ + login_response = session.get(urljoin(self.base_url, '/api/method/login'), params={ 'usr': self.user_name, 'pwd': self.user_password }) From a0678a4d5f96093f452c68e72833d3a51d5080ae Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 19:27:05 +0100 Subject: [PATCH 113/126] test: login twice --- .../connected_app/test_connected_app.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 324600d76e..6faa542a60 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -100,12 +100,21 @@ class TestConnectedApp(unittest.TestCase): def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" + def login(): + return session.get(urljoin(self.base_url, '/api/method/login'), params={ + 'usr': self.user_name, + 'pwd': self.user_password + }) + session = requests.Session() - login_response = session.get(urljoin(self.base_url, '/api/method/login'), params={ - 'usr': self.user_name, - 'pwd': self.user_password - }) - self.assertEqual(login_response.status_code, 200) + + # first login of a new user on a new site fails with "401 UNAUTHORIZED" + # when anybody fixes that, the two lines below can be removed + first_login = login() + self.assertEqual(first_login.status_code, 401) + + second_login = login() + self.assertEqual(second_login.status_code, 200) authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) From 83822b14c6e6aded305c00fefee679833c4ee2ab Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 14:07:19 +0530 Subject: [PATCH 114/126] fix: Updated if condition --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 6ac10c8534..77716ee60a 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,7 +456,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if (this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)) { + if (this.$input.cache[doctype] && !this.$input.cache[doctype][""].some(d => d.value === value)) { value = ""; } From bcae1cc294fac0aeeb90f68d57f7db1f673865aa Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:29:23 +0530 Subject: [PATCH 115/126] revert --- frappe/public/js/frappe/form/controls/link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 77716ee60a..019ea5ca58 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,8 +456,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if (this.$input.cache[doctype] && !this.$input.cache[doctype][""].some(d => d.value === value)) { - value = ""; + if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ + value = "" } return frappe.call({ From 099474a441f922fab40e8ac1aa1fc26c851b7344 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:29:30 +0530 Subject: [PATCH 116/126] Revert "Link Field Validation doesn't use Filter criteria defined for link field" This reverts commit 962fff15c4a350362edeedabb5cf2a452972f045. --- frappe/public/js/frappe/form/controls/link.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 019ea5ca58..4c0fe39f60 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -455,11 +455,6 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } - // check if value exist in the filtered dropdown values - if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ - value = "" - } - return frappe.call({ method:'frappe.desk.form.utils.validate_link', type: "GET", From bddb7034cfe67f3669c1a97b915621b03cab422d Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:36:33 +0530 Subject: [PATCH 117/126] fix: Geolocation field with Column Break & Section Break (Collapsible) does not seems to work. --- frappe/public/js/frappe/form/controls/geolocation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 9e4d1d82ec..dfd0f4d174 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -17,7 +17,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ this.map_area.prependTo($input_wrapper); this.$wrapper.find('.control-input').addClass("hidden"); - if ($input_wrapper.is(':visible')) { + if (this.frm) { this.make_map(); } else { $(document).on('frappe.ui.Dialog:shown', () => { From 9591d01c2c2458b459e132d0cd28d3f777cf865c Mon Sep 17 00:00:00 2001 From: robert kimutai Date: Wed, 20 Jan 2021 12:52:12 +0300 Subject: [PATCH 118/126] chore: Update CONTRIBUTING.md (#12241) --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e1f16970fe..5be3a87884 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos ### General Issue Guidelines 1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. -2. **Report each issue separately:** Don't club multiple, unreleated issues in one note. +2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. 3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. ### Bug Report Guidelines From f47d2c32b144dc19ddac2a273644d973c2895561 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 20 Jan 2021 18:44:18 +0100 Subject: [PATCH 119/126] feat: Add translation context (#12043) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/form/controls/code.js | 9 ++++++--- frappe/public/js/frappe/form/sidebar/form_sidebar.js | 4 ++-- frappe/public/js/frappe/utils/user.js | 2 +- frappe/public/js/frappe/widgets/onboarding_widget.js | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index f3c51e0232..6df7094c26 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -10,7 +10,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ .appendTo(this.input_area); this.expanded = false; - this.$expand_button = $(``).click(() => { + this.$expand_button = $(``).click(() => { this.expanded = !this.expanded; this.refresh_height(); this.toggle_label(); @@ -38,8 +38,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, toggle_label() { - const button_label = this.expanded ? __('Collapse') : __('Expand'); - this.$expand_button && this.$expand_button.text(button_label); + this.$expand_button && this.$expand_button.text(this.get_button_label()); + }, + + get_button_label() { + return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.'); }, set_language() { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index eab09c1e10..eb70b255eb 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -99,7 +99,7 @@ frappe.ui.form.Sidebar = class { __("{0} edited this {1}", [ frappe.user.full_name(this.frm.doc.modified_by).bold(), "
" + comment_when(this.frm.doc.modified), - ]) + ], "For example, 'Jon Doe edited this 5 minutes ago'.") ); this.sidebar .find(".created-by") @@ -107,7 +107,7 @@ frappe.ui.form.Sidebar = class { __("{0} created this {1}", [ frappe.user.full_name(this.frm.doc.owner).bold(), "
" + comment_when(this.frm.doc.creation), - ]) + ], "For example, 'Jon Doe created this 5 minutes ago'.") ); this.refresh_like(); diff --git a/frappe/public/js/frappe/utils/user.js b/frappe/public/js/frappe/utils/user.js index 311f208750..64db23d306 100644 --- a/frappe/public/js/frappe/utils/user.js +++ b/frappe/public/js/frappe/utils/user.js @@ -55,7 +55,7 @@ $.extend(frappe.user, { name: 'Guest', full_name: function(uid) { return uid === frappe.session.user ? - __("You") : + __("You", null, "Name of the current user. For example: You edited this 5 hours ago.") : frappe.user_info(uid).fullname; }, image: function(uid) { diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index abacc6f354..8ef003cc67 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -444,7 +444,7 @@ export default class OnboardingWidget extends Widget { set_actions() { this.action_area.empty(); const dismiss = $( - `
${__('Dismiss')}
` + `
${__('Dismiss', null, 'Stop showing the onboarding widget.')}
` ); dismiss.on("click", () => { let dismissed = JSON.parse( From 19c6e0218db9b1dd95132693a96a8174fac2dc94 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 20 Jan 2021 22:57:25 +0000 Subject: [PATCH 120/126] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-PYYAML-590151 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3cc92264a2..e128790e45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ pypng==0.0.20 PyQRCode==1.2.1 python-dateutil==2.8.1 pytz==2019.3 -PyYAML==5.3.1 +PyYAML==5.4 rauth==0.7.3 redis==3.5.3 requests-oauthlib==1.3.0 From 33ea496a8bd5953714941cfde4d2c1da1982c0b3 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Jan 2021 13:12:42 +0530 Subject: [PATCH 121/126] feat: Added get_datetime_in_timezone in frappe.utils to get datetime in specific timezones * Added util in safe_exec to access via Server Scripts and System Console --- frappe/utils/data.py | 12 ++++++++++-- frappe/utils/safe_exec.py | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index c24b9f186e..c60e64b015 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -154,14 +154,22 @@ def get_time_zone(): return frappe.cache().get_value("time_zone", _get_time_zone) -def convert_utc_to_user_timezone(utc_timestamp): +def convert_utc_to_timezone(utc_timestamp, time_zone): from pytz import timezone, UnknownTimeZoneError utcnow = timezone('UTC').localize(utc_timestamp) try: - return utcnow.astimezone(timezone(get_time_zone())) + return utcnow.astimezone(timezone(time_zone)) except UnknownTimeZoneError: return utcnow +def get_datetime_in_timezone(time_zone): + utc_timestamp = datetime.datetime.utcnow() + return convert_utc_to_timezone(utc_timestamp, time_zone) + +def convert_utc_to_user_timezone(utc_timestamp): + time_zone = get_time_zone() + return convert_utc_to_timezone(utc_timestamp, time_zone) + def now(): """return current datetime as yyyy-mm-dd hh:mm:ss""" if frappe.flags.current_date: diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2aacf5eda8..06a192c05e 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -222,6 +222,7 @@ VALID_UTILS = ( "get_last_day_of_week", "get_last_day", "get_time", +"get_datetime_in_timezone", "get_datetime_str", "get_date_str", "get_time_str", From 16f2b29cb3c1395e8fbaae50d3076b410b4555b7 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Jan 2021 13:15:14 +0530 Subject: [PATCH 122/126] style: Trim extra whitespace --- frappe/utils/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index c60e64b015..da2c910e20 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -377,7 +377,7 @@ def format_duration(seconds, hide_days=False): example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float """ - + seconds = cint(seconds) total_duration = { From 5c9cc655cfd9b09a40a7f8b5ba555b94899a8225 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Jan 2021 11:50:04 +0530 Subject: [PATCH 123/126] fix: Insufficient Permission for Leads > Dashboard Chart (#12243) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 2fa36b5514..b19f6cf9f0 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -73,7 +73,7 @@ def has_permission(doc, ptype, user): if doc.report_name in allowed_reports: return True else: - allowed_doctypes = [frappe.permissions.get_doctypes_with_read()] + allowed_doctypes = frappe.permissions.get_doctypes_with_read() if doc.document_type in allowed_doctypes: return True From 9b59b59e44211812fe24443ba66cfd56fd9e02cf Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 22 Jan 2021 13:13:50 +0530 Subject: [PATCH 124/126] test: Update TestNewsletter.test_unsubscribe * Update selecting email to unsubscribe email logic * style fixes --- .../doctype/newsletter/test_newsletter.py | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index ee7f123b7e..bd8fadc29c 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -2,58 +2,66 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, unittest -from frappe.utils import getdate, add_days +import unittest +from random import choice -from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email -from six.moves.urllib.parse import unquote +import frappe +from frappe.email.doctype.newsletter.newsletter import ( + confirmed_unsubscribe, + send_scheduled_email, +) +from frappe.email.doctype.newsletter.newsletter import get_newsletter_list +from frappe.email.queue import flush +from frappe.utils import add_days, getdate test_dependencies = ["Email Group"] +emails = [ + "test_subscriber1@example.com", + "test_subscriber2@example.com", + "test_subscriber3@example.com", + "test1@example.com", +] -emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", - "test_subscriber3@example.com", "test1@example.com"] class TestNewsletter(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") - frappe.db.sql('delete from `tabEmail Group Member`') + frappe.db.sql("delete from `tabEmail Group Member`") + + if not frappe.db.exists("Email Group", "_Test Email Group"): + frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert() - group_exist=frappe.db.exists("Email Group", "_Test Email Group") - if len(group_exist) == 0: - frappe.get_doc({ - "doctype": "Email Group", - "title": "_Test Email Group" - }).insert() for email in emails: - frappe.get_doc({ - "doctype": "Email Group Member", - "email": email, - "email_group": "_Test Email Group" - }).insert() + frappe.get_doc({ + "doctype": "Email Group Member", + "email": email, + "email_group": "_Test Email Group" + }).insert() def test_send(self): - name = self.send_newsletter() + self.send_newsletter() - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] self.assertEqual(len(email_queue_list), 4) - recipients = [e.recipients[0].recipient for e in email_queue_list] - for email in emails: - self.assertTrue(email in recipients) + + recipients = set([e.recipients[0].recipient for e in email_queue_list]) + self.assertTrue(set(emails).issubset(recipients)) def test_unsubscribe(self): - # test unsubscribe name = self.send_newsletter() - from frappe.email.queue import flush + to_unsubscribe = choice(emails) + group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"]) + flush(from_test=True) - to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0]) - group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"]) confirmed_unsubscribe(to_unsubscribe, group[0].email_group) name = self.send_newsletter() - - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [ + frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue") + ] self.assertEqual(len(email_queue_list), 3) recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: if email != to_unsubscribe: self.assertTrue(email in recipients) @@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase): def test_portal(self): self.send_newsletter(1) frappe.set_user("test1@example.com") - from frappe.email.doctype.newsletter.newsletter import get_newsletter_list newsletters = get_newsletter_list("Newsletter", None, None, 0) self.assertEqual(len(newsletters), 1) @@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase): self.assertEqual(len(email_queue_list), 4) recipients = [e.recipients[0].recipient for e in email_queue_list] for email in emails: - self.assertTrue(email in recipients) \ No newline at end of file + self.assertTrue(email in recipients) From a2fba77116609c089f281ee92c15cccb13c0e610 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:37:37 +0530 Subject: [PATCH 125/126] fix: Skip translation of gender while creating it (#12260) --- frappe/desk/page/setup_wizard/install_fixtures.py | 4 ++-- frappe/utils/oauth.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 60e1f3242a..6d3aaee22b 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -18,14 +18,14 @@ def install(): @frappe.whitelist() def update_genders(): - default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")] + default_genders = ["Male", "Female", "Other","Transgender", "Genderqueer", "Non-Conforming","Prefer not to say"] records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] for record in records: frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) @frappe.whitelist() def update_salutations(): - default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] + default_salutations = ["Mr", "Ms", 'Mx', "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] for record in records: doc = frappe.new_doc(record.get("doctype")) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index d090aabffc..e7672cedb3 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -230,12 +230,19 @@ def update_oauth_user(user, data, provider): save = True user = frappe.new_doc("User") + + gender = (data.get("gender") or "").title() + + if not frappe.db.exists("Gender", gender): + doc = frappe.new_doc("Gender", {"gender": gender}) + doc.insert(ignore_permissions=True) + user.update({ "doctype":"User", "first_name": get_first_name(data), "last_name": get_last_name(data), "email": get_email(data), - "gender": (data.get("gender") or "").title(), + "gender": gender, "enabled": 1, "new_password": frappe.generate_hash(get_email(data)), "location": data.get("location"), From a4396a83d080e7b07d3fca67159025c981f64562 Mon Sep 17 00:00:00 2001 From: Pierre-Louis Bonicoli Date: Mon, 25 Jan 2021 11:06:02 +0100 Subject: [PATCH 126/126] ci: fix a regex used in Travis config Fix this line in Procfile: redis_# socketio: redis-server config/redis_socketio.conf --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2331217363..23fb525138 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,11 +104,11 @@ install: - cd ./frappe-bench - - sed -i 's/watch:/# watch:/g' Procfile - - sed -i 's/schedule:/# schedule:/g' Procfile + - sed -i 's/^watch:/# watch:/g' Procfile + - sed -i 's/^schedule:/# schedule:/g' Procfile - - if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi - - if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi