Merge lp:~al-maisan/launchpad/bedit-347768 into lp:launchpad

Proposed by Muharem Hrnjadovic
Status: Merged
Approved by: Jonathan Lange
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~al-maisan/launchpad/bedit-347768
Merge into: lp:launchpad
Diff against target: 417 lines
2 files modified
lib/canonical/launchpad/security.py (+56/-3)
lib/lp/code/tests/test_branch.py (+186/-33)
To merge this branch: bzr merge lp:~al-maisan/launchpad/bedit-347768
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+13561@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :

Hello there!

The branch at hand extends the 'launchpad.Edit' authorisation check for
branches targeting source packages as follows: if a user is allowed to
upload the linked source package he is also authorised to edit the
branch in question.

Pre-implementation call (belated) with jml.

Tests to run:

    bin/test -t test_branch.TestWriteToBranch

No lint errors or warning.

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (19.5 KiB)

Hi Muharem,

Thanks for looking into this and following up on our implementation call. This branch is good, but it's missing a crucial behaviour: we only want this extended permission checking for officially linked source package branches. That is, for branches like lp:ubuntu/karmic/ssss rather than lp:~jml/ubuntu/karmic/ssss/thingamajig.

This linking is also tied into how we determine the pocket for a branch. I've made some comments on this below.

Let me know if you have any questions.

jml

> === modified file 'lib/canonical/launchpad/security.py'
> --- lib/canonical/launchpad/security.py 2009-10-16 15:00:55 +0000
> +++ lib/canonical/launchpad/security.py 2009-10-19 10:45:43 +0000
> @@ -7,9 +7,10 @@
> __all__ = ['AuthorizationBase']
>
> from zope.interface import implements, Interface
> -from zope.component import getAdapter, getUtility
> +from zope.component import getUtility
>
> from canonical.launchpad.interfaces.account import IAccount
> +from lp.archiveuploader.permission import verify_upload
> from lp.registry.interfaces.announcement import IAnnouncement
> from lp.soyuz.interfaces.archive import IArchive
> from lp.soyuz.interfaces.archivepermission import (
> @@ -1591,7 +1592,57 @@
>
> def checkAuthenticated(self, user):
> return (user.inTeam(self.obj.owner) or
> - user_has_special_branch_access(user))
> + user_has_special_branch_access(user) or
> + can_upload_linked_package(user, self.obj))
> +
> +
> +def can_upload_linked_package(person, branch):
> + """True if person may upload the package linked to `branch`."""

Blank line here please.

> + def get_current_release():
> + """Get current release for the source package linked to branch."""
> + ds = branch.distroseries
> + package = branch.sourcepackage
> + releases = ds.getCurrentSourceReleases([package.sourcepackagename])
> + return releases.get(package, None)
> +
> + def get_property_value(obj, property_name):
> + """Get value of requested property or None."""
> + value = None
> + try:
> + value = getattr(obj, property_name)
> + except AttributeError:
> + return None
> + return value
> +

I don't know what the purpose of this method is. 'getattr(obj, name, None)'
will do exactly the same thing.

> + # Check whether we're dealing with a source package branch and
> + # whether person is authorised to upload the respective source
> + # package.
> + if branch.sourcepackage is None:
> + # No package .. hmm .. this can't be a source package branch
> + # then. Abort.
> + return False
> +
> + if branch.distroseries is None:
> + # No distro series? Abort.
> + return False
> +

There's a database constraint to ensure that you cannot have sourcepackage
without distroseries. I don't have a problem with doing an extra check here,
but you can also feel free to remove it.

> + current_release = get_current_release()
> +
> + # Do we have a pocket and, if yes, can we upload to it?
> + pocket = get_property_value(current_release, 'pocket')
> + if not (pocket is None ...

review: Needs Fixing
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :
Download full text (20.9 KiB)

Jonathan Lange wrote:
> Review: Needs Fixing
> Hi Muharem,
>
> Thanks for looking into this and following up on our implementation
> call. This branch is good, but it's missing a crucial behaviour: we
> only want this extended permission checking for officially linked
> source package branches. That is, for branches like
> lp:ubuntu/karmic/ssss rather than
> lp:~jml/ubuntu/karmic/ssss/thingamajig.

Hello Jonathan,

thanks for looking :) What is the recommended way to test for an
officially linked branch? Test the URL?
Check for the presence of a SeriesSourcePackageBranch instance?
Use SeriesSourcePackageBranchSet.findForBranch() and take it from there?

> This linking is also tied into how we determine the pocket for a
> branch. I've made some comments on this below.
>
> Let me know if you have any questions.
>
> jml
>
>> === modified file 'lib/canonical/launchpad/security.py'
>> --- lib/canonical/launchpad/security.py 2009-10-16 15:00:55 +0000
>> +++ lib/canonical/launchpad/security.py 2009-10-19 10:45:43 +0000
>> @@ -7,9 +7,10 @@
>> __all__ = ['AuthorizationBase']
>>
>> from zope.interface import implements, Interface
>> -from zope.component import getAdapter, getUtility
>> +from zope.component import getUtility
>>
>> from canonical.launchpad.interfaces.account import IAccount
>> +from lp.archiveuploader.permission import verify_upload
>> from lp.registry.interfaces.announcement import IAnnouncement
>> from lp.soyuz.interfaces.archive import IArchive
>> from lp.soyuz.interfaces.archivepermission import (
>> @@ -1591,7 +1592,57 @@
>>
>> def checkAuthenticated(self, user):
>> return (user.inTeam(self.obj.owner) or
>> - user_has_special_branch_access(user))
>> + user_has_special_branch_access(user) or
>> + can_upload_linked_package(user, self.obj))
>> +
>> +
>> +def can_upload_linked_package(person, branch):
>> + """True if person may upload the package linked to `branch`."""
>
> Blank line here please.

Yup.

>
>> + def get_current_release():
>> + """Get current release for the source package linked to branch."""
>> + ds = branch.distroseries
>> + package = branch.sourcepackage
>> + releases = ds.getCurrentSourceReleases([package.sourcepackagename])
>> + return releases.get(package, None)
>> +
>> + def get_property_value(obj, property_name):
>> + """Get value of requested property or None."""
>> + value = None
>> + try:
>> + value = getattr(obj, property_name)
>> + except AttributeError:
>> + return None
>> + return value
>> +
>
> I don't know what the purpose of this method is. 'getattr(obj, name, None)'
> will do exactly the same thing.

Whoops .. sorry .. EHACKINGLATE :P

>> + # Check whether we're dealing with a source package branch and
>> + # whether person is authorised to upload the respective source
>> + # package.
>> + if branch.sourcepackage is None:
>> + # No package .. hmm .. this can't be a source package branch
>> + # then. Abort.
>> + return False
>> +
>> + if branch.distroseries is None:
>> + # No distro ...

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (4.6 KiB)

On Mon, Oct 19, 2009 at 12:51 PM, Muharem Hrnjadovic
<email address hidden> wrote:
> Jonathan Lange wrote:
>> Review: Needs Fixing
>> Hi Muharem,
>>
>> Thanks for looking into this and following up on our implementation
>> call.  This branch is good, but it's missing a crucial behaviour: we
>> only want this extended permission checking for officially linked
>> source package branches.  That is, for branches like
>> lp:ubuntu/karmic/ssss rather than
>> lp:~jml/ubuntu/karmic/ssss/thingamajig.
>
> Hello Jonathan,
>
> thanks for looking :) What is the recommended way to test for an
> officially linked branch? Test the URL?
> Check for the presence of a SeriesSourcePackageBranch instance?
> Use SeriesSourcePackageBranchSet.findForBranch() and take it from there?
>

I would use getUtility(IFindOfficialBranchLinks).findForBranch(). I
would do this check instead of checking that the branch is a
sourcepackage branch, and I would check for upload rights to any of
the packages that are officially linked.

...
>>
>>> +    def get_current_release():
>>> +        """Get current release for the source package linked to branch."""
>>> +        ds = branch.distroseries
>>> +        package = branch.sourcepackage
>>> +        releases = ds.getCurrentSourceReleases([package.sourcepackagename])
>>> +        return releases.get(package, None)
>>> +
>>> +    def get_property_value(obj, property_name):
>>> +        """Get value of requested property or None."""
>>> +        value = None
>>> +        try:
>>> +            value = getattr(obj, property_name)
>>> +        except AttributeError:
>>> +            return None
>>> +        return value
>>> +
>>
>> I don't know what the purpose of this method is. 'getattr(obj, name, None)'
>> will do exactly the same thing.
>
> Whoops .. sorry .. EHACKINGLATE :P
>

No worries :)

>>> +    # Check whether we're dealing with a source package branch and
>>> +    # whether person is authorised to upload the respective source
>>> +    # package.
>>> +    if branch.sourcepackage is None:
>>> +        # No package .. hmm .. this can't be a source package branch
>>> +        # then. Abort.
>>> +        return False
>>> +
>>> +    if branch.distroseries is None:
>>> +        # No distro series? Abort.
>>> +        return False
>>> +
>>
>> There's a database constraint to ensure that you cannot have
>> sourcepackage without distroseries. I don't have a problem with doing
>> an extra check here, but you can also feel free to remove it.
>
> OK. Will do.
>

As I mention above, I think that if you use findForBranch, you won't
need these checks at all.

>>> +    current_release = get_current_release()
>>> +
>>> +    # Do we have a pocket and, if yes, can we upload to it?
>>> +    pocket = get_property_value(current_release, 'pocket')
>>> +    if not (pocket is None or branch.distroseries.canUploadToPocket(pocket)):
>>> +        return False
>>> +
>>
>> People have different preferences here, but I find this way of
>> phrasing the 'if' statement to be a little confusing. I mentally have
>> to apply DeMorgan's Law.
>>
>> if pocket is not None and not branch.distroseries.canUploadToPocket(pocket):
>>     return False
>
> Fair enough.
>

M...

Read more...

Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :
Download full text (5.6 KiB)

Jonathan Lange wrote:
> On Mon, Oct 19, 2009 at 12:51 PM, Muharem Hrnjadovic
> <email address hidden> wrote:
>> Jonathan Lange wrote:
>>> Review: Needs Fixing
>>> Hi Muharem,
>>>
>>> Thanks for looking into this and following up on our implementation
>>> call. This branch is good, but it's missing a crucial behaviour: we
>>> only want this extended permission checking for officially linked
>>> source package branches. That is, for branches like
>>> lp:ubuntu/karmic/ssss rather than
>>> lp:~jml/ubuntu/karmic/ssss/thingamajig.
>> Hello Jonathan,
>>
>> thanks for looking :) What is the recommended way to test for an
>> officially linked branch? Test the URL?
>> Check for the presence of a SeriesSourcePackageBranch instance?
>> Use SeriesSourcePackageBranchSet.findForBranch() and take it from there?
>>
>
> I would use getUtility(IFindOfficialBranchLinks).findForBranch(). I
> would do this check instead of checking that the branch is a
> sourcepackage branch, and I would check for upload rights to any of
> the packages that are officially linked.

Packages? Is this a one-branch-to-many-packages relationship? From
looking at the code it looked like a 1-to-1 relationship to me ..

Anyway, after revising the code as suggested I get security exceptions
(please see http://pastebin.ubuntu.com/296865/).
I guess the ISeriesSourcePackageBranch interface needs to be split into
a read-only and an edit portion respectively. As it stands all access
to it requires the "launchpad.Edit" permissions.

  <class

class="lp.code.model.seriessourcepackagebranch.SeriesSourcePackageBranch">
    <allow
interface="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
    <require
        permission="launchpad.Edit"

set_schema="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
  </class>

>
> ...
>>>> + def get_current_release():
>>>> + """Get current release for the source package linked to branch."""
>>>> + ds = branch.distroseries
>>>> + package = branch.sourcepackage
>>>> + releases = ds.getCurrentSourceReleases([package.sourcepackagename])
>>>> + return releases.get(package, None)
>>>> +
>>>> + def get_property_value(obj, property_name):
>>>> + """Get value of requested property or None."""
>>>> + value = None
>>>> + try:
>>>> + value = getattr(obj, property_name)
>>>> + except AttributeError:
>>>> + return None
>>>> + return value
>>>> +
>>> I don't know what the purpose of this method is. 'getattr(obj, name, None)'
>>> will do exactly the same thing.
>> Whoops .. sorry .. EHACKINGLATE :P
>>
>
> No worries :)
>
>>>> + # Check whether we're dealing with a source package branch and
>>>> + # whether person is authorised to upload the respective source
>>>> + # package.
>>>> + if branch.sourcepackage is None:
>>>> + # No package .. hmm .. this can't be a source package branch
>>>> + # then. Abort.
>>>> + return False
>>>> +
>>>> + if branch.distroseries is None:
>>>> + # No distro series? Abort.
>>>> + return False
>>>> +
>>> There's a database constraint to ensure that y...

Read more...

Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (3.2 KiB)

On Mon, Oct 19, 2009 at 3:15 PM, Muharem Hrnjadovic
<email address hidden> wrote:
> Jonathan Lange wrote:
>> On Mon, Oct 19, 2009 at 12:51 PM, Muharem Hrnjadovic
>> <email address hidden> wrote:
>>> Jonathan Lange wrote:
>>>> Review: Needs Fixing
>>>> Hi Muharem,
>>>>
>>>> Thanks for looking into this and following up on our implementation
>>>> call.  This branch is good, but it's missing a crucial behaviour: we
>>>> only want this extended permission checking for officially linked
>>>> source package branches.  That is, for branches like
>>>> lp:ubuntu/karmic/ssss rather than
>>>> lp:~jml/ubuntu/karmic/ssss/thingamajig.
>>> Hello Jonathan,
>>>
>>> thanks for looking :) What is the recommended way to test for an
>>> officially linked branch? Test the URL?
>>> Check for the presence of a SeriesSourcePackageBranch instance?
>>> Use SeriesSourcePackageBranchSet.findForBranch() and take it from there?
>>>
>>
>> I would use getUtility(IFindOfficialBranchLinks).findForBranch(). I
>> would do this check instead of checking that the branch is a
>> sourcepackage branch, and I would check for upload rights to any of
>> the packages that are officially linked.
>
> Packages? Is this a one-branch-to-many-packages relationship? From
> looking at the code it looked like a 1-to-1 relationship to me ..
>

One branch can belong to at most one package.

However, a branch can be an official package branch for many
(sourcepackage, pocket) combinations, even if the branch itself does
not belong to a source package. This is deliberate.

> Anyway, after revising the code as suggested I get security exceptions
> (please see http://pastebin.ubuntu.com/296865/).
> I guess the ISeriesSourcePackageBranch interface needs to be split into
> a read-only and an edit portion respectively. As it stands all access
> to it requires the "launchpad.Edit" permissions.
>
>  <class
>
> class="lp.code.model.seriessourcepackagebranch.SeriesSourcePackageBranch">
>    <allow
> interface="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
>    <require
>        permission="launchpad.Edit"
>
> set_schema="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
>  </class>
>

Something's wrong here.

ISeriesSourcePackageBranch is used to determine the short names of
package branches (e.g. lp:ubuntu/emacs-snapshot). This works
anonymously. (bazaar_identity calls
branch.associatedSuiteSourcePackages() which gets
IFindOfficialBranchLinks).

From the pastebin,
  ForbiddenAttribute: ('pocket', <storm.store.ResultSet object at 0xb359a90>)

This means that you cannot get pocket from a ResultSet. This makes
sense, since result sets are storm objects and don't have pockets.

  File "/home/mhr/canonical/lp-branches/bedit-347768/lib/canonical/launchpad/security.py",
line 1616, in can_upload_linked_package
    if not branch.distroseries.canUploadToPocket(sspb.pocket):
AttributeError: 'NoneType' object has no attribute 'canUploadToPocket'

This error is correct, since the condition is checking the wrong
thing. The question is not, "can we upload to this pocket in the
distroseries that the branch belongs to" but rather "can we upload to
this pocket in the dist...

Read more...

Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :
Download full text (3.4 KiB)

Jonathan Lange wrote:
> On Mon, Oct 19, 2009 at 3:15 PM, Muharem Hrnjadovic
> <email address hidden> wrote:
>> Jonathan Lange wrote:
>>> On Mon, Oct 19, 2009 at 12:51 PM, Muharem Hrnjadovic
>>> <email address hidden> wrote:
>>>> Jonathan Lange wrote:
>>>>> Review: Needs Fixing

[..]

>>>> thanks for looking :) What is the recommended way to test for an
>>>> officially linked branch? Test the URL?
>>>> Check for the presence of a SeriesSourcePackageBranch instance?
>>>> Use SeriesSourcePackageBranchSet.findForBranch() and take it from there?
>>>>
>>> I would use getUtility(IFindOfficialBranchLinks).findForBranch(). I
>>> would do this check instead of checking that the branch is a
>>> sourcepackage branch, and I would check for upload rights to any of
>>> the packages that are officially linked.

Right. This is what the revised can_upload_linked_package() logic now
does.

Please see the enclosed incremental diff for details.

>> Packages? Is this a one-branch-to-many-packages relationship? From
>> looking at the code it looked like a 1-to-1 relationship to me ..
>>
>
> One branch can belong to at most one package.
>
> However, a branch can be an official package branch for many
> (sourcepackage, pocket) combinations, even if the branch itself does
> not belong to a source package. This is deliberate.
>
>> Anyway, after revising the code as suggested I get security exceptions
>> (please see http://pastebin.ubuntu.com/296865/).
>> I guess the ISeriesSourcePackageBranch interface needs to be split into
>> a read-only and an edit portion respectively. As it stands all access
>> to it requires the "launchpad.Edit" permissions.
>>
>> <class
>>
>> class="lp.code.model.seriessourcepackagebranch.SeriesSourcePackageBranch">
>> <allow
>> interface="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
>> <require
>> permission="launchpad.Edit"
>>
>> set_schema="lp.code.interfaces.seriessourcepackagebranch.ISeriesSourcePackageBranch"/>
>> </class>
>>
>
> Something's wrong here.
>
> ISeriesSourcePackageBranch is used to determine the short names of
> package branches (e.g. lp:ubuntu/emacs-snapshot). This works
> anonymously. (bazaar_identity calls
> branch.associatedSuiteSourcePackages() which gets
> IFindOfficialBranchLinks).
>
>>From the pastebin,
> ForbiddenAttribute: ('pocket', <storm.store.ResultSet object at 0xb359a90>)
>
> This means that you cannot get pocket from a ResultSet. This makes
> sense, since result sets are storm objects and don't have pockets.
>
> File "/home/mhr/canonical/lp-branches/bedit-347768/lib/canonical/launchpad/security.py",
> line 1616, in can_upload_linked_package
> if not branch.distroseries.canUploadToPocket(sspb.pocket):
> AttributeError: 'NoneType' object has no attribute 'canUploadToPocket'
>
> This error is correct, since the condition is checking the wrong
> thing. The question is not, "can we upload to this pocket in the
> distroseries that the branch belongs to" but rather "can we upload to
> this pocket in the distroseries that the branch is official for".
>
> i.e. sspb.distroseries.canUploadToPocket(sspb.pocket)

I am both relieved (that the error is so easil...

Read more...

1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2009-10-19 10:33:14 +0000
3+++ lib/canonical/launchpad/security.py 2009-10-20 07:51:12 +0000
4@@ -84,7 +84,8 @@
5 from lp.registry.interfaces.productseries import IProductSeries
6 from lp.registry.interfaces.project import IProject, IProjectSet
7 from lp.code.interfaces.seriessourcepackagebranch import (
8- ISeriesSourcePackageBranch, IMakeOfficialBranchLinks)
9+ IFindOfficialBranchLinks, IMakeOfficialBranchLinks,
10+ ISeriesSourcePackageBranch)
11 from lp.registry.interfaces.sourcepackage import ISourcePackage
12 from lp.soyuz.interfaces.sourcepackagerelease import (
13 ISourcePackageRelease)
14@@ -1598,48 +1599,47 @@
15
16 def can_upload_linked_package(person, branch):
17 """True if person may upload the package linked to `branch`."""
18- def get_current_release():
19+
20+ def get_current_release(sspb):
21 """Get current release for the source package linked to branch."""
22- ds = branch.distroseries
23- package = branch.sourcepackage
24- releases = ds.getCurrentSourceReleases([package.sourcepackagename])
25+ package = sspb.sourcepackage
26+ releases = sspb.distroseries.getCurrentSourceReleases(
27+ [package.sourcepackagename])
28 return releases.get(package, None)
29
30- def get_property_value(obj, property_name):
31- """Get value of requested property or None."""
32- value = None
33- try:
34- value = getattr(obj, property_name)
35- except AttributeError:
36- return None
37- return value
38-
39- # Check whether we're dealing with a source package branch and
40- # whether person is authorised to upload the respective source
41+ # No associated `SeriesSourcePackageBranch` data -> not an official
42+ # branch. Abort.
43+ result_set = getUtility(IFindOfficialBranchLinks).findForBranch(branch)
44+ if result_set.count() < 1:
45+ return False
46+
47+ # XXX al-maisan, 2009-10-20: a branch may currently be associated with a
48+ # number of (distroseries, sourcepackagename, pocket) combinations.
49+ # This does not seem right. But until the database model is fixed we work
50+ # around this by assuming that things are fine as long as we find at least
51+ # one combination that allows us to upload the corresponding source
52 # package.
53- if branch.sourcepackage is None:
54- # No package .. hmm .. this can't be a source package branch
55- # then. Abort.
56- return False
57-
58- if branch.distroseries is None:
59- # No distro series? Abort.
60- return False
61-
62- current_release = get_current_release()
63-
64- # Do we have a pocket and, if yes, can we upload to it?
65- pocket = get_property_value(current_release, 'pocket')
66- if not (pocket is None or branch.distroseries.canUploadToPocket(pocket)):
67- return False
68-
69- archive = branch.distroseries.distribution.main_archive
70- component = get_property_value(current_release, 'component')
71+
72+ # See whether we can upload to any of the distro series/pocket
73+ # combinations.
74+ sspb = None
75+ for sspb in result_set:
76+ # Can we upload to the respective pocket?
77+ if sspb.distroseries.canUploadToPocket(sspb.pocket):
78+ break
79+ else:
80+ # Loop terminated normally i.e. we could not upload to any of the
81+ # (distroseries, pocket) combinations found.
82+ return False
83+
84+ archive = sspb.distroseries.distribution.main_archive
85+ # Find the component the linked source package was published in.
86+ current_release = get_current_release(sspb)
87+ component = getattr(current_release, 'component', None)
88
89 # Is person authorised to upload the source package this branch
90 # is targeting?
91- result = verify_upload(
92- person, branch.sourcepackage.sourcepackagename, archive, component)
93+ result = verify_upload(person, sspb.sourcepackagename, archive, component)
94 # verify_upload() indicates that person *is* allowed to upload by
95 # returning None.
96 return result is None
97
98=== modified file 'lib/lp/code/tests/test_branch.py'
99--- lib/lp/code/tests/test_branch.py 2009-10-19 10:12:44 +0000
100+++ lib/lp/code/tests/test_branch.py 2009-10-20 07:39:30 +0000
101@@ -17,6 +17,8 @@
102 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
103 CodeReviewNotificationLevel)
104 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
105+from lp.registry.interfaces.distroseries import DistroSeriesStatus
106+from lp.registry.interfaces.pocket import PackagePublishingPocket
107 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
108 from lp.testing import run_with_login, TestCaseWithFactory
109
110@@ -232,7 +234,14 @@
111 def makeOfficialPackageBranch(self):
112 """Make a branch linked to the pocket of a source package."""
113 branch = self.factory.makePackageBranch()
114- pocket = self.factory.getAnyPocket()
115+ # Make sure the (distroseries, pocket) combination used allows us to
116+ # upload to it.
117+ stable_states = (
118+ DistroSeriesStatus.SUPPORTED, DistroSeriesStatus.CURRENT)
119+ if branch.distroseries.status in stable_states:
120+ pocket = PackagePublishingPocket.BACKPORTS
121+ else:
122+ pocket = PackagePublishingPocket.RELEASE
123 sourcepackage = branch.sourcepackage
124 suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
125 registrant = self.factory.makePerson()
Revision history for this message
Jonathan Lange (jml) wrote :

Looks good, thanks.

You might want to try:
  sspbs = branch.associatedSuiteSourcePackages()

rather than:
  result_set = getUtility(IFindOfficialBranchLinks).findForBranch(branch)

(I only remembered the first one after I mentioned the second).

But no big deal.

Thanks again,
jml

review: Approve
Revision history for this message
Muharem Hrnjadovic (al-maisan) wrote :

Jonathan Lange wrote:
> Review: Approve
> Looks good, thanks.
>
> You might want to try:
> sspbs = branch.associatedSuiteSourcePackages()
>
> rather than:
> result_set = getUtility(IFindOfficialBranchLinks).findForBranch(branch)
>
> (I only remembered the first one after I mentioned the second).

Done. Please see attached incremental diff.

Thanks for suggesting this!

Best regards

--
Muharem Hrnjadovic <email address hidden>
Public key id : B2BBFCFC
Key fingerprint : A5A3 CC67 2B87 D641 103F 5602 219F 6B60 B2BB FCFC

1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2009-10-20 07:53:28 +0000
3+++ lib/canonical/launchpad/security.py 2009-10-20 09:45:16 +0000
4@@ -84,8 +84,7 @@
5 from lp.registry.interfaces.productseries import IProductSeries
6 from lp.registry.interfaces.project import IProject, IProjectSet
7 from lp.code.interfaces.seriessourcepackagebranch import (
8- IFindOfficialBranchLinks, IMakeOfficialBranchLinks,
9- ISeriesSourcePackageBranch)
10+ IMakeOfficialBranchLinks, ISeriesSourcePackageBranch)
11 from lp.registry.interfaces.sourcepackage import ISourcePackage
12 from lp.soyuz.interfaces.sourcepackagerelease import (
13 ISourcePackageRelease)
14@@ -1600,17 +1599,20 @@
15 def can_upload_linked_package(person, branch):
16 """True if person may upload the package linked to `branch`."""
17
18- def get_current_release(sspb):
19- """Get current release for the source package linked to branch."""
20- package = sspb.sourcepackage
21- releases = sspb.distroseries.getCurrentSourceReleases(
22+ def get_current_release(ssp):
23+ """Get current release for the source package linked to branch.
24+
25+ This function uses the `ISuiteSourcePackage` instance supplied.
26+ """
27+ package = ssp.sourcepackage
28+ releases = ssp.distroseries.getCurrentSourceReleases(
29 [package.sourcepackagename])
30 return releases.get(package, None)
31
32- # No associated `SeriesSourcePackageBranch` data -> not an official
33- # branch. Abort.
34- result_set = getUtility(IFindOfficialBranchLinks).findForBranch(branch)
35- if result_set.count() < 1:
36+ # No associated `ISuiteSourcePackage` data -> not an official branch.
37+ # Abort.
38+ ssp_list = branch.associatedSuiteSourcePackages()
39+ if len(ssp_list) < 1:
40 return False
41
42 # XXX al-maisan, 2009-10-20: a branch may currently be associated with a
43@@ -1620,26 +1622,26 @@
44 # one combination that allows us to upload the corresponding source
45 # package.
46
47- # See whether we can upload to any of the distro series/pocket
48- # combinations.
49- sspb = None
50- for sspb in result_set:
51+ # Go through the associated `ISuiteSourcePackage` instances and see
52+ # whether we can upload to any of the distro series/pocket combinations.
53+ ssp = None
54+ for ssp in ssp_list:
55 # Can we upload to the respective pocket?
56- if sspb.distroseries.canUploadToPocket(sspb.pocket):
57+ if ssp.distroseries.canUploadToPocket(ssp.pocket):
58 break
59 else:
60 # Loop terminated normally i.e. we could not upload to any of the
61 # (distroseries, pocket) combinations found.
62 return False
63
64- archive = sspb.distroseries.distribution.main_archive
65+ archive = ssp.distroseries.distribution.main_archive
66 # Find the component the linked source package was published in.
67- current_release = get_current_release(sspb)
68+ current_release = get_current_release(ssp)
69 component = getattr(current_release, 'component', None)
70
71 # Is person authorised to upload the source package this branch
72 # is targeting?
73- result = verify_upload(person, sspb.sourcepackagename, archive, component)
74+ result = verify_upload(person, ssp.sourcepackagename, archive, component)
75 # verify_upload() indicates that person *is* allowed to upload by
76 # returning None.
77 return result is None
Revision history for this message
Jonathan Lange (jml) wrote :

On Tue, Oct 20, 2009 at 10:54 AM, Muharem Hrnjadovic
<email address hidden> wrote:
> Jonathan Lange wrote:
>> Review: Approve
>> Looks good, thanks.
>>
>> You might want to try:
>>   sspbs = branch.associatedSuiteSourcePackages()
>>
>> rather than:
>>   result_set = getUtility(IFindOfficialBranchLinks).findForBranch(branch)
>>
>> (I only remembered the first one after I mentioned the second).
>
> Done. Please see attached incremental diff.
>
> Thanks for suggesting this!
>

My pleasure.

Please go ahead and merge the branch.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2009-10-16 15:00:55 +0000
3+++ lib/canonical/launchpad/security.py 2009-10-20 09:49:18 +0000
4@@ -7,9 +7,10 @@
5 __all__ = ['AuthorizationBase']
6
7 from zope.interface import implements, Interface
8-from zope.component import getAdapter, getUtility
9+from zope.component import getUtility
10
11 from canonical.launchpad.interfaces.account import IAccount
12+from lp.archiveuploader.permission import verify_upload
13 from lp.registry.interfaces.announcement import IAnnouncement
14 from lp.soyuz.interfaces.archive import IArchive
15 from lp.soyuz.interfaces.archivepermission import (
16@@ -83,7 +84,7 @@
17 from lp.registry.interfaces.productseries import IProductSeries
18 from lp.registry.interfaces.project import IProject, IProjectSet
19 from lp.code.interfaces.seriessourcepackagebranch import (
20- ISeriesSourcePackageBranch, IMakeOfficialBranchLinks)
21+ IMakeOfficialBranchLinks, ISeriesSourcePackageBranch)
22 from lp.registry.interfaces.sourcepackage import ISourcePackage
23 from lp.soyuz.interfaces.sourcepackagerelease import (
24 ISourcePackageRelease)
25@@ -1591,7 +1592,59 @@
26
27 def checkAuthenticated(self, user):
28 return (user.inTeam(self.obj.owner) or
29- user_has_special_branch_access(user))
30+ user_has_special_branch_access(user) or
31+ can_upload_linked_package(user, self.obj))
32+
33+
34+def can_upload_linked_package(person, branch):
35+ """True if person may upload the package linked to `branch`."""
36+
37+ def get_current_release(ssp):
38+ """Get current release for the source package linked to branch.
39+
40+ This function uses the `ISuiteSourcePackage` instance supplied.
41+ """
42+ package = ssp.sourcepackage
43+ releases = ssp.distroseries.getCurrentSourceReleases(
44+ [package.sourcepackagename])
45+ return releases.get(package, None)
46+
47+ # No associated `ISuiteSourcePackage` data -> not an official branch.
48+ # Abort.
49+ ssp_list = branch.associatedSuiteSourcePackages()
50+ if len(ssp_list) < 1:
51+ return False
52+
53+ # XXX al-maisan, 2009-10-20: a branch may currently be associated with a
54+ # number of (distroseries, sourcepackagename, pocket) combinations.
55+ # This does not seem right. But until the database model is fixed we work
56+ # around this by assuming that things are fine as long as we find at least
57+ # one combination that allows us to upload the corresponding source
58+ # package.
59+
60+ # Go through the associated `ISuiteSourcePackage` instances and see
61+ # whether we can upload to any of the distro series/pocket combinations.
62+ ssp = None
63+ for ssp in ssp_list:
64+ # Can we upload to the respective pocket?
65+ if ssp.distroseries.canUploadToPocket(ssp.pocket):
66+ break
67+ else:
68+ # Loop terminated normally i.e. we could not upload to any of the
69+ # (distroseries, pocket) combinations found.
70+ return False
71+
72+ archive = ssp.distroseries.distribution.main_archive
73+ # Find the component the linked source package was published in.
74+ current_release = get_current_release(ssp)
75+ component = getattr(current_release, 'component', None)
76+
77+ # Is person authorised to upload the source package this branch
78+ # is targeting?
79+ result = verify_upload(person, ssp.sourcepackagename, archive, component)
80+ # verify_upload() indicates that person *is* allowed to upload by
81+ # returning None.
82+ return result is None
83
84
85 class AdminBranch(AuthorizationBase):
86
87=== modified file 'lib/lp/code/tests/test_branch.py'
88--- lib/lp/code/tests/test_branch.py 2009-06-25 04:06:00 +0000
89+++ lib/lp/code/tests/test_branch.py 2009-10-20 09:49:18 +0000
90@@ -8,50 +8,104 @@
91 from zope.component import getUtility
92 from zope.security.proxy import removeSecurityProxy
93
94+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
95+from canonical.launchpad.webapp.authorization import check_permission
96+from canonical.testing import DatabaseFunctionalLayer
97+
98+from lp.archiveuploader.permission import verify_upload
99 from lp.code.enums import (
100 BranchSubscriptionDiffSize, BranchSubscriptionNotificationLevel,
101 CodeReviewNotificationLevel)
102-from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
103-from canonical.launchpad.security import AccessBranch
104-from lp.testing import TestCaseWithFactory
105-from canonical.testing import DatabaseFunctionalLayer
106-
107-
108-class TestAccessBranch(TestCaseWithFactory):
109+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
110+from lp.registry.interfaces.distroseries import DistroSeriesStatus
111+from lp.registry.interfaces.pocket import PackagePublishingPocket
112+from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
113+from lp.testing import run_with_login, TestCaseWithFactory
114+
115+
116+class PermissionTest(TestCaseWithFactory):
117
118 layer = DatabaseFunctionalLayer
119
120- def assertAuthenticatedAccess(self, branch, person, can_access):
121- branch = removeSecurityProxy(branch)
122- self.assertEqual(
123- can_access, AccessBranch(branch).checkAuthenticated(person))
124-
125- def assertUnauthenticatedAccess(self, branch, can_access):
126- branch = removeSecurityProxy(branch)
127- self.assertEqual(
128- can_access, AccessBranch(branch).checkUnauthenticated())
129+ def assertPermission(self, can_access, person, secure_object, permission):
130+ """Assert that 'person' can or cannot access 'secure_object'.
131+
132+ :param can_access: Whether or not the person can access the object.
133+ :param person: The `IPerson` who is trying to access the object.
134+ :param secure_object: The secured object.
135+ :param permission: The Launchpad permission that 'person' is trying to
136+ access 'secure_object' with.
137+ """
138+ self.assertEqual(
139+ can_access,
140+ run_with_login(
141+ person, check_permission, permission, secure_object))
142+
143+ def assertAuthenticatedView(self, branch, person, can_access):
144+ """Can 'branch' be accessed by 'person'?
145+
146+ :param branch: The `IBranch` we're curious about.
147+ :param person: The `IPerson` trying to access it.
148+ :param can_access: Whether we expect 'person' be able to access it.
149+ """
150+ self.assertPermission(can_access, person, branch, 'launchpad.View')
151+
152+ def assertUnauthenticatedView(self, branch, can_access):
153+ """Can 'branch' be accessed anonymously?
154+
155+ :param branch: The `IBranch` we're curious about.
156+ :param can_access: Whether we expect to access it anonymously.
157+ """
158+ self.assertAuthenticatedView(branch, None, can_access)
159+
160+ def assertCanEdit(self, person, secured_object):
161+ """Assert 'person' can edit 'secured_object'.
162+
163+ That is, assert 'person' has 'launchpad.Edit' permissions on
164+ 'secured_object'.
165+
166+ :param person: An `IPerson`. None means anonymous.
167+ :param secured_object: An object, secured through the Zope security
168+ layer.
169+ """
170+ self.assertPermission(True, person, secured_object, 'launchpad.Edit')
171+
172+ def assertCannotEdit(self, person, secured_object):
173+ """Assert 'person' cannot edit 'secured_object'.
174+
175+ That is, assert 'person' does not have 'launchpad.Edit' permissions on
176+ 'secured_object'.
177+
178+ :param person: An `IPerson`. None means anonymous.
179+ :param secured_object: An object, secured through the Zope security
180+ layer.
181+ """
182+ self.assertPermission(False, person, secured_object, 'launchpad.Edit')
183+
184+
185+class TestAccessBranch(PermissionTest):
186
187 def test_publicBranchUnauthenticated(self):
188 # Public branches can be accessed without authentication.
189 branch = self.factory.makeAnyBranch()
190- self.assertUnauthenticatedAccess(branch, True)
191+ self.assertUnauthenticatedView(branch, True)
192
193 def test_publicBranchArbitraryUser(self):
194 # Public branches can be accessed by anyone.
195 branch = self.factory.makeAnyBranch()
196 person = self.factory.makePerson()
197- self.assertAuthenticatedAccess(branch, person, True)
198+ self.assertAuthenticatedView(branch, person, True)
199
200 def test_privateBranchUnauthenticated(self):
201 # Private branches cannot be accessed without authentication.
202 branch = self.factory.makeAnyBranch(private=True)
203- self.assertUnauthenticatedAccess(branch, False)
204+ self.assertUnauthenticatedView(branch, False)
205
206 def test_privateBranchOwner(self):
207 # The owner of a branch can always access it.
208 owner = self.factory.makePerson()
209 branch = self.factory.makeAnyBranch(private=True, owner=owner)
210- self.assertAuthenticatedAccess(branch, owner, True)
211+ self.assertAuthenticatedView(branch, owner, True)
212
213 def test_privateBranchOwnerMember(self):
214 # Any member of the team that owns the branch can access it.
215@@ -60,19 +114,20 @@
216 person = self.factory.makePerson()
217 removeSecurityProxy(team).addMember(person, team_owner)
218 branch = self.factory.makeAnyBranch(private=True, owner=team)
219- self.assertAuthenticatedAccess(branch, person, True)
220+ self.assertAuthenticatedView(branch, person, True)
221
222 def test_privateBranchBazaarExperts(self):
223 # The Bazaar experts can access any branch.
224 celebs = getUtility(ILaunchpadCelebrities)
225 branch = self.factory.makeAnyBranch(private=True)
226- self.assertAuthenticatedAccess(branch, celebs.bazaar_experts, True)
227+ self.assertAuthenticatedView(
228+ branch, celebs.bazaar_experts.teamowner, True)
229
230 def test_privateBranchAdmins(self):
231 # Launchpad admins can access any branch.
232 celebs = getUtility(ILaunchpadCelebrities)
233 branch = self.factory.makeAnyBranch(private=True)
234- self.assertAuthenticatedAccess(branch, celebs.admin, True)
235+ self.assertAuthenticatedView(branch, celebs.admin.teamowner, True)
236
237 def test_privateBranchSubscriber(self):
238 # If you are subscribed to a branch, you can access it.
239@@ -82,13 +137,13 @@
240 person, BranchSubscriptionNotificationLevel.NOEMAIL,
241 BranchSubscriptionDiffSize.NODIFF,
242 CodeReviewNotificationLevel.NOEMAIL)
243- self.assertAuthenticatedAccess(branch, person, True)
244+ self.assertAuthenticatedView(branch, person, True)
245
246 def test_privateBranchAnyoneElse(self):
247 # In general, you can't access a private branch.
248 branch = self.factory.makeAnyBranch(private=True)
249 person = self.factory.makePerson()
250- self.assertAuthenticatedAccess(branch, person, False)
251+ self.assertAuthenticatedView(branch, person, False)
252
253 def test_stackedOnPrivateBranchUnauthenticated(self):
254 # If a branch is stacked on a private branch, then you cannot access
255@@ -96,7 +151,7 @@
256 stacked_on_branch = self.factory.makeAnyBranch(private=True)
257 stacked_branch = self.factory.makeAnyBranch(
258 stacked_on=stacked_on_branch)
259- self.assertUnauthenticatedAccess(stacked_branch, False)
260+ self.assertUnauthenticatedView(stacked_branch, False)
261
262 def test_stackedOnPrivateBranchAuthenticated(self):
263 # If a branch is stacked on a private branch, you can only access it
264@@ -105,7 +160,7 @@
265 stacked_branch = self.factory.makeAnyBranch(
266 stacked_on=stacked_on_branch)
267 person = self.factory.makePerson()
268- self.assertAuthenticatedAccess(stacked_branch, person, False)
269+ self.assertAuthenticatedView(stacked_branch, person, False)
270
271 def test_manyLevelsOfStackingUnauthenticated(self):
272 # If a branch is stacked on a branch stacked on a private branch, you
273@@ -113,7 +168,7 @@
274 stacked_on_branch = self.factory.makeAnyBranch(private=True)
275 branch_a = self.factory.makeAnyBranch(stacked_on=stacked_on_branch)
276 branch_b = self.factory.makeAnyBranch(stacked_on=branch_a)
277- self.assertUnauthenticatedAccess(branch_b, False)
278+ self.assertUnauthenticatedView(branch_b, False)
279
280 def test_manyLevelsOfStackingAuthenticated(self):
281 # If a branch is stacked on a branch stacked on a private branch, you
282@@ -122,7 +177,7 @@
283 branch_a = self.factory.makeAnyBranch(stacked_on=stacked_on_branch)
284 branch_b = self.factory.makeAnyBranch(stacked_on=branch_a)
285 person = self.factory.makePerson()
286- self.assertAuthenticatedAccess(branch_b, person, False)
287+ self.assertAuthenticatedView(branch_b, person, False)
288
289 def test_loopedPublicStackedOn(self):
290 # It's possible, although nonsensical, for branch stackings to form a
291@@ -132,7 +187,7 @@
292 stacked_branch = self.factory.makeAnyBranch()
293 removeSecurityProxy(stacked_branch).stacked_on = stacked_branch
294 person = self.factory.makePerson()
295- self.assertAuthenticatedAccess(stacked_branch, person, True)
296+ self.assertAuthenticatedView(stacked_branch, person, True)
297
298 def test_loopedPrivateStackedOn(self):
299 # It's possible, although nonsensical, for branch stackings to form a
300@@ -142,7 +197,7 @@
301 stacked_branch = self.factory.makeAnyBranch(private=True)
302 removeSecurityProxy(stacked_branch).stacked_on = stacked_branch
303 person = self.factory.makePerson()
304- self.assertAuthenticatedAccess(stacked_branch, person, False)
305+ self.assertAuthenticatedView(stacked_branch, person, False)
306
307 def test_loopedPublicStackedOnUnauthenticated(self):
308 # It's possible, although nonsensical, for branch stackings to form a
309@@ -151,8 +206,106 @@
310 # being logged in.
311 stacked_branch = self.factory.makeAnyBranch()
312 removeSecurityProxy(stacked_branch).stacked_on = stacked_branch
313- self.assertUnauthenticatedAccess(stacked_branch, True)
314-
315+ self.assertUnauthenticatedView(stacked_branch, True)
316+
317+
318+class TestWriteToBranch(PermissionTest):
319+ """Test who can write to branches."""
320+
321+ def test_owner_can_write(self):
322+ # The owner of a branch can write to the branch.
323+ branch = self.factory.makeAnyBranch()
324+ self.assertCanEdit(branch.owner, branch)
325+
326+ def test_random_person_cannot_write(self):
327+ # Arbitrary logged in people cannot write to branches.
328+ branch = self.factory.makeAnyBranch()
329+ person = self.factory.makePerson()
330+ self.assertCannotEdit(person, branch)
331+
332+ def test_member_of_owning_team_can_write(self):
333+ # Members of the team that owns a branch can write to the branch.
334+ team = self.factory.makeTeam()
335+ person = self.factory.makePerson()
336+ removeSecurityProxy(team).addMember(person, team.teamowner)
337+ branch = self.factory.makeAnyBranch(owner=team)
338+ self.assertCanEdit(person, branch)
339+
340+ def makeOfficialPackageBranch(self):
341+ """Make a branch linked to the pocket of a source package."""
342+ branch = self.factory.makePackageBranch()
343+ # Make sure the (distroseries, pocket) combination used allows us to
344+ # upload to it.
345+ stable_states = (
346+ DistroSeriesStatus.SUPPORTED, DistroSeriesStatus.CURRENT)
347+ if branch.distroseries.status in stable_states:
348+ pocket = PackagePublishingPocket.BACKPORTS
349+ else:
350+ pocket = PackagePublishingPocket.RELEASE
351+ sourcepackage = branch.sourcepackage
352+ suite_sourcepackage = sourcepackage.getSuiteSourcePackage(pocket)
353+ registrant = self.factory.makePerson()
354+ ubuntu_branches = getUtility(ILaunchpadCelebrities).ubuntu_branches
355+ run_with_login(
356+ ubuntu_branches.teamowner,
357+ ICanHasLinkedBranch(suite_sourcepackage).setBranch,
358+ branch, registrant)
359+ return branch
360+
361+ def test_owner_can_write_to_official_package_branch(self):
362+ # The owner of an official package branch can write to it, just like a
363+ # regular person.
364+ branch = self.makeOfficialPackageBranch()
365+ self.assertCanEdit(branch.owner, branch)
366+
367+ def assertCanUpload(self, person, spn, archive, component,
368+ strict_component=True):
369+ """Assert that 'person' can upload 'spn' to 'archive'."""
370+ # For now, just check that doesn't raise an exception.
371+ self.assertIs(
372+ None,
373+ verify_upload(person, spn, archive, component, strict_component))
374+
375+ def assertCannotUpload(self, reason, person, spn, archive, component):
376+ """Assert that 'person' cannot upload to the archive.
377+
378+ :param reason: The expected reason for not being able to upload. A
379+ string.
380+ :param person: The person trying to upload.
381+ :param spn: The `ISourcePackageName` being uploaded to. None if the
382+ package does not yet exist.
383+ :param archive: The `IArchive` being uploaded to.
384+ :param component: The IComponent to which the package belongs.
385+ """
386+ exception = verify_upload(person, spn, archive, component)
387+ self.assertEqual(reason, str(exception))
388+
389+ def test_package_upload_permissions_grant_branch_edit(self):
390+ # If you can upload to the package, then you are also allowed to write
391+ # to the branch.
392+
393+ permission_set = getUtility(IArchivePermissionSet)
394+ # Only admins or techboard members can add permissions normally. That
395+ # restriction isn't relevant to these tests.
396+ self.permission_set = removeSecurityProxy(permission_set)
397+ branch = self.makeOfficialPackageBranch()
398+ package = branch.sourcepackage
399+ person = self.factory.makePerson()
400+
401+ # Person is not allowed to edit the branch presently.
402+ self.assertCannotEdit(person, branch)
403+
404+ # Now give 'person' permission to upload to 'package'.
405+ archive = branch.distroseries.distribution.main_archive
406+ spn = package.sourcepackagename
407+ self.permission_set.newPackageUploader(archive, person, spn)
408+ # Make sure person *is* authorised to upload the source package
409+ # targeted by the branch at hand.
410+ self.assertCanUpload(person, spn, archive, None)
411+
412+ # Now person can edit the branch on the basis of the upload
413+ # permissions granted above.
414+ self.assertCanEdit(person, branch)
415
416
417 def test_suite():