You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2016/04/29 23:01:28 UTC
allura git commit: [#8082] options for rate-limiting ticket and wiki
page creation, per-user across the whole site
Repository: allura
Updated Branches:
refs/heads/db/8082 [created] f9ca424f3
[#8082] options for rate-limiting ticket and wiki page creation, per-user across the whole site
Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/f9ca424f
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/f9ca424f
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/f9ca424f
Branch: refs/heads/db/8082
Commit: f9ca424f30a226aed3d5e0d9ab63777ae8633156
Parents: ec3cf88
Author: Dave Brondsema <da...@brondsema.net>
Authored: Fri Apr 29 17:01:20 2016 -0400
Committer: Dave Brondsema <da...@brondsema.net>
Committed: Fri Apr 29 17:01:20 2016 -0400
----------------------------------------------------------------------
Allura/allura/model/artifact.py | 32 ++++++++++++++++----
Allura/development.ini | 24 +++++++++------
ForgeTracker/forgetracker/model/ticket.py | 7 +++++
ForgeTracker/forgetracker/tracker_main.py | 4 +--
.../forgewiki/tests/functional/test_root.py | 29 ++++++++++++++++--
ForgeWiki/forgewiki/wiki_main.py | 10 +++---
requirements.txt | 2 +-
7 files changed, 84 insertions(+), 24 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/Allura/allura/model/artifact.py
----------------------------------------------------------------------
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 29931b7..e6b4b89 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -431,21 +431,23 @@ class Artifact(MappedClass, SearchIndexable):
return h.gen_message_id(self._id)
@classmethod
- def is_limit_exceeded(cls, app_config):
+ def is_limit_exceeded(cls, app_config, user=None, count_by_user=None):
"""
Returns True if any of artifact creation rate limits are exceeded,
False otherwise
"""
pkg = cls.__module__.split('.', 1)[0]
opt = u'{}.rate_limits'.format(pkg)
- count = cls.query.find(dict(app_config_id=app_config._id)).count()
+ count_in_app = cls.query.find(dict(app_config_id=app_config._id)).count()
provider = plugin.ProjectRegistrationProvider.get()
start = provider.registration_date(app_config.project)
- # have to have the replace because, the generation_time is offset-aware
- # UTC and h.rate_limit uses offset-naive UTC dates
+ # need the replace because the generation_time is offset-aware UTC and h.rate_limit uses offset-naive UTC dates
start = start.replace(tzinfo=None)
+
try:
- h.rate_limit(opt, count, start)
+ h.rate_limit(opt, count_in_app, start)
+ if user and count_by_user is not None:
+ h.rate_limit(opt + '_per_user', count_by_user, user.registration_date())
except forge_exc.RatelimitError:
return True
return False
@@ -458,7 +460,9 @@ class Snapshot(Artifact):
session = artifact_orm_session
name = 'artifact_snapshot'
unique_indexes = [('artifact_class', 'artifact_id', 'version')]
- indexes = [('artifact_id', 'version')]
+ indexes = [('artifact_id', 'version'),
+ 'author.id',
+ ]
_id = FieldProperty(S.ObjectId)
artifact_id = FieldProperty(S.ObjectId)
@@ -617,6 +621,22 @@ class VersionedArtifact(Artifact):
HC = self.__mongometa__.history_class
HC.query.remove(dict(artifact_id=self._id))
+ @classmethod
+ def is_limit_exceeded(cls, *args, **kwargs):
+ if 'user' in kwargs:
+ # count distinct items, not total (e.g. many edits to a single wiki page doesn't count against you)
+ HC = cls.__mongometa__.history_class
+ distinct_artifacts_by_user = HC.query.find({'author.id': kwargs['user']._id}).distinct('artifact_id')
+ """
+ # some useful debugging:
+ log.info(distinct_artifacts_by_user)
+ for art_id in distinct_artifacts_by_user:
+ art = cls.query.get(_id=art_id)
+ log.info(' ' + art.url())
+ """
+ kwargs['count_by_user'] = len(distinct_artifacts_by_user)
+ return super(VersionedArtifact, cls).is_limit_exceeded(*args, **kwargs)
+
class Message(Artifact):
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/Allura/development.ini
----------------------------------------------------------------------
diff --git a/Allura/development.ini b/Allura/development.ini
index bad2383..3760896 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -450,21 +450,27 @@ forgemail.port = 8825
; your mail and DNS configuration
forgemail.domain = .in.localhost
-; Specify the number of projects allowed to be created by a user
-; depending on the age of their user account.
-; Keys are number of seconds, values are max number of projects allowed
-; (including the default user-project, so you probably want to add 1)
-; This example allows for 1 project if the account is less than an hour old
-; and 5 projects if the account is less than a day old. No limits after that
+; Specify the number of projects allowed to be created by a user depending on the age of their user account.
+; Keys are number of seconds, values are max number allowed until that time period is reached.
+; NOTE: this includes the default user-profile project, so you probably want to set any limits higher by 1.
+; This example allows for up to 2 total projects if the account is less than an hour old
+; and 6 total projects if the account is less than a day old. No limits after that.
;project.rate_limits = {"3600": 2, "86400": 6}
-; Specify the number of artifacts allowed to be created by a user
-; depending on the age of the project.
+; Specify the number of artifacts allowed to be created in a project, depending on the age of the project
; Currently supports only tickets and wiki page creation rate limiting.
-; See project.rate_limits help text above for keys & values meaning.
+; Keys are number of seconds, values are max number allowed until that time period is reached
;forgewiki.rate_limits = {"3600": 100, "172800": 10000}
;forgetracker.rate_limits = {"3600": 100, "172800": 10000}
+; Number of different pages a user can create or edit, per time period, across all projects
+; NOTE: this includes default "Home" wiki page created for the user-project and any other projects created by the user
+; Keys are number of seconds, values are max number allowed until that time period is reached
+;forgewiki.rate_limits_per_user = {"60": 1, "120": 3, "900": 5, "1800": 7, "3600": 10, "7200": 15, "86400": 20, "604800": 50, "2592000": 200}
+; Number of tickets a user can create, per time period, across all projects
+; Keys are number of seconds, values are max number allowed until that time period is reached
+;forgetracker.rate_limits_per_user = {"60": 1, "120": 3, "900": 5, "1800": 7, "3600": 10, "7200": 15, "86400": 20, "604800": 50, "2592000": 200}
+
; set this to "false" if you are deploying to production and want performance improvements
auto_reload_templates = true
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/ForgeTracker/forgetracker/model/ticket.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index c21a7d8..3cc89a2 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -615,6 +615,7 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
'ticket_num',
('app_config_id', 'custom_fields._milestone'),
'import_id',
+ 'reported_by_id',
]
unique_indexes = [
('app_config_id', 'ticket_num'),
@@ -1295,6 +1296,12 @@ class Ticket(VersionedArtifact, ActivityObject, VotableArtifact):
self.app.config.options.get('AllowEmailPosting', True),
discussion_disabled=self.discussion_disabled)
+ @classmethod
+ def is_limit_exceeded(cls, *args, **kwargs):
+ if 'user' in kwargs:
+ kwargs['count_by_user'] = cls.query.find({'reported_by_id': kwargs['user']._id}).count()
+ return super(Ticket, cls).is_limit_exceeded(*args, **kwargs)
+
class TicketAttachment(BaseAttachment):
thumbnail_size = (100, 100)
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/ForgeTracker/forgetracker/tracker_main.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 7e9036f..50ea154 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -657,7 +657,7 @@ class RootController(BaseController, FeedController):
require_access(c.app, 'read')
def rate_limit(self, redir='..'):
- if TM.Ticket.is_limit_exceeded(c.app.config):
+ if TM.Ticket.is_limit_exceeded(c.app.config, user=c.user):
msg = 'Ticket creation rate limit exceeded. '
log.warn(msg + c.app.config.url())
flash(msg + 'Please try again later.', 'error')
@@ -1830,7 +1830,7 @@ class RootRestController(BaseController, AppRestControllerMixin):
@validate(W.ticket_form, error_handler=h.json_validation_error)
def new(self, ticket_form=None, **post_data):
require_access(c.app, 'create')
- if TM.Ticket.is_limit_exceeded(c.app.config):
+ if TM.Ticket.is_limit_exceeded(c.app.config, user=c.user):
msg = 'Ticket creation rate limit exceeded. '
log.warn(msg + c.app.config.url())
raise forge_exc.HTTPTooManyRequests()
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/ForgeWiki/forgewiki/tests/functional/test_root.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/tests/functional/test_root.py b/ForgeWiki/forgewiki/tests/functional/test_root.py
index 26c0949..fbe529f 100644
--- a/ForgeWiki/forgewiki/tests/functional/test_root.py
+++ b/ForgeWiki/forgewiki/tests/functional/test_root.py
@@ -804,7 +804,7 @@ class TestRootController(TestController):
assert_equal(wf['status'], 'error')
assert_equal(
wf['message'],
- 'Page creation rate limit exceeded. Please try again later.')
+ 'Page create/edit rate limit exceeded. Please try again later.')
def test_rate_limit_update(self):
# Set rate limit to unlimit
@@ -826,10 +826,35 @@ class TestRootController(TestController):
assert_equal(wf['status'], 'error')
assert_equal(
wf['message'],
- 'Page creation rate limit exceeded. Please try again later.')
+ 'Page create/edit rate limit exceeded. Please try again later.')
p = model.Page.query.get(title='page2')
assert_equal(p, None)
+ def test_rate_limit_by_user(self):
+ # also test that multiple edits to a page counts as one page towards the limit
+
+ # test/wiki/Home and test/sub1/wiki already were created by this user
+ # and proactively get the user-project wiki created (otherwise it'll be created during the subsequent edits)
+ self.app.get('/u/test-admin/wiki/')
+ with h.push_config(config, **{'forgewiki.rate_limits_per_user': '{"3600": 5}'}):
+ r = self.app.post('/p/test/wiki/page123/update', # page 4 (remember, 3 other projects' wiki pages)
+ dict(text='Starting a new page, ok', title='page123'))
+ assert_equal(self.webflash(r), '')
+ r = self.app.post('/p/test/wiki/page123/update',
+ dict(text='Editing some', title='page123'))
+ assert_equal(self.webflash(r), '')
+ r = self.app.post('/p/test/wiki/page123/update',
+ dict(text='Still editing', title='page123'))
+ assert_equal(self.webflash(r), '')
+ r = self.app.post('/p/test/wiki/pageABC/update', # page 5
+ dict(text='Another new page', title='pageABC'))
+ assert_equal(self.webflash(r), '')
+ r = self.app.post('/p/test/wiki/pageZZZZZ/update', # page 6
+ dict(text='This new page hits the limit', title='pageZZZZZ'))
+ wf = json.loads(self.webflash(r))
+ assert_equal(wf['status'], 'error')
+ assert_equal(wf['message'], 'Page create/edit rate limit exceeded. Please try again later.')
+
def test_sidebar_admin_menu(self):
r = self.app.get('/p/test/wiki/Home/')
menu = r.html.find('div', {'id': 'sidebar-admin-menu'})
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index c5325b9..2350509 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -534,8 +534,8 @@ class PageController(BaseController, FeedController):
self.rate_limit()
def rate_limit(self):
- if WM.Page.is_limit_exceeded(c.app.config):
- msg = 'Page creation rate limit exceeded. '
+ if WM.Page.is_limit_exceeded(c.app.config, user=c.user):
+ msg = 'Page create/edit rate limit exceeded. '
log.warn(msg + c.app.config.url())
flash(msg + 'Please try again later.', 'error')
redirect('..')
@@ -607,6 +607,7 @@ class PageController(BaseController, FeedController):
@without_trailing_slash
@expose('jinja:forgewiki:templates/wiki/page_edit.html')
def edit(self):
+ self.rate_limit() # check before trying to save
page_exists = self.page
if self.page:
require_access(self.page, 'edit')
@@ -718,6 +719,7 @@ class PageController(BaseController, FeedController):
flash('You must provide a title for the page.', 'error')
redirect('edit')
title = title.replace('/', '-')
+ self.rate_limit()
if not self.page:
# the page doesn't exist yet, so create it
self.page = WM.Page.upsert(self.title)
@@ -861,8 +863,8 @@ class PageRestController(BaseController):
with h.notifications_disabled(c.project):
if not self.page:
require_access(c.app, 'create')
- if WM.Page.is_limit_exceeded(c.app.config):
- log.warn('Page creation rate limit exceeded. %s',
+ if WM.Page.is_limit_exceeded(c.app.config, user=c.user):
+ log.warn('Page create/edit rate limit exceeded. %s',
c.app.config.url())
raise forge_exc.HTTPTooManyRequests()
self.page = WM.Page.upsert(title)
http://git-wip-us.apache.org/repos/asf/allura/blob/f9ca424f/requirements.txt
----------------------------------------------------------------------
diff --git a/requirements.txt b/requirements.txt
index 4b3945b..eff06b7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,7 +21,7 @@ httplib2==0.7.4
iso8601==0.1.4
Jinja2==2.8
Markdown==2.2.0
-Ming==0.5.2
+Ming==0.5.4
oauth2==1.5.170
# tg2 dep PasteDeploy must specified before TurboGears2, to avoid a version/allow-hosts problem
Paste==1.7.5.1