Merge ~ilasc/launchpad:oci-recipe-edit-existing-rules into launchpad:master

Proposed by Ioana Lasc
Status: Merged
Approved by: Ioana Lasc
Approved revision: bcc0ec04b9f36615961761520bf344a1c57a42d8
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ilasc/launchpad:oci-recipe-edit-existing-rules
Merge into: launchpad:master
Diff against target: 614 lines (+422/-7)
7 files modified
lib/lp/oci/browser/configure.zcml (+7/-0)
lib/lp/oci/browser/ocirecipe.py (+172/-1)
lib/lp/oci/browser/tests/test_ocirecipe.py (+161/-0)
lib/lp/oci/templates/ocirecipe-edit-push-rules.pt (+41/-0)
lib/lp/oci/templates/ocirecipe-index.pt (+5/-0)
lib/lp/registry/browser/person.py (+26/-6)
lib/lp/security.py (+10/-0)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Thiago F. Pappacena (community) Approve
Review via email: mp+387293@code.launchpad.net

Commit message

Edit push rules and credentials on Recipe

Description of the change

This branch adds the capability to edit and delete existing push rules on the Recipe page and from there also navigate to a screen where the user can edit & delete existing OCI registry credentials and add new ones.

The capability to add new push rules will follow in a separate MP.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Thanks for splitting the MP, ilasc. It helped a lot!

I have added a couple of comments, mostly about permission checking and a doubt the URL and implementation for the "Edit credentials" screen here and on /~/+edit-oci-registry-credentials.

review: Needs Information
df8fb5e... by Ioana Lasc

Redirect to Person page for registry credential CRUD

5bf7dbe... by Ioana Lasc

Add edit permissions to OCIPushRule

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thiago thank you for the helpful comments!

Fully agreed on all: edit credentials here was a duplication of the Person page capabilities and we didn't really need to remove the security proxy to edit / destroy push rules in code.

MP is now ready for another look and much shorter!

Revision history for this message
Tom Wardill (twom) :
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Couple of small details about code style (see also twom comments) and avoiding duplicated URLs for the same set of objects. But otherwise, LGTM.

review: Approve
8d719cd... by Ioana Lasc

Address code review comments

49f4c67... by Ioana Lasc

Remove accidentally checked in banner.css

bcc0ec0... by Ioana Lasc

Fix issue with image name

Revision history for this message
Ioana Lasc (ilasc) wrote :

Thiago this is the branch I mentioned in Standup, addressed all comments and replied to Tom's.

The change I mentioned is in the last commit that adds the missing Unit Tests for "multiple push rules already in the DB when editing the image name on 1 rule".

It might be worth having another look at the MP to see if you spot anything else as there were code updates.

Thanks Thiago

review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

The new test look really good. Good catch on spotting the bug!

I've added a minor comment, but the MP seems to be good to go.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/oci/browser/configure.zcml b/lib/lp/oci/browser/configure.zcml
2index b66e509..0aa6da8 100644
3--- a/lib/lp/oci/browser/configure.zcml
4+++ b/lib/lp/oci/browser/configure.zcml
5@@ -61,6 +61,13 @@
6 template="../../app/templates/generic-edit.pt" />
7 <browser:page
8 for="lp.oci.interfaces.ocirecipe.IOCIRecipe"
9+ class="lp.oci.browser.ocirecipe.OCIRecipeEditPushRulesView"
10+ permission="launchpad.Edit"
11+ name="+edit-push-rules"
12+ template="../templates/ocirecipe-edit-push-rules.pt" />
13+
14+ <browser:page
15+ for="lp.oci.interfaces.ocirecipe.IOCIRecipe"
16 class="lp.oci.browser.ocirecipe.OCIRecipeDeleteView"
17 permission="launchpad.Edit"
18 name="+delete"
19diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
20index 356ac1c..cac47ac 100644
21--- a/lib/lp/oci/browser/ocirecipe.py
22+++ b/lib/lp/oci/browser/ocirecipe.py
23@@ -11,6 +11,7 @@ __all__ = [
24 'OCIRecipeAdminView',
25 'OCIRecipeContextMenu',
26 'OCIRecipeDeleteView',
27+ 'OCIRecipeEditPushRulesView',
28 'OCIRecipeEditView',
29 'OCIRecipeNavigation',
30 'OCIRecipeNavigationMenu',
31@@ -23,10 +24,13 @@ from lazr.restful.interface import (
32 use_template,
33 )
34 from zope.component import getUtility
35+from zope.formlib.form import FormFields
36 from zope.interface import Interface
37 from zope.schema import (
38+ Bool,
39 Choice,
40 List,
41+ TextLine,
42 )
43
44 from lp.app.browser.launchpadform import (
45@@ -36,6 +40,7 @@ from lp.app.browser.launchpadform import (
46 )
47 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
48 from lp.app.browser.tales import format_link
49+from lp.app.errors import UnexpectedFormData
50 from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
51 from lp.buildmaster.interfaces.processor import IProcessorSet
52 from lp.code.browser.widgets.gitref import GitRefWidget
53@@ -138,12 +143,17 @@ class OCIRecipeContextMenu(ContextMenu):
54
55 facet = 'overview'
56
57- links = ('request_builds',)
58+ links = ('request_builds', 'edit_push_rules')
59
60 @enabled_with_permission('launchpad.Edit')
61 def request_builds(self):
62 return Link('+request-builds', 'Request builds', icon='add')
63
64+ @enabled_with_permission('launchpad.Edit')
65+ def edit_push_rules(self):
66+ return Link(
67+ '+edit-push-rules', 'Edit push rules', icon='edit')
68+
69
70 class OCIProjectRecipesView(LaunchpadView):
71 """Default view for the list of OCI recipes of an OCI project."""
72@@ -243,6 +253,165 @@ def new_builds_notification_text(builds, already_pending=None):
73 return builds_text
74
75
76+class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
77+ """View for +ocirecipe-edit-push-rules.pt."""
78+
79+ class schema(Interface):
80+ """Schema for editing push rules."""
81+
82+ @cachedproperty
83+ def push_rules(self):
84+ return list(
85+ getUtility(IOCIPushRuleSet).findByRecipe(self.context))
86+
87+ @property
88+ def has_push_rules(self):
89+ return len(self.push_rules) > 0
90+
91+ def _getFieldName(self, name, rule_id):
92+ """Get the combined field name for an `OCIPushRule` ID.
93+
94+ In order to be able to render a table, we encode the rule ID
95+ in the form field name.
96+ """
97+ return "%s.%d" % (name, rule_id)
98+
99+ def _parseFieldName(self, field_name):
100+ """Parse a combined field name as described in `_getFieldName`.
101+
102+ :raises UnexpectedFormData: if the field name cannot be parsed or
103+ the `OCIPushRule` cannot be found.
104+ """
105+ field_bits = field_name.split(".")
106+ if len(field_bits) != 2:
107+ raise UnexpectedFormData(
108+ "Cannot parse field name: %s" % field_name)
109+ field_type = field_bits[0]
110+ try:
111+ rule_id = int(field_bits[1])
112+ except ValueError:
113+ raise UnexpectedFormData(
114+ "Cannot parse field name: %s" % field_name)
115+ return field_type, rule_id
116+
117+ def setUpFields(self):
118+ """See `LaunchpadEditFormView`."""
119+ LaunchpadEditFormView.setUpFields(self)
120+
121+ image_fields = []
122+ delete_fields = []
123+ creds = []
124+ for elem in list(self.context.push_rules):
125+ image_fields.append(
126+ TextLine(
127+ title=u'Image name',
128+ __name__=self._getFieldName('image_name', elem.id),
129+ default=elem.image_name,
130+ required=True, readonly=False))
131+ delete_fields.append(
132+ Bool(
133+ title=u'Delete',
134+ __name__=self._getFieldName('delete', elem.id),
135+ default=False,
136+ required=True, readonly=False))
137+ creds.append(
138+ TextLine(
139+ title=u'Username',
140+ __name__=self._getFieldName('username', elem.id),
141+ default=elem.registry_credentials.username,
142+ required=True, readonly=True))
143+ creds.append(
144+ TextLine(
145+ title=u'Registry URL',
146+ __name__=self._getFieldName('url', elem.id),
147+ default=elem.registry_credentials.url,
148+ required=True, readonly=True))
149+
150+ self.form_fields += FormFields(*image_fields)
151+ self.form_fields += FormFields(*creds)
152+ self.form_fields += FormFields(*delete_fields)
153+
154+ @property
155+ def label(self):
156+ return 'Edit OCI push rules for %s' % self.context.name
157+
158+ page_title = 'Edit OCI push rules'
159+
160+ @property
161+ def cancel_url(self):
162+ return canonical_url(self.context)
163+
164+ def getRulesWidgets(self, rule):
165+
166+ widgets_by_name = {widget.name: widget for widget in self.widgets}
167+ image_field_name = (
168+ "field." + self._getFieldName("image_name", rule.id))
169+ username_field_name = (
170+ "field." + self._getFieldName("username", rule.id))
171+ url_field_name = (
172+ "field." + self._getFieldName("url", rule.id))
173+ delete_field_name = (
174+ "field." + self._getFieldName("delete", rule.id))
175+ return {
176+ "image_name": widgets_by_name[image_field_name],
177+ "username": widgets_by_name[username_field_name],
178+ "url": widgets_by_name[url_field_name],
179+ "delete": widgets_by_name[delete_field_name],
180+ }
181+
182+ def parseData(self, data):
183+ """Rearrange form data to make it easier to process."""
184+ parsed_data = {}
185+
186+ for field_name in sorted(
187+ name for name in data if name.split(".")[0] == "image_name"):
188+ _, rule_id = self._parseFieldName(field_name)
189+ image_field_name = self._getFieldName("image_name", rule_id)
190+ delete_field_name = self._getFieldName("delete", rule_id)
191+ if data.get(delete_field_name):
192+ action = "delete"
193+ else:
194+ action = "change"
195+ parsed_data.setdefault(rule_id, {
196+ "image_name": data.get(image_field_name),
197+ "action": action,
198+ })
199+
200+ return parsed_data
201+
202+ def updatePushRulesFromData(self, parsed_data):
203+ rules_map = {
204+ rule.id: rule
205+ for rule in self.context.push_rules}
206+ for rule_id, parsed_rules in parsed_data.items():
207+ rule = rules_map.get(rule_id)
208+ action = parsed_rules["action"]
209+
210+ if action == "change":
211+ image_name = parsed_rules["image_name"]
212+ if not image_name:
213+ self.setFieldError(
214+ self._getFieldName(
215+ "image_name", rule_id),
216+ "Image name must be set.")
217+ else:
218+ if rule.image_name != image_name:
219+ rule.setNewImageName(image_name)
220+ elif action == "delete":
221+ rule.destroySelf()
222+ else:
223+ raise AssertionError("unknown action: %s" % action)
224+
225+ @action("Save")
226+ def save(self, action, data):
227+ parsed_data = self.parseData(data)
228+ self.updatePushRulesFromData(parsed_data)
229+
230+ if not self.errors:
231+ self.request.response.addNotification("Saved push rules")
232+ self.next_url = canonical_url(self.context)
233+
234+
235 class OCIRecipeRequestBuildsView(LaunchpadFormView):
236 """A view for requesting builds of an OCI recipe."""
237
238@@ -322,6 +491,7 @@ class IOCIRecipeEditSchema(Interface):
239 "build_file",
240 "build_daily",
241 "require_virtualized",
242+ "push_rules",
243 ])
244
245
246@@ -453,6 +623,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin):
247 "git_ref",
248 "build_file",
249 "build_daily",
250+ "push_rules",
251 )
252 custom_widget_git_ref = GitRefWidget
253
254diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
255index e25d463..24a9c6b 100644
256--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
257+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
258@@ -19,6 +19,8 @@ from fixtures import FakeLogger
259 import pytz
260 import soupmatchers
261 from testtools.matchers import (
262+ Equals,
263+ MatchesDict,
264 MatchesSetwise,
265 MatchesStructure,
266 )
267@@ -35,11 +37,19 @@ from lp.oci.browser.ocirecipe import (
268 OCIRecipeEditView,
269 OCIRecipeView,
270 )
271+from lp.oci.interfaces.ocipushrule import (
272+ IOCIPushRuleSet,
273+ OCIPushRuleAlreadyExists,
274+ )
275 from lp.oci.interfaces.ocirecipe import (
276 CannotModifyOCIRecipeProcessor,
277 IOCIRecipeSet,
278 OCI_RECIPE_ALLOW_CREATE,
279 )
280+from lp.oci.interfaces.ociregistrycredentials import (
281+ IOCIRegistryCredentialsSet,
282+ )
283+from lp.oci.tests.helpers import OCIConfigHelperMixin
284 from lp.services.database.constants import UTC_NOW
285 from lp.services.features.testing import FeatureFixture
286 from lp.services.propertycache import get_property_cache
287@@ -831,6 +841,157 @@ class TestOCIRecipeRequestBuildsView(BaseTestOCIRecipeView):
288 extract_text(find_main_content(browser.contents)))
289
290
291+class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
292+ BaseTestOCIRecipeView):
293+ def setUp(self):
294+ super(TestOCIRecipeEditPushRulesView, self).setUp()
295+ self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
296+ self.distroseries = self.factory.makeDistroSeries(
297+ distribution=self.ubuntu, name="shiny", displayname="Shiny")
298+ self.architectures = []
299+ for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
300+ das = self.factory.makeDistroArchSeries(
301+ distroseries=self.distroseries, architecturetag=architecture,
302+ processor=getUtility(IProcessorSet).getByName(processor))
303+ das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
304+ self.architectures.append(das)
305+ self.useFixture(FeatureFixture({
306+ OCI_RECIPE_ALLOW_CREATE: "on",
307+ "oci.build_series.%s" % self.distroseries.distribution.name:
308+ self.distroseries.name,
309+ }))
310+ oci_project = self.factory.makeOCIProject(
311+ pillar=self.distroseries.distribution,
312+ ociprojectname="oci-project-name")
313+ self.recipe = self.factory.makeOCIRecipe(
314+ name="recipe-name", registrant=self.person, owner=self.person,
315+ oci_project=oci_project)
316+
317+ self.setConfig()
318+
319+ def test_edit_oci_push_rules(self):
320+ url = unicode(self.factory.getUniqueURL())
321+ credentials = {'username': 'foo', 'password': 'bar'}
322+ registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
323+ owner=self.person,
324+ url=url,
325+ credentials=credentials)
326+ image_name = self.factory.getUniqueUnicode()
327+ push_rule = getUtility(IOCIPushRuleSet).new(
328+ recipe=self.recipe,
329+ registry_credentials=registry_credentials,
330+ image_name=image_name)
331+ browser = self.getViewBrowser(self.recipe, user=self.person)
332+ browser.getLink("Edit push rules").click()
333+ # assert image name is displayed correctly
334+ with person_logged_in(self.person):
335+ self.assertEqual(image_name, browser.getControl(
336+ name="field.image_name.%d" % push_rule.id).value)
337+
338+ # assert image name is required
339+ with person_logged_in(self.person):
340+ browser.getControl(
341+ name="field.image_name.%d" % push_rule.id).value = ""
342+ browser.getControl("Save").click()
343+ self.assertIn("Required input is missing", browser.contents)
344+
345+ # set image name to valid string
346+ with person_logged_in(self.person):
347+ browser.getControl(
348+ name="field.image_name.%d" % push_rule.id).value = "image1"
349+ browser.getControl("Save").click()
350+ # and assert model changed
351+ with person_logged_in(self.person):
352+ self.assertEqual(
353+ push_rule.image_name, "image1")
354+ # Create a second push rule and test we call setNewImageName only
355+ # in cases where image name is different than the one on the model
356+ # otherwise we get the exception on rows the user doesn't actually
357+ # edit as the image name stays the same as it already is on the
358+ # model and setNewImageName will obviously find that rule in the
359+ # db with the same details
360+ second_rule = getUtility(IOCIPushRuleSet).new(
361+ recipe=self.recipe,
362+ registry_credentials=registry_credentials,
363+ image_name="second image")
364+ browser = self.getViewBrowser(self.recipe, user=self.person)
365+ browser.getLink("Edit push rules").click()
366+ with person_logged_in(self.person):
367+ browser.getControl(
368+ name="field.image_name.%d" % push_rule.id).value = "image2"
369+ browser.getControl("Save").click()
370+ with person_logged_in(self.person):
371+ self.assertEqual(
372+ push_rule.image_name, "image2")
373+
374+ # Attempt to set the same name on the second rule
375+ # will result in expected exception
376+ browser = self.getViewBrowser(self.recipe, user=self.person)
377+ browser.getLink("Edit push rules").click()
378+ with person_logged_in(self.person):
379+ browser.getControl(
380+ name="field.image_name.%d" % second_rule.id).value = "image2"
381+ self.assertRaises(OCIPushRuleAlreadyExists,
382+ browser.getControl("Save").click)
383+
384+ def test_delete_oci_push_rules(self):
385+ url = unicode(self.factory.getUniqueURL())
386+ credentials = {'username': 'foo', 'password': 'bar'}
387+ registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
388+ owner=self.person,
389+ url=url,
390+ credentials=credentials)
391+ image_name = self.factory.getUniqueUnicode()
392+ push_rule = getUtility(IOCIPushRuleSet).new(
393+ recipe=self.recipe,
394+ registry_credentials=registry_credentials,
395+ image_name=image_name)
396+ browser = self.getViewBrowser(self.recipe, user=self.person)
397+ browser.getLink("Edit push rules").click()
398+ with person_logged_in(self.person):
399+ browser.getControl(
400+ name="field.delete.%d" % push_rule.id).value = True
401+ browser.getControl("Save").click()
402+
403+ with person_logged_in(self.person):
404+ self.assertIsNone(
405+ getUtility(IOCIPushRuleSet).getByID(push_rule.id))
406+
407+ def test_edit_oci_registry_creds(self):
408+ url = unicode(self.factory.getUniqueURL())
409+ credentials = {'username': 'foo', 'password': 'bar'}
410+ image_name = self.factory.getUniqueUnicode()
411+ registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
412+ owner=self.person,
413+ url=url,
414+ credentials=credentials)
415+ getUtility(IOCIPushRuleSet).new(
416+ recipe=self.recipe,
417+ registry_credentials=registry_credentials,
418+ image_name=image_name)
419+ browser = self.getViewBrowser(self.recipe, user=self.person)
420+ browser.getLink("Edit push rules").click()
421+ browser.getLink("Edit OCI registry credentials").click()
422+
423+ browser.getControl(name="field.add_url").value = url
424+ browser.getControl(name="field.add_username").value = "new_username"
425+ browser.getControl(name="field.add_password").value = "password"
426+ browser.getControl(name="field.add_confirm_password"
427+ ).value = "password"
428+
429+ browser.getControl("Save").click()
430+ with person_logged_in(self.person):
431+ creds = list(getUtility(
432+ IOCIRegistryCredentialsSet).findByOwner(
433+ self.person))
434+
435+ self.assertEqual(url, creds[1].url)
436+ self.assertThat(
437+ (creds[1]).getCredentials(),
438+ MatchesDict({"username": Equals("new_username"),
439+ "password": Equals("password")}))
440+
441+
442 class TestOCIProjectRecipesView(BaseTestOCIRecipeView):
443 def setUp(self):
444 super(TestOCIProjectRecipesView, self).setUp()
445diff --git a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt
446new file mode 100644
447index 0000000..a1adb89
448--- /dev/null
449+++ b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt
450@@ -0,0 +1,41 @@
451+<html
452+ xmlns="http://www.w3.org/1999/xhtml"
453+ xmlns:tal="http://xml.zope.org/namespaces/tal"
454+ xmlns:metal="http://xml.zope.org/namespaces/metal"
455+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
456+ metal:use-macro="view/macro:page/main_only"
457+ i18n:domain="launchpad">
458+<body>
459+
460+<div metal:fill-slot="main">
461+ <div metal:use-macro="context/@@launchpad_form/form">
462+ <metal:formbody fill-slot="widgets">
463+ <p condition="context/required:launchpad.Edit">
464+ <a class="sprite edit" tal:attributes="href context/owner/fmt:url/+edit-oci-registry-credentials">Edit OCI registry credentials</a>
465+ </p>
466+ <table class="form">
467+ <tr tal:repeat="rule context/push_rules">
468+ <tal:rules_widgets
469+ define="rules_widgets python:view.getRulesWidgets(rule);
470+ parity python:'even' if repeat['rule'].even() else 'odd'">
471+ <td tal:define="widget nocall:rules_widgets/image_name">
472+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
473+ </td>
474+ <td tal:define="widget nocall:rules_widgets/username">
475+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
476+ </td>
477+ <td tal:define="widget nocall:rules_widgets/url">
478+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
479+ </td>
480+ <td tal:define="widget nocall:rules_widgets/delete">
481+ <metal:widget use-macro="context/@@launchpad_form/widget_div" />
482+ </td>
483+ </tal:rules_widgets>
484+ </tr>
485+ </table>
486+ </metal:formbody>
487+</div>
488+
489+</div>
490+</body>
491+</html>
492diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt
493index cf7c549..88a95b3 100644
494--- a/lib/lp/oci/templates/ocirecipe-index.pt
495+++ b/lib/lp/oci/templates/ocirecipe-index.pt
496@@ -143,6 +143,11 @@
497 This OCI recipe has no push rules defined yet.
498 </p>
499
500+ <div tal:define="link context/menu:context/edit_push_rules"
501+ tal:condition="link/enabled">
502+ <tal:edit-push-rules replace="structure link/fmt:link"/>
503+ </div>
504+
505 </div>
506
507 </body>
508diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
509index 83e5757..a8ef6fc 100644
510--- a/lib/lp/registry/browser/person.py
511+++ b/lib/lp/registry/browser/person.py
512@@ -102,7 +102,10 @@ from zope.schema.vocabulary import (
513 SimpleVocabulary,
514 )
515 from zope.security.interfaces import Unauthorized
516-from zope.security.proxy import removeSecurityProxy
517+from zope.security.proxy import (
518+ isinstance as zope_isinstance,
519+ removeSecurityProxy,
520+ )
521
522 from lp import _
523 from lp.app.browser.launchpadform import (
524@@ -138,10 +141,12 @@ from lp.code.errors import InvalidNamespace
525 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
526 from lp.code.interfaces.gitlookup import IGitTraverser
527 from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
528+from lp.oci.interfaces.ocirecipe import IOCIRecipe
529 from lp.oci.interfaces.ociregistrycredentials import (
530 IOCIRegistryCredentialsSet,
531 OCIRegistryCredentialsAlreadyExist,
532 )
533+from lp.oci.model.ocirecipe import OCIRecipe
534 from lp.registry.browser import BaseRdfView
535 from lp.registry.browser.branding import BrandingChangeView
536 from lp.registry.browser.menu import (
537@@ -199,7 +204,10 @@ from lp.registry.interfaces.teammembership import (
538 )
539 from lp.registry.interfaces.wikiname import IWikiNameSet
540 from lp.registry.mail.notification import send_direct_contact_email
541-from lp.registry.model.person import get_recipients
542+from lp.registry.model.person import (
543+ get_recipients,
544+ Person,
545+ )
546 from lp.services.config import config
547 from lp.services.database.decoratedresultset import DecoratedResultSet
548 from lp.services.database.sqlbase import flush_database_updates
549@@ -3669,8 +3677,14 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
550
551 @cachedproperty
552 def oci_registry_credentials(self):
553- return list(getUtility(
554- IOCIRegistryCredentialsSet).findByOwner(self.context))
555+ if IPerson.providedBy(self.context):
556+ owner = self.context
557+ elif IOCIRecipe.providedBy(self.context):
558+ owner = self.context.owner
559+ else:
560+ raise ValueError("Invalid context for this view")
561+
562+ return list(getUtility(IOCIRegistryCredentialsSet).findByOwner(owner))
563
564 schema = Interface
565
566@@ -3883,6 +3897,12 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
567 credentials.destroySelf()
568
569 def addCredentials(self, parsed_add_credentials):
570+ if IPerson.providedBy(self.context):
571+ owner = self.context
572+ elif IOCIRecipe.providedBy(self.context):
573+ owner = self.context.owner
574+ else:
575+ raise ValueError("Invalid context for this view")
576 url = parsed_add_credentials["url"]
577 password = parsed_add_credentials["password"]
578 confirm_password = parsed_add_credentials["confirm_password"]
579@@ -3902,7 +3922,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
580 'password': password}
581 try:
582 getUtility(IOCIRegistryCredentialsSet).new(
583- owner=self.context,
584+ owner=owner,
585 url=url,
586 credentials=credentials)
587 except OCIRegistryCredentialsAlreadyExist:
588@@ -3915,7 +3935,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
589 credentials = {'username': username}
590 try:
591 getUtility(IOCIRegistryCredentialsSet).new(
592- owner=self.context,
593+ owner=owner,
594 url=url,
595 credentials=credentials)
596 except OCIRegistryCredentialsAlreadyExist:
597diff --git a/lib/lp/security.py b/lib/lp/security.py
598index bb87821..696c55d 100644
599--- a/lib/lp/security.py
600+++ b/lib/lp/security.py
601@@ -3571,3 +3571,13 @@ class ViewOCIRegistryCredentials(AuthorizationBase):
602 class ViewOCIPushRule(AnonymousAuthorization):
603 """Anyone can view an `IOCIPushRule`."""
604 usedfor = IOCIPushRule
605+
606+
607+class OCIPushRuleEdit(AuthorizationBase):
608+ permission = 'launchpad.Edit'
609+ usedfor = IOCIPushRule
610+
611+ def checkAuthenticated(self, user):
612+ return (
613+ user.isOwner(self.obj.recipe) or
614+ user.in_commercial_admin or user.in_admin)

Subscribers

People subscribed via source and target branches

to status/vote changes: