← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ilasc/launchpad:oci-recipe-push-rules-add into launchpad:master

 

Ioana Lasc has proposed merging ~ilasc/launchpad:oci-recipe-push-rules-add into launchpad:master.

Commit message:
Add the add push rule section to edit rules page

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/388458

On the edit oci push rules page we now have the "add new rule" section.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/launchpad:oci-recipe-push-rules-add into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index cac47ac..7b7cc61 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -30,7 +30,9 @@ from zope.schema import (
     Bool,
     Choice,
     List,
+    Password,
     TextLine,
+    ValidationError,
     )
 
 from lp.app.browser.launchpadform import (
@@ -44,7 +46,10 @@ from lp.app.errors import UnexpectedFormData
 from lp.app.widgets.itemswidgets import LabeledMultiCheckBoxWidget
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.code.browser.widgets.gitref import GitRefWidget
-from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
+from lp.oci.interfaces.ocipushrule import (
+    IOCIPushRuleSet,
+    OCIPushRuleAlreadyExists,
+    )
 from lp.oci.interfaces.ocirecipe import (
     IOCIRecipe,
     IOCIRecipeSet,
@@ -55,6 +60,10 @@ from lp.oci.interfaces.ocirecipe import (
     OCIRecipeFeatureDisabled,
     )
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.oci.interfaces.ociregistrycredentials import (
+    IOCIRegistryCredentialsSet,
+    OCIRegistryCredentialsAlreadyExist,
+    )
 from lp.services.features import getFeatureFlag
 from lp.services.helpers import english_list
 from lp.services.propertycache import cachedproperty
@@ -253,7 +262,7 @@ def new_builds_notification_text(builds, already_pending=None):
         return builds_text
 
 
-class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
+class OCIRecipeEditPushRulesView(LaunchpadFormView):
     """View for +ocirecipe-edit-push-rules.pt."""
 
     class schema(Interface):
@@ -296,10 +305,14 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
 
     def setUpFields(self):
         """See `LaunchpadEditFormView`."""
-        LaunchpadEditFormView.setUpFields(self)
+        LaunchpadFormView.setUpFields(self)
 
         image_fields = []
+        username_fields = []
+        password_fields = []
+        url_fields = []
         delete_fields = []
+        existing_credentials = []
         creds = []
         for elem in list(self.context.push_rules):
             image_fields.append(
@@ -326,11 +339,61 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
                     __name__=self._getFieldName('url', elem.id),
                     default=elem.registry_credentials.url,
                     required=True, readonly=True))
+        url_fields.append(
+            TextLine(
+                title=u'Registry URL',
+                __name__=u'add_url',
+                required=False, readonly=False))
+        image_fields.append(
+            TextLine(
+                title=u'Image name',
+                __name__=u'add_image_name',
+                required=False, readonly=False))
+        username_fields.append(
+            TextLine(
+                title=u'Username',
+                __name__=u'add_username',
+                required=False, readonly=False))
+        password_fields.append(
+            Password(
+                title=u'Password',
+                __name__=u'add_password',
+                required=False, readonly=False))
+        password_fields.append(
+            Password(
+                title=u'Confirm password',
+                __name__=u'add_confirm_password',
+                required=False, readonly=False))
+        existing_credentials.append(
+            Choice(
+                vocabulary='OCIRegistryCredentials',
+                title='Choose credentials',
+                required=False,
+                __name__=u'existing_credentials'))
+
+        use_existing_creds = Bool(
+                    title=u'Use existing credentials',
+                    __name__='use_existing_creds',
+                    default=False,
+                    required=True, readonly=False)
+
+        add_new_creds = Bool(
+                    title=u'Enter new credentials',
+                    __name__='add_new_creds',
+                    default=False,
+                    required=True, readonly=False)
 
         self.form_fields += FormFields(*image_fields)
         self.form_fields += FormFields(*creds)
         self.form_fields += FormFields(*delete_fields)
 
+        self.form_fields += FormFields(*url_fields)
+        self.form_fields += FormFields(use_existing_creds)
+        self.form_fields += FormFields(add_new_creds)
+        self.form_fields += FormFields(*username_fields)
+        self.form_fields += FormFields(*password_fields)
+        self.form_fields += FormFields(*existing_credentials)
+
     @property
     def label(self):
         return 'Edit OCI push rules for %s' % self.context.name
@@ -362,7 +425,29 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
     def parseData(self, data):
         """Rearrange form data to make it easier to process."""
         parsed_data = {}
-
+        add_image_name = data.get("add_image_name")
+        add_url = data.get("add_url")
+        add_username = data.get("add_username")
+        add_password = data.get("add_password")
+        add_confirm_password = data.get("add_confirm_password")
+        add_existing_credentials = data.get("existing_credentials")
+
+        # parse data from the Add new rule section of the form
+        if (add_url or add_username or add_password or
+                add_confirm_password or add_image_name or
+                add_existing_credentials):
+            parsed_data.setdefault(None, {
+                "image_name": add_image_name,
+                "url": add_url,
+                "username": add_username,
+                "password": add_password,
+                "confirm_password": add_confirm_password,
+                "existing_credentials": data["existing_credentials"],
+                "add_new_creds": data["add_new_creds"],
+                "use_existing_creds": data["use_existing_creds"],
+                "action": "add",
+            })
+        # parse data from the Edit existing rule section of the form
         for field_name in sorted(
                 name for name in data if name.split(".")[0] == "image_name"):
             _, rule_id = self._parseFieldName(field_name)
@@ -379,6 +464,78 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
 
         return parsed_data
 
+    def addNewRule(self, parsed_data):
+        add_data = parsed_data[None]
+        image_name = add_data.get("image_name")
+        url = add_data.get("url")
+        password = add_data.get("password")
+        confirm_password = add_data.get("confirm_password")
+        username = add_data.get("username")
+        existing_credentials = add_data.get("existing_credentials")
+        checked_use_existing_credentials = add_data.get("use_existing_creds")
+        checked_add_new_credentials = add_data.get("add_new_creds")
+
+        if image_name:
+            if checked_use_existing_credentials:
+                if existing_credentials:
+                    try:
+                        getUtility(IOCIPushRuleSet).new(
+                            self.context,
+                            existing_credentials,
+                            image_name)
+                    except OCIPushRuleAlreadyExists:
+                        self.setFieldError("add_image_name",
+                                           "A push rule already exists"
+                                           " with the same URL, image "
+                                           "name and credentials.")
+                        return
+            if checked_add_new_credentials:
+                if url:
+                    if password == confirm_password:
+                        credentials = {
+                            'username': username,
+                            'password': password}
+                        try:
+                            creds = getUtility(
+                                IOCIRegistryCredentialsSet
+                            ).getOrCreate(
+                                owner=self.context.owner,
+                                url=url,
+                                credentials=credentials)
+                        except OCIRegistryCredentialsAlreadyExist:
+                            self.setFieldError(
+                                "add_url",
+                                "Credentials already exist with"
+                                " the same URL and username.")
+                            return
+                        except ValidationError:
+                            self.setFieldError(
+                                "add_url", "Not a valid URL.")
+                            return
+                        try:
+                            getUtility(IOCIPushRuleSet).new(
+                                self.context, creds, image_name)
+                        except OCIPushRuleAlreadyExists:
+                            self.setFieldError(
+                                "add_image_name",
+                                "A push rule already exists with the "
+                                "same URL, image name, and "
+                                "credentials.")
+                            return
+                    else:
+                        self.setFieldError(
+                            "password",
+                            "Please make sure the new password matches"
+                            " the confirm password field.")
+                else:
+                    self.setFieldError(
+                        "add_url",
+                        "Registry URL must be set.")
+        else:
+            self.setFieldError(
+                "add_image_name",
+                "Image name must be set.")
+
     def updatePushRulesFromData(self, parsed_data):
         rules_map = {
             rule.id: rule
@@ -399,6 +556,8 @@ class OCIRecipeEditPushRulesView(LaunchpadEditFormView):
                         rule.setNewImageName(image_name)
             elif action == "delete":
                 rule.destroySelf()
+            elif action == "add":
+                self.addNewRule(parsed_data)
             else:
                 raise AssertionError("unknown action: %s" % action)
 
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 24a9c6b..5ae9ff6 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -27,6 +27,7 @@ from testtools.matchers import (
 from zope.component import getUtility
 from zope.publisher.interfaces import NotFound
 from zope.security.interfaces import Unauthorized
+from zope.security.proxy import removeSecurityProxy
 from zope.testbrowser.browser import LinkNotFoundError
 
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
@@ -957,6 +958,146 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
             self.assertIsNone(
                 getUtility(IOCIPushRuleSet).getByID(push_rule.id))
 
+    def test_add_oci_push_rules_validations(self):
+        # Test add new rule works when there are
+        # no rules in the DB
+        browser = self.getViewBrowser(self.recipe, user=self.person)
+        browser.getLink("Edit push rules").click()
+
+        # Save does not error if there are no changes
+        # on the form
+        browser.getControl("Save").click()
+        self.assertIn("Saved push rules", browser.contents)
+
+        # Only with image name we fail with Registry URL must be set
+        browser.getLink("Edit push rules").click()
+        browser.getControl(
+            name="field.add_image_name").value = "imagename1"
+        browser.getControl(
+            name="field.add_new_creds").value = True
+        browser.getControl("Save").click()
+        self.assertIn("Registry URL must be set", browser.contents)
+
+        # No image name entered on the form
+        # we assume user is only editing and we allow
+        # Save on the form
+        browser.getControl(
+            name="field.add_image_name").value = ""
+        browser.getControl("Save").click()
+        self.assertIn("Saved push rules", browser.contents)
+
+    def test_add_oci_push_rules(self):
+        # Image name and Registry URL will create an empty Credentials object
+        # and a valid push rule based on the empty creds
+
+        url = unicode(self.factory.getUniqueURL())
+        browser = self.getViewBrowser(self.recipe, user=self.person)
+        browser.getLink("Edit push rules").click()
+        browser.getControl(
+            name="field.add_image_name").value = "imagename1"
+        browser.getControl(
+            name="field.add_url").value = url
+        browser.getControl(
+            name="field.add_new_creds").value = True
+        browser.getControl("Save").click()
+        with person_logged_in(self.person):
+            rules = list(removeSecurityProxy(
+                getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+        self.assertEqual(len(rules), 1)
+        rule = rules[0]
+        self.assertIsNotNone(rule.registry_credentials)
+        self.assertEqual(u'imagename1', rule.image_name)
+        self.assertEqual(url, rule.registry_url)
+        self.assertEqual(url,
+                         rule.registry_credentials.url)
+        self.assertEqual(
+            None,
+            rule.registry_credentials.username)
+        with person_logged_in(self.person):
+            self.assertThat(
+                rule.registry_credentials.getCredentials(),
+                MatchesDict(
+                    {"password": Equals(None)}))
+
+        # Previously added registry credentials can now be chosen
+        # from the radio widget when adding a new rule
+        browser.getLink("Edit push rules").click()
+
+        browser.getControl(
+            name="field.use_existing_creds").value = True
+        browser.getControl(
+            name="field.add_image_name").value = "imagename1"
+        browser.getControl(
+            name="field.existing_credentials").value = (
+            browser.getControl(
+                name="field.existing_credentials").options[1].strip())
+        browser.getControl("Save").click()
+        self.assertIn(
+            "A push rule already exists with the same URL, "
+            "image name and credentials.", browser.contents)
+
+        # We display correctly the radio buttons widget when
+        # username is empty in registry credentials and
+        # allow correctly adding new rule based on it
+        browser.getControl(
+            name="field.use_existing_creds").value = True
+        browser.getControl(
+            name="field.add_image_name").value = "imagename2"
+        browser.getControl("Save").click()
+        with person_logged_in(self.person):
+            rules = list(removeSecurityProxy(
+                getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+        self.assertEqual(len(rules), 2)
+        rule = rules[1]
+        self.assertIsNotNone(rule.registry_credentials)
+        self.assertEqual(u'imagename2', rule.image_name)
+        self.assertEqual(url, rule.registry_url)
+        self.assertEqual(
+            url,
+            rule.registry_credentials.url)
+        self.assertEqual(
+            None,
+            rule.registry_credentials.username)
+        with person_logged_in(self.person):
+            self.assertThat(
+                rule.registry_credentials.getCredentials(),
+                MatchesDict({
+                    "password": Equals(None)}))
+
+        browser.getLink("Edit push rules").click()
+        browser.getControl(
+            name="field.add_new_creds").value = True
+        browser.getControl(
+            name="field.add_image_name").value = "imagename3"
+        browser.getControl(
+            name="field.add_url").value = url
+        browser.getControl(
+            name="field.add_username").value = "username"
+        browser.getControl(
+            name="field.add_password").value = "password"
+        browser.getControl(
+            name="field.add_confirm_password").value = "password"
+        browser.getControl("Save").click()
+        with person_logged_in(self.person):
+            rules = list(removeSecurityProxy(
+                getUtility(IOCIPushRuleSet).findByRecipe(self.recipe)))
+        self.assertEqual(len(rules), 3)
+        rule = rules[2]
+        self.assertIsNotNone(rule.registry_credentials)
+        self.assertEqual("imagename3", rule.image_name)
+        self.assertEqual(url, rule.registry_url)
+        self.assertEqual(url,
+                         rule.registry_credentials.url)
+        self.assertEqual(
+            "username",
+            rule.registry_credentials.username)
+        with person_logged_in(self.person):
+            self.assertThat(
+                rule.registry_credentials.getCredentials(),
+                MatchesDict(
+                    {"username": Equals("username"),
+                     "password": Equals("password")}))
+
     def test_edit_oci_registry_creds(self):
         url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
diff --git a/lib/lp/oci/javascript/ocirecipe.edit.js b/lib/lp/oci/javascript/ocirecipe.edit.js
new file mode 100644
index 0000000..85cd2db
--- /dev/null
+++ b/lib/lp/oci/javascript/ocirecipe.edit.js
@@ -0,0 +1,79 @@
+/* Copyright 2015-2020 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * @module Y.lp.oci.ocirecipe.edit
+ * @requires node, DOM
+ */
+YUI.add('lp.oci.ocirecipe.edit', function(Y) {
+    Y.log('loading lp.oci.ocirecipe.edit');
+    var module = Y.namespace('lp.oci.ocirecipe.edit');
+
+    module.set_enabled = function(field_id, is_enabled) {
+        var field = Y.DOM.byId(field_id);
+        if (field !== null) {
+            field.disabled = !is_enabled;
+        }
+    };
+
+    module.onclick_add_new_creds = function(e) {
+        var add_new_creds = Y.one(
+            'input[name="field.add_new_creds"]');
+        var use_existing_creds = Y.one(
+            'input[name="field.use_existing_creds"]');
+
+        if (add_new_creds.get('checked') === true) {
+            use_existing_creds.set('checked', false);
+            Y.one('[id="field.existing_credentials"]').set('disabled', true);
+            Y.one('[id="field.add_url"]').set('disabled', false);
+            Y.one('[id="field.add_username"]').set('disabled', false);
+            Y.one('[id="field.add_password"]').set('disabled', false);
+            Y.one('[id="field.add_confirm_password"]').set('disabled', false);
+
+        }
+        else{
+            use_existing_creds.set('checked', true);
+            Y.one('[id="field.existing_credentials"]').set('disabled', false);
+            Y.one('[id="field.add_url"]').set('disabled', true);
+            Y.one('[id="field.add_username"]').set('disabled', true);
+            Y.one('[id="field.add_password"]').set('disabled', true);
+            Y.one('[id="field.add_confirm_password"]').set('disabled', true);
+
+        }
+    };
+
+    module.use_existing_creds = function(e) {
+        var add_new_creds = Y.one(
+            'input[name="field.add_new_creds"]');
+        var use_existing_creds = Y.one(
+            'input[name="field.use_existing_creds"]');
+
+        if (use_existing_creds.get('checked') === true) {
+            add_new_creds.set('checked', false);
+            Y.one('[id="field.existing_credentials"]').set('disabled', false);
+            Y.one('[id="field.add_url"]').set('disabled', true);
+            Y.one('[id="field.add_username"]').set('disabled', true);
+            Y.one('[id="field.add_password"]').set('disabled', true);
+            Y.one('[id="field.add_confirm_password"]').set('disabled', true);
+
+        }
+        else{
+            add_new_creds.set('checked', true);
+            Y.one('[id="field.existing_credentials"]').set('disabled', true);
+            Y.one('[id="field.add_url"]').set('disabled', false);
+            Y.one('[id="field.add_username"]').set('disabled', false);
+            Y.one('[id="field.add_password"]').set('disabled', false);
+            Y.one('[id="field.add_confirm_password"]').set('disabled', false);
+
+        }
+    };
+
+    module.setup = function() {
+        Y.all('input[name="field.use_existing_creds"]').on(
+            'click', module.use_existing_creds);
+        Y.all('input[name="field.add_new_creds"]').on(
+            'click', module.onclick_add_new_creds);
+        // Set the initial state.
+        module.onclick_add_new_creds();
+        module.use_existing_creds();
+    };
+}, '0.1', {'requires': ['node', 'DOM']});
diff --git a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt
index a1adb89..7c1a662 100644
--- a/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt
+++ b/lib/lp/oci/templates/ocirecipe-edit-push-rules.pt
@@ -7,6 +7,14 @@
   i18n:domain="launchpad">
 <body>
 
+<metal:block fill-slot="head_epilogue">
+  <style type="text/css">
+    .subordinate {
+      margin: 0.5em 0 0.5em 4em;
+    }
+  </style>
+</metal:block>
+
 <div metal:fill-slot="main">
     <div metal:use-macro="context/@@launchpad_form/form">
     <metal:formbody fill-slot="widgets">
@@ -32,10 +40,69 @@
             </td>
           </tal:rules_widgets>
         </tr>
+        <tr>
+          <td>
+            <h2>Add new push rule</h2>
+          </td>
+        </tr>
+        <tr>
+          <td tal:define="widget nocall:view/widgets/add_image_name">
+            <metal:widget use-macro="context/@@launchpad_form/widget_div" />
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <tal:widget define="widget nocall:view/widgets/use_existing_creds">
+              <metal:block use-macro="context/@@launchpad_form/widget_div" />
+            </tal:widget>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <table class="subordinate">
+              <tal:widget define="widget nocall:view/widgets/existing_credentials">
+                <metal:row use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
+            </table>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <tal:widget define="widget nocall:view/widgets/add_new_creds">
+              <metal:block use-macro="context/@@launchpad_form/widget_row" />
+            </tal:widget>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <table class="subordinate">
+              <tal:widget define="widget nocall:view/widgets/add_url">
+                <metal:row use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
+              <tal:widget define="widget nocall:view/widgets/add_username">
+                <metal:row use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
+              <tal:widget define="widget nocall:view/widgets/add_password">
+                <metal:row use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
+              <tal:widget define="widget nocall:view/widgets/add_confirm_password">
+                <metal:row use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
+            </table>
+          </td>
+        </tr>
       </table>
     </metal:formbody>
 </div>
 
+  <script type="text/javascript">
+    LPJS.use('lp.oci.ocirecipe.edit', function(Y) {
+      Y.on('domready', function(e) {
+        Y.lp.oci.ocirecipe.edit.setup();
+      }, window);
+    });
+  </script>
+
 </div>
 </body>
 </html>
diff --git a/lib/lp/oci/vocabularies.py b/lib/lp/oci/vocabularies.py
index aa1fae3..65209e1 100644
--- a/lib/lp/oci/vocabularies.py
+++ b/lib/lp/oci/vocabularies.py
@@ -8,10 +8,13 @@ from __future__ import absolute_import, print_function, unicode_literals
 __metaclass__ = type
 __all__ = []
 
+from zope.component import getUtility
 from zope.interface import implementer
 from zope.schema.vocabulary import SimpleTerm
 
+from lp.oci.interfaces.ociregistrycredentials import IOCIRegistryCredentialsSet
 from lp.oci.model.ocirecipe import OCIRecipe
+from lp.oci.model.ociregistrycredentials import OCIRegistryCredentials
 from lp.services.webapp.vocabulary import (
     IHugeVocabulary,
     StormVocabularyBase,
@@ -35,6 +38,51 @@ class OCIRecipeDistroArchSeriesVocabulary(StormVocabularyBase):
         return len(self.context.getAllowedArchitectures())
 
 
+class OCIRegistryCredentialsVocabulary(StormVocabularyBase):
+
+    _table = OCIRegistryCredentials
+
+    def toTerm(self, obj):
+        if obj.username:
+            token = "%s %s" % (
+                obj.url,
+                obj.username)
+        else:
+            token = obj.url
+
+        return SimpleTerm(obj, token)
+
+    @property
+    def _entries(self):
+        return list(getUtility(
+            IOCIRegistryCredentialsSet).findByOwner(self.context.owner))
+
+    def __contains__(self, value):
+        """See `IVocabulary`."""
+        return value in self._entries
+
+    def __iter__(self):
+        for obj in self._entries:
+            yield self.toTerm(obj)
+
+    def __len__(self):
+        return len(self._entries)
+
+    def getTermByToken(self, token):
+        """See `IVocabularyTokenized`."""
+        try:
+            if ' ' in token:
+                url, username = token.split(' ')
+            else:
+                username = None
+                url = token
+            for obj in self._entries:
+                if obj.url == url and obj.username == username:
+                    return self.toTerm(obj)
+        except ValueError:
+            raise LookupError(token)
+
+
 @implementer(IHugeVocabulary)
 class OCIRecipeVocabulary(StormVocabularyBase):
     """All OCI Recipes of a given OCI project."""
diff --git a/lib/lp/oci/vocabularies.zcml b/lib/lp/oci/vocabularies.zcml
index 1a6b75c..24a57d8 100644
--- a/lib/lp/oci/vocabularies.zcml
+++ b/lib/lp/oci/vocabularies.zcml
@@ -16,6 +16,16 @@
     </class>
 
     <securedutility
+        name="OCIRegistryCredentials"
+        component="lp.oci.vocabularies.OCIRegistryCredentialsVocabulary"
+        provides="zope.schema.interfaces.IVocabularyFactory">
+        <allow interface="zope.schema.interfaces.IVocabularyFactory" />
+    </securedutility>
+
+    <class class="lp.oci.vocabularies.OCIRegistryCredentialsVocabulary">
+        <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
+    </class>
+    <securedutility
         name="OCIRecipe"
         component="lp.oci.vocabularies.OCIRecipeVocabulary"
         provides="zope.schema.interfaces.IVocabularyFactory">