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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « appengine/monorail/framework/__init__.py ('k') | appengine/monorail/framework/alerts.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """A set of functions to test action limits.
7
8 Action limits help prevent an individual user from abusing the system
9 by performing an excessive number of operations. E.g., creating
10 thousands of projects.
11
12 If the user reaches a soft limit within a given time period, the
13 servlets will start demanding that the user solve a CAPTCHA.
14
15 If the user reaches a hard limit within a given time period, any further
16 requests to perform that type of action will fail.
17
18 When the user reaches a lifetime limit, they are shown an error page.
19 We can increase the lifetime limit for individual users who contact us.
20 """
21
22 import logging
23 import time
24
25 from framework import framework_constants
26 from proto import user_pb2
27
28
29 # Action types
30 PROJECT_CREATION = 1
31 ISSUE_COMMENT = 2
32 ISSUE_ATTACHMENT = 3
33 ISSUE_BULK_EDIT = 4
34 FLAG_SPAM = 5
35 API_REQUEST = 6
36
37 ACTION_TYPE_NAMES = {
38 'project_creation': PROJECT_CREATION,
39 'issue_comment': ISSUE_COMMENT,
40 'issue_attachment': ISSUE_ATTACHMENT,
41 'issue_bulk_edit': ISSUE_BULK_EDIT,
42 'flag_spam': FLAG_SPAM,
43 'api_request': API_REQUEST,
44 }
45
46 # Action Limit definitions
47 # {action_type: (period, soft_limit, hard_limit, life_max),...}
48 ACTION_LIMITS = {
49 PROJECT_CREATION: (framework_constants.SECS_PER_DAY, 2, 5, 25),
50 ISSUE_COMMENT: (framework_constants.SECS_PER_DAY / 4, 5, 100, 10000),
51 ISSUE_ATTACHMENT: (framework_constants.SECS_PER_DAY, 25, 100, 1000),
52 ISSUE_BULK_EDIT: (framework_constants.SECS_PER_DAY, 100, 500, 10000),
53 FLAG_SPAM: (framework_constants.SECS_PER_DAY, 100, 100, 10000),
54 API_REQUEST: (framework_constants.SECS_PER_DAY, 100000, 100000, 10000000),
55 }
56
57
58 # Determine scaling of CAPTCHA frequency.
59 MAX_SOFT_LIMITS = max([ACTION_LIMITS[key][2] - ACTION_LIMITS[key][1]
60 for key in ACTION_LIMITS])
61 SQUARES = {i**2 for i in range(1, MAX_SOFT_LIMITS)}
62 SQUARES.add(1)
63
64
65 def NeedCaptcha(user, action_type, now=None, skip_lifetime_check=False):
66 """Check that the user is under the limit on a given action.
67
68 Args:
69 user: instance of user_pb2.User.
70 action_type: int action type.
71 now: int time in millis. Defaults to int(time.time()). Used for testing.
72 skip_lifetime_check: No limit for lifetime actions.
73
74 Raises:
75 ExcessiveActivityException: when user is over hard or lifetime limits.
76
77 Returns:
78 False if user is under the soft-limit. True if user is over the
79 soft-limit, but under the hard and lifetime limits.
80 """
81 if not user: # Anything that can be done by anon users (which is not
82 return False # much) can be done any number of times w/o CAPTCHA.
83 if not now:
84 now = int(time.time())
85
86 period, soft, hard, life_max = ACTION_LIMITS[action_type]
87 actionlimit_pb = GetLimitPB(user, action_type)
88
89 # First, users with no action limits recorded must be below limits.
90 # And, users that we explicitly trust as non-abusers are allowed to take
91 # and unlimited number of actions. And, site admins are trusted non-abusers.
92 if (not actionlimit_pb or user.ignore_action_limits or
93 user.is_site_admin):
94 return False
95
96 # Second, check if user has reached lifetime limit.
97 if actionlimit_pb.lifetime_limit:
98 life_max = actionlimit_pb.lifetime_limit
99 if actionlimit_pb.period_soft_limit:
100 soft = actionlimit_pb.period_soft_limit
101 if actionlimit_pb.period_hard_limit:
102 hard = actionlimit_pb.period_hard_limit
103 if (not skip_lifetime_check and life_max is not None
104 and actionlimit_pb.lifetime_count >= life_max):
105 raise ExcessiveActivityException()
106
107 # Third, if user can begin a new time period, they are free to go ahead.
108 if now - actionlimit_pb.reset_timestamp > period:
109 return False
110
111 # Fourth, check for hard rate limits.
112 if hard is not None and actionlimit_pb.recent_count >= hard:
113 raise ExcessiveActivityException()
114
115 # Finally, check the soft limit in this time period.
116 action_limit = False
117 if soft is not None:
118 recent_count = actionlimit_pb.recent_count
119 if recent_count == soft:
120 action_limit = True
121 elif recent_count > soft:
122 remaining_soft = hard - recent_count
123 if remaining_soft in SQUARES:
124 action_limit = True
125
126 if action_limit:
127 logging.info('soft limit captcha: %d', recent_count)
128 return action_limit
129
130
131 def GetLimitPB(user, action_type):
132 """Return the apporiate action limit PB part of the given User PB."""
133 if action_type == PROJECT_CREATION:
134 if not user.project_creation_limit:
135 user.project_creation_limit = user_pb2.ActionLimit()
136 return user.project_creation_limit
137 elif action_type == ISSUE_COMMENT:
138 if not user.issue_comment_limit:
139 user.issue_comment_limit = user_pb2.ActionLimit()
140 return user.issue_comment_limit
141 elif action_type == ISSUE_ATTACHMENT:
142 if not user.issue_attachment_limit:
143 user.issue_attachment_limit = user_pb2.ActionLimit()
144 return user.issue_attachment_limit
145 elif action_type == ISSUE_BULK_EDIT:
146 if not user.issue_bulk_edit_limit:
147 user.issue_bulk_edit_limit = user_pb2.ActionLimit()
148 return user.issue_bulk_edit_limit
149 elif action_type == FLAG_SPAM:
150 if not user.flag_spam_limit:
151 user.flag_spam_limit = user_pb2.ActionLimit()
152 return user.flag_spam_limit
153 elif action_type == API_REQUEST:
154 if not user.api_request_limit:
155 user.api_request_limit = user_pb2.ActionLimit()
156 return user.api_request_limit
157 raise Exception('unexpected action type %r' % action_type)
158
159
160 def ResetRecentActions(user, action_type):
161 """Reset the recent counter for an action.
162
163 Args:
164 user: instance of user_pb2.User.
165 action_type: int action type.
166 """
167 al = GetLimitPB(user, action_type)
168 al.recent_count = 0
169 al.reset_timestamp = 0
170
171
172 def CountAction(user, action_type, delta=1, now=int(time.time())):
173 """Reset recent counter if eligible, then increment recent and lifetime.
174
175 Args:
176 user: instance of user_pb2.User.
177 action_type: int action type.
178 delta: int number to increment count by.
179 now: int time in millis. Defaults to int(time.time()). Used for testing.
180 """
181 al = GetLimitPB(user, action_type)
182 period = ACTION_LIMITS[action_type][0]
183
184 if now - al.reset_timestamp > period:
185 al.reset_timestamp = now
186 al.recent_count = 0
187
188 al.recent_count = al.recent_count + delta
189 al.lifetime_count = al.lifetime_count + delta
190
191
192 def CustomizeLimit(user, action_type, soft_limit, hard_limit, lifetime_limit):
193 """Set custom action limits for a user.
194
195 The recent counters are reset to zero, so the user will not run into
196 a hard limit.
197
198 Args:
199 user: instance of user_pb2.User.
200 action_type: int action type.
201 soft_limit: soft limit of period.
202 hard_limit: hard limit of period.
203 lifetime_limit: lifetime limit.
204 """
205 al = GetLimitPB(user, action_type)
206 al.lifetime_limit = lifetime_limit
207 al.period_soft_limit = soft_limit
208 al.period_hard_limit = hard_limit
209
210 # The mutator will mark the ActionLimit as present, but does not
211 # necessarily *initialize* the protobuf. We need to ensure that the
212 # lifetime_count is set (a required field). Additional required
213 # fields will be set below.
214 if not al.lifetime_count:
215 al.lifetime_count = 0
216
217 # Clear the recent counters so the user will not hit the period limit.
218 al.recent_count = 0
219 al.reset_timestamp = 0
220
221
222 class Error(Exception):
223 """Base exception class for this package."""
224
225
226 class ExcessiveActivityException(Error):
227 """No user with the specified name exists."""
OLDNEW
« 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