← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:ocirecipe-filter-private into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ocirecipe-filter-private into launchpad:master with ~pappacena/launchpad:ocirecipe-subscription-ui as a prerequisite.

Commit message:
Filtering private OCI recipes for users that doesn't have permission

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/399886
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ocirecipe-filter-private into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 801b005..5eee7e3 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -217,7 +217,8 @@ class OCIProjectRecipesView(LaunchpadView):
 
     @property
     def recipes(self):
-        recipes = getUtility(IOCIRecipeSet).findByOCIProject(self.context)
+        recipes = getUtility(IOCIRecipeSet).findByOCIProject(
+            self.context, visible_by_user=self.user)
         return recipes.order_by('name')
 
     @property
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 4890679..143d553 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -2011,14 +2011,20 @@ class TestOCIProjectRecipesView(BaseTestOCIRecipeView):
             pillar=self.distroseries.distribution,
             ociprojectname="oci-project-name")
 
-    def makeRecipes(self, count=1):
+    def makeRecipes(self, count=1, **kwargs):
         with person_logged_in(self.person):
             owner = self.factory.makePerson()
             return [self.factory.makeOCIRecipe(
-                registrant=owner, owner=owner, oci_project=self.oci_project)
+                registrant=owner, owner=owner, oci_project=self.oci_project,
+                **kwargs)
                 for _ in range(count)]
 
     def test_shows_no_recipe(self):
+        """Should shows correct message when there are no visible recipes."""
+        # Create a private OCI recipe that should not be shown.
+        self.factory.makeOCIRecipe(
+            oci_project=self.oci_project,
+            information_type=InformationType.PRIVATESECURITY)
         browser = self.getViewBrowser(
             self.oci_project, "+recipes", user=self.person)
         main_text = extract_text(find_main_content(browser.contents))
@@ -2031,10 +2037,17 @@ class TestOCIProjectRecipesView(BaseTestOCIRecipeView):
     def test_paginates_recipes(self):
         batch_size = 5
         self.pushConfig("launchpad", default_batch_size=batch_size)
-        recipes = self.makeRecipes(10)
+        # We will create 1 private recipe with proper permission in the
+        # list, and 9 others. This way, we should have 10 recipes in the list.
+        [private_recipe] = self.makeRecipes(
+            1, information_type=InformationType.PRIVATESECURITY)
+        with admin_logged_in():
+            private_recipe.subscribe(self.person, private_recipe.owner)
+        recipes = self.makeRecipes(9)
+        recipes.append(private_recipe)
+
         browser = self.getViewBrowser(
             self.oci_project, "+recipes", user=self.person)
-
         main_text = extract_text(find_main_content(browser.contents))
         no_wrap_main_text = main_text.replace('\n', ' ')
         with person_logged_in(self.person):
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index 7ab9bcb..3d1fc88 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -539,7 +539,7 @@ class IOCIRecipeSet(Interface):
     def findByOwner(owner):
         """Return all OCI Recipes with the given `owner`."""
 
-    def findByOCIProject(oci_project):
+    def findByOCIProject(oci_project, visible_by_user=None):
         """Return all OCI recipes with the given `oci_project`."""
 
     def preloadDataForOCIRecipes(recipes, user):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 774e9f0..9364383 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 __all__ = [
+    'get_ocirecipe_privacy_filter',
     'OCIRecipe',
     'OCIRecipeBuildRequest',
     'OCIRecipeSet',
@@ -833,10 +834,12 @@ class OCIRecipeSet:
         """See `IOCIRecipeSet`."""
         return IStore(OCIRecipe).find(OCIRecipe, OCIRecipe.owner == owner)
 
-    def findByOCIProject(self, oci_project):
+    def findByOCIProject(self, oci_project, visible_by_user=None):
         """See `IOCIRecipeSet`."""
         return IStore(OCIRecipe).find(
-            OCIRecipe, OCIRecipe.oci_project == oci_project)
+            OCIRecipe,
+            OCIRecipe.oci_project == oci_project,
+            get_ocirecipe_privacy_filter(visible_by_user))
 
     def findByGitRepository(self, repository, paths=None):
         """See `IOCIRecipeSet`."""
diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
index 7c3f719..2f07dd6 100644
--- a/lib/lp/registry/browser/ociproject.py
+++ b/lib/lp/registry/browser/ociproject.py
@@ -17,9 +17,7 @@ __all__ = [
 
 from six.moves.urllib.parse import urlsplit
 from zope.component import getUtility
-from zope.formlib import form
 from zope.interface import implementer
-from zope.schema import Choice
 
 from lp.app.browser.launchpadform import (
     action,
@@ -199,7 +197,7 @@ class OCIProjectNavigationMenu(NavigationMenu):
 
     def view_recipes(self):
         enabled = not getUtility(IOCIRecipeSet).findByOCIProject(
-            self.context).is_empty()
+            self.context, visible_by_user=self.user).is_empty()
         return Link(
             '+recipes', 'View all recipes', icon='info', enabled=enabled)
 
@@ -219,7 +217,7 @@ class OCIProjectContextMenu(ContextMenu):
 
     def view_recipes(self):
         enabled = not getUtility(IOCIRecipeSet).findByOCIProject(
-            self.context).is_empty()
+            self.context, visible_by_user=self.user).is_empty()
         return Link(
             '+recipes', 'View all recipes', icon='info', enabled=enabled)
 
@@ -238,12 +236,18 @@ class OCIProjectIndexView(LaunchpadView):
         return urlsplit(config.codehosting.git_ssh_root).hostname
 
     @cachedproperty
+    def official_recipes(self):
+        return self.context.getOfficialRecipes(visible_by_user=self.user)
+
+    @cachedproperty
     def official_recipe_count(self):
-        return self.context.getOfficialRecipes().count()
+        return self.context.getOfficialRecipes(
+            visible_by_user=self.user).count()
 
     @cachedproperty
     def other_recipe_count(self):
-        return self.context.getUnofficialRecipes().count()
+        return self.context.getUnofficialRecipes(
+            visible_by_user=self.user).count()
 
 
 class OCIProjectEditView(LaunchpadEditFormView):
diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py
index a63aa78..7c18245 100644
--- a/lib/lp/registry/browser/tests/test_ociproject.py
+++ b/lib/lp/registry/browser/tests/test_ociproject.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright 2019-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test OCI project views."""
@@ -14,6 +14,7 @@ from datetime import datetime
 import pytz
 from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import InformationType
 from lp.oci.tests.helpers import OCIConfigHelperMixin
 from lp.registry.interfaces.ociproject import (
     OCI_PROJECT_ALLOW_CREATE,
@@ -111,6 +112,49 @@ class TestOCIProjectView(OCIConfigHelperMixin, BrowserTestCase):
             Name: oci-name
             """, self.getMainText(oci_project))
 
+    def test_hides_recipes_link_if_no_recipe_is_present(self):
+        oci_project = self.factory.makeOCIProject(ociprojectname="oci-name")
+        browser = self.getViewBrowser(oci_project)
+        actions = extract_text(
+            find_tag_by_id(browser.contents, 'global-actions'))
+        expected_links = ["Create OCI recipe"]
+        self.assertEqual("\n".join(expected_links), actions)
+
+    def test_shows_recipes_link_if_public_recipe_is_present(self):
+        oci_project = self.factory.makeOCIProject(ociprojectname="oci-name")
+        self.factory.makeOCIRecipe(oci_project=oci_project)
+        browser = self.getViewBrowser(oci_project)
+        actions = extract_text(
+            find_tag_by_id(browser.contents, 'global-actions'))
+        expected_links = ["Create OCI recipe", "View all recipes"]
+        self.assertEqual("\n".join(expected_links), actions)
+
+    def test_hides_recipes_link_if_only_non_visible_recipe_exists(self):
+        oci_project = self.factory.makeOCIProject(ociprojectname="oci-name")
+        self.factory.makeOCIRecipe(
+            oci_project=oci_project,
+            information_type=InformationType.PRIVATESECURITY)
+        another_user = self.factory.makePerson()
+        browser = self.getViewBrowser(oci_project, user=another_user)
+        actions = extract_text(
+            find_tag_by_id(browser.contents, 'global-actions'))
+        expected_links = ["Create OCI recipe"]
+        self.assertEqual("\n".join(expected_links), actions)
+
+    def test_shows_recipes_link_if_user_has_access_to_private_recipe(self):
+        oci_project = self.factory.makeOCIProject(ociprojectname="oci-name")
+        recipe = self.factory.makeOCIRecipe(
+            oci_project=oci_project,
+            information_type=InformationType.PRIVATESECURITY)
+        another_user = self.factory.makePerson()
+        with admin_logged_in():
+            recipe.subscribe(another_user, recipe.owner)
+        browser = self.getViewBrowser(oci_project, user=another_user)
+        actions = extract_text(
+            find_tag_by_id(browser.contents, 'global-actions'))
+        expected_links = ["Create OCI recipe", "View all recipes"]
+        self.assertEqual("\n".join(expected_links), actions)
+
     def test_git_repo_hint(self):
         owner = self.factory.makePerson(name="a-usr")
         pillar = self.factory.makeProduct(name="a-pillar")
@@ -188,6 +232,7 @@ class TestOCIProjectView(OCIConfigHelperMixin, BrowserTestCase):
             pillar=distribution, ociprojectname="oci-name")
         self.factory.makeOCIRecipe(oci_project=oci_project, official=False)
         self.factory.makeOCIRecipe(oci_project=oci_project, official=False)
+
         browser = self.getViewBrowser(
             oci_project, view_name="+index", user=distribution.owner)
         self.assertNotIn("Official recipes", browser.contents)
@@ -198,10 +243,48 @@ class TestOCIProjectView(OCIConfigHelperMixin, BrowserTestCase):
             "There are no recipes registered for this OCI project.",
             browser.contents)
 
+    def test_shows_private_recipes_with_proper_grants(self):
+        distribution = self.factory.makeDistribution(displayname="My Distro")
+        oci_project = self.factory.makeOCIProject(
+            pillar=distribution, ociprojectname="oci-name")
+        official_recipe = self.factory.makeOCIRecipe(
+            oci_project=oci_project, official=True,
+            information_type=InformationType.PRIVATESECURITY)
+        unofficial_recipe = self.factory.makeOCIRecipe(
+            oci_project=oci_project, official=False,
+            information_type=InformationType.PRIVATESECURITY)
+
+        granted_user = self.factory.makePerson()
+        with admin_logged_in():
+            unofficial_recipe.subscribe(granted_user, official_recipe.owner)
+            official_recipe.subscribe(granted_user, official_recipe.owner)
+            official_recipe_url = canonical_url(
+                official_recipe, force_local_path=True)
+        browser = self.getViewBrowser(oci_project, user=granted_user)
+
+        self.assertIn(
+            "There is <strong>1</strong> unofficial recipe.", browser.contents)
+        self.assertIn("<h3>Official recipes</h3>", browser.contents)
+
+        recipes_tag = find_tag_by_id(browser.contents, 'mirrors_list')
+        rows = recipes_tag.find_all('tr')
+        self.assertEqual(2, len(rows), 'We should have a header and 1 row')
+        self.assertIn(official_recipe_url, str(rows[1]))
+
     def test_shows_no_recipes(self):
         distribution = self.factory.makeDistribution(displayname="My Distro")
         oci_project = self.factory.makeOCIProject(
             pillar=distribution, ociprojectname="oci-name")
+
+        # Make sure we don't include private recipes that the visitor
+        # doesn't have access to.
+        self.factory.makeOCIRecipe(
+            oci_project=oci_project,
+            information_type=InformationType.PRIVATESECURITY)
+        self.factory.makeOCIRecipe(
+            oci_project=oci_project, official=True,
+            information_type=InformationType.PRIVATESECURITY)
+
         browser = self.getViewBrowser(
             oci_project, view_name="+index", user=distribution.owner)
         self.assertNotIn("Official recipes", browser.contents)
diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py
index b1d456b..00a6563 100644
--- a/lib/lp/registry/interfaces/ociproject.py
+++ b/lib/lp/registry/interfaces/ociproject.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2019-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """OCI Project interfaces."""
@@ -86,19 +86,19 @@ class IOCIProjectView(IHasGitRepositories, Interface):
     def getSeriesByName(name):
         """Get an OCIProjectSeries for this OCIProject by series' name."""
 
-    def getRecipeByNameAndOwner(recipe_name, owner_name):
+    def getRecipeByNameAndOwner(recipe_name, owner_name, visible_by_user=None):
         """Returns the exact match search for recipe_name AND owner_name."""
 
-    def getRecipes():
+    def getRecipes(visible_by_user=None):
         """Returns the set of OCI recipes for this project."""
 
-    def searchRecipes(query):
+    def searchRecipes(query, visible_by_user=None):
         """Searches for recipes in this OCI project."""
 
-    def getOfficialRecipes():
+    def getOfficialRecipes(visible_by_user=None):
         """Gets the official recipes for this OCI project."""
 
-    def getUnofficialRecipes():
+    def getUnofficialRecipes(visible_by_user=None):
         """Gets the unofficial recipes for this OCI project."""
 
     def getDefaultGitRepository(person):
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index b034602..079df1e 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -198,40 +198,47 @@ class OCIProject(BugTargetBase, StormBase):
     def getSeriesByName(self, name):
         return self.series.find(OCIProjectSeries.name == name).one()
 
-    def getRecipes(self):
+    def getRecipes(self, visible_by_user=None):
         """See `IOCIProject`."""
-        from lp.oci.model.ocirecipe import OCIRecipe
+        from lp.oci.model.ocirecipe import (
+            OCIRecipe,
+            get_ocirecipe_privacy_filter,
+        )
         rs = IStore(OCIRecipe).find(
             OCIRecipe,
             OCIRecipe.owner_id == Person.id,
-            OCIRecipe.oci_project == self)
+            OCIRecipe.oci_project == self,
+            get_ocirecipe_privacy_filter(visible_by_user))
         return rs.order_by(Person.name, OCIRecipe.name)
 
-    def getRecipeByNameAndOwner(self, recipe_name, owner_name):
+    def getRecipeByNameAndOwner(self, recipe_name, owner_name,
+                                visible_by_user=None):
         """See `IOCIProject`."""
         from lp.oci.model.ocirecipe import OCIRecipe
-        q = self.getRecipes().find(
+        q = self.getRecipes(visible_by_user=visible_by_user).find(
             OCIRecipe.name == recipe_name,
             Person.name == owner_name)
         return q.one()
 
-    def searchRecipes(self, query):
+    def searchRecipes(self, query, visible_by_user=None):
         """See `IOCIProject`."""
         from lp.oci.model.ocirecipe import OCIRecipe
-        q = self.getRecipes().find(
+        q = self.getRecipes(visible_by_user=visible_by_user).find(
             OCIRecipe.name.contains_string(query) |
             Person.name.contains_string(query))
         return q.order_by(Person.name, OCIRecipe.name)
 
-    def getOfficialRecipes(self):
+    def getOfficialRecipes(self, visible_by_user=None):
         """See `IOCIProject`."""
         from lp.oci.model.ocirecipe import OCIRecipe
-        return self.getRecipes().find(OCIRecipe._official == True)
+        return self.getRecipes(
+            visible_by_user=visible_by_user).find(OCIRecipe._official == True)
 
-    def getUnofficialRecipes(self):
+    def getUnofficialRecipes(self, visible_by_user=None):
         """See `IOCIProject`."""
         from lp.oci.model.ocirecipe import OCIRecipe
-        return self.getRecipes().find(OCIRecipe._official == False)
+        return self.getRecipes(
+            visible_by_user=visible_by_user).find(OCIRecipe._official == False)
 
     def setOfficialRecipeStatus(self, recipe, status):
         """See `IOCIProject`."""
diff --git a/lib/lp/registry/templates/ociproject-index.pt b/lib/lp/registry/templates/ociproject-index.pt
index 1f14e8e..75023b0 100644
--- a/lib/lp/registry/templates/ociproject-index.pt
+++ b/lib/lp/registry/templates/ociproject-index.pt
@@ -77,7 +77,7 @@
           <th>Date created</th>
         </tr>
 
-        <tr tal:repeat="recipe context/getOfficialRecipes">
+        <tr tal:repeat="recipe view/official_recipes">
           <td>
             <a tal:content="recipe/name"
                 tal:attributes="href recipe/fmt:url" />

Follow ups