diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 895da8c5c5..573dcd7f17 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -2,21 +2,21 @@ # MIT License. See license.txt from __future__ import unicode_literals, print_function + +from bs4 import BeautifulSoup + import frappe from frappe.model.document import Document from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today from frappe import throw, msgprint, _ -from frappe.utils.password import update_password as _update_password, check_password +from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit from frappe.desk.notifications import clear_notifications from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications from frappe.utils.user import get_system_managers -from bs4 import BeautifulSoup -import frappe.permissions -import frappe.share -import frappe.defaults from frappe.website.utils import is_signup_enabled -from frappe.utils.background_jobs import enqueue from frappe.rate_limiter import rate_limit +from frappe.utils.background_jobs import enqueue + STANDARD_USERS = ("Guest", "Administrator") @@ -838,7 +838,7 @@ def sign_up(email, full_name, redirect_to): return 2, _("Please ask your administrator to verify your sign-up") @frappe.whitelist(allow_guest=True) -@rate_limit(key='user', limit=3, seconds = 24*60*60, methods=['POST']) +@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) def reset_password(user): if user=="Administrator": return 'not allowed' diff --git a/frappe/hooks.py b/frappe/hooks.py index 3e206f0ad3..c9914237fe 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -207,8 +207,7 @@ scheduler_events = { "frappe.deferred_insert.save_to_db", "frappe.desk.form.document_follow.send_hourly_updates", "frappe.integrations.doctype.google_calendar.google_calendar.sync", - "frappe.email.doctype.newsletter.newsletter.send_scheduled_email", - "frappe.utils.password.delete_password_reset_cache" + "frappe.email.doctype.newsletter.newsletter.send_scheduled_email" ], "daily": [ "frappe.email.queue.set_expiry_for_email_queue", diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py index e7f467f10d..40db8fe892 100644 --- a/frappe/rate_limiter.py +++ b/frappe/rate_limiter.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals from datetime import datetime from functools import wraps -from typing import Union +from typing import Union, Callable from werkzeug.wrappers import Response @@ -84,7 +84,7 @@ class RateLimiter: if self.rejected: return Response(_("Too Many Requests"), status=429) -def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'): +def rate_limit(key: str, limit: Union[int, Callable] = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'): """Decorator to rate limit an endpoint. This will limit Number of requests per endpoint to `limit` within `seconds`. @@ -92,6 +92,7 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[ :param key: Key is used to identify the requests uniqueness :param limit: Maximum number of requests to allow with in window time + :type limit: Callable or Integer :param seconds: window time to allow requests :param methods: Limit the validation for these methods. `ALL` is a wildcard that applies rate limit on all methods. @@ -106,6 +107,8 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[ if methods != 'ALL' and frappe.request.method.upper() not in methods: return frappe.call(fun, **frappe.form_dict) + _limit = limit() if callable(limit) else limit + identity = frappe.form_dict[key] cache_key = f"rl:{frappe.form_dict.cmd}:{identity}" @@ -114,7 +117,7 @@ def rate_limit(key: str, limit: int = 5, seconds: int= 24*60*60, methods: Union[ frappe.cache().set_value(cache_key, 0, expires_in_sec=seconds) value = frappe.cache().incrby(cache_key, 1) - if value > limit: + if value > _limit: frappe.throw(_("You hit the rate limit because of too many requests. Please try after sometime.")) return frappe.call(fun, **frappe.form_dict) diff --git a/frappe/utils/password.py b/frappe/utils/password.py index 177a3118fb..19a538f703 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -90,14 +90,6 @@ def delete_login_failed_cache(user): frappe.cache().hdel('login_failed_count', user) frappe.cache().hdel('locked_account_time', user) - -def delete_password_reset_cache(user=None): - if user: - frappe.cache().hdel('password_reset_link_count', user) - else: - frappe.cache().delete_key('password_reset_link_count') - - def update_password(user, pwd, doctype='User', fieldname='password', logout_all_sessions=False): ''' Update the password for the User @@ -179,3 +171,6 @@ def get_encryption_key(): frappe.local.conf.encryption_key = encryption_key return frappe.local.conf.encryption_key + +def get_password_reset_limit(): + return frappe.db.get_single_value("System Settings", "password_reset_limit") or 0