← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/refactor-launchpad-container into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/refactor-launchpad-container into lp:launchpad.

Commit message:
Refactor LaunchpadContainer to work with scope URLs and to automatically follow the canonical URL chain.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/refactor-launchpad-container/+merge/287412

Refactor LaunchpadContainer to work with scope URLs and to automatically follow the canonical URL chain.  This is in preparation for introducing caveats, where we'll definitely want to work with URL paths rather than objects.

LaunchpadPrincipal.scope_url will change again, but this is a relatively non-painful way to allow the LaunchpadContainer changes to land in isolation.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/refactor-launchpad-container into lp:launchpad.
=== modified file 'lib/lp/bugs/publisher.py'
--- lib/lp/bugs/publisher.py	2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/publisher.py	2016-02-28 19:25:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Bugs' custom publication."""
@@ -55,12 +55,8 @@
 
 class LaunchpadBugContainer(LaunchpadContainer):
 
-    def isWithin(self, scope):
-        """Is this bug within the given scope?
-
-        A bug is in the scope of any of its bugtasks' targets.
-        """
+    def getParentContainers(self):
+        """See `ILaunchpadContainer`."""
+        # A bug is within any of its bugtasks' targets.
         for bugtask in self.context.bugtasks:
-            if ILaunchpadContainer(bugtask.target).isWithin(scope):
-                return True
-        return False
+            yield ILaunchpadContainer(bugtask.target)

=== modified file 'lib/lp/code/publisher.py'
--- lib/lp/code/publisher.py	2015-07-08 16:05:11 +0000
+++ lib/lp/code/publisher.py	2016-02-28 19:25:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Code's custom publication."""
@@ -13,6 +13,7 @@
     ]
 
 
+from zope.component import queryAdapter
 from zope.interface import implementer
 from zope.publisher.interfaces.browser import (
     IBrowserRequest,
@@ -56,12 +57,10 @@
 
 class LaunchpadBranchContainer(LaunchpadContainer):
 
-    def isWithin(self, scope):
-        """Is this branch within the given scope?
-
-        If a branch has a product, it is always in the scope that product or
-        its project.  Otherwise it's not in any scope.
-        """
-        if self.context.product is None:
-            return False
-        return ILaunchpadContainer(self.context.product).isWithin(scope)
+    def getParentContainers(self):
+        """See `ILaunchpadContainer`."""
+        # A branch is within its target.
+        adapter = queryAdapter(
+            self.context.target.context, ILaunchpadContainer)
+        if adapter is not None:
+            yield adapter

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2016-02-05 20:28:29 +0000
+++ lib/lp/registry/configure.zcml	2016-02-28 19:25:32 +0000
@@ -613,7 +613,7 @@
     <adapter
         for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
         provides="lp.services.webapp.interfaces.ILaunchpadContainer"
-        factory="lp.registry.publisher.LaunchpadDistributionSourcePackageContainer"/>
+        factory="lp.services.webapp.publisher.LaunchpadContainer"/>
     <adapter
         provides="lp.app.interfaces.launchpad.IServiceUsage"
         for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"

=== modified file 'lib/lp/registry/doc/launchpad-container.txt'
--- lib/lp/registry/doc/launchpad-container.txt	2015-01-29 18:43:52 +0000
+++ lib/lp/registry/doc/launchpad-container.txt	2016-02-28 19:25:32 +0000
@@ -17,51 +17,51 @@
     >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
     >>> evolution = ubuntu.getSourcePackage('evolution')
 
-The ILaunchpadContainer defines only the isWithin(context) method, which
-returns True if this context is the given one or is within it.
+The ILaunchpadContainer defines only the isWithin(scope_url) method, which
+returns True if this context is at the given URL or is within it.
 
 A product is within itself or its project group.
 
-    >>> ILaunchpadContainer(firefox).isWithin(firefox)
-    True
-    >>> ILaunchpadContainer(firefox).isWithin(mozilla)
-    True
-    >>> ILaunchpadContainer(firefox).isWithin(ubuntu)
+    >>> ILaunchpadContainer(firefox).isWithin('/firefox')
+    True
+    >>> ILaunchpadContainer(firefox).isWithin('/mozilla')
+    True
+    >>> ILaunchpadContainer(firefox).isWithin('/ubuntu')
     False
     >>> verifyObject(ILaunchpadContainer, ILaunchpadContainer(firefox))
     True
 
 A project group is only within itself.
 
-    >>> ILaunchpadContainer(mozilla).isWithin(mozilla)
+    >>> ILaunchpadContainer(mozilla).isWithin('/mozilla')
     True
-    >>> ILaunchpadContainer(mozilla).isWithin(firefox)
+    >>> ILaunchpadContainer(mozilla).isWithin('/firefox')
     False
-    >>> ILaunchpadContainer(mozilla).isWithin(ubuntu)
+    >>> ILaunchpadContainer(mozilla).isWithin('/ubuntu')
     False
     >>> verifyObject(ILaunchpadContainer, ILaunchpadContainer(mozilla))
     True
 
 A distribution is only within itself.
 
-    >>> ILaunchpadContainer(ubuntu).isWithin(ubuntu)
+    >>> ILaunchpadContainer(ubuntu).isWithin('/ubuntu')
     True
-    >>> ILaunchpadContainer(ubuntu).isWithin(mozilla)
+    >>> ILaunchpadContainer(ubuntu).isWithin('/mozilla')
     False
-    >>> ILaunchpadContainer(ubuntu).isWithin(firefox)
+    >>> ILaunchpadContainer(ubuntu).isWithin('/firefox')
     False
     >>> verifyObject(ILaunchpadContainer, ILaunchpadContainer(ubuntu))
     True
 
 A distribution source package is within itself or its distribution.
 
-    >>> ILaunchpadContainer(evolution).isWithin(evolution)
-    True
-    >>> ILaunchpadContainer(evolution).isWithin(ubuntu)
-    True
-    >>> ILaunchpadContainer(evolution).isWithin(firefox)
+    >>> ILaunchpadContainer(evolution).isWithin('/ubuntu/+source/evolution')
+    True
+    >>> ILaunchpadContainer(evolution).isWithin('/ubuntu')
+    True
+    >>> ILaunchpadContainer(evolution).isWithin('/firefox')
     False
-    >>> ILaunchpadContainer(evolution).isWithin(mozilla)
+    >>> ILaunchpadContainer(evolution).isWithin('/mozilla')
     False
     >>> verifyObject(ILaunchpadContainer, ILaunchpadContainer(evolution))
     True
@@ -69,9 +69,7 @@
 An ILaunchpadContainer will never be within something which doesn't
 provide ILaunchpadContainer as well.
 
-    >>> from lp.registry.interfaces.person import IPersonSet
-    >>> salgado = getUtility(IPersonSet).getByName('salgado')
-    >>> ILaunchpadContainer(firefox).isWithin(salgado)
+    >>> ILaunchpadContainer(firefox).isWithin('/~salgado')
     False
 
 
@@ -80,12 +78,14 @@
 Bugs are associated to our pillars through their bug tasks, so a bug is
 said to be within any of its bugtasks' targets.
 
+    >>> from operator import attrgetter
     >>> from lp.bugs.interfaces.bug import IBugSet
-    >>> from operator import attrgetter
+    >>> from lp.services.webapp.publisher import canonical_url
     >>> bug_1 = getUtility(IBugSet).get(1)
     >>> bugtasks = bug_1.bugtasks
     >>> targets = [task.target for task in bug_1.bugtasks]
-    >>> [(target.title, ILaunchpadContainer(bug_1).isWithin(target))
+    >>> [(target.title, ILaunchpadContainer(bug_1).isWithin(
+    ...     canonical_url(target, force_local_path=True)))
     ...  for target in sorted(targets, key=attrgetter('title'))]
     [(u'Mozilla Firefox', True),
      (...mozilla-firefox... package in Debian', True),
@@ -95,29 +95,28 @@
 
     >>> evolution in targets
     False
-    >>> ILaunchpadContainer(bug_1).isWithin(evolution)
+    >>> ILaunchpadContainer(bug_1).isWithin('/ubuntu/+source/evolution')
     False
 
 
 == Branches ==
 
-A branch is within its product, in case it is associated with one.
+A branch is within its target.
 
+    >>> from lp.code.interfaces.branchnamespace import get_branch_namespace
+    >>> from lp.registry.interfaces.person import IPersonSet
     >>> sample_person = getUtility(IPersonSet).getByName('name12')
-    >>> from lp.code.interfaces.branchnamespace import (
-    ...     get_branch_namespace)
     >>> firefox_main = get_branch_namespace(
     ...     sample_person, product=firefox).getByName('main')
-    >>> ILaunchpadContainer(firefox_main).isWithin(firefox)
-    True
-    >>> ILaunchpadContainer(firefox_main).isWithin(mozilla)
-    True
-
-If the branch is not associated with a product, then it's not within
-anything.
-
-    >>> junk= get_branch_namespace(sample_person).getByName('junk.dev')
+    >>> ILaunchpadContainer(firefox_main).isWithin('/firefox')
+    True
+    >>> ILaunchpadContainer(firefox_main).isWithin('/mozilla')
+    True
+
+But it's not within anything other than its target.
+
+    >>> junk = get_branch_namespace(sample_person).getByName('junk.dev')
     >>> print junk.product
     None
-    >>> ILaunchpadContainer(junk).isWithin(firefox)
+    >>> ILaunchpadContainer(junk).isWithin('/firefox')
     False

=== modified file 'lib/lp/registry/publisher.py'
--- lib/lp/registry/publisher.py	2015-01-29 16:28:30 +0000
+++ lib/lp/registry/publisher.py	2016-02-28 19:25:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """ILaunchpadContainer adapters."""
@@ -6,29 +6,17 @@
 __metaclass__ = type
 __all__ = [
     'LaunchpadProductContainer',
-    'LaunchpadDistributionSourcePackageContainer',
     ]
 
 
+from lp.services.webapp.interfaces import ILaunchpadContainer
 from lp.services.webapp.publisher import LaunchpadContainer
 
 
 class LaunchpadProductContainer(LaunchpadContainer):
 
-    def isWithin(self, scope):
-        """Is this product within the given scope?
-
-        A product is within itself or its project group.
-        """
-
-        return scope == self.context or scope == self.context.projectgroup
-
-
-class LaunchpadDistributionSourcePackageContainer(LaunchpadContainer):
-
-    def isWithin(self, scope):
-        """Is this distribution source package within the given scope?
-
-        A distribution source package is within its distribution.
-        """
-        return scope == self.context or scope == self.context.distribution
+    def getParentContainers(self):
+        """See `ILaunchpadContainer`."""
+        # A project is within its project group.
+        if self.context.projectgroup is not None:
+            yield ILaunchpadContainer(self.context.projectgroup)

=== modified file 'lib/lp/services/webapp/authentication.py'
--- lib/lp/services/webapp/authentication.py	2016-01-26 15:14:01 +0000
+++ lib/lp/services/webapp/authentication.py	2016-02-28 19:25:32 +0000
@@ -176,7 +176,7 @@
     """
 
     def getPrincipal(self, id, access_level=AccessLevel.WRITE_PRIVATE,
-                     scope=None):
+                     scope_url=None):
         """Return an `ILaunchpadPrincipal` for the account with the given id.
 
         Return None if there is no account with the given id.
@@ -184,9 +184,9 @@
         The `access_level` can be used for further restricting the capability
         of the principal.  By default, no further restriction is added.
 
-        Similarly, when a `scope` is given, the principal's capabilities will
-        apply only to things within that scope.  For everything else that is
-        not private, the principal will have only read access.
+        Similarly, when a `scope_url` is given, the principal's capabilities
+        will apply only to things within that scope.  For everything else
+        that is not private, the principal will have only read access.
 
         Note that we currently need to be able to retrieve principals for
         invalid People, as the login machinery needs the principal to
@@ -198,14 +198,14 @@
         except LookupError:
             return None
 
-        return self._principalForAccount(account, access_level, scope)
+        return self._principalForAccount(account, access_level, scope_url)
 
     def getPrincipals(self, name):
         raise NotImplementedError
 
     def getPrincipalByLogin(self, login,
                             access_level=AccessLevel.WRITE_PRIVATE,
-                            scope=None):
+                            scope_url=None):
         """Return a principal based on the account with the email address
         signified by "login".
 
@@ -214,9 +214,9 @@
         The `access_level` can be used for further restricting the capability
         of the principal.  By default, no further restriction is added.
 
-        Similarly, when a `scope` is given, the principal's capabilities will
-        apply only to things within that scope.  For everything else that is
-        not private, the principal will have only read access.
+        Similarly, when a `scope_url` is given, the principal's capabilities
+        will apply only to things within that scope.  For everything else
+        that is not private, the principal will have only read access.
 
 
         Note that we currently need to be able to retrieve principals for
@@ -227,9 +227,10 @@
         person = getUtility(IPersonSet).getByEmail(login, filter_status=False)
         if person is None or person.account is None:
             return None
-        return self._principalForAccount(person.account, access_level, scope)
+        return self._principalForAccount(
+            person.account, access_level, scope_url)
 
-    def _principalForAccount(self, account, access_level, scope):
+    def _principalForAccount(self, account, access_level, scope_url):
         """Return a LaunchpadPrincipal for the given account.
 
         The LaunchpadPrincipal will also have the given access level and
@@ -239,7 +240,7 @@
         principal = LaunchpadPrincipal(
             naked_account.id, naked_account.displayname,
             naked_account.displayname, account,
-            access_level=access_level, scope=scope)
+            access_level=access_level, scope_url=scope_url)
         principal.__parent__ = self
         return principal
 
@@ -254,12 +255,12 @@
 class LaunchpadPrincipal:
 
     def __init__(self, id, title, description, account,
-                 access_level=AccessLevel.WRITE_PRIVATE, scope=None):
+                 access_level=AccessLevel.WRITE_PRIVATE, scope_url=None):
         self.id = unicode(id)
         self.title = title
         self.description = description
         self.access_level = access_level
-        self.scope = scope
+        self.scope_url = scope_url
         self.account = account
         self.person = IPerson(account, None)
 

=== modified file 'lib/lp/services/webapp/authorization.py'
--- lib/lp/services/webapp/authorization.py	2015-07-08 16:05:11 +0000
+++ lib/lp/services/webapp/authorization.py	2016-02-28 19:25:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -120,11 +120,11 @@
         principal's scope, the original access level is returned.  Otherwise
         the access level is READ_PUBLIC.
         """
-        if principal.scope is None:
+        if principal.scope_url is None:
             return principal.access_level
         else:
             container = nearest_adapter(object, ILaunchpadContainer)
-            if container.isWithin(principal.scope):
+            if container.isWithin(principal.scope_url):
                 return principal.access_level
             else:
                 return AccessLevel.READ_PUBLIC

=== modified file 'lib/lp/services/webapp/doc/webapp-authorization.txt'
--- lib/lp/services/webapp/doc/webapp-authorization.txt	2012-08-07 02:31:56 +0000
+++ lib/lp/services/webapp/doc/webapp-authorization.txt	2016-02-28 19:25:32 +0000
@@ -122,7 +122,7 @@
     >>> private_bug = getUtility(IBugSet).get(14)
     >>> logout()
     >>> principal.access_level = AccessLevel.WRITE_PRIVATE
-    >>> principal.scope = firefox
+    >>> principal.scope_url = '/firefox'
     >>> setupInteraction(principal)
     >>> check_permission('launchpad.Edit', firefox)
     True
@@ -146,7 +146,7 @@
 If the scope is a ProjectGroup or Distribution, then the access level will
 be used for anything which is part of that ProjectGroup/Distribution.
 
-    >>> principal.scope = mozilla
+    >>> principal.scope_url = '/mozilla'
     >>> setupInteraction(principal)
     >>> check_permission('launchpad.Edit', mozilla)
     True
@@ -157,7 +157,7 @@
     ...     IDistributionSet)
     >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
     >>> warty = ubuntu.getSeries('warty')
-    >>> principal.scope = ubuntu
+    >>> principal.scope_url = '/ubuntu'
     >>> setupInteraction(principal)
     >>> check_permission('launchpad.Edit', ubuntu)
     True
@@ -175,7 +175,7 @@
 in turn is within Mozilla), so the user's access level will apply to
 that bug task as well.
 
-    >>> principal.scope = mozilla
+    >>> principal.scope_url = '/mozilla'
     >>> setupInteraction(principal)
     >>> from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
     >>> bug_task = firefox.searchTasks(
@@ -185,7 +185,7 @@
 
 If no scope is specified, the access level will be used for everything.
 
-    >>> principal.scope = None
+    >>> principal.scope_url = None
     >>> setupInteraction(principal)
     >>> check_permission('launchpad.Edit', ubuntu)
     True

=== modified file 'lib/lp/services/webapp/doc/webapp-publication.txt'
--- lib/lp/services/webapp/doc/webapp-publication.txt	2016-02-08 12:20:20 +0000
+++ lib/lp/services/webapp/doc/webapp-publication.txt	2016-02-28 19:25:32 +0000
@@ -1090,8 +1090,8 @@
     Guilherme Salgado
     >>> principal.access_level
     <DBItem AccessLevel.WRITE_PUBLIC...
-    >>> principal.scope.name
-    u'firefox'
+    >>> principal.scope_url
+    u'/firefox'
 
 If the token is expired or doesn't exist, an Unauthorized exception is
 raised, though.

=== modified file 'lib/lp/services/webapp/interfaces.py'
--- lib/lp/services/webapp/interfaces.py	2016-01-26 15:47:37 +0000
+++ lib/lp/services/webapp/interfaces.py	2016-02-28 19:25:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -41,8 +41,11 @@
 class ILaunchpadContainer(Interface):
     """Marker interface for objects used as the context of something."""
 
-    def isWithin(scope):
-        """Return True if this context is within the given scope."""
+    def getParentContainers():
+        """Return the containers of each parent of this context."""
+
+    def isWithin(scope_url):
+        """Return True if this context is within the given scope URL."""
 
 
 class ILaunchpadRoot(IContainmentRoot):

=== modified file 'lib/lp/services/webapp/publisher.py'
--- lib/lp/services/webapp/publisher.py	2016-02-08 12:20:20 +0000
+++ lib/lp/services/webapp/publisher.py	2016-02-28 19:25:32 +0000
@@ -77,6 +77,7 @@
     defaultFlagValue,
     getFeatureFlag,
     )
+from lp.services.propertycache import cachedproperty
 from lp.services.utils import obfuscate_structure
 from lp.services.webapp.interfaces import (
     ICanonicalUrlData,
@@ -844,13 +845,36 @@
     def __init__(self, context):
         self.context = context
 
-    def isWithin(self, scope):
-        """Is this object within the given scope?
-
-        By default all objects are only within itself.  More specific adapters
-        must override this and implement the logic they want.
+    @cachedproperty
+    def _context_url(self):
+        try:
+            return canonical_url(self.context, force_local_path=True)
+        except NoCanonicalUrl:
+            return None
+
+    def getParentContainers(self):
+        """See `ILaunchpadContainer`.
+
+        By default, we only consider the parent of this object in the
+        canonical URL iteration.  Adapters for objects with more complex
+        parentage rules must override this method.
         """
-        return self.context == scope
+        # Circular import.
+        from lp.services.webapp.canonicalurl import nearest_adapter
+        urldata = ICanonicalUrlData(self.context, None)
+        if urldata is not None and urldata.inside is not None:
+            container = nearest_adapter(urldata.inside, ILaunchpadContainer)
+            yield container
+
+    def isWithin(self, scope_url):
+        """See `ILaunchpadContainer`."""
+        if self._context_url is None:
+            return False
+        if self._context_url == scope_url:
+            return True
+        return any(
+            parent.isWithin(scope_url)
+            for parent in self.getParentContainers())
 
 
 @implementer(IBrowserPublisher)

=== modified file 'lib/lp/services/webapp/servers.py'
--- lib/lp/services/webapp/servers.py	2016-02-10 00:51:55 +0000
+++ lib/lp/services/webapp/servers.py	2016-02-28 19:25:32 +0000
@@ -101,7 +101,10 @@
     )
 from lp.services.webapp.opstats import OpStats
 from lp.services.webapp.publication import LaunchpadBrowserPublication
-from lp.services.webapp.publisher import RedirectionView
+from lp.services.webapp.publisher import (
+    canonical_url,
+    RedirectionView,
+    )
 from lp.services.webapp.vhosts import allvhosts
 from lp.services.webservice.interfaces import IWebServiceApplication
 from lp.testopenid.interfaces.server import ITestOpenIDApplication
@@ -1331,9 +1334,13 @@
             # Everything is fine, let's return the principal.
             pass
         alsoProvides(request, IOAuthSignedRequest)
+        if token.context is not None:
+            scope_url = canonical_url(token.context, force_local_path=True)
+        else:
+            scope_url = None
         principal = getUtility(IPlacelessLoginSource).getPrincipal(
             token.person.account.id, access_level=token.permission,
-            scope=token.context)
+            scope_url=scope_url)
 
         return principal
 

=== modified file 'lib/lp/services/webapp/tests/test_authorization.py'
--- lib/lp/services/webapp/tests/test_authorization.py	2015-07-08 16:05:11 +0000
+++ lib/lp/services/webapp/tests/test_authorization.py	2016-02-28 19:25:32 +0000
@@ -1,10 +1,11 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for `lp.services.webapp.authorization`."""
 
 __metaclass__ = type
 
+from itertools import count
 from random import getrandbits
 import StringIO
 
@@ -38,10 +39,12 @@
     )
 from lp.services.webapp.interfaces import (
     AccessLevel,
+    ICanonicalUrlData,
     ILaunchpadContainer,
     ILaunchpadPrincipal,
     )
 from lp.services.webapp.metazcml import ILaunchpadPermission
+from lp.services.webapp.publisher import LaunchpadContainer
 from lp.services.webapp.servers import (
     LaunchpadBrowserRequest,
     LaunchpadTestRequest,
@@ -182,7 +185,7 @@
 class FakeLaunchpadPrincipal:
     """A minimal principal implementing `ILaunchpadPrincipal`"""
     person = FakePerson()
-    scope = None
+    scope_url = None
     access_level = ''
 
 
@@ -385,12 +388,13 @@
         self.security = LaunchpadSecurityPolicy()
         provideAdapter(
             adapt_loneobject_to_container, [ILoneObject], ILaunchpadContainer)
+        provideAdapter(LoneObjectURL, [ILoneObject], ICanonicalUrlData)
         self.addCleanup(zope.testing.cleanup.cleanUp)
 
     def test_no_scope(self):
         """Principal's access level is used when no scope is given."""
         self.principal.access_level = AccessLevel.WRITE_PUBLIC
-        self.principal.scope = None
+        self.principal.scope_url = None
         self.failUnlessEqual(
             self.security._getPrincipalsAccessLevel(
                 self.principal, LoneObject()),
@@ -400,7 +404,7 @@
         """Principal's access level is used when object is within scope."""
         obj = LoneObject()
         self.principal.access_level = AccessLevel.WRITE_PUBLIC
-        self.principal.scope = obj
+        self.principal.scope_url = '/+loneobject/%d' % obj.id
         self.failUnlessEqual(
             self.security._getPrincipalsAccessLevel(self.principal, obj),
             self.principal.access_level)
@@ -409,7 +413,7 @@
         """READ_PUBLIC is used when object is /not/ within scope."""
         obj = LoneObject()
         obj2 = LoneObject()  # This is out of obj's scope.
-        self.principal.scope = obj
+        self.principal.scope_url = '/+loneobject/%d' % obj.id
 
         self.principal.access_level = AccessLevel.WRITE_PUBLIC
         self.failUnlessEqual(
@@ -431,11 +435,31 @@
     """A marker interface for objects that only contain themselves."""
 
 
-@implementer(ILoneObject, ILaunchpadContainer)
-class LoneObject:
-
-    def isWithin(self, context):
-        return self == context
+@implementer(ILoneObject)
+class LoneObject(LaunchpadContainer):
+
+    _id_counter = count(1)
+
+    def __init__(self):
+        super(LoneObject, self).__init__(self)
+        self.id = LoneObject._id_counter.next()
+
+    def getParentContainers(self):
+        return []
+
+
+@implementer(ICanonicalUrlData)
+class LoneObjectURL:
+
+    rootsite = None
+    inside = None
+
+    def __init__(self, loneobject):
+        self.loneobject = loneobject
+
+    @property
+    def path(self):
+        return '+loneobject/%d' % self.loneobject.id
 
 
 def adapt_loneobject_to_container(loneobj):

=== removed file 'lib/lp/services/webapp/tests/test_webapp_authorization.py'
--- lib/lp/services/webapp/tests/test_webapp_authorization.py	2015-10-14 15:22:01 +0000
+++ lib/lp/services/webapp/tests/test_webapp_authorization.py	1970-01-01 00:00:00 +0000
@@ -1,86 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-
-from unittest import TestCase
-
-from zope.component import provideAdapter
-from zope.interface import (
-    implementer,
-    Interface,
-    )
-from zope.testing.cleanup import CleanUp
-
-from lp.services.webapp.authentication import LaunchpadPrincipal
-from lp.services.webapp.authorization import LaunchpadSecurityPolicy
-from lp.services.webapp.interfaces import (
-    AccessLevel,
-    ILaunchpadContainer,
-    )
-
-
-class TestLaunchpadSecurityPolicy_getPrincipalsAccessLevel(
-    CleanUp, TestCase):
-
-    def setUp(self):
-        self.principal = LaunchpadPrincipal(
-            'foo.bar@xxxxxxxxxxxxx', 'foo', 'foo', object())
-        self.security = LaunchpadSecurityPolicy()
-        provideAdapter(
-            adapt_loneobject_to_container, [ILoneObject], ILaunchpadContainer)
-
-    def test_no_scope(self):
-        """Principal's access level is used when no scope is given."""
-        self.principal.access_level = AccessLevel.WRITE_PUBLIC
-        self.principal.scope = None
-        self.failUnlessEqual(
-            self.security._getPrincipalsAccessLevel(
-                self.principal, LoneObject()),
-            self.principal.access_level)
-
-    def test_object_within_scope(self):
-        """Principal's access level is used when object is within scope."""
-        obj = LoneObject()
-        self.principal.access_level = AccessLevel.WRITE_PUBLIC
-        self.principal.scope = obj
-        self.failUnlessEqual(
-            self.security._getPrincipalsAccessLevel(self.principal, obj),
-            self.principal.access_level)
-
-    def test_object_not_within_scope(self):
-        """READ_PUBLIC is used when object is /not/ within scope."""
-        obj = LoneObject()
-        obj2 = LoneObject()  # This is out of obj's scope.
-        self.principal.scope = obj
-
-        self.principal.access_level = AccessLevel.WRITE_PUBLIC
-        self.failUnlessEqual(
-            self.security._getPrincipalsAccessLevel(self.principal, obj2),
-            AccessLevel.READ_PUBLIC)
-
-        self.principal.access_level = AccessLevel.READ_PRIVATE
-        self.failUnlessEqual(
-            self.security._getPrincipalsAccessLevel(self.principal, obj2),
-            AccessLevel.READ_PUBLIC)
-
-        self.principal.access_level = AccessLevel.WRITE_PRIVATE
-        self.failUnlessEqual(
-            self.security._getPrincipalsAccessLevel(self.principal, obj2),
-            AccessLevel.READ_PUBLIC)
-
-
-class ILoneObject(Interface):
-    """A marker interface for objects that only contain themselves."""
-
-
-@implementer(ILoneObject, ILaunchpadContainer)
-class LoneObject:
-
-    def isWithin(self, context):
-        return self == context
-
-
-def adapt_loneobject_to_container(loneobj):
-    """Adapt a LoneObject to an `ILaunchpadContainer`."""
-    return loneobj


Follow ups