refactor: allow callable limit arg for ratelimit deco

As we make all configurations editable through dashboard(ex: password_reset_limit), it makes sense
to provide limit as a callable so that it can be accessed dynamically.
This commit is contained in:
leela 2021-03-05 13:21:21 +05:30
parent 14b3d344a5
commit de210260a7
4 changed files with 17 additions and 20 deletions

View file

@ -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'

View file

@ -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",

View file

@ -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)

View file

@ -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