← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/sweep into lp:launchpad

 

Brad Crittenden has proposed merging lp:~bac/launchpad/sweep into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #839784 in Launchpad itself: "Security adapters defer to explicitly named adapters"
  https://bugs.launchpad.net/launchpad/+bug/839784

For more details, see:
https://code.launchpad.net/~bac/launchpad/sweep/+merge/74112

= Summary =

In our security files, many forwarded/deferred adapters are referenced
via name, which is a means for bugs to creep in.  Francis suggested a
sweep of all of our security code and replacing named lookups with
getAdapter lookups.

== Proposed fix ==

A new class ForwardedAuthorizationBase was created for classes that
forward their authorizations to child components.

== Pre-implementation notes ==

Talk with Gary.

== Implementation details ==

As above.

== Tests ==

I added one new test:

bin/test -vvm lp.app -t TestForwardedAuthorizationBase

But since so many security adapters are affected, a full run of the
test suite is the only way to ensure there were no (tested)
regressions.

== Demo and Q/A ==

N/A

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/app/tests/test_security.py
  lib/lp/bugs/security.py
  lib/lp/app/security.py
-- 
https://code.launchpad.net/~bac/launchpad/sweep/+merge/74112
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/sweep into lp:launchpad.
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2011-08-31 19:45:02 +0000
+++ lib/canonical/launchpad/security.py	2011-09-05 15:17:40 +0000
@@ -37,6 +37,7 @@
 from lp.app.security import (
     AnonymousAuthorization,
     AuthorizationBase,
+    ForwardedAuthorization,
     )
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfig
 from lp.blueprints.interfaces.specification import (
@@ -1000,14 +1001,17 @@
             archive.getComponentsForQueueAdmin(user.person)) or user.in_admin
 
 
-class EditDistroSeriesDifference(AuthorizationBase):
+class EditDistroSeriesDifference(ForwardedAuthorization):
     """Anyone with lp.View on the distribution can edit a DSD."""
     permission = 'launchpad.Edit'
     usedfor = IDistroSeriesDifferenceEdit
 
-    def checkAuthenticated(self, user):
-        return self.forwardCheckAuthenticated(
-            user, self.obj.derived_series.distribution, 'launchpad.View')
+    def __init__(self, obj):
+        super(EditDistroSeriesDifference, self).__init__(
+            obj.derived_series.distribution, 'launchpad.View')
+
+    def checkUnauthenticated(self):
+        return False
 
 
 class SeriesDrivers(AuthorizationBase):
@@ -1918,14 +1922,14 @@
                 self.forwardCheckAuthenticated(user, self.obj.distribution))
 
 
-class AdminDistributionSourcePackageTranslations(AuthorizationBase):
+class AdminDistributionSourcePackageTranslations(ForwardedAuthorization):
     """DistributionSourcePackage objects link to a distribution."""
     permission = 'launchpad.TranslationsAdmin'
     usedfor = IDistributionSourcePackage
 
-    def checkAuthenticated(self, user):
-        """Distribution admins are admins for source packages as well."""
-        return self.forwardCheckAuthenticated(user, self.obj.distribution)
+    def __init__(self, obj):
+        super(AdminDistributionSourcePackageTranslations, self).__init__(
+            obj.distribution)
 
 
 class AdminProductSeriesTranslations(AuthorizationBase):
@@ -1971,33 +1975,23 @@
             self.branches))
 
 
-class PreviewDiffView(AuthorizationBase):
+class PreviewDiffView(ForwardedAuthorization):
     permission = 'launchpad.View'
     usedfor = IPreviewDiff
 
-    @property
-    def bmp_view(self):
-        return BranchMergeProposalView(self.obj.branch_merge_proposal)
-
-    def checkAuthenticated(self, user):
-        """Is the user able to view the preview diff?
-
-        The user can see a preview diff if they can see the merge proposal.
-        """
-        return self.bmp_view.checkAuthenticated(user)
-
-    def checkUnauthenticated(self):
-        """Is anyone able to view the branch merge proposal?
-
-        The user can see a preview diff if they can see the merge proposal.
-        """
-        return self.bmp_view.checkUnauthenticated()
-
-
-class CodeReviewVoteReferenceEdit(AuthorizationBase):
+    def __init__(self, obj):
+        super(PreviewDiffView, self).__init__(obj.branch_merge_proposal)
+
+
+class CodeReviewVoteReferenceEdit(ForwardedAuthorization):
     permission = 'launchpad.Edit'
     usedfor = ICodeReviewVoteReference
 
+    def __init__(self, obj):
+        super(CodeReviewVoteReferenceEdit, self).__init__(
+            obj.branch_merge_proposal.target_branch)
+        self.obj = obj
+
     def checkAuthenticated(self, user):
         """Only the affected teams may change the review request.
 
@@ -2008,57 +2002,28 @@
         Anyone with edit permissions on the target branch of the merge
         proposal can also edit the reviews.
         """
-        if user.inTeam(self.obj.reviewer) or user.inTeam(self.obj.registrant):
-            return True
-        target_access = EditBranch(
-            self.obj.branch_merge_proposal.target_branch)
-        return target_access.checkAuthenticated(user)
-
-
-class CodeReviewCommentView(AuthorizationBase):
+        return (user.inTeam(self.obj.reviewer) or
+                user.inTeam(self.obj.registrant) or
+                super(CodeReviewVoteReferenceEdit, self).checkAuthenticated(
+                    user))
+
+
+class CodeReviewCommentView(ForwardedAuthorization):
     permission = 'launchpad.View'
     usedfor = ICodeReviewComment
 
-    def checkAuthenticated(self, user):
-        """Is the user able to view the code review comment?
-
-        The user can see a code review comment if they can see the branch
-        merge proposal.
-        """
-        bmp_checker = BranchMergeProposalView(self.obj.branch_merge_proposal)
-        return bmp_checker.checkAuthenticated(user)
-
-    def checkUnauthenticated(self):
-        """Are not-logged-in people able to view the code review comment?
-
-        They can see a code review comment if they can see the branch merge
-        proposal.
-        """
-        bmp_checker = BranchMergeProposalView(self.obj.branch_merge_proposal)
-        return bmp_checker.checkUnauthenticated()
-
-
-class CodeReviewCommentDelete(AuthorizationBase):
+    def __init__(self, obj):
+        super(CodeReviewCommentView, self).__init__(
+            obj.branch_merge_proposal)
+
+
+class CodeReviewCommentDelete(ForwardedAuthorization):
     permission = 'launchpad.Edit'
     usedfor = ICodeReviewCommentDeletion
 
-    def checkAuthenticated(self, user):
-        """Is the user able to view the code review message?
-
-        The user can see a code review message if they can see the branch
-        merge proposal.
-        """
-        bmp_checker = BranchMergeProposalEdit(self.obj.branch_merge_proposal)
-        return bmp_checker.checkAuthenticated(user)
-
-    def checkUnauthenticated(self):
-        """Are not-logged-in people able to view the code review message?
-
-        They can see a code review message if they can see the branch merge
-        proposal.
-        """
-        bmp_checker = BranchMergeProposalEdit(self.obj.branch_merge_proposal)
-        return bmp_checker.checkUnauthenticated()
+    def __init__(self, obj):
+        super(CodeReviewCommentDelete, self).__init__(
+            obj.branch_merge_proposal)
 
 
 class BranchMergeProposalEdit(AuthorizationBase):
@@ -2319,7 +2284,7 @@
         return auth_edit.checkAuthenticated(user)
 
 
-class EditArchiveAuthToken(AuthorizationBase):
+class EditArchiveAuthToken(ForwardedAuthorization):
     """Restrict editing of archive tokens.
 
     The user should have append privileges to the context archive, or be an
@@ -2328,14 +2293,16 @@
     permission = "launchpad.Edit"
     usedfor = IArchiveAuthToken
 
+    def __init__(self, obj):
+        super(EditArchiveAuthToken, self).__init__(
+            obj.archive, 'launchpad.Append')
+
     def checkAuthenticated(self, user):
-        auth_append = AppendArchive(self.obj.archive)
-        if auth_append.checkAuthenticated(user):
-            return True
-        return user.in_admin
-
-
-class ViewPersonalArchiveSubscription(AuthorizationBase):
+        return (user.in_admin or
+                super(EditArchiveAuthToken, self).checkAuthenticated(user))
+
+
+class ViewPersonalArchiveSubscription(ForwardedAuthorization):
     """Restrict viewing of personal archive subscriptions (non-db class).
 
     The user should be the subscriber, have append privilege to the archive
@@ -2344,18 +2311,19 @@
     permission = "launchpad.View"
     usedfor = IPersonalArchiveSubscription
 
+    def __init__(self, obj):
+        super(ViewPersonalArchiveSubscription, self).__init__(
+            obj.archive, 'launchpad.Append')
+        self.obj = obj
+
     def checkAuthenticated(self, user):
-        if user.person == self.obj.subscriber:
-            return True
-        append_archive = AppendArchive(self.obj.archive)
-
-        if append_archive.checkAuthenticated(user):
-            return True
-
-        return user.in_admin
-
-
-class ViewArchiveSubscriber(AuthorizationBase):
+        if user.person == self.obj.subscriber or user.in_admin:
+            return True
+        return super(
+            ViewPersonalArchiveSubscription, self).checkAuthenticated(user)
+
+
+class ViewArchiveSubscriber(ForwardedAuthorization):
     """Restrict viewing of archive subscribers.
 
     The user should be the subscriber, have append privilege to the
@@ -2364,15 +2332,17 @@
     permission = "launchpad.View"
     usedfor = IArchiveSubscriber
 
+    def __init__(self, obj):
+        super(ViewArchiveSubscriber, self).__init__(
+            obj, 'launchpad.Edit')
+        self.obj = obj
+
     def checkAuthenticated(self, user):
-        auth_edit = EditArchiveSubscriber(self.obj)
-        result = auth_edit.checkAuthenticated(user)
-        if not result:
-            result = user.inTeam(self.obj.subscriber)
-        return result
-
-
-class EditArchiveSubscriber(AuthorizationBase):
+        return (user.inTeam(self.obj.subscriber) or
+                super(ViewArchiveSubscriber, self).checkAuthenticated(user))
+
+
+class EditArchiveSubscriber(ForwardedAuthorization):
     """Restrict editing of archive subscribers.
 
     The user should have append privilege to the archive or be an admin.
@@ -2380,11 +2350,13 @@
     permission = "launchpad.Edit"
     usedfor = IArchiveSubscriber
 
+    def __init__(self, obj):
+        super(EditArchiveSubscriber, self).__init__(
+            obj.archive, 'launchpad.Append')
+
     def checkAuthenticated(self, user):
-        auth_append = AppendArchive(self.obj.archive)
-        if auth_append.checkAuthenticated(user):
-            return True
-        return user.in_admin
+        return (user.in_admin or
+                super(EditArchiveSubscriber, self).checkAuthenticated(user))
 
 
 class DerivedAuthorization(AuthorizationBase):
@@ -2443,13 +2415,13 @@
         super(ViewSourcePackagePublishingHistory, self).__init__(obj.archive)
 
 
-class EditPublishing(AuthorizationBase):
+class EditPublishing(ForwardedAuthorization):
     """Restrict editing of source and binary packages.."""
     permission = "launchpad.Edit"
     usedfor = IPublishingEdit
 
-    def checkAuthenticated(self, user):
-        return AppendArchive(self.obj.archive).checkAuthenticated(user)
+    def __init__(self, obj):
+        super(EditPublishing, self).__init__(obj.archive, 'launchpad.Append')
 
 
 class ViewBinaryPackagePublishingHistory(ViewSourcePackagePublishingHistory):
@@ -2479,8 +2451,8 @@
     def checkAuthenticated(self, user):
         """Verify that the user can view the sourcepackagerelease."""
         for archive in self.obj.published_archives:
-            auth_archive = ViewArchive(archive)
-            if auth_archive.checkAuthenticated(user):
+            adapter = getAdapter(archive, IAuthorization, self.permission)
+            if adapter.checkAuthenticated(user):
                 return True
         return False
 

=== modified file 'lib/lp/app/security.py'
--- lib/lp/app/security.py	2011-05-27 20:10:36 +0000
+++ lib/lp/app/security.py	2011-09-05 15:17:40 +0000
@@ -8,6 +8,7 @@
 __all__ = [
     'AnonymousAuthorization',
     'AuthorizationBase',
+    'ForwardedAuthorization',
     ]
 
 from zope.component import queryAdapter
@@ -104,3 +105,31 @@
     def checkAuthenticated(self, user):
         """Any authorized user can see this object."""
         return True
+
+
+class ForwardedAuthorization(AuthorizationBase):
+
+    permission = None
+    usedfor = None
+
+    def __init__(self, forwarded_object, permission=None):
+        self.forwarded_object = forwarded_object
+        if permission is not None:
+            self.permission = permission
+
+    @property
+    def forwarded_adapter(self):
+        return queryAdapter(
+            self.forwarded_object, IAuthorization, self.permission)
+
+    def checkAuthenticated(self, user):
+        adapter = self.forwarded_adapter
+        if adapter is None:
+            return False
+        return adapter.checkAuthenticated(user)
+
+    def checkUnauthenticated(self):
+        adapter = self.forwarded_adapter
+        if adapter is None:
+            return False
+        return adapter.checkUnauthenticated()

=== modified file 'lib/lp/app/tests/test_security.py'
--- lib/lp/app/tests/test_security.py	2011-04-28 10:42:45 +0000
+++ lib/lp/app/tests/test_security.py	2011-09-05 15:17:40 +0000
@@ -3,7 +3,10 @@
 
 __metaclass__ = type
 
-from zope.component import getSiteManager
+from zope.component import (
+    getSiteManager,
+    queryAdapter,
+    )
 from zope.interface import (
     implements,
     Interface,
@@ -11,11 +14,31 @@
 
 from canonical.testing.layers import ZopelessDatabaseLayer
 from lp.app.interfaces.security import IAuthorization
-from lp.app.security import AuthorizationBase
+from lp.app.security import (
+    AuthorizationBase,
+    ForwardedAuthorization,
+    )
 from lp.testing import TestCaseWithFactory
 from lp.testing.fakemethod import FakeMethod
 
 
+def registerFakeSecurityAdapter(interface, permission, adapter=None):
+    """Register an instance of FakeSecurityAdapter.
+
+    Create a factory for an instance of FakeSecurityAdapter and register
+    it as an adapter for the given interface and permission name.
+    """
+    if adapter is None:
+        adapter = FakeSecurityAdapter()
+
+    def adapter_factory(adaptee):
+        return adapter
+
+    getSiteManager().registerAdapter(
+        adapter_factory, (interface,), IAuthorization, permission)
+    return adapter
+
+
 class FakeSecurityAdapter(AuthorizationBase):
 
     def __init__(self, adaptee=None):
@@ -24,13 +47,13 @@
         self.checkUnauthenticated = FakeMethod()
 
 
-class DummyInterface(Interface):
+class IDummy(Interface):
     """Marker interface to test forwarding."""
 
 
-class DummyClass:
-    """An implementation of DummyInterface."""
-    implements(DummyInterface)
+class Dummy:
+    """An implementation of IDummy."""
+    implements(IDummy)
 
 
 class TestAuthorizationBase(TestCaseWithFactory):
@@ -59,33 +82,18 @@
             (0, adapter.checkAuthenticated.call_count),
             (1, adapter.checkUnauthenticated.call_count))
 
-    def _registerFakeSecurityAdpater(self, interface, permission):
-        """Register an instance of FakeSecurityAdapter.
-
-        Create a factory for an instance of FakeSecurityAdapter and register
-        it as an adapter for the given interface and permission name.
-        """
-        adapter = FakeSecurityAdapter()
-
-        def adapter_factory(adaptee):
-            return adapter
-
-        getSiteManager().registerAdapter(
-            adapter_factory, (interface,), IAuthorization, permission)
-        return adapter
-
     def test_forwardCheckAuthenticated_object_changes(self):
         # Requesting a check for the same permission on a different object.
         permission = self.factory.getUniqueString()
-        next_adapter = self._registerFakeSecurityAdpater(
-            DummyInterface, permission)
+        next_adapter = registerFakeSecurityAdapter(
+            IDummy, permission)
 
         adapter = FakeSecurityAdapter()
         adapter.permission = permission
         adapter.usedfor = None
         adapter.checkPermissionIsRegistered = FakeMethod(result=True)
 
-        adapter.forwardCheckAuthenticated(None, DummyClass())
+        adapter.forwardCheckAuthenticated(None, Dummy())
 
         self.assertVectorEqual(
             (1, adapter.checkPermissionIsRegistered.call_count),
@@ -94,12 +102,12 @@
     def test_forwardCheckAuthenticated_permission_changes(self):
         # Requesting a check for a different permission on the same object.
         next_permission = self.factory.getUniqueString()
-        next_adapter = self._registerFakeSecurityAdpater(
-            DummyInterface, next_permission)
+        next_adapter = registerFakeSecurityAdapter(
+            IDummy, next_permission)
 
-        adapter = FakeSecurityAdapter(DummyClass())
+        adapter = FakeSecurityAdapter(Dummy())
         adapter.permission = self.factory.getUniqueString()
-        adapter.usedfor = DummyInterface
+        adapter.usedfor = IDummy
         adapter.checkPermissionIsRegistered = FakeMethod(result=True)
 
         adapter.forwardCheckAuthenticated(None, permission=next_permission)
@@ -112,15 +120,15 @@
         # Requesting a check for a different permission and a different
         # object.
         next_permission = self.factory.getUniqueString()
-        next_adapter = self._registerFakeSecurityAdpater(
-            DummyInterface, next_permission)
+        next_adapter = registerFakeSecurityAdapter(
+            IDummy, next_permission)
 
         adapter = FakeSecurityAdapter()
         adapter.permission = self.factory.getUniqueString()
         adapter.usedfor = None
         adapter.checkPermissionIsRegistered = FakeMethod(result=True)
 
-        adapter.forwardCheckAuthenticated(None, DummyClass(), next_permission)
+        adapter.forwardCheckAuthenticated(None, Dummy(), next_permission)
 
         self.assertVectorEqual(
             (1, adapter.checkPermissionIsRegistered.call_count),
@@ -130,8 +138,62 @@
         # If the requested forwarding adapter does not exist, return False.
         adapter = FakeSecurityAdapter()
         adapter.permission = self.factory.getUniqueString()
-        adapter.usedfor = DummyInterface
+        adapter.usedfor = IDummy
         adapter.checkPermissionIsRegistered = FakeMethod(result=True)
 
         self.assertFalse(
-            adapter.forwardCheckAuthenticated(None, DummyClass()))
+            adapter.forwardCheckAuthenticated(None, Dummy()))
+
+
+class FakeForwardedAuthorization(ForwardedAuthorization):
+    def __init__(self, obj, permission=None):
+        super(FakeForwardedAuthorization, self).__init__(
+            obj.child_obj, permission)
+
+
+class FakeForwardedObject:
+    implements(IDummy)
+    def __init__(self):
+        self.child_obj = Dummy()
+
+
+class TestForwardedAuthorizationBase(TestCaseWithFactory):
+
+    layer = ZopelessDatabaseLayer
+
+    def test_ForwardedAuthorization_same_permissions(self):
+
+        permission = self.factory.getUniqueString()
+        fake_obj = FakeForwardedObject()
+        outer_adapter = FakeForwardedAuthorization(fake_obj)
+        outer_adapter.permission = permission
+
+        inner_adapter = FakeSecurityAdapter()
+        inner_adapter.permission = permission
+        registerFakeSecurityAdapter(IDummy, permission, inner_adapter)
+        user = object()
+        outer_adapter.checkAuthenticated(user)
+        outer_adapter.checkUnauthenticated()
+        self.assertVectorEqual(
+            (1, inner_adapter.checkAuthenticated.call_count),
+            (1, inner_adapter.checkUnauthenticated.call_count))
+
+    def test_ForwardedAuthorization_different_permissions(self):
+        perm_inner = 'inner'
+        perm_outer = 'outer'
+        fake_obj = FakeForwardedObject()
+        outer_adapter = FakeForwardedAuthorization(fake_obj, perm_inner)
+        registerFakeSecurityAdapter(IDummy, perm_outer, outer_adapter)
+
+        inner_adapter = FakeSecurityAdapter()
+        inner_adapter.permission = perm_inner
+        registerFakeSecurityAdapter(IDummy, perm_inner, inner_adapter)
+
+        user = object()
+        adapter = queryAdapter(
+            FakeForwardedObject(), IAuthorization, perm_outer)
+        adapter.checkAuthenticated(user)
+        adapter.checkUnauthenticated()
+        self.assertVectorEqual(
+            (1, inner_adapter.checkAuthenticated.call_count),
+            (1, inner_adapter.checkUnauthenticated.call_count))

=== modified file 'lib/lp/bugs/security.py'
--- lib/lp/bugs/security.py	2011-05-12 21:33:10 +0000
+++ lib/lp/bugs/security.py	2011-09-05 15:17:40 +0000
@@ -11,11 +11,11 @@
 from lp.app.security import (
     AnonymousAuthorization,
     AuthorizationBase,
+    ForwardedAuthorization,
     )
 from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugattachment import IBugAttachment
 from lp.bugs.interfaces.bugbranch import IBugBranch
-from lp.bugs.interfaces.bugmessage import IBugMessage
 from lp.bugs.interfaces.bugnomination import IBugNomination
 from lp.bugs.interfaces.bugsubscription import IBugSubscription
 from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
@@ -98,11 +98,10 @@
     def __init__(self, bug_branch):
         # The same permissions as for the BugBranch's bug should apply
         # to the BugBranch itself.
-        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
-            self, bug_branch.bug)
-
-
-class ViewBugAttachment(PublicToAllOrPrivateToExplicitSubscribersForBug):
+        super(EditBugBranch, self).__init__(bug_branch.bug)
+
+
+class ViewBugAttachment(ForwardedAuthorization):
     """Security adapter for viewing a bug attachment.
 
     If the user is authorized to view the bug, he's allowed to view the
@@ -112,12 +111,10 @@
     usedfor = IBugAttachment
 
     def __init__(self, bugattachment):
-        PublicToAllOrPrivateToExplicitSubscribersForBug.__init__(
-            self, bugattachment.bug)
-
-
-class EditBugAttachment(
-    EditPublicByLoggedInUserAndPrivateByExplicitSubscribers):
+        super(ViewBugAttachment, self).__init__(bugattachment.bug)
+
+
+class EditBugAttachment(ForwardedAuthorization):
     """Security adapter for editing a bug attachment.
 
     If the user is authorized to view the bug, he's allowed to edit the
@@ -127,8 +124,7 @@
     usedfor = IBugAttachment
 
     def __init__(self, bugattachment):
-        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
-            self, bugattachment.bug)
+        super(EditBugAttachment, self).__init__(bugattachment.bug)
 
 
 class ViewBugSubscription(AnonymousAuthorization):