← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/new-perm-for-archive-subscribers into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/new-perm-for-archive-subscribers into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/new-perm-for-archive-subscribers/+merge/148974

Currently, subscribers to P3As are given launchpad.View on the archive. This makes restricting things that we don't want subscribers seeing, such as Archive:+packages full of gross hacks. I have ripped those out, and added a new permission, launchpad.SubscriberView and allowed it to see Archive:+index. Archive:+packages remains as launchpad.View.
-- 
https://code.launchpad.net/~stevenk/launchpad/new-perm-for-archive-subscribers/+merge/148974
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/new-perm-for-archive-subscribers into lp:launchpad.
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py	2013-02-06 04:22:43 +0000
+++ lib/lp/app/browser/tales.py	2013-02-18 04:04:21 +0000
@@ -778,7 +778,6 @@
 
             return "sprite %s" % sprite_string
 
-
     def default_logo_resource(self, context):
         # XXX: mars 2008-08-22 bug=260468
         # This should be refactored.  We shouldn't have to do type-checking
@@ -1041,7 +1040,7 @@
 
         if self._context.priority:
             priority = self._context.priority.title.lower()
-            sprite_str = sprite_str +  "-%s" % priority
+            sprite_str = sprite_str + "-%s" % priority
 
         if self._context.private:
             sprite_str = sprite_str + ' private'
@@ -2602,8 +2601,9 @@
         privacy = IPrivacy(view_context, None)
         if privacy is None or not privacy.private:
             return False
-        can_view = check_permission('launchpad.View', view_context)
-        return not can_view
+        if check_permission('launchpad.SubscriberView', view_context):
+            return False
+        return not check_permission('launchpad.View', view_context)
 
     def pagetype(self):
         return getattr(self.context, '__pagetype__', 'unset')

=== modified file 'lib/lp/permissions.zcml'
--- lib/lp/permissions.zcml	2012-08-20 16:38:10 +0000
+++ lib/lp/permissions.zcml	2013-02-18 04:04:21 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -16,6 +16,8 @@
     id="launchpad.LimitedView" title="View basic details like name, URL" access_level="read" />
 
   <permission
+    id="launchpad.SubscriberView" title="View shallow details" access_level="read" />
+  <permission
     id="launchpad.AnyPerson" title="Any Authenticated Person"
     access_level="write" />
 

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py	2013-01-25 04:59:23 +0000
+++ lib/lp/security.py	2013-02-18 04:04:21 +0000
@@ -118,7 +118,6 @@
 from lp.registry.interfaces.irc import IIrcID
 from lp.registry.interfaces.location import IPersonLocation
 from lp.registry.interfaces.milestone import (
-    IAbstractMilestone,
     IMilestone,
     IProjectGroupMilestone,
     )
@@ -2469,6 +2468,18 @@
     usedfor = IHWDeviceClass
 
 
+class SubscriberViewArchive(AuthorizationBase):
+    """Restrict viewing of private archives."""
+    permission = 'launchpad.SubscriberView'
+    usedfor = IArchive
+
+    def checkAuthenticated(self, user):
+        filter = get_enabled_archive_filter(
+            user.person, include_subscribed=True)
+        return not IStore(self.obj).find(
+            Archive.id, And(Archive.id == self.obj.id, filter)).is_empty()
+
+
 class ViewArchive(AuthorizationBase):
     """Restrict viewing of private archives.
 
@@ -2498,8 +2509,7 @@
         if user.inTeam(self.obj.owner):
             return True
 
-        filter = get_enabled_archive_filter(
-            user.person, include_subscribed=True)
+        filter = get_enabled_archive_filter(user.person)
         return not IStore(self.obj).find(
             Archive.id, And(Archive.id == self.obj.id, filter)).is_empty()
 

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py	2012-12-12 05:27:55 +0000
+++ lib/lp/soyuz/browser/archive.py	2013-02-18 04:04:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for archive."""
@@ -56,7 +56,6 @@
     SimpleTerm,
     SimpleVocabulary,
     )
-from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
 from lp import _
@@ -1005,16 +1004,6 @@
     """Detailed packages view for an archive."""
     implements(IArchivePackagesActionMenu)
 
-    def initialize(self):
-        super(ArchivePackagesView, self).initialize()
-        if self.context.private:
-            # To see the +packages page, you must be an uploader, or a
-            # commercial admin.
-            if not check_permission('launchpad.Append', self.context):
-                admins = getUtility(ILaunchpadCelebrities).commercial_admin
-                if not self.user.inTeam(admins):
-                    raise Unauthorized
-
     @property
     def page_title(self):
         return smartquote('Packages in "%s"' % self.context.displayname)

=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml	2012-11-01 03:41:36 +0000
+++ lib/lp/soyuz/browser/configure.zcml	2013-02-18 04:04:21 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -251,7 +251,7 @@
         class="lp.app.browser.launchpad.Macro"/>
     <browser:page
         for="lp.soyuz.interfaces.archive.IArchive"
-        permission="launchpad.View"
+        permission="launchpad.SubscriberView"
         name="+index"
         template="../templates/archive-index.pt"
         class="lp.soyuz.browser.archive.ArchiveView"/>

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2013-02-06 09:22:35 +0000
+++ lib/lp/soyuz/configure.zcml	2013-02-18 04:04:21 +0000
@@ -403,6 +403,9 @@
         <allow
             interface="lp.soyuz.interfaces.archive.IArchiveRestricted"/>
         <require
+            permission="launchpad.SubscriberView"
+            interface="lp.soyuz.interfaces.archive.IArchiveSubscriberView"/>
+        <require
             permission="launchpad.View"
             interface="lp.soyuz.interfaces.archive.IArchiveView"/>
         <require

=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2013-02-05 06:13:57 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2013-02-18 04:04:21 +0000
@@ -22,10 +22,11 @@
     'IArchiveAppend',
     'IArchiveCommercial',
     'IArchiveEdit',
-    'IArchiveView',
     'IArchiveEditDependenciesForm',
+    'IArchiveSubscriberView',
     'IArchivePublic',
     'IArchiveSet',
+    'IArchiveView',
     'IDistributionArchive',
     'InsufficientUploadRights',
     'InvalidComponent',
@@ -355,16 +356,109 @@
         """
 
 
-class IArchiveView(IHasBuildRecords):
-    """Archive interface for operations restricted by view privilege."""
-
-    title = TextLine(title=_("Name"), required=False, readonly=True)
-
+class IArchiveSubscriberView(Interface):
+
+    description = exported(
+        Text(
+            title=_("Description"), required=False,
+            description=_(
+                "A short description of the archive. URLs are allowed and "
+                "will be rendered as links.")))
+    is_active = Bool(
+        title=_("True if the archive is in the active state"),
+        required=False, readonly=True)
+    is_copy = Attribute("True if this archive is a copy archive.")
+    num_pkgs_building = Attribute(
+        "Tuple of packages building and waiting to build")
     publish = Bool(
         title=_("Publish"), required=False,
         description=_("Whether or not to update the APT repository.  If "
             "disabled, nothing will be published.  If the archive is "
             "private then additionally no builds will be dispatched."))
+    series_with_sources = Attribute(
+        "DistroSeries to which this archive has published sources")
+    signing_key = Object(
+        title=_('Repository sigining key.'), required=False, schema=IGPGKey)
+
+    @rename_parameters_as(name="source_name", distroseries="distro_series")
+    @operation_parameters(
+        name=TextLine(title=_("Source package name"), required=False),
+        version=TextLine(title=_("Version"), required=False),
+        status=Choice(
+            title=_('Package Publishing Status'),
+            description=_('The status of this publishing record'),
+            # Really PackagePublishingStatus, circular import fixed below.
+            vocabulary=DBEnumeratedType,
+            required=False),
+        distroseries=Reference(
+            # Really IDistroSeries, fixed below to avoid circular import.
+            Interface,
+            title=_("Distroseries name"), required=False),
+        pocket=Choice(
+            title=_("Pocket"),
+            description=_("The pocket into which this entry is published"),
+            # Really PackagePublishingPocket, circular import fixed below.
+            vocabulary=DBEnumeratedType,
+            required=False, readonly=True),
+        exact_match=Bool(
+            title=_("Exact Match"),
+            description=_("Whether or not to filter source names by exact"
+                          " matching."),
+            required=False),
+        created_since_date=Datetime(
+            title=_("Created Since Date"),
+            description=_("Return entries whose `date_created` is greater "
+                          "than or equal to this date."),
+            required=False),
+        component_name=TextLine(title=_("Component name"), required=False),
+        )
+    # Really returns ISourcePackagePublishingHistory, see below for
+    # patch to avoid circular import.
+    @call_with(eager_load=True)
+    @operation_returns_collection_of(Interface)
+    @export_read_operation()
+    def getPublishedSources(name=None, version=None, status=None,
+                            distroseries=None, pocket=None,
+                            exact_match=False, created_since_date=None,
+                            eager_load=False, component_name=None):
+        """All `ISourcePackagePublishingHistory` target to this archive.
+
+        :param name: source name filter (exact match or SQL LIKE controlled
+                     by 'exact_match' argument).
+                     Name can be a single string or a list of strings.
+        :param version: source version filter (always exact match).
+        :param status: `PackagePublishingStatus` filter, can be a sequence.
+        :param distroseries: `IDistroSeries` filter.
+        :param pocket: `PackagePublishingPocket` filter.  This may be an
+            iterable of more than one pocket or a single pocket.
+        :param exact_match: either or not filter source names by exact
+                             matching.
+        :param created_since_date: Only return results whose `date_created`
+            is greater than or equal to this date.
+        :param component_name: component filter. Only return source packages
+            that are in this component.
+
+        :return: SelectResults containing `ISourcePackagePublishingHistory`,
+            ordered by name. If there are multiple results for the same
+            name then they are sub-ordered newest first.
+        """
+
+    def newAuthToken(person, token=None, date_created=None):
+        """Create a new authorisation token.
+
+        :param person: An IPerson whom this token is for
+        :param token: Optional unicode text to use as the token. One will be
+            generated if not given
+        :param date_created: Optional, defaults to now
+
+        :return: A new IArchiveAuthToken
+        """
+
+
+class IArchiveView(IHasBuildRecords):
+    """Archive interface for operations restricted by view privilege."""
+
+    title = TextLine(title=_("Name"), required=False, readonly=True)
 
     require_virtualized = exported(
         Bool(
@@ -402,9 +496,6 @@
         "Concatenation of the source and binary packages published in this "
         "archive. Its content is used for indexed searches across archives.")
 
-    signing_key = Object(
-        title=_('Repository sigining key.'), required=False, schema=IGPGKey)
-
     debug_archive = Attribute(
         "The archive into which debug binaries should be uploaded.")
 
@@ -418,14 +509,6 @@
 
     is_partner = Attribute("True if this archive is a partner archive.")
 
-    is_copy = Attribute("True if this archive is a copy archive.")
-
-    is_active = Bool(
-        title=_("True if the archive is in the active state"),
-        required=False, readonly=True)
-
-    series_with_sources = Attribute(
-        "DistroSeries to which this archive has published sources")
     number_of_sources = Attribute(
         'The number of sources published in the context archive.')
     number_of_binaries = Attribute(
@@ -912,9 +995,6 @@
         :return: True if the person is allowed to upload the source package.
         """
 
-    num_pkgs_building = Attribute(
-        "Tuple of packages building and waiting to build")
-
     def updatePackageDownloadCount(bpr, day, country, count):
         """Update the daily download count for a given package.
 
@@ -949,13 +1029,6 @@
             # Really IArchiveDependency
             readonly=True))
 
-    description = exported(
-        Text(
-            title=_("Description"), required=False,
-            description=_(
-                "A short description of the archive. URLs are allowed and "
-                "will be rendered as links.")))
-
     signing_key_fingerprint = exported(
         Text(
             title=_("Archive signing key fingerprint"), required=False,
@@ -963,69 +1036,6 @@
                           "for this PPA or None if there is no signing "
                           "key available.")))
 
-    @rename_parameters_as(name="source_name", distroseries="distro_series")
-    @operation_parameters(
-        name=TextLine(title=_("Source package name"), required=False),
-        version=TextLine(title=_("Version"), required=False),
-        status=Choice(
-            title=_('Package Publishing Status'),
-            description=_('The status of this publishing record'),
-            # Really PackagePublishingStatus, circular import fixed below.
-            vocabulary=DBEnumeratedType,
-            required=False),
-        distroseries=Reference(
-            # Really IDistroSeries, fixed below to avoid circular import.
-            Interface,
-            title=_("Distroseries name"), required=False),
-        pocket=Choice(
-            title=_("Pocket"),
-            description=_("The pocket into which this entry is published"),
-            # Really PackagePublishingPocket, circular import fixed below.
-            vocabulary=DBEnumeratedType,
-            required=False, readonly=True),
-        exact_match=Bool(
-            title=_("Exact Match"),
-            description=_("Whether or not to filter source names by exact"
-                          " matching."),
-            required=False),
-        created_since_date=Datetime(
-            title=_("Created Since Date"),
-            description=_("Return entries whose `date_created` is greater "
-                          "than or equal to this date."),
-            required=False),
-        component_name=TextLine(title=_("Component name"), required=False),
-        )
-    # Really returns ISourcePackagePublishingHistory, see below for
-    # patch to avoid circular import.
-    @call_with(eager_load=True)
-    @operation_returns_collection_of(Interface)
-    @export_read_operation()
-    def getPublishedSources(name=None, version=None, status=None,
-                            distroseries=None, pocket=None,
-                            exact_match=False, created_since_date=None,
-                            eager_load=False, component_name=None):
-        """All `ISourcePackagePublishingHistory` target to this archive.
-
-        :param name: source name filter (exact match or SQL LIKE controlled
-                     by 'exact_match' argument).
-                     Name can be a single string or a list of strings.
-        :param version: source version filter (always exact match).
-        :param status: `PackagePublishingStatus` filter, can be a sequence.
-        :param distroseries: `IDistroSeries` filter.
-        :param pocket: `PackagePublishingPocket` filter.  This may be an
-            iterable of more than one pocket or a single pocket.
-        :param exact_match: either or not filter source names by exact
-                             matching.
-        :param created_since_date: Only return results whose `date_created`
-            is greater than or equal to this date.
-        :param component_name: component filter. Only return source packages
-            that are in this component.
-
-        :return: SelectResults containing `ISourcePackagePublishingHistory`,
-            ordered by name. If there are multiple results for the same
-            name then they are sub-ordered newest first.
-        """
-
     @rename_parameters_as(
         name="binary_name", distroarchseries="distro_arch_series")
     @operation_parameters(
@@ -1305,17 +1315,6 @@
         :return: A IArchiveAuthToken, or None if the user has none.
         """
 
-    def newAuthToken(person, token=None, date_created=None):
-        """Create a new authorisation token.
-
-        :param person: An IPerson whom this token is for
-        :param token: Optional unicode text to use as the token. One will be
-            generated if not given
-        :param date_created: Optional, defaults to now
-
-        :return: A new IArchiveAuthToken
-        """
-
     @call_with(person=REQUEST_USER)
     @operation_parameters(
         source_name=TextLine(title=_("Source package name")),
@@ -1938,8 +1937,9 @@
             "with a higher score will build sooner.")))
 
 
-class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView,
-               IArchiveCommercial, IArchiveRestricted):
+class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit,
+               IArchiveSubscriberView, IArchiveView, IArchiveCommercial,
+               IArchiveRestricted):
     """Main Archive interface."""
     export_as_webservice_entry()
 

=== modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py'
--- lib/lp/soyuz/tests/test_archive_subscriptions.py	2012-05-26 21:20:16 +0000
+++ lib/lp/soyuz/tests/test_archive_subscriptions.py	2013-02-18 04:04:21 +0000
@@ -1,8 +1,10 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2013 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test Archive features."""
 
+from urlparse import urljoin
+
 from zope.security.interfaces import Unauthorized
 
 from lp.registry.interfaces.person import PersonVisibility
@@ -85,12 +87,6 @@
             self.archive, '+index', principal=self.subscriber)
         self.assertIn(self.archive.displayname, view.render())
 
-        # Just to double check, by default, the subscriber still can't see the
-        # +packages view which requires extra permissions.
-        self.assertRaises(
-            Unauthorized, create_initialized_view,
-            self.archive, '+packages', principal=self.subscriber)
-
     def test_new_subscription_sends_email(self):
         # Creating a new subscription sends an email to all members
         # of the person or team subscribed.
@@ -146,3 +142,12 @@
         self.assertIsNotNone(find_tag_by_id(content, 'ppa-install'))
         self.assertIsNotNone(
             find_tag_by_id(content, 'portlet-latest-updates'))
+
+    def test_unauthorized_subscriber_for_plus_packages(self):
+        with person_logged_in(self.owner):
+            self.archive.newSubscription(
+                self.subscriber, registrant=self.archive.owner)
+        with person_logged_in(self.subscriber):
+            url = urljoin(canonical_url(self.archive), '+packages')
+        browser = setupBrowserForUser(self.subscriber)
+        self.assertRaises(Unauthorized, browser.open, url)


Follow ups