← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad with lp:~wgrant/launchpad/bulk-insert-errywhere as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/multipolicy-3/+merge/94501

This is the next part of the new access policy model. It's basically the next generation of <https://code.launchpad.net/~wgrant/launchpad/observer-model/+merge/82852>.

I've basically just got basic operations on AccessPolicies, AccessArtifacts, and their corresponding Grants. All the methods are bulk operations. There's also the required person merge code for the grants.

Two tables in the new schema (AccessPolicyArtifact and AccessPolicyGrantFlat) aren't yet represented, because the branch was too big already.
-- 
https://code.launchpad.net/~wgrant/launchpad/multipolicy-3/+merge/94501
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/multipolicy-3 into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2012-02-20 11:52:14 +0000
+++ database/schema/security.cfg	2012-02-24 06:28:21 +0000
@@ -1998,6 +1998,7 @@
 
 [person-merge-job]
 groups=script
+public.accessartifactgrant              = SELECT, UPDATE, DELETE
 public.accesspolicygrant                = SELECT, UPDATE, DELETE
 public.account                          = SELECT, UPDATE
 public.announcement                     = SELECT, UPDATE

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-02-23 21:00:22 +0000
+++ lib/lp/registry/configure.zcml	2012-02-24 06:28:21 +0000
@@ -1906,4 +1906,49 @@
         for="lp.registry.interfaces.person.IPerson"
         provides="lp.registry.interfaces.role.IPersonRoles"
         factory="lp.registry.model.personroles.PersonRoles" />
+
+    <class
+        class="lp.registry.model.accesspolicy.AccessPolicy">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessPolicy"/>
+    </class>
+    <securedutility
+        component="lp.registry.model.accesspolicy.AccessPolicy"
+        provides="lp.registry.interfaces.accesspolicy.IAccessPolicySource">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessPolicySource"/>
+    </securedutility>
+    <class
+        class="lp.registry.model.accesspolicy.AccessArtifact">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessArtifact"/>
+    </class>
+    <securedutility
+        component="lp.registry.model.accesspolicy.AccessArtifact"
+        provides="lp.registry.interfaces.accesspolicy.IAccessArtifactSource">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessArtifactSource"/>
+    </securedutility>
+    <class
+        class="lp.registry.model.accesspolicy.AccessArtifactGrant">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessArtifactGrant"/>
+    </class>
+    <securedutility
+        component="lp.registry.model.accesspolicy.AccessArtifactGrant"
+        provides="lp.registry.interfaces.accesspolicy.IAccessArtifactGrantSource">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessArtifactGrantSource"/>
+    </securedutility>
+    <class
+        class="lp.registry.model.accesspolicy.AccessPolicyGrant">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessPolicyGrant"/>
+    </class>
+    <securedutility
+        component="lp.registry.model.accesspolicy.AccessPolicyGrant"
+        provides="lp.registry.interfaces.accesspolicy.IAccessPolicyGrantSource">
+        <allow
+            interface="lp.registry.interfaces.accesspolicy.IAccessPolicyGrantSource"/>
+    </securedutility>
 </configure>

=== modified file 'lib/lp/registry/enums.py'
--- lib/lp/registry/enums.py	2012-01-17 21:45:24 +0000
+++ lib/lp/registry/enums.py	2012-02-24 06:28:21 +0000
@@ -5,9 +5,10 @@
 
 __metaclass__ = type
 __all__ = [
-    'PersonTransferJobType',
+    'AccessPolicyType',
     'DistroSeriesDifferenceStatus',
     'DistroSeriesDifferenceType',
+    'PersonTransferJobType',
     ]
 
 from lazr.enum import (
@@ -16,6 +17,48 @@
     )
 
 
+class AccessPolicyType(DBEnumeratedType):
+    """Access policy type.
+
+    The policies used to control which users and teams can see various
+    Launchpad artifacts, including bugs and branches.
+    """
+
+    PUBLIC = DBItem(1, """
+        Public
+
+        Everyone can see this information.
+        """)
+
+    PUBLICSECURITY = DBItem(2, """
+        Public Security
+
+        Everyone can see this information pertaining to a resolved security
+        related bug.
+        """)
+
+    EMBARGOEDSECURITY = DBItem(3, """
+        Embargoed Security
+
+        Only users with permission to see the project's security related
+        artifacts can see this information.
+        """)
+
+    USERDATA = DBItem(4, """
+        User Data
+
+        Only users with permission to see the project's artifacts containing
+        user data can see this information.
+        """)
+
+    PROPRIETARY = DBItem(5, """
+        Proprietary
+
+        Only users with permission to see the project's artifacts containing
+        proprietary data can see this information.
+        """)
+
+
 class DistroSeriesDifferenceStatus(DBEnumeratedType):
     """Distribution series difference status.
 

=== added file 'lib/lp/registry/interfaces/accesspolicy.py'
--- lib/lp/registry/interfaces/accesspolicy.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/interfaces/accesspolicy.py	2012-02-24 06:28:21 +0000
@@ -0,0 +1,141 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interfaces for pillar and artifact access policies."""
+
+__metaclass__ = type
+
+__all__ = [
+    'IAccessArtifact',
+    'IAccessArtifactGrant',
+    'IAccessArtifactGrantSource',
+    'IAccessArtifactSource',
+    'IAccessPolicy',
+    'IAccessPolicyGrant',
+    'IAccessPolicyGrantSource',
+    'IAccessPolicySource',
+    ]
+
+from zope.interface import (
+    Attribute,
+    Interface,
+    )
+
+
+class IAccessArtifact(Interface):
+    id = Attribute("ID")
+    concrete_artifact = Attribute("Concrete artifact")
+
+
+class IAccessArtifactGrant(Interface):
+    grantee = Attribute("Grantee")
+    grantor = Attribute("Grantor")
+    date_created = Attribute("Date created")
+    abstract_artifact = Attribute("Abstract artifact")
+
+    concrete_artifact = Attribute("Concrete artifact")
+
+
+class IAccessPolicy(Interface):
+    id = Attribute("ID")
+    pillar = Attribute("Pillar")
+    type = Attribute("Type")
+
+
+class IAccessPolicyGrant(Interface):
+    grantee = Attribute("Grantee")
+    grantor = Attribute("Grantor")
+    date_created = Attribute("Date created")
+    policy = Attribute("Access policy")
+
+
+class IAccessArtifactSource(Interface):
+
+    def ensure(concrete_artifacts):
+        """Return `IAccessArtifact`s for the concrete artifacts.
+
+        Creates abstract artifacts if they don't already exist.
+        """
+
+    def find(concrete_artifacts):
+        """Return the `IAccessArtifact`s for the artifacts, if they exist.
+
+        Use ensure() if you want to create them if they don't yet exist.
+        """
+
+    def delete(concrete_artifacts):
+        """Delete the `IAccessArtifact`s for the concrete artifact.
+
+        Also revokes any `IAccessArtifactGrant`s for the artifacts.
+        """
+
+
+class IAccessArtifactGrantSource(Interface):
+
+    def grant(grants):
+        """Create `IAccessArtifactGrant`s.
+
+        :param grants: a collection of
+            (`IAccessArtifact`, grantee `IPerson`, grantor `IPerson`) triples
+            to grant.
+        """
+
+    def find(grants):
+        """Return the specified `IAccessArtifactGrant`s if they exist.
+
+        :param grants: a collection of (`IAccessArtifact`, grantee `IPerson`)
+            pairs.
+        """
+
+    def findByArtifact(artifacts):
+        """Return all `IAccessArtifactGrant` objects for the artifacts."""
+
+    def revokeByArtifact(artifacts):
+        """Delete all `IAccessArtifactGrant` objects for the artifacts."""
+
+
+class IAccessPolicySource(Interface):
+
+    def create(pillars_and_types):
+        """Create an `IAccessPolicy` for the given pillars and types.
+
+        :param pillars_and_types: a collection of
+            (`IProduct` or `IDistribution`, `IAccessPolicyType`) pairs to
+            create `IAccessPolicy` objects for.
+        :return: a collection of the created `IAccessPolicy` objects.
+        """
+
+    def find(pillars_and_types):
+        """Return the `IAccessPolicy`s for the given pillars and types.
+
+        :param pillars_and_types: a collection of
+            (`IProduct` or `IDistribution`, `IAccessPolicyType`) pairs to
+            find.
+        """
+
+    def findByID(ids):
+        """Return the `IAccessPolicy`s with the given IDs."""
+
+    def findByPillar(pillars):
+        """Return a `ResultSet` of all `IAccessPolicy`s for the pillars."""
+
+
+class IAccessPolicyGrantSource(Interface):
+
+    def grant(grants):
+        """Create `IAccessPolicyGrant`s.
+
+        :param grants: a collection of
+            (`IAccessPolicy`, grantee `IPerson`, grantor `IPerson`) triples
+            to grant.
+        """
+
+    def find(grants):
+        """Return the specified `IAccessPolicyGrant`s if they exist.
+
+        :param grants: a collection of (`IAccessPolicy`, grantee `IPerson`)
+            pairs.
+        """
+
+    def findByPolicy(policies):
+        """Return all `IAccessPolicyGrant` objects for the artifacts."""

=== added file 'lib/lp/registry/model/accesspolicy.py'
--- lib/lp/registry/model/accesspolicy.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/accesspolicy.py	2012-02-24 06:28:21 +0000
@@ -0,0 +1,263 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Model classes for pillar and artifact access policies."""
+
+__metaclass__ = type
+__all__ = [
+    'AccessArtifact',
+    'AccessPolicy',
+    'AccessPolicyGrant',
+    ]
+
+from storm.expr import (
+    And,
+    Or,
+    )
+from storm.properties import (
+    DateTime,
+    Int,
+    )
+from storm.references import Reference
+from zope.component import getUtility
+from zope.interface import implements
+
+from lp.registry.enums import AccessPolicyType
+from lp.registry.interfaces.accesspolicy import (
+    IAccessArtifact,
+    IAccessArtifactGrant,
+    IAccessArtifactGrantSource,
+    IAccessPolicy,
+    IAccessPolicyGrant,
+    )
+from lp.services.database.bulk import create
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.lpstorm import IStore
+from lp.services.database.stormbase import StormBase
+
+
+class AccessArtifact(StormBase):
+    implements(IAccessArtifact)
+
+    __storm_table__ = 'AccessArtifact'
+
+    id = Int(primary=True)
+    bug_id = Int(name='bug')
+    bug = Reference(bug_id, 'Bug.id')
+    branch_id = Int(name='branch')
+    branch = Reference(branch_id, 'Branch.id')
+
+    @property
+    def concrete_artifact(self):
+        artifact = self.bug or self.branch
+        assert artifact is not None
+        return artifact
+
+    @classmethod
+    def _constraintForConcrete(cls, concrete_artifact):
+        from lp.bugs.interfaces.bug import IBug
+        from lp.code.interfaces.branch import IBranch
+        if IBug.providedBy(concrete_artifact):
+            col = cls.bug
+        elif IBranch.providedBy(concrete_artifact):
+            col = cls.branch
+        else:
+            raise ValueError(
+                "%r is not a valid artifact" % concrete_artifact)
+        return col == concrete_artifact
+
+    @classmethod
+    def find(cls, concrete_artifacts):
+        """See `IAccessArtifactSource`."""
+        return IStore(cls).find(
+            cls,
+            Or(*(
+                cls._constraintForConcrete(artifact)
+                for artifact in concrete_artifacts)))
+
+    @classmethod
+    def ensure(cls, concrete_artifacts):
+        """See `IAccessArtifactSource`."""
+        from lp.bugs.interfaces.bug import IBug
+        from lp.code.interfaces.branch import IBranch
+
+        existing = list(cls.find(concrete_artifacts))
+        if len(existing) == len(concrete_artifacts):
+            return existing
+
+        # Not everything exists. Create missing ones.
+        needed = (
+            set(concrete_artifacts) -
+            set(abstract.concrete_artifact for abstract in existing))
+
+        insert_values = []
+        for concrete in needed:
+            if IBug.providedBy(concrete):
+                insert_values.append((concrete, None))
+            elif IBranch.providedBy(concrete):
+                insert_values.append((None, concrete))
+            else:
+                raise ValueError("%r is not a supported artifact" % concrete)
+        new = create((cls.bug, cls.branch), insert_values, get_objects=True)
+        return list(existing) + new
+
+    @classmethod
+    def delete(cls, concrete_artifacts):
+        """See `IAccessPolicyArtifactSource`."""
+        abstracts = list(cls.find(concrete_artifacts))
+        ids = [abstract.id for abstract in abstracts]
+        if len(ids) == 0:
+            return
+        getUtility(IAccessArtifactGrantSource).revokeByArtifact(abstracts)
+        IStore(abstract).find(cls, cls.id.is_in(ids)).remove()
+
+
+class AccessPolicy(StormBase):
+    implements(IAccessPolicy)
+
+    __storm_table__ = 'AccessPolicy'
+
+    id = Int(primary=True)
+    product_id = Int(name='product')
+    product = Reference(product_id, 'Product.id')
+    distribution_id = Int(name='distribution')
+    distribution = Reference(distribution_id, 'Distribution.id')
+    type = DBEnum(allow_none=True, enum=AccessPolicyType)
+
+    @property
+    def pillar(self):
+        return self.product or self.distribution
+
+    @classmethod
+    def create(cls, policies):
+        from lp.registry.interfaces.distribution import IDistribution
+        from lp.registry.interfaces.product import IProduct
+
+        insert_values = []
+        for pillar, type in policies:
+            if IProduct.providedBy(pillar):
+                insert_values.append((pillar, None, type))
+            elif IDistribution.providedBy(pillar):
+                insert_values.append((None, pillar, type))
+            else:
+                raise ValueError("%r is not a supported pillar" % pillar)
+        return create(
+            (cls.product, cls.distribution, cls.type), insert_values,
+            get_objects=True)
+
+    @classmethod
+    def _constraintForPillar(cls, pillar):
+        from lp.registry.interfaces.distribution import IDistribution
+        from lp.registry.interfaces.product import IProduct
+        if IProduct.providedBy(pillar):
+            col = cls.product
+        elif IDistribution.providedBy(pillar):
+            col = cls.distribution
+        else:
+            raise ValueError("%r is not a supported pillar" % pillar)
+        return col == pillar
+
+    @classmethod
+    def find(cls, pillars_and_types):
+        """See `IAccessPolicySource`."""
+        return IStore(cls).find(
+            cls,
+            Or(*(
+                And(cls._constraintForPillar(pillar), cls.type == type)
+                for (pillar, type) in pillars_and_types)))
+
+    @classmethod
+    def findByID(cls, ids):
+        """See `IAccessPolicySource`."""
+        return IStore(cls).find(cls, cls.id.is_in(ids))
+
+    @classmethod
+    def findByPillar(cls, pillars):
+        """See `IAccessPolicySource`."""
+        return IStore(cls).find(
+            cls,
+            Or(*(cls._constraintForPillar(pillar) for pillar in pillars)))
+
+
+class AccessArtifactGrant(StormBase):
+    implements(IAccessArtifactGrant)
+
+    __storm_table__ = 'AccessArtifactGrant'
+    __storm_primary__ = 'abstract_artifact_id', 'grantee_id'
+
+    abstract_artifact_id = Int(name='artifact')
+    abstract_artifact = Reference(
+        abstract_artifact_id, 'AccessArtifact.id')
+    grantee_id = Int(name='grantee')
+    grantee = Reference(grantee_id, 'Person.id')
+    grantor_id = Int(name='grantor')
+    grantor = Reference(grantor_id, 'Person.id')
+    date_created = DateTime()
+
+    @property
+    def concrete_artifact(self):
+        if self.abstract_artifact is not None:
+            return self.abstract_artifact.concrete_artifact
+
+    @classmethod
+    def grant(cls, grants):
+        """See `IAccessArtifactGrantSource`."""
+        return create(
+            (cls.abstract_artifact, cls.grantee, cls.grantor), grants,
+            get_objects=True)
+
+    @classmethod
+    def find(cls, grants):
+        """See `IAccessArtifactGrantSource`."""
+        return IStore(cls).find(
+            cls,
+            Or(*(
+                And(cls.abstract_artifact == artifact, cls.grantee == grantee)
+                for (artifact, grantee) in grants)))
+
+    @classmethod
+    def findByArtifact(cls, artifacts):
+        """See `IAccessArtifactGrantSource`."""
+        ids = [artifact.id for artifact in artifacts]
+        return IStore(cls).find(cls, cls.abstract_artifact_id.is_in(ids))
+
+    @classmethod
+    def revokeByArtifact(cls, artifacts):
+        """See `IAccessPolicyGrantSource`."""
+        cls.findByArtifact(artifacts).remove()
+
+
+class AccessPolicyGrant(StormBase):
+    implements(IAccessPolicyGrant)
+
+    __storm_table__ = 'AccessPolicyGrant'
+    __storm_primary__ = 'policy_id', 'grantee_id'
+
+    policy_id = Int(name='policy')
+    policy = Reference(policy_id, 'AccessPolicy.id')
+    grantee_id = Int(name='grantee')
+    grantee = Reference(grantee_id, 'Person.id')
+    grantor_id = Int(name='grantor')
+    grantor = Reference(grantor_id, 'Person.id')
+    date_created = DateTime()
+
+    @classmethod
+    def grant(cls, grants):
+        """See `IAccessPolicyGrantSource`."""
+        return create(
+            (cls.policy, cls.grantee, cls.grantor), grants, get_objects=True)
+
+    @classmethod
+    def find(cls, grants):
+        """See `IAccessPolicyGrantSource`."""
+        return IStore(cls).find(
+            cls,
+            Or(*(
+                And(cls.policy == policy, cls.grantee == grantee)
+                for (policy, grantee) in grants)))
+
+    @classmethod
+    def findByPolicy(cls, policies):
+        """See `IAccessPolicyGrantSource`."""
+        ids = [policy.id for policy in policies]
+        return IStore(cls).find(cls, cls.policy_id.is_in(ids))

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-02-22 18:51:23 +0000
+++ lib/lp/registry/model/person.py	2012-02-24 06:28:21 +0000
@@ -3526,6 +3526,42 @@
         skip.append(
             (decorator_table.lower(), person_pointer_column.lower()))
 
+    def _mergeAccessArtifactGrant(self, cur, from_id, to_id):
+        # Update only the AccessArtifactGrants that will not conflict.
+        cur.execute('''
+            UPDATE AccessArtifactGrant
+            SET grantee=%(to_id)d
+            WHERE
+                grantee = %(from_id)d
+                AND artifact NOT IN (
+                    SELECT artifact
+                    FROM AccessArtifactGrant
+                    WHERE grantee = %(to_id)d
+                    )
+            ''' % vars())
+        # and delete those left over.
+        cur.execute('''
+            DELETE FROM AccessArtifactGrant WHERE grantee = %(from_id)d
+            ''' % vars())
+
+    def _mergeAccessPolicyGrant(self, cur, from_id, to_id):
+        # Update only the AccessPolicyGrants that will not conflict.
+        cur.execute('''
+            UPDATE AccessPolicyGrant
+            SET grantee=%(to_id)d
+            WHERE
+                grantee = %(from_id)d
+                AND policy NOT IN (
+                    SELECT policy
+                    FROM AccessPolicyGrant
+                    WHERE grantee = %(to_id)d
+                    )
+            ''' % vars())
+        # and delete those left over.
+        cur.execute('''
+            DELETE FROM AccessPolicyGrant WHERE grantee = %(from_id)d
+            ''' % vars())
+
     def _mergeBranches(self, from_person, to_person):
         # This shouldn't use removeSecurityProxy.
         branches = getUtility(IBranchCollection).ownedBy(from_person)
@@ -4079,6 +4115,11 @@
             % vars())
         skip.append(('gpgkey', 'owner'))
 
+        self._mergeAccessArtifactGrant(cur, from_id, to_id)
+        self._mergeAccessPolicyGrant(cur, from_id, to_id)
+        skip.append(('accessartifactgrant', 'grantee'))
+        skip.append(('accesspolicygrant', 'grantee'))
+
         # Update the Branches that will not conflict, and fudge the names of
         # ones that *do* conflict.
         self._mergeBranches(from_person, to_person)

=== added file 'lib/lp/registry/tests/test_accesspolicy.py'
--- lib/lp/registry/tests/test_accesspolicy.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_accesspolicy.py	2012-02-24 06:28:21 +0000
@@ -0,0 +1,307 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+from storm.exceptions import LostObjectError
+from testtools.matchers import (
+    AllMatch,
+    )
+from zope.component import getUtility
+
+from lp.registry.enums import AccessPolicyType
+from lp.registry.interfaces.accesspolicy import (
+    IAccessPolicy,
+    IAccessArtifact,
+    IAccessArtifactGrant,
+    IAccessArtifactGrantSource,
+    IAccessArtifactSource,
+    IAccessPolicyGrant,
+    IAccessPolicyGrantSource,
+    IAccessPolicySource,
+    )
+from lp.services.database.lpstorm import IStore
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import Provides
+
+
+class TestAccessPolicy(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_provides_interface(self):
+        self.assertThat(
+            self.factory.makeAccessPolicy(), Provides(IAccessPolicy))
+
+    def test_pillar(self):
+        product = self.factory.makeProduct()
+        policy = self.factory.makeAccessPolicy(pillar=product)
+        self.assertEqual(product, policy.pillar)
+
+
+class TestAccessPolicySource(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_create(self):
+        wanted = [
+            (self.factory.makeProduct(), AccessPolicyType.PROPRIETARY),
+            (self.factory.makeDistribution(), AccessPolicyType.USERDATA),
+            ]
+        policies = getUtility(IAccessPolicySource).create(wanted)
+        self.assertThat(
+            policies,
+            AllMatch(Provides(IAccessPolicy)))
+        self.assertContentEqual(
+            wanted,
+            [(policy.pillar, policy.type) for policy in policies])
+
+    def test_find(self):
+        # find() finds the right policies.
+        product = self.factory.makeProduct()
+        distribution = self.factory.makeDistribution()
+        other_product = self.factory.makeProduct()
+
+        wanted = [
+            (product, AccessPolicyType.PROPRIETARY),
+            (product, AccessPolicyType.USERDATA),
+            (distribution, AccessPolicyType.PROPRIETARY),
+            (distribution, AccessPolicyType.USERDATA),
+            (other_product, AccessPolicyType.PROPRIETARY),
+            ]
+        getUtility(IAccessPolicySource).create(wanted)
+
+        query = [
+            (product, AccessPolicyType.PROPRIETARY),
+            (product, AccessPolicyType.USERDATA),
+            (distribution, AccessPolicyType.USERDATA),
+            ]
+        self.assertContentEqual(
+            query,
+            [(policy.pillar, policy.type) for policy in
+             getUtility(IAccessPolicySource).find(query)])
+
+        query = [(distribution, AccessPolicyType.PROPRIETARY)]
+        self.assertContentEqual(
+            query,
+            [(policy.pillar, policy.type) for policy in
+             getUtility(IAccessPolicySource).find(query)])
+
+    def test_findByID(self):
+        # findByID finds the right policies.
+        policies = [self.factory.makeAccessPolicy() for i in range(2)]
+        self.factory.makeAccessPolicy()
+        self.assertContentEqual(
+            policies,
+            getUtility(IAccessPolicySource).findByID(
+                [policy.id for policy in policies]))
+
+    def test_findByPillar(self):
+        # findByPillar finds only the relevant policies.
+        product = self.factory.makeProduct()
+        distribution = self.factory.makeProduct()
+        other_product = self.factory.makeProduct()
+        wanted = [
+            (pillar, type)
+            for type in AccessPolicyType.items
+            for pillar in (product, distribution, other_product)]
+        policies = getUtility(IAccessPolicySource).create(wanted)
+        self.assertContentEqual(
+            policies,
+            getUtility(IAccessPolicySource).findByPillar(
+                [product, distribution, other_product]))
+        self.assertContentEqual(
+            [policy for policy in policies if policy.pillar == product],
+            getUtility(IAccessPolicySource).findByPillar([product]))
+
+
+class TestAccessArtifact(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_provides_interface(self):
+        self.assertThat(
+            self.factory.makeAccessArtifact(),
+            Provides(IAccessArtifact))
+
+
+class TestAccessArtifactSourceOnce(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_ensure_other_fails(self):
+        # ensure() rejects unsupported objects.
+        self.assertRaises(
+            ValueError,
+            getUtility(IAccessArtifactSource).ensure,
+            [self.factory.makeProduct()])
+
+
+class BaseAccessArtifactTests:
+    layer = DatabaseFunctionalLayer
+
+    def getConcreteArtifact(self):
+        raise NotImplementedError()
+
+    def test_ensure(self):
+        # ensure() creates abstract artifacts which map to the
+        # concrete ones.
+        concretes = [self.getConcreteArtifact() for i in range(2)]
+        abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+        self.assertContentEqual(
+            concretes,
+            [abstract.concrete_artifact for abstract in abstracts])
+
+    def test_find(self):
+        # find() finds abstract artifacts which map to the concrete ones.
+        concretes = [self.getConcreteArtifact() for i in range(2)]
+        abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+        self.assertContentEqual(
+            abstracts, getUtility(IAccessArtifactSource).find(concretes))
+
+    def test_ensure_twice(self):
+        # ensure() will reuse an existing matching abstract artifact if
+        # it exists.
+        concrete1 = self.getConcreteArtifact()
+        concrete2 = self.getConcreteArtifact()
+        [abstract1] = getUtility(IAccessArtifactSource).ensure([concrete1])
+
+        abstracts = getUtility(IAccessArtifactSource).ensure(
+            [concrete1, concrete2])
+        self.assertIn(abstract1, abstracts)
+        self.assertContentEqual(
+            [concrete1, concrete2],
+            [abstract.concrete_artifact for abstract in abstracts])
+
+    def test_delete(self):
+        # delete() removes the abstract artifacts and any associated
+        # grants.
+        concretes = [self.getConcreteArtifact() for i in range(2)]
+        abstracts = getUtility(IAccessArtifactSource).ensure(concretes)
+        grant = self.factory.makeAccessArtifactGrant(artifact=abstracts[0])
+
+        # Make some other grants to ensure they're unaffected.
+        other_grants = [
+            self.factory.makeAccessArtifactGrant(
+                artifact=self.factory.makeAccessArtifact()),
+            self.factory.makeAccessPolicyGrant(
+                policy=self.factory.makeAccessPolicy()),
+            ]
+
+        getUtility(IAccessArtifactSource).delete(concretes)
+        IStore(grant).invalidate()
+        self.assertRaises(LostObjectError, getattr, grant, 'grantor')
+        self.assertRaises(
+            LostObjectError, getattr, abstracts[0], 'concrete_artifact')
+
+        for other_grant in other_grants:
+            self.assertIsNot(None, other_grant.grantor)
+
+    def test_delete_noop(self):
+        # delete() works even if there's no abstract artifact.
+        concrete = self.getConcreteArtifact()
+        getUtility(IAccessArtifactSource).delete([concrete])
+
+
+class TestAccessArtifactBranch(BaseAccessArtifactTests,
+                               TestCaseWithFactory):
+
+    def getConcreteArtifact(self):
+        return self.factory.makeBranch()
+
+
+class TestAccessArtifactBug(BaseAccessArtifactTests,
+                            TestCaseWithFactory):
+
+    def getConcreteArtifact(self):
+        return self.factory.makeBug()
+
+
+class TestAccessArtifactGrant(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_provides_interface(self):
+        self.assertThat(
+            self.factory.makeAccessArtifactGrant(),
+            Provides(IAccessArtifactGrant))
+
+    def test_concrete_artifact(self):
+        bug = self.factory.makeBug()
+        abstract = self.factory.makeAccessArtifact(bug)
+        grant = self.factory.makeAccessArtifactGrant(artifact=abstract)
+        self.assertEqual(bug, grant.concrete_artifact)
+
+
+class TestAccessArtifactGrantSource(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_grant(self):
+        wanted = [
+            (self.factory.makeAccessArtifact(), self.factory.makePerson(),
+             self.factory.makePerson()),
+            (self.factory.makeAccessArtifact(), self.factory.makePerson(),
+             self.factory.makePerson()),
+            ]
+        grants = getUtility(IAccessArtifactGrantSource).grant(wanted)
+        self.assertContentEqual(
+            wanted,
+            [(g.abstract_artifact, g.grantee, g.grantor) for g in grants])
+
+    def test_find(self):
+        # find() finds the right grants.
+        grants = [self.factory.makeAccessArtifactGrant() for i in range(2)]
+        self.assertContentEqual(
+            grants,
+            getUtility(IAccessArtifactGrantSource).find(
+                [(g.abstract_artifact, g.grantee) for g in grants]))
+
+    def test_findByArtifact(self):
+        # findByArtifact() finds only the relevant grants.
+        artifact = self.factory.makeAccessArtifact()
+        grants = [
+            self.factory.makeAccessArtifactGrant(artifact=artifact)
+            for i in range(3)]
+        self.factory.makeAccessArtifactGrant()
+        self.assertContentEqual(
+            grants,
+            getUtility(IAccessArtifactGrantSource).findByArtifact([artifact]))
+
+
+class TestAccessPolicyGrant(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_provides_interface(self):
+        self.assertThat(
+            self.factory.makeAccessPolicyGrant(),
+            Provides(IAccessPolicyGrant))
+
+
+class TestAccessPolicyGrantSource(TestCaseWithFactory):
+    layer = DatabaseFunctionalLayer
+
+    def test_grant(self):
+        wanted = [
+            (self.factory.makeAccessPolicy(), self.factory.makePerson(),
+             self.factory.makePerson()),
+            (self.factory.makeAccessPolicy(), self.factory.makePerson(),
+             self.factory.makePerson()),
+            ]
+        grants = getUtility(IAccessPolicyGrantSource).grant(wanted)
+        self.assertContentEqual(
+            wanted, [(g.policy, g.grantee, g.grantor) for g in grants])
+
+    def test_find(self):
+        # find() finds the right grants.
+        grants = [self.factory.makeAccessPolicyGrant() for i in range(2)]
+        self.assertContentEqual(
+            grants,
+            getUtility(IAccessPolicyGrantSource).find(
+                [(g.policy, g.grantee) for g in grants]))
+
+    def test_findByPolicy(self):
+        # findByPolicy() finds only the relevant grants.
+        policy = self.factory.makeAccessPolicy()
+        grants = [
+            self.factory.makeAccessPolicyGrant(policy=policy)
+            for i in range(3)]
+        self.factory.makeAccessPolicyGrant()
+        self.assertContentEqual(
+            grants,
+            getUtility(IAccessPolicyGrantSource).findByPolicy([policy]))

=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py	2012-02-20 10:32:23 +0000
+++ lib/lp/registry/tests/test_personset.py	2012-02-24 06:28:21 +0000
@@ -13,6 +13,7 @@
 
 from testtools.matchers import (
     LessThan,
+    MatchesStructure,
     )
 
 from zope.component import getUtility
@@ -23,6 +24,7 @@
     InvalidName,
     NameAlreadyTaken,
     )
+from lp.registry.interfaces.accesspolicy import IAccessPolicyGrantSource
 from lp.registry.interfaces.karma import IKarmaCacheManager
 from lp.registry.interfaces.mailinglist import MailingListStatus
 from lp.registry.interfaces.mailinglistsubscription import (
@@ -527,6 +529,48 @@
         dsp = self.factory.makeDistributionSourcePackage()
         self.assertConflictingSubscriptionDeletes(dsp)
 
+    def test_merge_accesspolicygrants(self):
+        # AccessPolicyGrants are transferred from the duplicate.
+        person = self.factory.makePerson()
+        grant = self.factory.makeAccessPolicyGrant()
+        self._do_premerge(grant.grantee, person)
+
+        source = getUtility(IAccessPolicyGrantSource)
+        self.assertEqual(
+            grant.grantee, source.findByPolicy([grant.policy]).one().grantee)
+        with person_logged_in(person):
+            self._do_merge(grant.grantee, person)
+        self.assertEqual(
+            person, source.findByPolicy([grant.policy]).one().grantee)
+
+    def test_merge_accesspolicygrants_conflicts(self):
+        # Conflicting AccessPolicyGrants are deleted.
+        policy = self.factory.makeAccessPolicy()
+
+        person = self.factory.makePerson()
+        person_grantor = self.factory.makePerson()
+        person_grant = self.factory.makeAccessPolicyGrant(
+            grantee=person, grantor=person_grantor, policy=policy)
+        person_grant_date = person_grant.date_created
+
+        duplicate = self.factory.makePerson()
+        duplicate_grantor = self.factory.makePerson()
+        self.factory.makeAccessPolicyGrant(
+            grantee=duplicate, grantor=duplicate_grantor, policy=policy)
+
+        self._do_premerge(duplicate, person)
+        with person_logged_in(person):
+            self._do_merge(duplicate, person)
+
+        # Only one grant for the policy exists: the retained person's.
+        source = getUtility(IAccessPolicyGrantSource)
+        self.assertThat(
+            source.findByPolicy([policy]).one(),
+            MatchesStructure.byEquality(
+                policy=policy,
+                grantee=person,
+                date_created=person_grant_date))
+
     def test_mergeAsync(self):
         # mergeAsync() creates a new `PersonMergeJob`.
         from_person = self.factory.makePerson()

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2012-02-20 02:07:55 +0000
+++ lib/lp/testing/factory.py	2012-02-24 06:28:21 +0000
@@ -138,9 +138,16 @@
     IHWSubmissionSet,
     )
 from lp.registry.enums import (
+    AccessPolicyType,
     DistroSeriesDifferenceStatus,
     DistroSeriesDifferenceType,
     )
+from lp.registry.interfaces.accesspolicy import (
+    IAccessArtifactGrantSource,
+    IAccessArtifactSource,
+    IAccessPolicyGrantSource,
+    IAccessPolicySource,
+    )
 from lp.registry.interfaces.distribution import (
     IDistribution,
     IDistributionSet,
@@ -4356,6 +4363,42 @@
             target_distroseries, target_pocket,
             package_version=package_version, requester=requester)
 
+    def makeAccessPolicy(self, pillar=None,
+                         type=AccessPolicyType.PROPRIETARY):
+        if pillar is None:
+            pillar = self.makeProduct()
+        policies = getUtility(IAccessPolicySource).create([(pillar, type)])
+        return policies[0]
+
+    def makeAccessArtifact(self, concrete=None):
+        if concrete is None:
+            concrete = self.makeBranch()
+        artifacts = getUtility(IAccessArtifactSource).ensure([concrete])
+        return artifacts[0]
+
+    def makeAccessArtifactGrant(self, artifact=None, grantee=None,
+                                grantor=None):
+        if artifact is None:
+            artifact = self.makeAccessArtifact()
+        if grantee is None:
+            grantee = self.makePerson()
+        if grantor is None:
+            grantor = self.makePerson()
+        [grant] = getUtility(IAccessArtifactGrantSource).grant(
+            [(artifact, grantee, grantor)])
+        return grant
+
+    def makeAccessPolicyGrant(self, policy=None, grantee=None, grantor=None):
+        if policy is None:
+            policy = self.makeAccessPolicy()
+        if grantee is None:
+            grantee = self.makePerson()
+        if grantor is None:
+            grantor = self.makePerson()
+        [grant] = getUtility(IAccessPolicyGrantSource).grant(
+            [(policy, grantee, grantor)])
+        return grant
+
     def makeFakeFileUpload(self, filename=None, content=None):
         """Return a zope.publisher.browser.FileUpload like object.