launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26176
[Merge] ~pappacena/launchpad:ocirecipe-private-flag-ui into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ocirecipe-private-flag-ui into launchpad:master with ~pappacena/launchpad:ocirecipe-private-flag as a prerequisite.
Commit message:
UI to allow editing "private" attribute of OCI recipe
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/397310
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ocirecipe-private-flag-ui into launchpad:master.
diff --git a/lib/lp/oci/browser/configure.zcml b/lib/lp/oci/browser/configure.zcml
index 6c254fb..c5697ba 100644
--- a/lib/lp/oci/browser/configure.zcml
+++ b/lib/lp/oci/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2020 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2020-2021 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -27,6 +27,12 @@
for="lp.oci.interfaces.ocirecipe.IOCIRecipe"
class="lp.oci.browser.ocirecipe.OCIRecipeView"
permission="launchpad.View"
+ name="+portlet-privacy"
+ template="../templates/ocirecipe-portlet-privacy.pt"/>
+ <browser:page
+ for="lp.oci.interfaces.ocirecipe.IOCIRecipe"
+ class="lp.oci.browser.ocirecipe.OCIRecipeView"
+ permission="launchpad.View"
name="+index"
template="../templates/ocirecipe-index.pt" />
<browser:page
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 4f4253a..4bac850 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -67,6 +67,7 @@ from lp.oci.interfaces.ocirecipe import (
IOCIRecipeSet,
NoSuchOCIRecipe,
OCI_RECIPE_ALLOW_CREATE,
+ OCI_RECIPE_PRIVATE_FEATURE_FLAG,
OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
OCIRecipeFeatureDisabled,
)
@@ -695,6 +696,7 @@ class IOCIRecipeEditSchema(Interface):
use_template(IOCIRecipe, include=[
"name",
+ "private",
"owner",
"description",
"git_ref",
@@ -880,13 +882,15 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin,
@action("Create OCI recipe", name="create")
def create_action(self, action, data):
+ private = not getUtility(IOCIRecipeSet).isValidPrivacy(
+ False, data['owner'], data["git_ref"])
recipe = getUtility(IOCIRecipeSet).new(
name=data["name"], registrant=self.user, owner=data["owner"],
oci_project=self.context, git_ref=data["git_ref"],
build_file=data["build_file"], description=data["description"],
build_daily=data["build_daily"], build_args=data["build_args"],
build_path=data["build_path"], processors=data["processors"],
- official=data.get('official_recipe', False))
+ official=data.get('official_recipe', False), private=private)
self.next_url = canonical_url(recipe)
@@ -920,6 +924,22 @@ class BaseOCIRecipeEditView(LaunchpadEditFormView):
"""See `LaunchpadFormView`."""
return {IOCIRecipeEditSchema: self.context}
+ def validate(self, data):
+ super(BaseOCIRecipeEditView, self).validate(data)
+ if data.get('private', self.context.private) is False:
+ if 'private' in data or 'owner' in data:
+ owner = data.get('owner', self.context.owner)
+ if owner is not None and owner.private:
+ self.setFieldError(
+ 'private' if 'private' in data else 'owner',
+ 'A public OCI recipe cannot have a private owner.')
+ if 'private' in data or 'git_ref' in data:
+ ref = data.get('git_ref', self.context.git_ref)
+ if ref is not None and ref.private:
+ self.setFieldError(
+ 'private' if 'private' in data else 'git_ref',
+ 'A public OCI recipe cannot use a private repository.')
+
class OCIRecipeAdminView(BaseOCIRecipeEditView):
"""View for administering OCI recipes."""
@@ -930,7 +950,17 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView):
page_title = "Administer"
- field_names = ("require_virtualized", "allow_internet")
+ field_names = ("private", "require_virtualized", "allow_internet")
+
+ def validate(self, data):
+ super(OCIRecipeAdminView, self).validate(data)
+ # BaseSnapEditView.validate checks the rules for 'private' in
+ # combination with other attributes.
+ if (data.get('private', None) is True and
+ not getFeatureFlag(OCI_RECIPE_PRIVATE_FEATURE_FLAG)):
+ self.setFieldError(
+ 'private',
+ 'You do not have permission to create private OCI recipes.')
class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin,
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 970c49c..929cf9c 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -34,6 +34,7 @@ from zope.security.proxy import removeSecurityProxy
from zope.testbrowser.browser import LinkNotFoundError
from lp.app.browser.tales import GitRepositoryFormatterAPI
+from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.processor import IProcessorSet
@@ -56,6 +57,7 @@ from lp.oci.interfaces.ociregistrycredentials import (
IOCIRegistryCredentialsSet,
)
from lp.oci.tests.helpers import OCIConfigHelperMixin
+from lp.registry.enums import PersonVisibility
from lp.registry.interfaces.person import IPersonSet
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
@@ -88,6 +90,7 @@ from lp.testing.matchers import (
from lp.testing.pages import (
extract_text,
find_main_content,
+ find_tag_by_id,
find_tags_by_class,
)
from lp.testing.publication import test_traverse
@@ -151,17 +154,16 @@ class BaseTestOCIRecipeView(BrowserTestCase):
name="test-person", displayname="Test Person")
-class TestOCIRecipeAddView(BaseTestOCIRecipeView):
+class TestOCIRecipeAddView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def setUp(self):
super(TestOCIRecipeAddView, self).setUp()
self.distroseries = self.factory.makeDistroSeries()
self.distribution = self.distroseries.distribution
- self.useFixture(FeatureFixture({
- OCI_RECIPE_ALLOW_CREATE: "on",
+ self.setConfig({
"oci.build_series.%s" % self.distribution.name:
self.distroseries.name,
- }))
+ })
def setUpDistroSeries(self):
"""Set up self.distroseries with some available processors."""
@@ -263,6 +265,32 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
"Build-time\nARG variables:\nVAR1=10\nVAR2=20",
MatchesTagText(content, "build-args"))
+ def test_create_new_ocirecipe_private(self):
+ # Private teams will automatically create private oci recipes.
+ login_person(self.person)
+ self.factory.makeTeam(
+ name='super-private', owner=self.person,
+ visibility=PersonVisibility.PRIVATE)
+
+ oci_project = self.factory.makeOCIProject()
+ [git_ref] = self.factory.makeGitRefs()
+ browser = self.getViewBrowser(
+ oci_project, view_name="+new-recipe", user=self.person)
+ browser.getControl(name="field.owner").value = ['super-private']
+ browser.getControl(name="field.name").value = "recipe-name"
+ browser.getControl("Description").value = "Recipe description"
+ browser.getControl(name="field.git_ref.repository").value = (
+ git_ref.repository.identity)
+ browser.getControl(name="field.git_ref.path").value = git_ref.path
+ browser.getControl("Create OCI recipe").click()
+
+ content = find_main_content(browser.contents)
+ self.assertEqual("recipe-name", extract_text(content.h1))
+ self.assertEqual(
+ 'This OCI recipe contains Private information',
+ extract_text(find_tag_by_id(browser.contents, "privacy"))
+ )
+
def test_create_new_recipe_users_teams_as_owner_options(self):
# Teams that the user is in are options for the OCI recipe owner.
self.factory.makeTeam(
@@ -484,11 +512,11 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView):
self.assertIn(error_message, browser.contents)
-class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
+class TestOCIRecipeAdminView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
def setUp(self):
super(TestOCIRecipeAdminView, self).setUp()
- self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'}))
+ self.setConfig()
def test_unauthorized(self):
# A non-admin user cannot administer an OCI recipe.
@@ -503,7 +531,7 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
user=self.person)
def test_admin_recipe(self):
- # Admins can change require_virtualized.
+ # Admins can change require_virtualized and privacy.
login("admin@xxxxxxxxxxxxx")
commercial_admin = self.factory.makePerson(
member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
@@ -514,6 +542,7 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
browser = self.getViewBrowser(recipe, user=commercial_admin)
browser.getLink("Administer OCI recipe").click()
+ browser.getControl("Private").selected = True
browser.getControl("Require virtualized builders").selected = False
browser.getControl("Allow external network access").selected = False
browser.getControl("Update OCI recipe").click()
@@ -521,6 +550,7 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
login_person(self.person)
self.assertFalse(recipe.require_virtualized)
self.assertFalse(recipe.allow_internet)
+ self.assertTrue(recipe.private)
def test_admin_recipe_sets_date_last_modified(self):
# Administering an OCI recipe sets the date_last_modified property.
@@ -538,6 +568,40 @@ class TestOCIRecipeAdminView(BaseTestOCIRecipeView):
self.assertSqlAttributeEqualsDate(
recipe, "date_last_modified", UTC_NOW)
+ def test_admin_privacy_owner_mismatch(self):
+ # Cannot make recipe public if it is still owned by private person.
+ login_person(self.person)
+ team = self.factory.makeTeam(
+ owner=self.person, visibility=PersonVisibility.PRIVATE)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=team, private=True)
+ commercial_admin = self.factory.makePerson(
+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
+ browser = self.getViewBrowser(recipe, user=commercial_admin)
+ browser.getLink("Administer OCI recipe").click()
+ browser.getControl("Private").selected = False
+ browser.getControl("Update OCI recipe").click()
+ self.assertEqual(
+ 'A public OCI recipe cannot have a private owner.',
+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
+ def test_admin_privacy_git_ref_mismatch(self):
+ # Cannot make recipe public if it still uses private git ref.
+ login_person(self.person)
+ [git_ref] = self.factory.makeGitRefs()
+ git_ref.repository.transitionToInformationType(
+ InformationType.PRIVATESECURITY, self.person)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person,
+ git_ref=git_ref, private=True)
+ admin = self.factory.makeAdministrator()
+ browser = self.getViewBrowser(recipe, '+admin', user=admin)
+ browser.getControl("Private").selected = False
+ browser.getControl("Update OCI recipe").click()
+ self.assertEqual(
+ 'A public OCI recipe cannot use a private repository.',
+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
@@ -619,6 +683,42 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView):
"Build schedule:\nBuilt daily\nEdit OCI recipe\n",
MatchesTagText(content, "build-schedule"))
+ def test_edit_public_recipe_private_owner(self):
+ login_person(self.person)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person)
+ private_team = self.factory.makeTeam(
+ owner=self.person, visibility=PersonVisibility.PRIVATE)
+ private_team_name = private_team.name
+ browser = self.getViewBrowser(recipe, user=self.person)
+ browser.getLink("Edit OCI recipe").click()
+ browser.getControl("Owner").value = [private_team_name]
+ browser.getControl("Update OCI recipe").click()
+ self.assertEqual(
+ "A public OCI recipe cannot have a private owner.",
+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
+ def test_edit_public_recipe_private_git_ref(self):
+ login_person(self.person)
+ recipe = self.factory.makeOCIRecipe(
+ registrant=self.person, owner=self.person,
+ git_ref=self.factory.makeGitRefs()[0])
+ login_person(self.person)
+ [private_ref] = self.factory.makeGitRefs(
+ owner=self.person,
+ information_type=InformationType.PRIVATESECURITY)
+ private_ref_identity = private_ref.repository.identity
+ private_ref_path = private_ref.path
+ browser = self.getViewBrowser(recipe, user=self.person)
+ browser.getLink("Edit OCI recipe").click()
+ browser.getControl(name="field.git_ref.repository").value = (
+ private_ref_identity)
+ browser.getControl(name="field.git_ref.path").value = private_ref_path
+ browser.getControl("Update OCI recipe").click()
+ self.assertEqual(
+ "A public OCI recipe cannot use a private repository.",
+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
def test_edit_recipe_invalid_branch(self):
oci_project = self.factory.makeOCIProject()
repository = self.factory.makeGitRepository()
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index cc67366..e9b30a7 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -211,8 +211,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
# callbacks.
self.private = private
self.registrant = registrant
- self.owner = owner
self.oci_project = oci_project
+ self.owner = owner
self.description = description
self.build_file = build_file
self._official = official
diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
index a3e67bb..829181a 100644
--- a/lib/lp/oci/templates/ocirecipe-index.pt
+++ b/lib/lp/oci/templates/ocirecipe-index.pt
@@ -18,6 +18,7 @@
</metal:registering>
<metal:side fill-slot="side">
+ <div tal:replace="structure context/@@+portlet-privacy" />
<div tal:replace="structure context/@@+global-actions"/>
</metal:side>
diff --git a/lib/lp/oci/templates/ocirecipe-portlet-privacy.pt b/lib/lp/oci/templates/ocirecipe-portlet-privacy.pt
new file mode 100644
index 0000000..024b1fa
--- /dev/null
+++ b/lib/lp/oci/templates/ocirecipe-portlet-privacy.pt
@@ -0,0 +1,16 @@
+<div
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ id="privacy"
+ tal:attributes="
+ class python: 'portlet private' if path('context/private') else 'portlet public'
+ "
+>
+ <span tal:attributes="class python: path('context/private') and 'sprite private' or 'sprite public'"
+ >This OCI recipe contains <strong
+ tal:content="python: 'Private' if path('context/private') else 'Public'"
+ ></strong> information</span>
+</div>
+
+
Follow ups