← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~andrey-fedoseev/launchpad:archive-subscriptions-links into launchpad:master

 

Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:archive-subscriptions-links into launchpad:master.

Commit message:
Archive subscriptions: add links to PPAs

LP: #860268

Add `checkViewPermission` to `ArchiveSet`

Update `ViewArchive` to use `ArchiveSet.checkViewPermission`

Add `result` argument to `precache_permission_for_objects`

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #860268 in Launchpad itself: "+archivesubscriptions doesn't link to the archives"
  https://bugs.launchpad.net/launchpad/+bug/860268

For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/423346

I added links to PPAs on the Archive Subscriptions page

For that, I need to check `launchpad.View` permission for each of the archives on that page.

Check permission for each individual archive may be expensive since it needs to run a few database queries per-archive. As a workaround, I added a method the `ArchiveSet.checkViewPermission` which runs the check for multiple archives at once.

This is ready for review, but I am going to add some tests for `ArchiveSet.checkViewPermission` before I merge it.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:archive-subscriptions-links into launchpad:master.
diff --git a/lib/lp/security.py b/lib/lp/security.py
index c8f518f..a3e8a58 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -232,7 +232,10 @@ from lp.snappy.interfaces.snappyseries import (
     ISnappySeriesSet,
     )
 from lp.snappy.interfaces.snapsubscription import ISnapSubscription
-from lp.soyuz.interfaces.archive import IArchive
+from lp.soyuz.interfaces.archive import (
+    IArchive,
+    IArchiveSet,
+    )
 from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
 from lp.soyuz.interfaces.archivesubscriber import (
@@ -2784,35 +2787,9 @@ class ViewArchive(AuthorizationBase):
     usedfor = IArchive
 
     def checkAuthenticated(self, user):
-        """Verify that the user can view the archive.
-
-        Anyone can see a public and enabled archive.
-
-        Only Launchpad admins and uploaders can view private or disabled
-        archives.
-        """
-        # No further checks are required if the archive is public and
-        # enabled.
-        if not self.obj.private and self.obj.enabled:
-            return True
-
-        # Administrator are allowed to view private archives.
-        if user.in_admin or user.in_commercial_admin:
-            return True
-
-        # Registry experts are allowed to view public but disabled archives
-        # (since they are allowed to disable archives).
-        if (not self.obj.private and not self.obj.enabled and
-                user.in_registry_experts):
-            return True
-
-        # Owners can view the PPA.
-        if user.inTeam(self.obj.owner):
-            return 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()
+        """Verify that the user can view the archive."""
+        archive_set: IArchiveSet = getUtility(IArchiveSet)
+        return archive_set.checkViewPermission([self.obj], user)[self.obj]
 
     def checkUnauthenticated(self):
         """Unauthenticated users can see the PPA if it's not private."""
diff --git a/lib/lp/services/webapp/authorization.py b/lib/lp/services/webapp/authorization.py
index 558e3fe..018baa3 100644
--- a/lib/lp/services/webapp/authorization.py
+++ b/lib/lp/services/webapp/authorization.py
@@ -337,7 +337,9 @@ def iter_authorization(objecttoauthorize, permission, principal, cache,
             yield result
 
 
-def precache_permission_for_objects(participation, permission_name, objects):
+def precache_permission_for_objects(
+    participation, permission_name, objects, result=True
+):
     """Precaches the permission for the objects into the policy cache."""
     if participation is None:
         participation = getInteraction().participations[0]
@@ -347,7 +349,7 @@ def precache_permission_for_objects(participation, permission_name, objects):
     for obj in objects:
         naked_obj = removeSecurityProxy(obj)
         obj_permission_cache = permission_cache.setdefault(naked_obj, {})
-        obj_permission_cache[permission_name] = True
+        obj_permission_cache[permission_name] = result
 
 
 def check_permission(permission_name, context):
diff --git a/lib/lp/services/webapp/tests/test_authorization.py b/lib/lp/services/webapp/tests/test_authorization.py
index 5e31460..ebac5e9 100644
--- a/lib/lp/services/webapp/tests/test_authorization.py
+++ b/lib/lp/services/webapp/tests/test_authorization.py
@@ -521,13 +521,29 @@ class TestPrecachePermissionForObjects(TestCase):
         # policy cache for the permission specified.
         class Boring:
             """A boring, but weakref-able object."""
-        objects = [Boring(), Boring()]
+        viewable_objects = [Boring(), Boring()]
+        non_viewable_objects = [Boring(), Boring()]
         request = LaunchpadTestRequest()
         login(ANONYMOUS, request)
-        precache_permission_for_objects(request, 'launchpad.View', objects)
+        precache_permission_for_objects(
+            request, 'launchpad.View', viewable_objects
+        )
+        precache_permission_for_objects(
+            request, 'launchpad.View', non_viewable_objects, result=False
+        )
         # Confirm that the objects have the permission set.
-        self.assertTrue(check_permission('launchpad.View', objects[0]))
-        self.assertTrue(check_permission('launchpad.View', objects[1]))
+        self.assertTrue(
+            check_permission('launchpad.View', viewable_objects[0])
+        )
+        self.assertTrue(
+            check_permission('launchpad.View', viewable_objects[1])
+        )
+        self.assertFalse(
+            check_permission('launchpad.View', non_viewable_objects[0])
+        )
+        self.assertFalse(
+            check_permission('launchpad.View', non_viewable_objects[1])
+        )
 
     def test_default_request(self):
         # If no request is provided, the current interaction is used.
diff --git a/lib/lp/soyuz/browser/archivesubscription.py b/lib/lp/soyuz/browser/archivesubscription.py
index 8cff69b..3edcec9 100644
--- a/lib/lp/soyuz/browser/archivesubscription.py
+++ b/lib/lp/soyuz/browser/archivesubscription.py
@@ -36,6 +36,7 @@ from lp.app.browser.launchpadform import (
 from lp.app.widgets.date import DateWidget
 from lp.app.widgets.popup import PersonPickerWidget
 from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.role import IPersonRoles
 from lp.services.fields import PersonChoice
 from lp.services.propertycache import (
     cachedproperty,
@@ -336,6 +337,29 @@ class PersonArchiveSubscriptionsView(LaunchpadView):
         for archive in archives:
             get_property_cache(archive)._known_subscribers = [self.context]
 
+        # Check if the user can view the archives and cache the permission
+        # check results
+        viewable_archives = []
+        non_viewable_archives = []
+        archive_set: IArchiveSet = getUtility(IArchiveSet)
+        for archive, has_view_permission in archive_set.checkViewPermission(
+                archives, IPersonRoles(self.user)
+        ).items():
+            if has_view_permission:
+                viewable_archives.append(archive)
+            else:
+                non_viewable_archives.append(archive)
+        precache_permission_for_objects(
+            None,
+            'launchpad.View',
+            viewable_archives, result=True
+        )
+        precache_permission_for_objects(
+            None,
+            'launchpad.View',
+            non_viewable_archives, result=False
+        )
+
         # Turn the result set into a list of dicts so it can be easily
         # accessed in TAL. Note that we need to ensure that only one
         # PersonalArchiveSubscription is included for each archive,
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index e011591..30abac0 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -54,6 +54,7 @@ __all__ = [
 
 import http.client
 import re
+import typing
 from urllib.parse import urlparse
 
 from lazr.restful.declarations import (
@@ -104,7 +105,10 @@ from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.gpg import IGPGKey
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.registry.interfaces.role import IHasOwner
+from lp.registry.interfaces.role import (
+    IHasOwner,
+    IPersonRoles,
+    )
 from lp.services.fields import (
     PersonChoice,
     PublicPersonChoice,
@@ -2503,6 +2507,23 @@ class IArchiveSet(Interface):
             that are currently published in the given archives.
         """
 
+    def checkViewPermission(
+        archives: typing.List[IArchive], user: IPersonRoles
+    ) -> typing.Dict[IArchive, bool]:
+        """
+        Given a collection of archives, check if the user can view
+        each of them.
+
+        Anyone can see a public and enabled archive.
+
+        Only Launchpad admins and uploaders can view private or disabled
+        archives.
+
+        :param archives: a collection of `IArchive` objects
+        :param user: a user (a `IPersonRoles` object)
+        :return: a mapping of `IArchive` -> `bool`, where the values represent
+            the result of the permission check.
+        """
 
 default_name_by_purpose = {
     ArchivePurpose.PRIMARY: 'primary',
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index 727d8b2..8d6e844 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -13,6 +13,7 @@ __all__ = [
 
 from operator import attrgetter
 import re
+import typing
 
 from lazr.lifecycle.event import ObjectCreatedEvent
 import six
@@ -23,6 +24,7 @@ from storm.expr import (
     Count,
     Desc,
     Exists,
+    In,
     Join,
     Not,
     Or,
@@ -3020,6 +3022,54 @@ class ArchiveSet:
 
         return results.order_by(SourcePackagePublishingHistory.id)
 
+    def checkViewPermission(
+        self, archives: typing.List[IArchive], user: IPersonRoles
+    ) -> typing.Dict[IArchive, bool]:
+        """See `IArchiveSet`."""
+        allowed_ids = set()
+        ids_to_check_in_database = []
+        for archive in archives:
+            # No further checks are required if the archive is public and
+            # enabled.
+            if not archive.private and archive.enabled:
+                allowed_ids.add(archive.id)
+
+            # Administrator are allowed to view private archives.
+            elif user.in_admin or user.in_commercial_admin:
+                allowed_ids.add(archive.id)
+
+            # Registry experts are allowed to view public but disabled archives
+            # (since they are allowed to disable archives).
+            elif (not archive.private and not archive.enabled and
+                    user.in_registry_experts):
+                allowed_ids.add(archive.id)
+
+            # Owners can view the PPA.
+            elif user.id == archive.owner.id:
+                allowed_ids.add(archive.id)
+
+            else:
+                ids_to_check_in_database.append(archive.id)
+
+        if ids_to_check_in_database:
+            filter = get_enabled_archive_filter(user.person)
+            user_teams = Select(
+                TeamParticipation.teamID,
+                where=TeamParticipation.person == user.person
+            )
+            is_owner = Archive.ownerID.is_in(user_teams)
+            store = IStore(Archive)
+            allowed_ids.update(
+                store.find(
+                    Archive.id,
+                    And(
+                        In(Archive.id, ids_to_check_in_database),
+                        Or(filter, is_owner)
+                    )
+                ).values(Archive.id)
+            )
+        return {archive: archive.id in allowed_ids for archive in archives}
+
     def empty_list(self):
         """See `IArchiveSet."""
         return []
diff --git a/lib/lp/soyuz/templates/person-archive-subscriptions.pt b/lib/lp/soyuz/templates/person-archive-subscriptions.pt
index 8711378..19d39ef 100644
--- a/lib/lp/soyuz/templates/person-archive-subscriptions.pt
+++ b/lib/lp/soyuz/templates/person-archive-subscriptions.pt
@@ -30,8 +30,12 @@
                 <tal:subscription_and_token
                     define="subscription subscription_with_token/subscription;
                             token subscription_with_token/token">
-                <td class="ppa_display_name">
-                  <tal:display_name content="subscription/archive/displayname">
+                <td class="ppa_display_name"
+                    tal:define="archive_link subscription/archive/fmt:link">
+                  <tal:link condition="archive_link" content="structure archive_link">
+                    Private PPA for Celso
+                  </tal:link>
+                  <tal:display_name condition="not: archive_link" content="subscription/archive/displayname">
                     Private PPA for Celso
                   </tal:display_name>
                   (<tal:reference content="subscription/archive/fmt:reference">
diff --git a/lib/lp/soyuz/tests/test_archive_subscriptions.py b/lib/lp/soyuz/tests/test_archive_subscriptions.py
index 516fcd7..60f36d8 100644
--- a/lib/lp/soyuz/tests/test_archive_subscriptions.py
+++ b/lib/lp/soyuz/tests/test_archive_subscriptions.py
@@ -182,7 +182,14 @@ class PersonArchiveSubscriptions(TestCaseWithFactory):
     def test_query_count(self):
         subscriber = self.factory.makePerson()
         for x in range(10):
-            archive = self.factory.makeArchive(private=True)
+            archive_owner = self.factory.makePerson()
+            # some archives are owner by a user, others are owned by a team
+            archive = self.factory.makeArchive(
+                private=True,
+                owner=archive_owner if x < 5 else self.factory.makeTeam(
+                    members=[archive_owner]
+                )
+            )
             with person_logged_in(archive.owner):
                 if x >= 5:
                     team = self.factory.makeTeam(members=[subscriber])
@@ -196,7 +203,7 @@ class PersonArchiveSubscriptions(TestCaseWithFactory):
                 view = create_initialized_view(
                     subscriber, '+archivesubscriptions', principal=subscriber)
                 view.render()
-        self.assertThat(recorder, HasQueryCount(Equals(12)))
+        self.assertThat(recorder, HasQueryCount(Equals(16)))
 
     def test_getArchiveSubscriptions(self):
         # Anyone with 'View' permission on a given person is able to