Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 
 

571 righe
17 KiB

  1. # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See LICENSE
  3. from urllib.parse import quote
  4. import frappe
  5. import frappe.database
  6. import frappe.utils
  7. import frappe.utils.user
  8. from frappe import _, conf
  9. from frappe.core.doctype.activity_log.activity_log import add_authentication_log
  10. from frappe.modules.patch_handler import check_session_stopped
  11. from frappe.sessions import Session, clear_sessions, delete_session
  12. from frappe.translate import get_language
  13. from frappe.twofactor import (
  14. authenticate_for_2factor,
  15. confirm_otp_token,
  16. get_cached_user_pass,
  17. should_run_2fa,
  18. )
  19. from frappe.utils import cint, date_diff, datetime, get_datetime, today
  20. from frappe.utils.password import check_password
  21. from frappe.website.utils import get_home_page
  22. class HTTPRequest:
  23. def __init__(self):
  24. # set frappe.local.request_ip
  25. self.set_request_ip()
  26. # load cookies
  27. self.set_cookies()
  28. # set frappe.local.db
  29. self.connect()
  30. # login and start/resume user session
  31. self.set_session()
  32. # set request language
  33. self.set_lang()
  34. # match csrf token from current session
  35. self.validate_csrf_token()
  36. # write out latest cookies
  37. frappe.local.cookie_manager.init_cookies()
  38. # check session status
  39. check_session_stopped()
  40. @property
  41. def domain(self):
  42. if not getattr(self, "_domain", None):
  43. self._domain = frappe.request.host
  44. if self._domain and self._domain.startswith("www."):
  45. self._domain = self._domain[4:]
  46. return self._domain
  47. def set_request_ip(self):
  48. if frappe.get_request_header("X-Forwarded-For"):
  49. frappe.local.request_ip = (frappe.get_request_header("X-Forwarded-For").split(",")[0]).strip()
  50. elif frappe.get_request_header("REMOTE_ADDR"):
  51. frappe.local.request_ip = frappe.get_request_header("REMOTE_ADDR")
  52. else:
  53. frappe.local.request_ip = "127.0.0.1"
  54. def set_cookies(self):
  55. frappe.local.cookie_manager = CookieManager()
  56. def set_session(self):
  57. frappe.local.login_manager = LoginManager()
  58. def validate_csrf_token(self):
  59. if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"):
  60. if not frappe.local.session:
  61. return
  62. if (
  63. not frappe.local.session.data.csrf_token
  64. or frappe.local.session.data.device == "mobile"
  65. or frappe.conf.get("ignore_csrf", None)
  66. ):
  67. # not via boot
  68. return
  69. csrf_token = frappe.get_request_header("X-Frappe-CSRF-Token")
  70. if not csrf_token and "csrf_token" in frappe.local.form_dict:
  71. csrf_token = frappe.local.form_dict.csrf_token
  72. del frappe.local.form_dict["csrf_token"]
  73. if frappe.local.session.data.csrf_token != csrf_token:
  74. frappe.local.flags.disable_traceback = True
  75. frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)
  76. def set_lang(self):
  77. frappe.local.lang = get_language()
  78. def get_db_name(self):
  79. """get database name from conf"""
  80. return conf.db_name
  81. def connect(self):
  82. """connect to db, from ac_name or db_name"""
  83. frappe.local.db = frappe.database.get_db(
  84. user=self.get_db_name(), password=getattr(conf, "db_password", "")
  85. )
  86. class LoginManager:
  87. __slots__ = ("user", "info", "full_name", "user_type", "resume")
  88. def __init__(self):
  89. self.user = None
  90. self.info = None
  91. self.full_name = None
  92. self.user_type = None
  93. if (
  94. frappe.local.form_dict.get("cmd") == "login" or frappe.local.request.path == "/api/method/login"
  95. ):
  96. if self.login() is False:
  97. return
  98. self.resume = False
  99. # run login triggers
  100. self.run_trigger("on_session_creation")
  101. else:
  102. try:
  103. self.resume = True
  104. self.make_session(resume=True)
  105. self.get_user_info()
  106. self.set_user_info(resume=True)
  107. except AttributeError:
  108. self.user = "Guest"
  109. self.get_user_info()
  110. self.make_session()
  111. self.set_user_info()
  112. def login(self):
  113. if frappe.get_system_settings("disable_user_pass_login"):
  114. frappe.throw(_("Login with username and password is not allowed."), frappe.AuthenticationError)
  115. # clear cache
  116. frappe.clear_cache(user=frappe.form_dict.get("usr"))
  117. user, pwd = get_cached_user_pass()
  118. self.authenticate(user=user, pwd=pwd)
  119. if self.force_user_to_reset_password():
  120. doc = frappe.get_doc("User", self.user)
  121. frappe.local.response["redirect_to"] = doc.reset_password(
  122. send_email=False, password_expired=True
  123. )
  124. frappe.local.response["message"] = "Password Reset"
  125. return False
  126. if should_run_2fa(self.user):
  127. authenticate_for_2factor(self.user)
  128. if not confirm_otp_token(self):
  129. return False
  130. frappe.form_dict.pop("pwd", None)
  131. self.post_login()
  132. def post_login(self):
  133. self.run_trigger("on_login")
  134. validate_ip_address(self.user)
  135. self.validate_hour()
  136. self.get_user_info()
  137. self.make_session()
  138. self.setup_boot_cache()
  139. self.set_user_info()
  140. def get_user_info(self):
  141. self.info = frappe.get_cached_value(
  142. "User", self.user, ["user_type", "first_name", "last_name", "user_image"], as_dict=1
  143. )
  144. self.user_type = self.info.user_type
  145. def setup_boot_cache(self):
  146. frappe.cache_manager.build_table_count_cache()
  147. frappe.cache_manager.build_domain_restriced_doctype_cache()
  148. frappe.cache_manager.build_domain_restriced_page_cache()
  149. def set_user_info(self, resume=False):
  150. # set sid again
  151. frappe.local.cookie_manager.init_cookies()
  152. self.full_name = " ".join(filter(None, [self.info.first_name, self.info.last_name]))
  153. if self.info.user_type == "Website User":
  154. frappe.local.cookie_manager.set_cookie("system_user", "no")
  155. if not resume:
  156. frappe.local.response["message"] = "No App"
  157. frappe.local.response["home_page"] = "/" + get_home_page()
  158. else:
  159. frappe.local.cookie_manager.set_cookie("system_user", "yes")
  160. if not resume:
  161. frappe.local.response["message"] = "Logged In"
  162. frappe.local.response["home_page"] = "/app"
  163. if not resume:
  164. frappe.response["full_name"] = self.full_name
  165. # redirect information
  166. redirect_to = frappe.cache().hget("redirect_after_login", self.user)
  167. if redirect_to:
  168. frappe.local.response["redirect_to"] = redirect_to
  169. frappe.cache().hdel("redirect_after_login", self.user)
  170. frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
  171. frappe.local.cookie_manager.set_cookie("user_id", self.user)
  172. frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
  173. def clear_preferred_language(self):
  174. frappe.local.cookie_manager.delete_cookie("preferred_language")
  175. def make_session(self, resume=False):
  176. # start session
  177. frappe.local.session_obj = Session(
  178. user=self.user, resume=resume, full_name=self.full_name, user_type=self.user_type
  179. )
  180. # reset user if changed to Guest
  181. self.user = frappe.local.session_obj.user
  182. frappe.local.session = frappe.local.session_obj.data
  183. self.clear_active_sessions()
  184. def clear_active_sessions(self):
  185. """Clear other sessions of the current user if `deny_multiple_sessions` is not set"""
  186. if frappe.session.user == "Guest":
  187. return
  188. if not (
  189. cint(frappe.conf.get("deny_multiple_sessions"))
  190. or cint(frappe.db.get_system_setting("deny_multiple_sessions"))
  191. ):
  192. return
  193. clear_sessions(frappe.session.user, keep_current=True)
  194. def authenticate(self, user: str = None, pwd: str = None):
  195. from frappe.core.doctype.user.user import User
  196. if not (user and pwd):
  197. user, pwd = frappe.form_dict.get("usr"), frappe.form_dict.get("pwd")
  198. if not (user and pwd):
  199. self.fail(_("Incomplete login details"), user=user)
  200. user = User.find_by_credentials(user, pwd)
  201. if not user:
  202. self.fail("Invalid login credentials")
  203. # Current login flow uses cached credentials for authentication while checking OTP.
  204. # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway)
  205. # Tracker is activated for 2FA incase of OTP.
  206. ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict)
  207. tracker = None if ignore_tracker else get_login_attempt_tracker(user.name)
  208. if not user.is_authenticated:
  209. tracker and tracker.add_failure_attempt()
  210. self.fail("Invalid login credentials", user=user.name)
  211. elif not (user.name == "Administrator" or user.enabled):
  212. tracker and tracker.add_failure_attempt()
  213. self.fail("User disabled or missing", user=user.name)
  214. else:
  215. tracker and tracker.add_success_attempt()
  216. self.user = user.name
  217. def force_user_to_reset_password(self):
  218. if not self.user:
  219. return
  220. if self.user in frappe.STANDARD_USERS:
  221. return False
  222. reset_pwd_after_days = cint(
  223. frappe.db.get_single_value("System Settings", "force_user_to_reset_password")
  224. )
  225. if reset_pwd_after_days:
  226. last_password_reset_date = (
  227. frappe.db.get_value("User", self.user, "last_password_reset_date") or today()
  228. )
  229. last_pwd_reset_days = date_diff(today(), last_password_reset_date)
  230. if last_pwd_reset_days > reset_pwd_after_days:
  231. return True
  232. def check_password(self, user, pwd):
  233. """check password"""
  234. try:
  235. # returns user in correct case
  236. return check_password(user, pwd)
  237. except frappe.AuthenticationError:
  238. self.fail("Incorrect password", user=user)
  239. def fail(self, message, user=None):
  240. if not user:
  241. user = _("Unknown User")
  242. frappe.local.response["message"] = message
  243. add_authentication_log(message, user, status="Failed")
  244. frappe.db.commit()
  245. raise frappe.AuthenticationError
  246. def run_trigger(self, event="on_login"):
  247. for method in frappe.get_hooks().get(event, []):
  248. frappe.call(frappe.get_attr(method), login_manager=self)
  249. def validate_hour(self):
  250. """check if user is logging in during restricted hours"""
  251. login_before = int(frappe.db.get_value("User", self.user, "login_before", ignore=True) or 0)
  252. login_after = int(frappe.db.get_value("User", self.user, "login_after", ignore=True) or 0)
  253. if not (login_before or login_after):
  254. return
  255. from frappe.utils import now_datetime
  256. current_hour = int(now_datetime().strftime("%H"))
  257. if login_before and current_hour > login_before:
  258. frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
  259. if login_after and current_hour < login_after:
  260. frappe.throw(_("Login not allowed at this time"), frappe.AuthenticationError)
  261. def login_as_guest(self):
  262. """login as guest"""
  263. self.login_as("Guest")
  264. def login_as(self, user):
  265. self.user = user
  266. self.post_login()
  267. def logout(self, arg="", user=None):
  268. if not user:
  269. user = frappe.session.user
  270. self.run_trigger("on_logout")
  271. if user == frappe.session.user:
  272. delete_session(frappe.session.sid, user=user, reason="User Manually Logged Out")
  273. self.clear_cookies()
  274. else:
  275. clear_sessions(user)
  276. def clear_cookies(self):
  277. clear_cookies()
  278. class CookieManager:
  279. def __init__(self):
  280. self.cookies = {}
  281. self.to_delete = []
  282. def init_cookies(self):
  283. if not frappe.local.session.get("sid"):
  284. return
  285. # sid expires in 3 days
  286. expires = datetime.datetime.now() + datetime.timedelta(days=3)
  287. if frappe.session.sid:
  288. self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
  289. if frappe.session.session_country:
  290. self.set_cookie("country", frappe.session.session_country)
  291. def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
  292. if not secure and hasattr(frappe.local, "request"):
  293. secure = frappe.local.request.scheme == "https"
  294. # Cordova does not work with Lax
  295. if frappe.local.session.data.device == "mobile":
  296. samesite = None
  297. self.cookies[key] = {
  298. "value": value,
  299. "expires": expires,
  300. "secure": secure,
  301. "httponly": httponly,
  302. "samesite": samesite,
  303. }
  304. def delete_cookie(self, to_delete):
  305. if not isinstance(to_delete, (list, tuple)):
  306. to_delete = [to_delete]
  307. self.to_delete.extend(to_delete)
  308. def flush_cookies(self, response):
  309. for key, opts in self.cookies.items():
  310. response.set_cookie(
  311. key,
  312. quote((opts.get("value") or "").encode("utf-8")),
  313. expires=opts.get("expires"),
  314. secure=opts.get("secure"),
  315. httponly=opts.get("httponly"),
  316. samesite=opts.get("samesite"),
  317. )
  318. # expires yesterday!
  319. expires = datetime.datetime.now() + datetime.timedelta(days=-1)
  320. for key in set(self.to_delete):
  321. response.set_cookie(key, "", expires=expires)
  322. @frappe.whitelist()
  323. def get_logged_user():
  324. return frappe.session.user
  325. def clear_cookies():
  326. if hasattr(frappe.local, "session"):
  327. frappe.session.sid = ""
  328. frappe.local.cookie_manager.delete_cookie(
  329. ["full_name", "user_id", "sid", "user_image", "system_user"]
  330. )
  331. def validate_ip_address(user):
  332. """check if IP Address is valid"""
  333. from frappe.core.doctype.user.user import get_restricted_ip_list
  334. # Only fetch required fields - for perf
  335. user_fields = ["restrict_ip", "bypass_restrict_ip_check_if_2fa_enabled"]
  336. user_info = (
  337. frappe.get_cached_value("User", user, user_fields, as_dict=True)
  338. if not frappe.flags.in_test
  339. else frappe.db.get_value("User", user, user_fields, as_dict=True)
  340. )
  341. ip_list = get_restricted_ip_list(user_info)
  342. if not ip_list:
  343. return
  344. system_settings = (
  345. frappe.get_cached_doc("System Settings")
  346. if not frappe.flags.in_test
  347. else frappe.get_single("System Settings")
  348. )
  349. # check if bypass restrict ip is enabled for all users
  350. bypass_restrict_ip_check = system_settings.bypass_restrict_ip_check_if_2fa_enabled
  351. # check if two factor auth is enabled
  352. if system_settings.enable_two_factor_auth and not bypass_restrict_ip_check:
  353. # check if bypass restrict ip is enabled for login user
  354. bypass_restrict_ip_check = user_info.bypass_restrict_ip_check_if_2fa_enabled
  355. for ip in ip_list:
  356. if frappe.local.request_ip.startswith(ip) or bypass_restrict_ip_check:
  357. return
  358. frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
  359. def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True):
  360. """Get login attempt tracker instance.
  361. :param user_name: Name of the loggedin user
  362. :param raise_locked_exception: If set, raises an exception incase of user not allowed to login
  363. """
  364. sys_settings = frappe.get_doc("System Settings")
  365. track_login_attempts = sys_settings.allow_consecutive_login_attempts > 0
  366. tracker_kwargs = {}
  367. if track_login_attempts:
  368. tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail
  369. tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts
  370. tracker = LoginAttemptTracker(user_name, **tracker_kwargs)
  371. if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed():
  372. frappe.throw(
  373. _("Your account has been locked and will resume after {0} seconds").format(
  374. sys_settings.allow_login_after_fail
  375. ),
  376. frappe.SecurityException,
  377. )
  378. return tracker
  379. class LoginAttemptTracker:
  380. """Track login attemts of a user.
  381. Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
  382. """
  383. def __init__(
  384. self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60
  385. ):
  386. """Initialize the tracker.
  387. :param user_name: Name of the loggedin user
  388. :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
  389. :param lock_interval: Locking interval incase of maximum failed attempts
  390. """
  391. self.user_name = user_name
  392. self.lock_interval = datetime.timedelta(seconds=lock_interval)
  393. self.max_failed_logins = max_consecutive_login_attempts
  394. @property
  395. def login_failed_count(self):
  396. return frappe.cache().hget("login_failed_count", self.user_name)
  397. @login_failed_count.setter
  398. def login_failed_count(self, count):
  399. frappe.cache().hset("login_failed_count", self.user_name, count)
  400. @login_failed_count.deleter
  401. def login_failed_count(self):
  402. frappe.cache().hdel("login_failed_count", self.user_name)
  403. @property
  404. def login_failed_time(self):
  405. """First failed login attempt time within lock interval.
  406. For every user we track only First failed login attempt time within lock interval of time.
  407. """
  408. return frappe.cache().hget("login_failed_time", self.user_name)
  409. @login_failed_time.setter
  410. def login_failed_time(self, timestamp):
  411. frappe.cache().hset("login_failed_time", self.user_name, timestamp)
  412. @login_failed_time.deleter
  413. def login_failed_time(self):
  414. frappe.cache().hdel("login_failed_time", self.user_name)
  415. def add_failure_attempt(self):
  416. """Log user failure attempts into the system.
  417. Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
  418. """
  419. login_failed_time = self.login_failed_time
  420. login_failed_count = self.login_failed_count # Consecutive login failure count
  421. current_time = get_datetime()
  422. if not (login_failed_time and login_failed_count):
  423. login_failed_time, login_failed_count = current_time, 0
  424. if login_failed_time + self.lock_interval > current_time:
  425. login_failed_count += 1
  426. else:
  427. login_failed_time, login_failed_count = current_time, 1
  428. self.login_failed_time = login_failed_time
  429. self.login_failed_count = login_failed_count
  430. def add_success_attempt(self):
  431. """Reset login failures."""
  432. del self.login_failed_count
  433. del self.login_failed_time
  434. def is_user_allowed(self) -> bool:
  435. """Is user allowed to login
  436. User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure.
  437. """
  438. login_failed_time = self.login_failed_time
  439. login_failed_count = self.login_failed_count or 0
  440. current_time = get_datetime()
  441. if (
  442. login_failed_time
  443. and login_failed_time + self.lock_interval > current_time
  444. and login_failed_count > self.max_failed_logins
  445. ):
  446. return False
  447. return True