Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(28)

Unified Diff: appengine/monorail/framework/actionlimit.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/framework/__init__.py ('k') | appengine/monorail/framework/alerts.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/framework/actionlimit.py
diff --git a/appengine/monorail/framework/actionlimit.py b/appengine/monorail/framework/actionlimit.py
new file mode 100644
index 0000000000000000000000000000000000000000..b994c1fbf2501a3270a6f0baa8ca10922b60cabf
--- /dev/null
+++ b/appengine/monorail/framework/actionlimit.py
@@ -0,0 +1,227 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is govered by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""A set of functions to test action limits.
+
+Action limits help prevent an individual user from abusing the system
+by performing an excessive number of operations. E.g., creating
+thousands of projects.
+
+If the user reaches a soft limit within a given time period, the
+servlets will start demanding that the user solve a CAPTCHA.
+
+If the user reaches a hard limit within a given time period, any further
+requests to perform that type of action will fail.
+
+When the user reaches a lifetime limit, they are shown an error page.
+We can increase the lifetime limit for individual users who contact us.
+"""
+
+import logging
+import time
+
+from framework import framework_constants
+from proto import user_pb2
+
+
+# Action types
+PROJECT_CREATION = 1
+ISSUE_COMMENT = 2
+ISSUE_ATTACHMENT = 3
+ISSUE_BULK_EDIT = 4
+FLAG_SPAM = 5
+API_REQUEST = 6
+
+ACTION_TYPE_NAMES = {
+ 'project_creation': PROJECT_CREATION,
+ 'issue_comment': ISSUE_COMMENT,
+ 'issue_attachment': ISSUE_ATTACHMENT,
+ 'issue_bulk_edit': ISSUE_BULK_EDIT,
+ 'flag_spam': FLAG_SPAM,
+ 'api_request': API_REQUEST,
+ }
+
+# Action Limit definitions
+# {action_type: (period, soft_limit, hard_limit, life_max),...}
+ACTION_LIMITS = {
+ PROJECT_CREATION: (framework_constants.SECS_PER_DAY, 2, 5, 25),
+ ISSUE_COMMENT: (framework_constants.SECS_PER_DAY / 4, 5, 100, 10000),
+ ISSUE_ATTACHMENT: (framework_constants.SECS_PER_DAY, 25, 100, 1000),
+ ISSUE_BULK_EDIT: (framework_constants.SECS_PER_DAY, 100, 500, 10000),
+ FLAG_SPAM: (framework_constants.SECS_PER_DAY, 100, 100, 10000),
+ API_REQUEST: (framework_constants.SECS_PER_DAY, 100000, 100000, 10000000),
+ }
+
+
+# Determine scaling of CAPTCHA frequency.
+MAX_SOFT_LIMITS = max([ACTION_LIMITS[key][2] - ACTION_LIMITS[key][1]
+ for key in ACTION_LIMITS])
+SQUARES = {i**2 for i in range(1, MAX_SOFT_LIMITS)}
+SQUARES.add(1)
+
+
+def NeedCaptcha(user, action_type, now=None, skip_lifetime_check=False):
+ """Check that the user is under the limit on a given action.
+
+ Args:
+ user: instance of user_pb2.User.
+ action_type: int action type.
+ now: int time in millis. Defaults to int(time.time()). Used for testing.
+ skip_lifetime_check: No limit for lifetime actions.
+
+ Raises:
+ ExcessiveActivityException: when user is over hard or lifetime limits.
+
+ Returns:
+ False if user is under the soft-limit. True if user is over the
+ soft-limit, but under the hard and lifetime limits.
+ """
+ if not user: # Anything that can be done by anon users (which is not
+ return False # much) can be done any number of times w/o CAPTCHA.
+ if not now:
+ now = int(time.time())
+
+ period, soft, hard, life_max = ACTION_LIMITS[action_type]
+ actionlimit_pb = GetLimitPB(user, action_type)
+
+ # First, users with no action limits recorded must be below limits.
+ # And, users that we explicitly trust as non-abusers are allowed to take
+ # and unlimited number of actions. And, site admins are trusted non-abusers.
+ if (not actionlimit_pb or user.ignore_action_limits or
+ user.is_site_admin):
+ return False
+
+ # Second, check if user has reached lifetime limit.
+ if actionlimit_pb.lifetime_limit:
+ life_max = actionlimit_pb.lifetime_limit
+ if actionlimit_pb.period_soft_limit:
+ soft = actionlimit_pb.period_soft_limit
+ if actionlimit_pb.period_hard_limit:
+ hard = actionlimit_pb.period_hard_limit
+ if (not skip_lifetime_check and life_max is not None
+ and actionlimit_pb.lifetime_count >= life_max):
+ raise ExcessiveActivityException()
+
+ # Third, if user can begin a new time period, they are free to go ahead.
+ if now - actionlimit_pb.reset_timestamp > period:
+ return False
+
+ # Fourth, check for hard rate limits.
+ if hard is not None and actionlimit_pb.recent_count >= hard:
+ raise ExcessiveActivityException()
+
+ # Finally, check the soft limit in this time period.
+ action_limit = False
+ if soft is not None:
+ recent_count = actionlimit_pb.recent_count
+ if recent_count == soft:
+ action_limit = True
+ elif recent_count > soft:
+ remaining_soft = hard - recent_count
+ if remaining_soft in SQUARES:
+ action_limit = True
+
+ if action_limit:
+ logging.info('soft limit captcha: %d', recent_count)
+ return action_limit
+
+
+def GetLimitPB(user, action_type):
+ """Return the apporiate action limit PB part of the given User PB."""
+ if action_type == PROJECT_CREATION:
+ if not user.project_creation_limit:
+ user.project_creation_limit = user_pb2.ActionLimit()
+ return user.project_creation_limit
+ elif action_type == ISSUE_COMMENT:
+ if not user.issue_comment_limit:
+ user.issue_comment_limit = user_pb2.ActionLimit()
+ return user.issue_comment_limit
+ elif action_type == ISSUE_ATTACHMENT:
+ if not user.issue_attachment_limit:
+ user.issue_attachment_limit = user_pb2.ActionLimit()
+ return user.issue_attachment_limit
+ elif action_type == ISSUE_BULK_EDIT:
+ if not user.issue_bulk_edit_limit:
+ user.issue_bulk_edit_limit = user_pb2.ActionLimit()
+ return user.issue_bulk_edit_limit
+ elif action_type == FLAG_SPAM:
+ if not user.flag_spam_limit:
+ user.flag_spam_limit = user_pb2.ActionLimit()
+ return user.flag_spam_limit
+ elif action_type == API_REQUEST:
+ if not user.api_request_limit:
+ user.api_request_limit = user_pb2.ActionLimit()
+ return user.api_request_limit
+ raise Exception('unexpected action type %r' % action_type)
+
+
+def ResetRecentActions(user, action_type):
+ """Reset the recent counter for an action.
+
+ Args:
+ user: instance of user_pb2.User.
+ action_type: int action type.
+ """
+ al = GetLimitPB(user, action_type)
+ al.recent_count = 0
+ al.reset_timestamp = 0
+
+
+def CountAction(user, action_type, delta=1, now=int(time.time())):
+ """Reset recent counter if eligible, then increment recent and lifetime.
+
+ Args:
+ user: instance of user_pb2.User.
+ action_type: int action type.
+ delta: int number to increment count by.
+ now: int time in millis. Defaults to int(time.time()). Used for testing.
+ """
+ al = GetLimitPB(user, action_type)
+ period = ACTION_LIMITS[action_type][0]
+
+ if now - al.reset_timestamp > period:
+ al.reset_timestamp = now
+ al.recent_count = 0
+
+ al.recent_count = al.recent_count + delta
+ al.lifetime_count = al.lifetime_count + delta
+
+
+def CustomizeLimit(user, action_type, soft_limit, hard_limit, lifetime_limit):
+ """Set custom action limits for a user.
+
+ The recent counters are reset to zero, so the user will not run into
+ a hard limit.
+
+ Args:
+ user: instance of user_pb2.User.
+ action_type: int action type.
+ soft_limit: soft limit of period.
+ hard_limit: hard limit of period.
+ lifetime_limit: lifetime limit.
+ """
+ al = GetLimitPB(user, action_type)
+ al.lifetime_limit = lifetime_limit
+ al.period_soft_limit = soft_limit
+ al.period_hard_limit = hard_limit
+
+ # The mutator will mark the ActionLimit as present, but does not
+ # necessarily *initialize* the protobuf. We need to ensure that the
+ # lifetime_count is set (a required field). Additional required
+ # fields will be set below.
+ if not al.lifetime_count:
+ al.lifetime_count = 0
+
+ # Clear the recent counters so the user will not hit the period limit.
+ al.recent_count = 0
+ al.reset_timestamp = 0
+
+
+class Error(Exception):
+ """Base exception class for this package."""
+
+
+class ExcessiveActivityException(Error):
+ """No user with the specified name exists."""
« no previous file with comments | « appengine/monorail/framework/__init__.py ('k') | appengine/monorail/framework/alerts.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698