← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:legitimate-polls into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:legitimate-polls into launchpad:master.

Commit message:
Restrict poll creation to AnyLegitimatePerson

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/400340

This makes them a bit less trivial to abuse.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:legitimate-polls into launchpad:master.
diff --git a/lib/lp/registry/browser/poll.py b/lib/lp/registry/browser/poll.py
index 234139a..066407c 100644
--- a/lib/lp/registry/browser/poll.py
+++ b/lib/lp/registry/browser/poll.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -36,6 +36,7 @@ from lp.app.browser.launchpadform import (
     )
 from lp.registry.browser.person import PersonView
 from lp.registry.interfaces.poll import (
+    CannotCreatePoll,
     IPoll,
     IPollOption,
     IPollOptionSet,
@@ -55,6 +56,7 @@ from lp.services.webapp import (
     NavigationMenu,
     stepthrough,
     )
+from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.breadcrumb import TitleBreadcrumb
 
 
@@ -409,6 +411,12 @@ class PollAddView(LaunchpadFormView):
 
     page_title = 'New poll'
 
+    def __init__(self, context, request):
+        if not check_permission("launchpad.AnyLegitimatePerson", context):
+            raise CannotCreatePoll(
+                "You do not have permission to create polls.")
+        super(PollAddView, self).__init__(context, request)
+
     @property
     def cancel_url(self):
         """See `LaunchpadFormView`."""
diff --git a/lib/lp/registry/browser/tests/test_poll.py b/lib/lp/registry/browser/tests/test_poll.py
index 825bb81..fa1674d 100644
--- a/lib/lp/registry/browser/tests/test_poll.py
+++ b/lib/lp/registry/browser/tests/test_poll.py
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for IPoll views."""
@@ -7,10 +7,22 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 
+from datetime import (
+    datetime,
+    timedelta,
+    )
 import os
 
-from lp.registry.interfaces.poll import PollAlgorithm
-from lp.testing import TestCaseWithFactory
+import pytz
+
+from lp.registry.interfaces.poll import (
+    CannotCreatePoll,
+    PollAlgorithm,
+    )
+from lp.testing import (
+    BrowserTestCase,
+    TestCaseWithFactory,
+    )
 from lp.testing.layers import DatabaseFunctionalLayer
 from lp.testing.views import create_view
 
@@ -39,3 +51,38 @@ class TestPollVoteView(TestCaseWithFactory):
         self.assertEqual(
             'poll-vote-condorcet.pt',
             os.path.basename(view.template.filename))
+
+
+class TestPollAddView(BrowserTestCase):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPollAddView, self).setUp()
+        self.pushConfig(
+            "launchpad", min_legitimate_karma=5, min_legitimate_account_age=5)
+
+    def test_new_user(self):
+        # A brand new user cannot create polls.
+        new_person = self.factory.makePerson()
+        team = self.factory.makeTeam(owner=new_person)
+        self.assertRaises(
+            CannotCreatePoll,
+            self.getViewBrowser, team, view_name="+newpoll", user=new_person)
+
+    def test_legitimate_user(self):
+        # A user with some kind of track record can create polls.
+        person = self.factory.makePerson(karma=10)
+        team = self.factory.makeTeam(owner=person)
+        now = datetime.now(pytz.UTC)
+        browser = self.getViewBrowser(team, view_name="+newpoll", user=person)
+        browser.getControl("The unique name of this poll").value = "colour"
+        browser.getControl("The title of this poll").value = "Favourite Colour"
+        browser.getControl("The date and time when this poll opens").value = (
+            str(now + timedelta(days=1)))
+        browser.getControl("The date and time when this poll closes").value = (
+            str(now + timedelta(days=2)))
+        browser.getControl(
+            "The proposition that is going to be voted").value = (
+                "What is your favourite colour?")
+        browser.getControl("Continue").click()
diff --git a/lib/lp/registry/interfaces/poll.py b/lib/lp/registry/interfaces/poll.py
index 8fab288..6b894a4 100644
--- a/lib/lp/registry/interfaces/poll.py
+++ b/lib/lp/registry/interfaces/poll.py
@@ -1,7 +1,8 @@
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __all__ = [
+    'CannotCreatePoll',
     'IPoll',
     'IPollSet',
     'IPollSubset',
@@ -26,7 +27,9 @@ from lazr.enum import (
     DBEnumeratedType,
     DBItem,
     )
+from lazr.restful.declarations import error_status
 import pytz
+from six.moves import http_client
 from zope.component import getUtility
 from zope.interface import (
     Attribute,
@@ -112,6 +115,11 @@ class PollStatus:
     ALL = frozenset([OPEN, CLOSED, NOT_YET_OPENED])
 
 
+@error_status(http_client.FORBIDDEN)
+class CannotCreatePoll(Exception):
+    pass
+
+
 class IPoll(Interface):
     """A poll for a given proposition in a team."""
 
diff --git a/lib/lp/registry/model/poll.py b/lib/lp/registry/model/poll.py
index b34786b..419a620 100644
--- a/lib/lp/registry/model/poll.py
+++ b/lib/lp/registry/model/poll.py
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import absolute_import, print_function, unicode_literals
@@ -33,6 +33,7 @@ from zope.interface import implementer
 
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.interfaces.poll import (
+    CannotCreatePoll,
     IPoll,
     IPollOption,
     IPollOptionSet,
@@ -51,6 +52,7 @@ from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import sqlvalues
 from lp.services.database.stormbase import StormBase
 from lp.services.tokens import create_token
+from lp.services.webapp.authorization import check_permission
 
 
 @implementer(IPoll)
@@ -292,8 +294,13 @@ class PollSet:
     """See IPollSet."""
 
     def new(self, team, name, title, proposition, dateopens, datecloses,
-            secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):
+            secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE,
+            check_permissions=True):
         """See IPollSet."""
+        if (check_permissions and
+                not check_permission("launchpad.AnyLegitimatePerson", team)):
+            raise CannotCreatePoll(
+                "You do not have permission to create polls.")
         poll = Poll(
             team=team, name=name, title=title,
             proposition=proposition, dateopens=dateopens,
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index d4ed1fd..ec73f72 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -837,7 +837,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         return getUtility(IPollSet).new(
             team, name, title, proposition, dateopens, datecloses,
             PollSecrecy.SECRET, allowspoilt=True,
-            poll_type=poll_type)
+            poll_type=poll_type, check_permissions=False)
 
     def makeTranslationGroup(self, owner=None, name=None, title=None,
                              summary=None, url=None):