def make_reset_token(token_cls, user, issue_limit=3): """Generate a password reset token or account recovery token. Checks a ratelimit to ensure that the token isn't being reset too often. There is also a global check on resets, such that more than 1000 per hour will trigger obvious and loud breakage via a ValueError. Issuing (or failing to issue) a token are added to user's notes. """ # check if we've hit the ratelimit reset_count_key = "token:%s_count:%s" % (token_cls.__name__, user._id) time_slice = ratelimit.get_timeslice(int(token_cls._ttl)) usage = ratelimit.record_usage(reset_count_key, time_slice) if usage > issue_limit: user.add_note("Exceeded password/email reset max attempts.") return None # check if we've hit the global rate limit and fail badly reset_count_global = "token:%s_count_global" % (token_cls.__name__, ) global_time_slice = ratelimit.get_timeslice(60 * 60) global_usage = ratelimit.record_usage(reset_count_global, global_time_slice) if global_usage > 1000: raise ValueError( "Somebody's beating the hell out of the password reset endpoint") # all is well. issue the token. token = token_cls._new(user) return token
def make_reset_token(token_cls, user, issue_limit=3): """Generate a password reset token or account recovery token. Checks a ratelimit to ensure that the token isn't being reset too often. There is also a global check on resets, such that more than 1000 per hour will trigger obvious and loud breakage via a ValueError. Issuing (or failing to issue) a token are added to user's notes. """ # check if we've hit the ratelimit reset_count_key = "token:%s_count:%s" % (token_cls.__name__, user._id) time_slice = ratelimit.get_timeslice(int(token_cls._ttl)) usage = ratelimit.record_usage(reset_count_key, time_slice) if usage > issue_limit: user.add_note("Exceeded password/email reset max attempts.") return None # check if we've hit the global rate limit and fail badly reset_count_global = "token:%s_count_global" % (token_cls.__name__,) global_time_slice = ratelimit.get_timeslice(60 * 60) global_usage = ratelimit.record_usage(reset_count_global, global_time_slice) if global_usage > 1000: raise ValueError( "Somebody's beating the hell out of the password reset endpoint" ) # all is well. issue the token. token = token_cls._new(user) return token
def test_record_usage_multi(self): self.now = 24 * 3600 + 5 tsa = ratelimit.get_timeslice(3600) tsb = ratelimit.get_timeslice(700) ratelimit.record_usage_multi([('a', tsa), ('b', tsb)]) self.assertEquals(1, self.cache['rl:a-000000']) self.assertEquals(1, self.cache['rl:b-235500']) ratelimit.record_usage_multi([('a', tsa), ('b', tsb)]) self.assertEquals(2, self.cache['rl:a-000000']) self.assertEquals(2, self.cache['rl:b-235500'])
def test_record_usage(self): self.now = 24 * 3600 + 5 ts = ratelimit.get_timeslice(3600) ratelimit.record_usage('a', ts) self.assertEquals(1, self.cache['rl:a-000000']) ratelimit.record_usage('a', ts) self.assertEquals(2, self.cache['rl:a-000000']) self.now = 24 * 3600 + 5 * 3600 ts = ratelimit.get_timeslice(3600) ratelimit.record_usage('a', ts) self.assertEquals(1, self.cache['rl:a-050000'])
def ratelimit_expired(self, limit=3): key = "ratelimit:%s:%s" % (self.__class__.__name__, self.user_id) time_slice = ratelimit.get_timeslice(60 * 60) usage = ratelimit.record_usage(key, time_slice) if usage > limit: self.consume() return True
def test_record_usage_multi_across_slice_expiration(self): self.now = 24 * 3600 + 5 tsa = ratelimit.get_timeslice(3600) tsb = ratelimit.get_timeslice(700) real_incr = self.cache.incr evicted = False def fake_incr_multi(keys, delta=1): if evicted: for key in keys: del self.cache[key] raise pylibmc.NotFound() return real_incr(key) with patch.object(self.cache, 'incr_multi', fake_incr_multi): # Forcibly evict the key before incr is called, but after the # initial add call inside record_usage. evicted = True ratelimit.record_usage_multi([('a', tsa), ('b', tsb)]) self.assertEquals(1, self.cache['rl:a-000000']) self.assertEquals(1, self.cache['rl:b-235500'])
def test_record_usage_across_slice_expiration(self): self.now = 24 * 3600 + 5 ts = ratelimit.get_timeslice(3600) real_incr = self.cache.incr evicted = False def fake_incr(key): if evicted: del self.cache[key] raise pylibmc.NotFound() return real_incr(key) with patch.object(self.cache, 'incr', fake_incr): # Forcibly evict the key before incr() is called, but after the # initial add() call inside record_usage(). evicted = True ratelimit.record_usage('a', ts) self.assertEquals(1, self.cache['a-000000'])
def test_record_usage_across_slice_expiration(self): self.now = 24 * 3600 + 5 ts = ratelimit.get_timeslice(3600) real_incr = self.cache.incr evicted = False def fake_incr(key): if evicted: del self.cache[key] raise pylibmc.NotFound() return real_incr(key) with patch.object(self.cache, 'incr', fake_incr): # Forcibly evict the key before incr() is called, but after the # initial add() call inside record_usage(). evicted = True ratelimit.record_usage('a', ts) self.assertEquals(1, self.cache['rl:a-000000'])
def _has_exceeded_ratelimit(self, form, room): # grab the ratelimit (as average events per second) for the room's # current level, using the highest level configured that's not bigger # than the room. e.g. if ratelimits are defined for levels 1, 2, and 4 # and the room is level 3, this will give us the ratelimit specified # for 2. desired_avg_per_sec = 1 by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {}) for level, avg_per_sec in sorted(by_level.items(), key=lambda (x, y): int(x)): if int(level) > room.level: break desired_avg_per_sec = avg_per_sec # now figure out how many events per window that means window_size = g.live_config.get("robin_ratelimit_window", 10) allowed_events_per_window = int(desired_avg_per_sec * window_size) try: # now figure out how much they've actually used ratelimit_key = "robin/{}".format(c.user._id36) time_slice = ratelimit.get_timeslice(window_size) usage = ratelimit.get_usage(ratelimit_key, time_slice) # ratelimit them if too much if usage >= allowed_events_per_window: g.stats.simple_event("robin.ratelimit.exceeded") period_end = datetime.datetime.utcfromtimestamp(time_slice.end) period_end_utc = period_end.replace(tzinfo=pytz.UTC) until_reset = utils.timeuntil(period_end_utc) c.errors.add(errors.RATELIMIT, {"time": until_reset}, field="ratelimit", code=429) form.has_errors("ratelimit", errors.RATELIMIT) return True # or record the usage and move on ratelimit.record_usage(ratelimit_key, time_slice) except ratelimit.RatelimitError as exc: g.log.warning("ratelimit error: %s", exc) return False
def _has_exceeded_ratelimit(self, form, room): # grab the ratelimit (as average events per second) for the room's # current level, using the highest level configured that's not bigger # than the room. e.g. if ratelimits are defined for levels 1, 2, and 4 # and the room is level 3, this will give us the ratelimit specified # for 2. desired_avg_per_sec = 1 by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {}) for level, avg_per_sec in sorted(by_level.items(), key=lambda (x,y): int(x)): if int(level) > room.level: break desired_avg_per_sec = avg_per_sec # now figure out how many events per window that means window_size = g.live_config.get("robin_ratelimit_window", 10) allowed_events_per_window = int(desired_avg_per_sec * window_size) try: # now figure out how much they've actually used ratelimit_key = "robin/{}".format(c.user._id36) time_slice = ratelimit.get_timeslice(window_size) usage = ratelimit.get_usage(ratelimit_key, time_slice) # ratelimit them if too much if usage >= allowed_events_per_window: g.stats.simple_event("robin.ratelimit.exceeded") period_end = datetime.datetime.utcfromtimestamp(time_slice.end) period_end_utc = period_end.replace(tzinfo=pytz.UTC) until_reset = utils.timeuntil(period_end_utc) c.errors.add(errors.RATELIMIT, {"time": until_reset}, field="ratelimit", code=429) form.has_errors("ratelimit", errors.RATELIMIT) return True # or record the usage and move on ratelimit.record_usage(ratelimit_key, time_slice) except ratelimit.RatelimitError as exc: g.log.warning("ratelimit error: %s", exc) return False
def test_append_time_slice_1s(self): self.now = 14 ts = ratelimit.get_timeslice(1) key = ratelimit._append_time_slice('a', ts) self.assertEquals('a-000014', key)
def test_append_time_slice_1h(self): self.now = 3650 ts = ratelimit.get_timeslice(3600) key = ratelimit._append_time_slice('a', ts) self.assertEquals('a-010000', key)
def test_append_time_slice_1w(self): self.now = 7 * 24 * 3600 + 5 ts = ratelimit.get_timeslice(24 * 3600) key = ratelimit._append_time_slice('a', ts) self.assertEquals('a-@604800', key)
def test_make_ratelimit_cache_key_1w(self): self.now = 7 * 24 * 3600 + 5 ts = ratelimit.get_timeslice(24 * 3600) key = ratelimit._make_ratelimit_cache_key('a', ts) self.assertEquals('rl:a-@604800', key)
def test_make_ratelimit_cache_key_1h(self): self.now = 3650 ts = ratelimit.get_timeslice(3600) key = ratelimit._make_ratelimit_cache_key('a', ts) self.assertEquals('rl:a-010000', key)
def test_make_ratelimit_cache_key_1m(self): self.now = 65 ts = ratelimit.get_timeslice(60) key = ratelimit._make_ratelimit_cache_key('a', ts) self.assertEquals('rl:a-000100', key)
def test_make_ratelimit_cache_key_1s(self): self.now = 14 ts = ratelimit.get_timeslice(1) key = ratelimit._make_ratelimit_cache_key('a', ts) self.assertEquals('rl:a-000014', key)
def test_get_timeslice(self): self.now = 125 ts = ratelimit.get_timeslice(60) self.assertEquals(120, ts.beginning) self.assertEquals(180, ts.end) self.assertEquals(55, ts.remaining)
def test_get_usage(self): self.now = 24 * 3600 + 5 * 3600 ts = ratelimit.get_timeslice(3600) self.assertEquals(None, ratelimit.get_usage('a', ts)) ratelimit.record_usage('a', ts) self.assertEquals(1, ratelimit.get_usage('a', ts))
def test_append_time_slice_1m(self): self.now = 65 ts = ratelimit.get_timeslice(60) key = ratelimit._append_time_slice('a', ts) self.assertEquals('a-000100', key)