← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/branch-subscribe-aag into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/branch-subscribe-aag into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #933768 in Launchpad itself: "Update branch to use information_visibility_policy"
  https://bugs.launchpad.net/launchpad/+bug/933768

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/branch-subscribe-aag/+merge/108881

IBranch.subscribe() now forbids open and delegated teams from being subscribed to private branches. This necessitated moving the exception into lp.app from bugs. I have also carried over the test for the webservice, and have written a few tests for the branch case.

I have also added the ability to IBranch.subscribe() to add a AccessArtifactGrant if required when a person is subscribed to a branch and added a test for that behaviour too.
-- 
https://code.launchpad.net/~stevenk/launchpad/branch-subscribe-aag/+merge/108881
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/branch-subscribe-aag into lp:launchpad.
=== modified file 'lib/lp/app/errors.py'
--- lib/lp/app/errors.py	2011-06-16 20:12:00 +0000
+++ lib/lp/app/errors.py	2012-06-06 06:11:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Cross application type errors for launchpad."""
@@ -9,6 +9,7 @@
     'NameLookupFailed',
     'NotFoundError',
     'POSTToNonCanonicalURL',
+    'SubscriptionPrivacyViolation',
     'TranslationUnavailable',
     'UnexpectedFormData',
     'UserCannotUnsubscribePerson',
@@ -75,5 +76,10 @@
     """User does not have permission to unsubscribe person or team."""
 
 
+@error_status(httplib.BAD_REQUEST)
+class SubscriptionPrivacyViolation(Exception):
+    """The subscription would violate privacy policies."""
+
+
 # Slam a 401 response code onto all ForbiddenAttribute errors.
 error_status(httplib.UNAUTHORIZED)(ForbiddenAttribute)

=== added file 'lib/lp/app/tests/test_errors.py'
--- lib/lp/app/tests/test_errors.py	1970-01-01 00:00:00 +0000
+++ lib/lp/app/tests/test_errors.py	2012-06-06 06:11:04 +0000
@@ -0,0 +1,33 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from httplib import (
+    BAD_REQUEST,
+    UNAUTHORIZED,
+    )
+
+from lp.app.errors import (
+    SubscriptionPrivacyViolation,
+    UserCannotUnsubscribePerson,
+    )
+from lp.testing import TestCase
+from lp.testing.layers import FunctionalLayer
+from lp.testing.views import create_webservice_error_view
+
+
+class TestWebServiceErrors(TestCase):
+    """ Test that errors are correctly mapped to HTTP status codes."""
+
+    layer = FunctionalLayer
+
+    def test_UserCannotUnsubscribePerson_unauthorized(self):
+        error_view = create_webservice_error_view(
+            UserCannotUnsubscribePerson())
+        self.assertEqual(UNAUTHORIZED, error_view.status)
+
+    def test_SubscriptionPrivacyViolation_bad_request(self):
+        error_view = create_webservice_error_view(
+            SubscriptionPrivacyViolation())
+        self.assertEqual(BAD_REQUEST, error_view.status)

=== modified file 'lib/lp/bugs/browser/bugsubscription.py'
--- lib/lp/bugs/browser/bugsubscription.py	2012-03-13 00:45:33 +0000
+++ lib/lp/bugs/browser/bugsubscription.py	2012-06-06 06:11:04 +0000
@@ -37,11 +37,11 @@
     LaunchpadFormView,
     ReturnToReferrerMixin,
     )
+from lp.app.errors import SubscriptionPrivacyViolation
 from lp.bugs.browser.structuralsubscription import (
     expose_structural_subscription_data_to_js,
     )
 from lp.bugs.enums import BugNotificationLevel
-from lp.bugs.errors import SubscriptionPrivacyViolation
 from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugsubscription import IBugSubscription
 from lp.bugs.model.personsubscriptioninfo import PersonSubscriptions

=== modified file 'lib/lp/bugs/errors.py'
--- lib/lp/bugs/errors.py	2011-11-25 04:25:00 +0000
+++ lib/lp/bugs/errors.py	2012-06-06 06:11:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Errors used in the lp/bugs modules."""
@@ -8,7 +8,6 @@
     'BugCannotBePrivate',
     'InvalidBugTargetType',
     'InvalidDuplicateValue',
-    'SubscriptionPrivacyViolation',
 ]
 
 import httplib
@@ -29,10 +28,5 @@
 
 
 @error_status(httplib.BAD_REQUEST)
-class SubscriptionPrivacyViolation(Exception):
-    """The subscription would violate privacy policies."""
-
-
-@error_status(httplib.BAD_REQUEST)
 class BugCannotBePrivate(Exception):
     """The bug is not allowed to be private."""

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-06-05 02:03:44 +0000
+++ lib/lp/bugs/model/bug.py	2012-06-06 06:11:04 +0000
@@ -85,6 +85,7 @@
 from lp.app.enums import ServiceUsage
 from lp.app.errors import (
     NotFoundError,
+    SubscriptionPrivacyViolation,
     UserCannotUnsubscribePerson,
     )
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -105,7 +106,6 @@
 from lp.bugs.errors import (
     BugCannotBePrivate,
     InvalidDuplicateValue,
-    SubscriptionPrivacyViolation,
     )
 from lp.bugs.interfaces.bug import (
     IBug,
@@ -177,8 +177,8 @@
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.role import IPersonRoles
+from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sharingjob import IRemoveBugSubscriptionsJobSource
-from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.interfaces.sourcepackage import ISourcePackage
 from lp.registry.model.accesspolicy import reconcile_access_for_artifact
 from lp.registry.model.person import (

=== modified file 'lib/lp/bugs/tests/test_errors.py'
--- lib/lp/bugs/tests/test_errors.py	2012-01-01 02:58:52 +0000
+++ lib/lp/bugs/tests/test_errors.py	2012-06-06 06:11:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for bugs errors."""
@@ -15,7 +15,6 @@
 from lp.bugs.errors import (
     InvalidBugTargetType,
     InvalidDuplicateValue,
-    SubscriptionPrivacyViolation,
     )
 from lp.testing import TestCase
 from lp.testing.layers import FunctionalLayer
@@ -35,8 +34,3 @@
         error_view = create_webservice_error_view(
             InvalidDuplicateValue("Dup"))
         self.assertEqual(EXPECTATION_FAILED, error_view.status)
-
-    def test_SubscriptionPrivacyViolation_bad_request(self):
-        error_view = create_webservice_error_view(
-            SubscriptionPrivacyViolation())
-        self.assertEqual(BAD_REQUEST, error_view.status)

=== modified file 'lib/lp/code/browser/branchsubscription.py'
--- lib/lp/code/browser/branchsubscription.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/branchsubscription.py	2012-06-06 06:11:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -20,6 +20,7 @@
     LaunchpadEditFormView,
     LaunchpadFormView,
     )
+from lp.app.errors import SubscriptionPrivacyViolation
 from lp.code.enums import BranchSubscriptionNotificationLevel
 from lp.code.interfaces.branchsubscription import IBranchSubscription
 from lp.services.webapp import (
@@ -226,14 +227,17 @@
         person = data['person']
         subscription = self.context.getSubscription(person)
         if subscription is None:
-            self.context.subscribe(
-                person, notification_level, max_diff_lines, review_level,
-                self.user)
-
-            self.add_notification_message(
-                '%s has been subscribed to this branch with: '
-                % person.displayname, notification_level, max_diff_lines,
-                review_level)
+            try:
+                self.context.subscribe(
+                    person, notification_level, max_diff_lines, review_level,
+                    self.user)
+            except SubscriptionPrivacyViolation as error:
+                self.setFieldError('person', unicode(error))
+            else:
+                self.add_notification_message(
+                    '%s has been subscribed to this branch with: '
+                    % person.displayname, notification_level, max_diff_lines,
+                    review_level)
         else:
             self.add_notification_message(
                 '%s was already subscribed to this branch with: '

=== modified file 'lib/lp/code/browser/tests/test_branchsubscription.py'
--- lib/lp/code/browser/tests/test_branchsubscription.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/browser/tests/test_branchsubscription.py	2012-06-06 06:11:04 +0000
@@ -1,13 +1,18 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for BranchSubscriptions."""
 
 __metaclass__ = type
 
+from lp.registry.enums import InformationType
 from lp.services.webapp.interfaces import IPrimaryContext
-from lp.testing import TestCaseWithFactory
+from lp.testing import (
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.views import create_initialized_view
 
 
 class TestBranchSubscriptionPrimaryContext(TestCaseWithFactory):
@@ -22,3 +27,26 @@
         self.assertEqual(
             IPrimaryContext(subscription).context,
             IPrimaryContext(subscription.branch).context)
+
+
+class TestBranchSubscriptionAddOtherView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_cannot_subscribe_open_team_to_private_branch(self):
+        owner = self.factory.makePerson()
+        branch = self.factory.makeBranch(
+            information_type=InformationType.USERDATA, owner=owner)
+        team = self.factory.makeTeam()
+        form = {
+            'field.person': team.name,
+            'field.notification_level': 'NOEMAIL',
+            'field.max_diff_lines': 'NODIFF',
+            'field.review_level': 'NOEMAIL',
+            'field.actions.subscribe_action': 'Subscribe'}
+        with person_logged_in(owner):
+            view = create_initialized_view(
+                branch, '+addsubscriber', pricipal=owner, form=form)
+            self.assertContentEqual(
+                ['Open and delegated teams cannot be subscribed to private '
+                'branches.'], view.errors)

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2012-06-05 02:03:44 +0000
+++ lib/lp/code/model/branch.py	2012-06-06 06:11:04 +0000
@@ -2,8 +2,6 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212,W0141,F0401
-from lp.bugs.interfaces.bugtarget import IBugTarget
-from lp.registry.interfaces.accesspolicy import IAccessPolicyArtifactSource, IAccessPolicySource, IAccessArtifactSource
 
 __metaclass__ = type
 __all__ = [
@@ -52,11 +50,15 @@
     )
 
 from lp import _
-from lp.app.errors import UserCannotUnsubscribePerson
+from lp.app.errors import (
+    SubscriptionPrivacyViolation,
+    UserCannotUnsubscribePerson,
+    )
 from lp.app.interfaces.launchpad import (
     ILaunchpadCelebrities,
     IPrivacy,
     )
+from lp.app.interfaces.services import IService
 from lp.bugs.interfaces.bugtask import (
     BugTaskSearchParams,
     IBugTaskSet,
@@ -155,6 +157,7 @@
     SQLBase,
     sqlvalues,
     )
+from lp.services.features import getFeatureFlag
 from lp.services.helpers import shortlist
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
@@ -817,6 +820,11 @@
     def subscribe(self, person, notification_level, max_diff_lines,
                   code_review_level, subscribed_by):
         """See `IBranch`."""
+        if (person.is_team and self.information_type in
+            PRIVATE_INFORMATION_TYPES and person.anyone_can_join()):
+            raise SubscriptionPrivacyViolation(
+                "Open and delegated teams cannot be subscribed to private "
+                "branches.")
         # If the person is already subscribed, update the subscription with
         # the specified notification details.
         subscription = self.getSubscription(person)
@@ -831,6 +839,17 @@
             subscription.notification_level = notification_level
             subscription.max_diff_lines = max_diff_lines
             subscription.review_level = code_review_level
+        # Grant the subscriber access if they can't see the branch (if the
+        # database triggers aren't going to do it for us).
+        trigger_flag = 'disclosure.access_mirror_triggers.removed'
+        if bool(getFeatureFlag(trigger_flag)):
+            service = getUtility(IService, 'sharing')
+            branches, ignored = service.getVisibleArtifacts(
+                person, branches=[self])
+            if not branches:
+                service.ensureAccessGrants(
+                    subscribed_by, person, branches=[self],
+                    ignore_permissions=True)
         return subscription
 
     def getSubscription(self, person):
@@ -858,13 +877,12 @@
         """See `IBranch`."""
         return self.getSubscription(person) is not None
 
-    def unsubscribe(self, person, unsubscribed_by, **kwargs):
+    def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
         """See `IBranch`."""
         subscription = self.getSubscription(person)
         if subscription is None:
             # Silent success seems order of the day (like bugs).
             return
-        ignore_permissions = kwargs.get('ignore_permissions', False)
         if (not ignore_permissions
             and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
             raise UserCannotUnsubscribePerson(

=== modified file 'lib/lp/code/model/tests/test_branchsubscription.py'
--- lib/lp/code/model/tests/test_branchsubscription.py	2012-01-01 02:58:52 +0000
+++ lib/lp/code/model/tests/test_branchsubscription.py	2012-06-06 06:11:04 +0000
@@ -1,15 +1,19 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the BranchSubscrptions model object.."""
 
 __metaclass__ = type
 
-from lp.app.errors import UserCannotUnsubscribePerson
+from lp.app.errors import (
+    SubscriptionPrivacyViolation,
+    UserCannotUnsubscribePerson,
+    )
 from lp.code.enums import (
     BranchSubscriptionNotificationLevel,
     CodeReviewNotificationLevel,
     )
+from lp.registry.enums import InformationType
 from lp.testing import (
     person_logged_in,
     TestCaseWithFactory,
@@ -67,6 +71,30 @@
             subscription.person,
             self.factory.makePerson())
 
+    def test_cannot_subscribe_open_team_to_private_branch(self):
+        """It is forbidden to subscribe a open team to a private branch."""
+        owner = self.factory.makePerson()
+        branch = self.factory.makeBranch(
+            information_type=InformationType.USERDATA, owner=owner)
+        team = self.factory.makeTeam()
+        with person_logged_in(owner):
+            self.assertRaises(
+                SubscriptionPrivacyViolation, branch.subscribe, team, None,
+                None, None, owner)
+
+    def test_subscribe_gives_access(self):
+        """Subscribing a user to a branch gives them access."""
+        owner = self.factory.makePerson()
+        branch = self.factory.makeBranch(
+            information_type=InformationType.USERDATA, owner=owner)
+        subscribee = self.factory.makePerson()
+        with person_logged_in(owner):
+            self.assertFalse(branch.visibleByUser(subscribee))
+            branch.subscribe(
+                subscribee, BranchSubscriptionNotificationLevel.NOEMAIL,
+                None, CodeReviewNotificationLevel.NOEMAIL, owner)
+            self.assertTrue(branch.visibleByUser(subscribee))
+
 
 class TestBranchSubscriptionCanBeUnsubscribedbyUser(TestCaseWithFactory):
     """Tests for BranchSubscription.canBeUnsubscribedByUser."""


Follow ups