Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QOLDEV-638 throttle data request creation #93

Merged
merged 3 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions ckanext/datarequests/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@
except ImportError:
from cgi import escape

from ckan import model
from ckan import authz, model
from ckan.lib import mailer
from ckan.lib.redis import connect_to_redis
from ckan.plugins import toolkit as tk
from ckan.plugins.toolkit import h, config

Expand All @@ -38,6 +39,10 @@
# Avoid user_show lag
USERS_CACHE = {}

# Allow one request per account per five minutes
CREATION_THROTTLE_EXPIRY = 300
THROTTLE_ERROR = "Too many requests submitted, please wait {} minutes and try again"


def _get_user(user_id):
try:
Expand Down Expand Up @@ -180,6 +185,39 @@ def _send_mail(user_ids, action_type, datarequest):
log.exception("Error sending notification to {0}".format(user_id))


def throttle_datarequest(creator):
""" Check that the account is not creating requests too quickly.
This should happen after validation, so a request that fails
validation can be immediately corrected and resubmitted.
"""
if creator.sysadmin or authz.has_user_permission_for_some_org(creator.name, 'create_dataset'):
# privileged users can skip the throttle
return

# check cache to see if there's a record of a recent creation
cache_key = '{}.ckanext.datarequest.creation_attempts.{}'.format(
tk.config.get('ckan.site_id'), creator.id)
redis_conn = connect_to_redis()
try:
creation_attempts = int(redis_conn.get(cache_key) or 0)
except ValueError:
# shouldn't happen but let's play it safe
creation_attempts = 0

if creation_attempts:
# Increase the delay every time someone tries too soon
expiry = creation_attempts * CREATION_THROTTLE_EXPIRY
else:
expiry = CREATION_THROTTLE_EXPIRY
log.debug("Account %s has submitted %s request(s) recently, next allowed in %s seconds",
creator.id, creation_attempts, expiry)
# put a cap on the maximum delay
recorded_attempts = creation_attempts if creation_attempts >= 100 else creation_attempts + 1
redis_conn.set(cache_key, recorded_attempts, ex=expiry)
if creation_attempts:
raise tk.ValidationError({"": [THROTTLE_ERROR.format(int(expiry / 60))]})


def create_datarequest(context, data_dict):
'''
Action to create a new data request. The function checks the access rights
Expand Down Expand Up @@ -214,10 +252,14 @@ def create_datarequest(context, data_dict):
# Validate data
validator.validate_datarequest(context, data_dict)

# Ensure account isn't creating requests too fast
creator = context['auth_user_obj']
throttle_datarequest(creator)

# Store the data
data_req = db.DataRequest()
_undictize_datarequest_basic(data_req, data_dict)
data_req.user_id = context['auth_user_obj'].id
data_req.user_id = creator.id
data_req.open_time = datetime.datetime.utcnow()

session.add(data_req)
Expand All @@ -227,7 +269,7 @@ def create_datarequest(context, data_dict):

if datarequest_dict['organization']:
users = {user['id'] for user in datarequest_dict['organization']['users']}
users.discard(context['auth_user_obj'].id)
users.discard(creator.id)
_send_mail(users, 'new_datarequest', datarequest_dict)

return datarequest_dict
Expand Down
2 changes: 1 addition & 1 deletion test/features/comments.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Feature: Comments

@comment-add
Scenario: When a logged-in user submits a comment on a Data Request the comment should then be visible on the Comments tab of the Data Request
Given "CKANUser" as the persona
Given "TestOrgMember" as the persona
When I log in
And I create a datarequest
And I go to data request "$last_generated_title" comments
Expand Down
12 changes: 12 additions & 0 deletions test/features/datarequest.feature
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,15 @@ Feature: Datarequest
When I go to the "test_org_editor" profile page
And I press the element with xpath "//ul[contains(@class, 'nav-tabs')]//a[contains(string(), 'Data Requests')]"
Then I should see "No data requests found"

Scenario: An unprivileged user who tries to create multiple data requests close together should see an error
Given "CKANUser" as the persona
When I log in
And I create a datarequest
And I go to the data requests page
And I press "Add Data Request"
And I fill in title with random text
And I fill in "description" with "Test throttling"
And I press the element with xpath "//button[contains(@class, 'btn-primary')]"
Then I should see "Too many requests submitted, please wait"