← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:oci-registry-credentials-not-owner into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:oci-registry-credentials-not-owner into launchpad:master.

Commit message:
Check owner permissions when creating OCI registry credentials

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

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

It shouldn't be possible to create credentials owned by other people, or by teams of which the creating user isn't a member.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-registry-credentials-not-owner into launchpad:master.
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index c2dceb2..594b93d 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -543,7 +543,7 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
             credentials_set = getUtility(IOCIRegistryCredentialsSet)
             try:
                 credentials = credentials_set.getOrCreate(
-                    owner=self.context.owner, url=url,
+                    registrant=self.user, owner=self.context.owner, url=url,
                     credentials={'username': username, 'password': password})
             except OCIRegistryCredentialsAlreadyExist:
                 self.setFieldError(
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 8b17168..455ff57 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -911,8 +911,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         image_name = self.factory.getUniqueUnicode()
         push_rule = getUtility(IOCIPushRuleSet).new(
@@ -946,8 +945,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         image_name = self.factory.getUniqueUnicode()
         push_rule = getUtility(IOCIPushRuleSet).new(
@@ -1027,8 +1025,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         image_name = self.factory.getUniqueUnicode()
         push_rule = getUtility(IOCIPushRuleSet).new(
@@ -1092,8 +1089,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         image_names = [self.factory.getUniqueUnicode() for _ in range(2)]
         push_rules = [
@@ -1136,8 +1132,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         image_name = self.factory.getUniqueUnicode()
         push_rule = getUtility(IOCIPushRuleSet).new(
@@ -1242,7 +1237,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         existing_rule = self.factory.makeOCIPushRule(
             recipe=self.recipe,
             registry_credentials=self.factory.makeOCIRegistryCredentials(
-                owner=self.recipe.owner))
+                registrant=self.recipe.owner, owner=self.recipe.owner))
         existing_image_name = existing_rule.image_name
         existing_registry_url = existing_rule.registry_url
         existing_username = existing_rule.username
@@ -1267,7 +1262,8 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         existing_rule = self.factory.makeOCIPushRule(
             recipe=self.recipe,
             registry_credentials=self.factory.makeOCIRegistryCredentials(
-                owner=self.recipe.owner, credentials={}))
+                registrant=self.recipe.owner, owner=self.recipe.owner,
+                credentials={}))
         existing_registry_url = existing_rule.registry_url
         browser = self.getViewBrowser(self.recipe, user=self.person)
         browser.getLink("Edit push rules").click()
@@ -1367,8 +1363,7 @@ class TestOCIRecipeEditPushRulesView(OCIConfigHelperMixin,
         credentials = {'username': 'foo', 'password': 'bar'}
         image_name = self.factory.getUniqueUnicode()
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.person,
-            url=url,
+            registrant=self.person, owner=self.person, url=url,
             credentials=credentials)
         getUtility(IOCIPushRuleSet).new(
             recipe=self.recipe,
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index c433825..9beb6af 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -300,6 +300,7 @@ class IOCIRecipeEdit(IWebhookTarget):
     def destroySelf():
         """Delete this OCI recipe, provided that it has no builds."""
 
+    @call_with(registrant=REQUEST_USER)
     @operation_parameters(
         registry_url=TextLine(
             title=_("Registry URL"),
@@ -321,7 +322,7 @@ class IOCIRecipeEdit(IWebhookTarget):
     # Really IOCIPushRule, patched in lp.oci.interfaces.webservice.
     @export_factory_operation(Interface, [])
     @operation_for_version("devel")
-    def newPushRule(registry_url, image_name, credentials,
+    def newPushRule(registrant, registry_url, image_name, credentials,
                     credentials_owner=None):
         """Add a new rule for pushing builds of this recipe to a registry."""
 
diff --git a/lib/lp/oci/interfaces/ociregistrycredentials.py b/lib/lp/oci/interfaces/ociregistrycredentials.py
index 08c1bdb..c6d2707 100644
--- a/lib/lp/oci/interfaces/ociregistrycredentials.py
+++ b/lib/lp/oci/interfaces/ociregistrycredentials.py
@@ -10,6 +10,7 @@ __all__ = [
     'IOCIRegistryCredentials',
     'IOCIRegistryCredentialsSet',
     'OCIRegistryCredentialsAlreadyExist',
+    'OCIRegistryCredentialsNotOwner',
     'user_can_edit_credentials_for_owner',
     ]
 
@@ -20,6 +21,7 @@ from zope.schema import (
     Int,
     TextLine,
     )
+from zope.security.interfaces import Unauthorized
 
 from lp import _
 from lp.registry.interfaces.role import (
@@ -43,6 +45,11 @@ class OCIRegistryCredentialsAlreadyExist(Exception):
             "Credentials already exist with the same URL and username.")
 
 
+@error_status(http_client.UNAUTHORIZED)
+class OCIRegistryCredentialsNotOwner(Unauthorized):
+    """The registrant is not the owner or a member of its team."""
+
+
 class IOCIRegistryCredentialsView(Interface):
 
     id = Int(title=_("ID"), required=True, readonly=True)
@@ -97,10 +104,10 @@ class IOCIRegistryCredentials(IOCIRegistryCredentialsEdit,
 class IOCIRegistryCredentialsSet(Interface):
     """A utility to create and access OCI Registry Credentials."""
 
-    def new(owner, url, credentials):
+    def new(registrant, owner, url, credentials):
         """Create an `IOCIRegistryCredentials`."""
 
-    def getOrCreate(owner, url, credentials):
+    def getOrCreate(registrant, owner, url, credentials):
         """Get an `IOCIRegistryCredentials` that match the url and username
         or create a new object."""
 
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 2a83ccc..c167b15 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -498,7 +498,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
     def can_upload_to_registry(self):
         return not self.push_rules.is_empty()
 
-    def newPushRule(self, registry_url, image_name, credentials,
+    def newPushRule(self, registrant, registry_url, image_name, credentials,
                     credentials_owner=None):
         """See `IOCIRecipe`."""
         if credentials_owner is None:
@@ -508,7 +508,7 @@ class OCIRecipe(Storm, WebhookTargetMixin):
             # we give it a default.
             credentials_owner = self.owner
         oci_credentials = getUtility(IOCIRegistryCredentialsSet).getOrCreate(
-            credentials_owner, registry_url, credentials)
+            registrant, credentials_owner, registry_url, credentials)
         push_rule = getUtility(IOCIPushRuleSet).new(
             self, oci_credentials, image_name)
         Store.of(push_rule).flush()
diff --git a/lib/lp/oci/model/ociregistrycredentials.py b/lib/lp/oci/model/ociregistrycredentials.py
index d86b140..396124e 100644
--- a/lib/lp/oci/model/ociregistrycredentials.py
+++ b/lib/lp/oci/model/ociregistrycredentials.py
@@ -31,6 +31,7 @@ from lp.oci.interfaces.ociregistrycredentials import (
     IOCIRegistryCredentials,
     IOCIRegistryCredentialsSet,
     OCIRegistryCredentialsAlreadyExist,
+    OCIRegistryCredentialsNotOwner,
     )
 from lp.services.config import config
 from lp.services.crypto.interfaces import (
@@ -134,6 +135,17 @@ class OCIRegistryCredentials(Storm):
 @implementer(IOCIRegistryCredentialsSet)
 class OCIRegistryCredentialsSet:
 
+    def _checkOwner(self, registrant, owner):
+        if not registrant.inTeam(owner):
+            if owner.is_team:
+                raise OCIRegistryCredentialsNotOwner(
+                    "%s is not a member of %s." %
+                    (registrant.display_name, owner.display_name))
+            else:
+                raise OCIRegistryCredentialsNotOwner(
+                    "%s cannot create credentials owned by %s." %
+                    (registrant.display_name, owner.display_name))
+
     def _checkForExisting(self, owner, url, credentials):
         for existing in self.findByOwner(owner):
             url_match = existing.url == url
@@ -142,18 +154,20 @@ class OCIRegistryCredentialsSet:
                 return existing
         return None
 
-    def new(self, owner, url,  credentials):
+    def new(self, registrant, owner, url, credentials):
         """See `IOCIRegistryCredentialsSet`."""
+        self._checkOwner(registrant, owner)
         if self._checkForExisting(owner, url, credentials):
             raise OCIRegistryCredentialsAlreadyExist()
         return OCIRegistryCredentials(owner, url, credentials)
 
-    def getOrCreate(self, owner, url, credentials):
+    def getOrCreate(self, registrant, owner, url, credentials):
         """See `IOCIRegistryCredentialsSet`."""
+        self._checkOwner(registrant, owner)
         existing = self._checkForExisting(owner, url, credentials)
         if existing:
             return existing
-        return self.new(owner, url, credentials)
+        return self.new(registrant, owner, url, credentials)
 
     def findByOwner(self, owner):
         """See `IOCIRegistryCredentialsSet`."""
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index ad67a7a..840d0a0 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -11,6 +11,7 @@ from fixtures import FakeLogger
 from six import string_types
 from storm.exceptions import LostObjectError
 from storm.store import Store
+from testtools import ExpectedException
 from testtools.matchers import (
     ContainsDict,
     Equals,
@@ -48,6 +49,9 @@ from lp.oci.interfaces.ocirecipe import (
     )
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
 from lp.oci.interfaces.ocirecipejob import IOCIRecipeRequestBuildsJobSource
+from lp.oci.interfaces.ociregistrycredentials import (
+    OCIRegistryCredentialsNotOwner,
+    )
 from lp.oci.tests.helpers import (
     MatchesOCIRegistryCredentials,
     OCIConfigHelperMixin,
@@ -459,7 +463,7 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
 
         with person_logged_in(recipe.registrant):
             push_rule = recipe.newPushRule(
-                url, image_name, credentials,
+                recipe.registrant, url, image_name, credentials,
                 credentials_owner=recipe.registrant)
             self.assertThat(push_rule, MatchesStructure(
                 image_name=Equals(image_name),
@@ -480,7 +484,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
             "username": "test-username", "password": "test-password"}
 
         with person_logged_in(recipe.registrant):
-            push_rule = recipe.newPushRule(url, image_name, credentials)
+            push_rule = recipe.newPushRule(
+                recipe.registrant, url, image_name, credentials)
             self.assertThat(push_rule, MatchesStructure(
                 image_name=Equals(image_name),
                 registry_credentials=MatchesOCIRegistryCredentials(
@@ -499,7 +504,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
         with person_logged_in(recipe.owner):
             self.assertRaises(
                 ValidationError, recipe.newPushRule,
-                url, image_name, credentials, credentials_owner=recipe.owner)
+                recipe.owner, url, image_name, credentials,
+                credentials_owner=recipe.owner)
             # Avoid trying to flush the incomplete object on cleanUp.
             Store.of(recipe).rollback()
 
@@ -513,11 +519,41 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
 
         with person_logged_in(recipe.owner):
             recipe.newPushRule(
-                url, image_name, credentials, credentials_owner=recipe.owner)
+                recipe.owner, url, image_name, credentials,
+                credentials_owner=recipe.owner)
             self.assertRaises(
                 OCIPushRuleAlreadyExists,
                 recipe.newPushRule,
-                url, image_name, credentials, credentials_owner=recipe.owner)
+                recipe.owner, url, image_name, credentials,
+                credentials_owner=recipe.owner)
+
+    def test_newPushRule_not_owner(self):
+        # If the registrant is not the owner or a member of the owner team,
+        # push rule creation fails.
+        self.setConfig()
+        recipe = self.factory.makeOCIRecipe()
+        url = self.factory.getUniqueURL()
+        image_name = self.factory.getUniqueUnicode()
+        credentials = {
+            "username": "test-username", "password": "test-password"}
+        other_person = self.factory.makePerson()
+        other_team = self.factory.makeTeam(owner=other_person)
+
+        with person_logged_in(recipe.registrant):
+            expected_message = "%s cannot create credentials owned by %s." % (
+                recipe.registrant.display_name, other_person.display_name)
+            with ExpectedException(
+                    OCIRegistryCredentialsNotOwner, expected_message):
+                recipe.newPushRule(
+                    recipe.registrant, url, image_name, credentials,
+                    credentials_owner=other_person)
+            expected_message = "%s is not a member of %s." % (
+                recipe.registrant.display_name, other_team.display_name)
+            with ExpectedException(
+                    OCIRegistryCredentialsNotOwner, expected_message):
+                recipe.newPushRule(
+                    recipe.registrant, url, image_name, credentials,
+                    credentials_owner=other_team)
 
     def test_set_official_directly_is_forbidden(self):
         recipe = self.factory.makeOCIRecipe()
diff --git a/lib/lp/oci/tests/test_ociregistrycredentials.py b/lib/lp/oci/tests/test_ociregistrycredentials.py
index 6e42b28..c0e9ba1 100644
--- a/lib/lp/oci/tests/test_ociregistrycredentials.py
+++ b/lib/lp/oci/tests/test_ociregistrycredentials.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 import json
 
+from testtools import ExpectedException
 from testtools.matchers import (
     AfterPreprocessing,
     Equals,
@@ -20,6 +21,7 @@ from lp.oci.interfaces.ociregistrycredentials import (
     IOCIRegistryCredentials,
     IOCIRegistryCredentialsSet,
     OCIRegistryCredentialsAlreadyExist,
+    OCIRegistryCredentialsNotOwner,
     )
 from lp.oci.tests.helpers import OCIConfigHelperMixin
 from lp.services.crypto.interfaces import IEncryptedContainer
@@ -39,9 +41,9 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
         self.setConfig()
 
     def test_implements_interface(self):
+        owner = self.factory.makePerson()
         oci_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.factory.makePerson(),
-            url='http://example.org',
+            registrant=owner, owner=owner, url='http://example.org',
             credentials={'username': 'foo', 'password': 'bar'})
         self.assertProvides(oci_credentials, IOCIRegistryCredentials)
 
@@ -61,7 +63,7 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
     def test_credentials_set(self):
         owner = self.factory.makePerson()
         oci_credentials = self.factory.makeOCIRegistryCredentials(
-            owner=owner,
+            registrant=owner, owner=owner,
             url='http://example.org',
             credentials={'username': 'foo', 'password': 'bar'})
 
@@ -73,7 +75,7 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
     def test_credentials_set_empty(self):
         owner = self.factory.makePerson()
         oci_credentials = self.factory.makeOCIRegistryCredentials(
-            owner=owner,
+            registrant=owner, owner=owner,
             url='http://example.org',
             credentials={})
         with person_logged_in(owner):
@@ -83,7 +85,7 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
         owner = self.factory.makePerson()
         oci_credentials = removeSecurityProxy(
             self.factory.makeOCIRegistryCredentials(
-                owner=owner,
+                registrant=owner, owner=owner,
                 url='http://example.org',
                 credentials={"username": "test"}))
         container = getUtility(IEncryptedContainer, "oci-registry-secrets")
@@ -99,7 +101,7 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
         owner = self.factory.makePerson()
         oci_credentials = removeSecurityProxy(
             self.factory.makeOCIRegistryCredentials(
-                owner=owner,
+                registrant=owner, owner=owner,
                 url='http://example.org',
                 credentials={"password": "bar"}))
         container = getUtility(IEncryptedContainer, "oci-registry-secrets")
@@ -113,7 +115,7 @@ class TestOCIRegistryCredentials(OCIConfigHelperMixin, TestCaseWithFactory):
         owner = self.factory.makePerson()
         oci_credentials = removeSecurityProxy(
             self.factory.makeOCIRegistryCredentials(
-                owner=owner,
+                registrant=owner, owner=owner,
                 url='http://example.org',
                 credentials={
                     "username": "foo", "password": "bar", "other": "baz"}))
@@ -143,9 +145,7 @@ class TestOCIRegistryCredentialsSet(OCIConfigHelperMixin, TestCaseWithFactory):
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         oci_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=owner,
-            url=url,
-            credentials=credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
         self.assertEqual(oci_credentials.owner, owner)
         self.assertEqual(oci_credentials.url, url)
         self.assertEqual(oci_credentials.getCredentials(), credentials)
@@ -155,27 +155,46 @@ class TestOCIRegistryCredentialsSet(OCIConfigHelperMixin, TestCaseWithFactory):
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         getUtility(IOCIRegistryCredentialsSet).new(
-            owner=owner,
-            url=url,
-            credentials=credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
         self.assertRaises(
             OCIRegistryCredentialsAlreadyExist,
             getUtility(IOCIRegistryCredentialsSet).new,
-            owner, url, credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
+
+    def test_new_not_owner(self):
+        registrant = self.factory.makePerson()
+        other_person = self.factory.makePerson()
+        other_team = self.factory.makeTeam(owner=other_person)
+        url = self.factory.getUniqueURL()
+        credentials = {'username': 'foo', 'password': 'bar'}
+        expected_message = "%s cannot create credentials owned by %s." % (
+            registrant.display_name, other_person.display_name)
+        with ExpectedException(
+                OCIRegistryCredentialsNotOwner, expected_message):
+            getUtility(IOCIRegistryCredentialsSet).new(
+                registrant=registrant,
+                owner=other_person,
+                url=url,
+                credentials=credentials)
+        expected_message = "%s is not a member of %s." % (
+            registrant.display_name, other_team.display_name)
+        with ExpectedException(
+                OCIRegistryCredentialsNotOwner, expected_message):
+            getUtility(IOCIRegistryCredentialsSet).new(
+                registrant=registrant,
+                owner=other_team,
+                url=url,
+                credentials=credentials)
 
     def test_getOrCreate_existing(self):
         owner = self.factory.makePerson()
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         new = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=owner,
-            url=url,
-            credentials=credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
 
         existing = getUtility(IOCIRegistryCredentialsSet).getOrCreate(
-            owner=owner,
-            url=url,
-            credentials=credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
 
         self.assertEqual(new.id, existing.id)
 
@@ -184,18 +203,42 @@ class TestOCIRegistryCredentialsSet(OCIConfigHelperMixin, TestCaseWithFactory):
         url = self.factory.getUniqueURL()
         credentials = {'username': 'foo', 'password': 'bar'}
         new = getUtility(IOCIRegistryCredentialsSet).getOrCreate(
-            owner=owner,
-            url=url,
-            credentials=credentials)
+            registrant=owner, owner=owner, url=url, credentials=credentials)
 
         self.assertEqual(new.owner, owner)
         self.assertEqual(new.url, url)
         self.assertEqual(new.getCredentials(), credentials)
 
+    def test_getOrCreate_not_owner(self):
+        registrant = self.factory.makePerson()
+        other_person = self.factory.makePerson()
+        other_team = self.factory.makeTeam(owner=other_person)
+        url = self.factory.getUniqueURL()
+        credentials = {'username': 'foo', 'password': 'bar'}
+        expected_message = "%s cannot create credentials owned by %s." % (
+            registrant.display_name, other_person.display_name)
+        with ExpectedException(
+                OCIRegistryCredentialsNotOwner, expected_message):
+            getUtility(IOCIRegistryCredentialsSet).getOrCreate(
+                registrant=registrant,
+                owner=other_person,
+                url=url,
+                credentials=credentials)
+        expected_message = "%s is not a member of %s." % (
+            registrant.display_name, other_team.display_name)
+        with ExpectedException(
+                OCIRegistryCredentialsNotOwner, expected_message):
+            getUtility(IOCIRegistryCredentialsSet).getOrCreate(
+                registrant=registrant,
+                owner=other_team,
+                url=url,
+                credentials=credentials)
+
     def test_findByOwner(self):
         owner = self.factory.makePerson()
         for _ in range(3):
-            self.factory.makeOCIRegistryCredentials(owner=owner)
+            self.factory.makeOCIRegistryCredentials(
+                registrant=owner, owner=owner)
         # make some that have a different owner
         for _ in range(5):
             self.factory.makeOCIRegistryCredentials()
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index eed3ac1..facd4d2 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -3933,8 +3933,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
                     'password': password}
                 try:
                     getUtility(IOCIRegistryCredentialsSet).new(
-                        owner=owner,
-                        url=url,
+                        registrant=self.user, owner=owner, url=url,
                         credentials=credentials)
                 except OCIRegistryCredentialsAlreadyExist:
                     self.setFieldError(
@@ -3946,8 +3945,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
                 credentials = {'username': username}
                 try:
                     getUtility(IOCIRegistryCredentialsSet).new(
-                        owner=owner,
-                        url=url,
+                        registrant=self.user, owner=owner, url=url,
                         credentials=credentials)
                 except OCIRegistryCredentialsAlreadyExist:
                     self.setFieldError(
diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
index e3f74db..55ab037 100644
--- a/lib/lp/registry/browser/tests/test_person.py
+++ b/lib/lp/registry/browser/tests/test_person.py
@@ -1358,7 +1358,8 @@ class TestPersonOCIRegistryCredentialsView(
         url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
         getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.owner, url=url, credentials=credentials)
+            registrant=self.user, owner=self.owner, url=url,
+            credentials=credentials)
         login_person(self.user)
         view = create_initialized_view(
             self.owner, '+oci-registry-credentials', principal=self.user)
@@ -1374,7 +1375,8 @@ class TestPersonOCIRegistryCredentialsView(
         third_url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.owner, url=url, credentials=credentials)
+            registrant=self.user, owner=self.owner, url=url,
+            credentials=credentials)
 
         browser = self.getViewBrowser(
             self.owner, view_name='+oci-registry-credentials', user=self.user)
@@ -1453,7 +1455,8 @@ class TestPersonOCIRegistryCredentialsView(
         credentials = {'username': 'foo', 'password': 'bar'}
         image_name = self.factory.getUniqueUnicode()
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.owner, url=url, credentials=credentials)
+            registrant=self.user, owner=self.owner, url=url,
+            credentials=credentials)
         getUtility(IOCIPushRuleSet).new(
             recipe=self.recipe,
             registry_credentials=registry_credentials,
@@ -1502,7 +1505,8 @@ class TestPersonOCIRegistryCredentialsView(
         url = unicode(self.factory.getUniqueURL())
         credentials = {'username': 'foo', 'password': 'bar'}
         registry_credentials = getUtility(IOCIRegistryCredentialsSet).new(
-            owner=self.owner, url=url, credentials=credentials)
+            registrant=self.user, owner=self.owner, url=url,
+            credentials=credentials)
         IStore(registry_credentials).flush()
         registry_credentials_id = removeSecurityProxy(registry_credentials).id
         image_name = self.factory.getUniqueUnicode()
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index efc5c57..e8fd8ed 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -5096,11 +5096,13 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         store.add(job)
         return job
 
-    def makeOCIRegistryCredentials(self, owner=None, url=None,
+    def makeOCIRegistryCredentials(self, registrant=None, owner=None, url=None,
                                    credentials=None):
         """Make a new OCIRegistryCredentials."""
+        if registrant is None:
+            registrant = self.makePerson()
         if owner is None:
-            owner = self.makePerson()
+            owner = self.makeTeam(registrant)
         if url is None:
             url = self.getUniqueURL()
         if credentials is None:
@@ -5108,8 +5110,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                 'username': self.getUniqueUnicode(),
                 'password': self.getUniqueUnicode()}
         return getUtility(IOCIRegistryCredentialsSet).new(
-            owner=owner,
-            url=url,
+            registrant=registrant, owner=owner, url=url,
             credentials=credentials)
 
     def makeOCIPushRule(self, recipe=None, registry_credentials=None,