Repository: frappe/frappe

Issue:
deny_multiple_sessions doesn't let anyone login (develop)
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, use https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->

## Description of the issue

Setting the `deny_multiple_sessions` key in the site config or the corresponding field in System Settings logs out essentially everyone when they try to login again. And keeps them out. This happens due to the recent query refactor via https://github.com/frappe/frappe/commit/012edfe090006303897ca6a28c0f09412ec18f97. The query constructed now has triple quotes around the sid while checking for sid matches or during deletions. Thus, no match takes place and all sids are cleared out each time, even the new one!

https://github.com/frappe/frappe/blob/f785b02a73fa08c3aeefb90d28f953044edd4451/frappe/sessions.py#L66-L69

This could be fixed by getting rid of the `frappe.db.escape` but I'm not sure that's safe.

## Steps to reproduce the issue

1. Turn on Deny Multiple Sessions
2. Try to login

### Observed result

Login Success => Session Ended

### Expected result

Login Success => Session Started


Discussion:
A pypika issue I found might be something that we are trying to do
https://github.com/kayak/pypika/issues/3

Resolving patch:
diff --git a/frappe/sessions.py b/frappe/sessions.py
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -68,9 +68,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None):
 	session = DocType("Sessions")
 	session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device)))
 	if keep_current:
-		session_id = session_id.where(session.sid != frappe.db.escape(frappe.session.sid))
-
-	query = session_id.select(session.sid).offset(offset).limit(100).orderby(session.lastupdate, order=Order.desc)
+		session_id = session_id.where(session.sid != frappe.session.sid)
+
+	query = (
+		session_id.select(session.sid)
+		.offset(offset)
+		.limit(100)
+		.orderby(session.lastupdate, order=Order.desc)
+	)
 
 	return query.run(pluck=True)
 


Test patch:
diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py
--- a/frappe/tests/test_auth.py
+++ b/frappe/tests/test_auth.py
@@ -4,38 +4,44 @@
 import unittest
 
 import frappe
-from frappe.auth import HTTPRequest, LoginAttemptTracker
+import frappe.utils
+from frappe.auth import LoginAttemptTracker
 from frappe.frappeclient import FrappeClient, AuthError
-from frappe.utils import set_request
+
+
+def add_user(email, password, username=None, mobile_no=None):
+	first_name = email.split('@', 1)[0]
+	user = frappe.get_doc(
+			dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no)
+		).insert()
+	user.new_password = password
+	user.add_roles("System Manager")
+	frappe.db.commit()
+
 
 class TestAuth(unittest.TestCase):
-	def __init__(self, *args, **kwargs):
-		super(TestAuth, self).__init__(*args, **kwargs)
-		self.test_user_email = 'test_auth@test.com'
-		self.test_user_name = 'test_auth_user'
-		self.test_user_mobile = '+911234567890'
-		self.test_user_password = 'pwd_012'
-
-	def setUp(self):
-		self.tearDown()
-
-		self.add_user(self.test_user_email, self.test_user_password,
-			username=self.test_user_name, mobile_no=self.test_user_mobile)
-
-	def tearDown(self):
-		frappe.delete_doc('User', self.test_user_email, force=True)
-
-	def add_user(self, email, password, username=None, mobile_no=None):
-		first_name = email.split('@', 1)[0]
-		user = frappe.get_doc(
-				dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no)
-			).insert()
-		user.new_password = password
-		user.save()
-		frappe.db.commit()
+	@classmethod
+	def setUpClass(cls):
+		cls.HOST_NAME = (
+			frappe.get_site_config().host_name
+			or frappe.utils.get_site_url(frappe.local.site)
+		)
+		cls.test_user_email = 'test_auth@test.com'
+		cls.test_user_name = 'test_auth_user'
+		cls.test_user_mobile = '+911234567890'
+		cls.test_user_password = 'pwd_012'
+
+		cls.tearDownClass()
+		add_user(email=cls.test_user_email, password=cls.test_user_password,
+			username=cls.test_user_name, mobile_no=cls.test_user_mobile)
+
+	@classmethod
+	def tearDownClass(cls):
+		frappe.delete_doc('User', cls.test_user_email, force=True)
 
 	def set_system_settings(self, k, v):
 		frappe.db.set_value("System Settings", "System Settings", k, v)
+		frappe.clear_cache()
 		frappe.db.commit()
 
 	def test_allow_login_using_mobile(self):
@@ -43,12 +49,12 @@ def test_allow_login_using_mobile(self):
 		self.set_system_settings('allow_login_using_user_name', 0)
 
 		# Login by both email and mobile should work
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
 
 		# login by username should fail
 		with self.assertRaises(AuthError):
-			FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
+			FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
 
 	def test_allow_login_using_only_email(self):
 		self.set_system_settings('allow_login_using_mobile_number', 0)
@@ -56,14 +62,14 @@ def test_allow_login_using_only_email(self):
 
 		# Login by mobile number should fail
 		with self.assertRaises(AuthError):
-			FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
+			FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
 
 		# login by username should fail
 		with self.assertRaises(AuthError):
-			FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
+			FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
 
 		# Login by email should work
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
 
 	def test_allow_login_using_username(self):
 		self.set_system_settings('allow_login_using_mobile_number', 0)
@@ -71,20 +77,39 @@ def test_allow_login_using_username(self):
 
 		# Mobile login should fail
 		with self.assertRaises(AuthError):
-			FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
+			FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
 
 		# Both email and username logins should work
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
 
 	def test_allow_login_using_username_and_mobile(self):
 		self.set_system_settings('allow_login_using_mobile_number', 1)
 		self.set_system_settings('allow_login_using_user_name', 1)
 
 		# Both email and username and mobile logins should work
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
-		FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
+		FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
+
+	def test_deny_multiple_login(self):
+		self.set_system_settings('deny_multiple_sessions', 1)
+
+		first_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
+		first_login.get_list("ToDo")
+
+		second_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
+		second_login.get_list("ToDo")
+		with self.assertRaises(Exception):
+			first_login.get_list("ToDo")
+
+		third_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
+		with self.assertRaises(Exception):
+			first_login.get_list("ToDo")
+		with self.assertRaises(Exception):
+			second_login.get_list("ToDo")
+		third_login.get_list("ToDo")
+
 
 class TestLoginAttemptTracker(unittest.TestCase):
 	def test_account_lock(self):