Merge lp:~jml/launchpad/package-permission-love into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/package-permission-love
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~jml/launchpad/package-permission-love
Reviewer Review Type Date Requested Status
Celso Providelo (community) approve Approve
Julian Edwards (community) code Approve
Review via email: mp+10830@code.launchpad.net

Commit message

[ui=rs] Extract a 'verify_upload' function from nascentupload that encapsulates almost everything need to check whether someone can upload a package. Removes a useless filename from the 'not allowed to upload to component' error message.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Hello reviewer,

This branch is a very important branch, and a very risky one too. Please take
extra care when reviewing it. It's important because much of the Ubuntu
Distributed Development work depends on it. It's risky because it changes
poorly-understood security code.

This extracts out a function called 'verify_upload' from the nascentupload
code in Soyuz. The function is intended to provide a canonical answer to the
question, "can this person upload to this package in this archive?".

Sadly, it doesn't actually provide a canonical answer just yet. There's logic
in the upload policy code that needs to be moved into it. Specifically, it
needs to grow pocket-based permission checks. However, since the branch is
quite large already, I think I'll save that for another change.

The function has been placed in 'lp.archiveuploader.permission'. I don't think
this is a good long-term location, and I'm happy to move it if you think of a
better one. However, I think I'd prefer to wait until 'verify_upload' checks
the pocket as well before I encourage its re-use by putting it in a more
public location.

The implementation of the function is quite straight-forward, if you know what
it's supposed to do. I hope that the unit tests and the docstring explain this
clearly.

I probably got a bit ahead of myself with the exceptions. I'm happy to change
them on request.

Since this is a refactoring patch, and since I don't _really_ understand the
code, I wanted to make as few behavioural changes as possible. However, I was
forced to make some:

  * The error message for when you cannot upload to a particular component no
    longer includes the name of the DSC file. wgrant informed me that the file
    name was never very useful anyway.

  * The binary upload check happens much sooner. It has nothing to do with
    ACLs, so we might as well check it first.

I had to do a fair bit of exploratory work to get this patch up-and-running,
particularly to get the unit tests looking like actual unit tests. Not all of
it ended up being used, but it's all actually useful. Here's a run-down:

 * 'makeComponent' added to the object factory.

 * 'makeArchive' can now create non-PPA archives.

 * Added 'makePackageset'.

 * Tweaked a decorator to use 'mergeFunctionMetadata'. This means that it'll
   have a useful name when it appears in exception stack traces.

 * Changed one of the soyuz helpers, 'getPubSource' so that you can use it to
   get a SourcePackagePublishingHistory record without actually doing a
   simulated upload.

 * Added a launchpad.Edit permission policy for packagesets. This allows them
   to be used in a non-Zopeless environment.

I also did a few incidental cleanups:

 * Changed a docstring in archivepermisson.py to actually match reality. I
   checked this one with Muharem, who was the original author.

 * Re-used an existing local variable in a permission check, rather than
   violating the Law of Demeter.

 * Various whitespace and lint.

Thanks for getting to the end of the cover letter! I'd like to get this patch
right, so please ask as many questions as possible.

Revision history for this message
Julian Edwards (julian-edwards) wrote :
Download full text (53.2 KiB)

Hi Jono, thanks for taking this change on, it's much appreciated!

I am happy to bless this branch, if you make the changes I recommend below
and when Celso has also looked at it.
r=bigjools

>The function has been placed in 'lp.archiveuploader.permission'. I don't think
>this is a good long-term location, and I'm happy to move it if you think of a
>better one. However, I think I'd prefer to wait until 'verify_upload' checks
>the pocket as well before I encourage its re-use by putting it in a more
>public location.

It should go in lp.soyuz somewhere, but I'm not sure where yet either.

>I probably got a bit ahead of myself with the exceptions. I'm happy to change
>them on request.

Yeah, I don't like them so much. See my inline comments.

> * The error message for when you cannot upload to a particular component no
> longer includes the name of the DSC file. wgrant informed me that the file
> name was never very useful anyway.

Should be ok as he says, it doesn't add any value and in fact could be confusing to newbies.

> * The binary upload check happens much sooner. It has nothing to do with
> ACLs, so we might as well check it first.

Should be ok as long as the upload processor tests still pass, I think.

>Thanks for getting to the end of the cover letter! I'd like to get this patch
>right, so please ask as many questions as possible.

Thanks for the other clean ups you made, this is a great branch!

> === modified file '.bzrignore'
> --- .bzrignore 2009-08-05 23:08:37 +0000
> +++ .bzrignore 2009-08-27 07:16:25 +0000
> @@ -49,4 +49,5 @@
> ./download-cache
> ./_pythonpath.py
> ./production-configs
> +lib/lp/archiveuploader/tests/data/suite/bar_1.0-2/bar_1.0.orig.tar.gz
> bzr.dev
>

You don't need to do this. The file is left behind by a test that
uses the bar_1.0-2 package when it downloads the .orig file from the
librarian. The right thing to do is to fix the test to clean up after
itself. We just need to figure out which test is doing it! I'll look
for it after I do this review.

> === modified file 'lib/canonical/launchpad/security.py'
> --- lib/canonical/launchpad/security.py 2009-08-20 12:36:07 +0000
> +++ lib/canonical/launchpad/security.py 2009-08-27 07:16:25 +0000
> @@ -63,7 +63,7 @@
> IMilestone, IProjectMilestone)
> from canonical.launchpad.interfaces.oauth import (
> IOAuthAccessToken, IOAuthRequestToken)
> -from lp.soyuz.interfaces.packageset import IPackagesetSet
> +from lp.soyuz.interfaces.packageset import IPackageset, IPackagesetSet
> from lp.translations.int...

review: Approve (code)
Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (24.4 KiB)

On Sat, Aug 29, 2009 at 3:38 AM, Julian
Edwards<email address hidden> wrote:
> Review: Approve code
> Hi Jono, thanks for taking this change on, it's much appreciated!
>
> I am happy to bless this branch, if you make the changes I recommend below
> and when Celso has also looked at it.
> r=bigjools
>
>>The function has been placed in 'lp.archiveuploader.permission'. I don't think
>>this is a good long-term location, and I'm happy to move it if you think of a
>>better one. However, I think I'd prefer to wait until 'verify_upload' checks
>>the pocket as well before I encourage its re-use by putting it in a more
>>public location.
>
> It should go in lp.soyuz somewhere, but I'm not sure where yet either.
>

OK. I'll leave it where it is for now.

>>I probably got a bit ahead of myself with the exceptions. I'm happy to change
>>them on request.
>
> Yeah, I don't like them so much.  See my inline comments.
>

We dislike different things about them, I expect. Discussed further below.

>>Thanks for getting to the end of the cover letter! I'd like to get this patch
>>right, so please ask as many questions as possible.
>
> Thanks for the other clean ups you made, this is a great branch!
>

My pleasure. It's personally rewarding to watch this code become more usable.

>> === modified file '.bzrignore'
>> --- .bzrignore        2009-08-05 23:08:37 +0000
>> +++ .bzrignore        2009-08-27 07:16:25 +0000
>> @@ -49,4 +49,5 @@
>>  ./download-cache
>>  ./_pythonpath.py
>>  ./production-configs
>> +lib/lp/archiveuploader/tests/data/suite/bar_1.0-2/bar_1.0.orig.tar.gz
>>  bzr.dev
>>
>
> You don't need to do this.  The file is left behind by a test that
> uses the bar_1.0-2 package when it downloads the .orig file from the
> librarian.  The right thing to do is to fix the test to clean up after
> itself.  We just need to figure out which test is doing it! I'll look
> for it after I do this review.
>

Fair enough. I've removed the ignore line, filed bug 421705 and
assigned it to you.

>>  class DriverSpecification(AuthorizationBase):
>> @@ -514,6 +512,7 @@
>>          """IProjectMilestone is a fake content object."""
>>          return False
>>
>> +
>>  class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
>>      permission = 'launchpad.Edit'
>>      usedfor = IMilestone
>> @@ -2268,6 +2267,15 @@
>>              or user.inTeam(celebrities.admin))
>>
>>
>> +class EditPackageset(AuthorizationBase):
>> +    permission = 'launchpad.Edit'
>> +    usedfor = IPackageset
>> +
>> +    def checkAuthenticated(self, user):
>> +        """The owner of a package set can edit the object."""
>> +        return user.inTeam(self.obj.owner)
>> +
>> +
>
> Nice change.
>

Yeah, but it's made some tests fail. I've fixed them up as part of this reply.

>> === modified file 'lib/lp/archiveuploader/nascentupload.py'
>> --- lib/lp/archiveuploader/nascentupload.py   2009-07-17 00:26:05 +0000
>> +++ lib/lp/archiveuploader/nascentupload.py   2009-08-28 05:25:25 +0000
>> @@ -28,8 +28,8 @@
>>  from lp.archiveuploader.nascentuploadfile import (
>>      UploadError, UploadWarning, CustomUploadFile, SourceUploadFile,
>>      BaseBinaryUploadFile)
>> +from lp.archiveuploader.perm...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 31 August 2009 03:51:07 Jonathan Lange wrote:
> > Whenever I see exceptions used, I ask myself "is this caused by an
> > exceptional circumstance?" In this case, I'm not sure it is, but I can
> > see why you've used one - so that you can return an error string as well.
> > Is that a good enough reason to use an exception here? I guess the
> > code's neater but I still have a nagging feeling that it's bad form.
> >
> > How does this look in comparison:
> >
> > error = verify_upload(
> > signer, source_name, archive, self.changes.dsc.component,
> > not self.is_new)
> > if error is not None:
> > self.reject(error)
>
> I've never really understood the reasoning behind the "exceptional
> circumstance" argument.

Exceptions should be thrown only when there is an abnormal condition that the
code cannot handle. Exceptions should convey: "Woa! Something went wrong
here and I can't deal with it. BOOM." In this case, you're using them as a
routine return value, which doesn't fit that requirement. Also bearing in
mind that they're pretty expensive, if a piece of code can return a value to
indicate its results, then it should do so.

Obviously, "abnormal condition" is a little subjective, but I think in this
case it's clear cut, at least to me, since you're asking for a yes or no
answer.

> I've changed the code to return reason objects rather than raising
> exceptions -- returning a string is too ugly for me right now. This
> change doesn't effect the structure of the function at all.

Okay, it looks better, thanks.

> I've also added an assertCannotUpload to the test case and made the
> tests that check for negative conditions use that.

Excellent, thanks.

> > I suspect a lot of our existing tests for ACLs are now duplicated by your
> > new ones. I don't think we need to fix that in this branch, but it would
> > be great if you could file a bug about that so we remember to check/fix
> > in the future! I love deleting un-needed tests.
>
> Filed bug 421702 to track this.

Thanks.

> I also love deleting unneeded tests.

And correcting my grammar ;)

> mergeFunctionMetadata(f, g) takes the __name__, __doc__ and other
> properties from 'f' and mutates 'g' so that it has the same name,
> docstring and other properties. This is particularly useful for
> decorators, since the returned function (the modified 'g') has a
> useful name when it appears in a stack trace, rather than 'decorated'
> or 'function' or something equally miserable.

Ah! Nice.

> I did a merge from stable while replying to reviews, so I can't
> generate an interdiff easily.

:( I hope the Code diff updating stuff lands soon. Although I don't think it
generates partial diffs. Ho hum.

> Do I still need to wait for cprov's review?

Well he told me he'd done a review, but had not replied yet. I'll ask him
when he's online and reply to confirm.

Cheers
J

Revision history for this message
Celso Providelo (cprov) wrote :
Download full text (3.3 KiB)

On Tue, Sep 1, 2009 at 8:24 AM, Julian
Edwards<email address hidden> wrote:
> On Monday 31 August 2009 03:51:07 Jonathan Lange wrote:
>> > Whenever I see exceptions used, I ask myself "is this caused by an
>> > exceptional circumstance?"  In this case, I'm not sure it is, but I can
>> > see why you've used one - so that you can return an error string as well.
>> >  Is that a good enough reason to use an exception here?  I guess the
>> > code's neater but I still have a nagging feeling that it's bad form.
>> >
>> > How does this look in comparison:
>> >
>> >    error = verify_upload(
>> >        signer, source_name, archive, self.changes.dsc.component,
>> >        not self.is_new)
>> >    if error is not None:
>> >        self.reject(error)
>>
>> I've never really understood the reasoning behind the "exceptional
>> circumstance" argument.
>
> Exceptions should be thrown only when there is an abnormal condition that the
> code cannot handle.  Exceptions should convey: "Woa!  Something went wrong
> here and I can't deal with it.  BOOM."  In this case, you're using them as a
> routine return value, which doesn't fit that requirement.  Also bearing in
> mind that they're pretty expensive, if a piece of code can return a value to
> indicate its results, then it should do so.
>
> Obviously, "abnormal condition" is a little subjective, but I think in this
> case it's clear cut, at least to me, since you're asking for a yes or no
> answer.

I don't feel totally against the previous approach of raising an
exception representing the reason why the upload was rejected,
although I'd expect it to return a boolean by its name.

If one day we decide to store rejected uploads (for tracing/stats
purpose) we will probably have to map the rejection reasons to a
DBEnum, which would still make sense since there is indeed only a
limited number of situations covered by this code.

The truth is, the proposed change already results in much clearer
callsites, the differences between raising or returning the
rejection_reason doesn't affect it. So, thanks for it :)

[snip]

>> Do I still need to wait for cprov's review?
>
> Well he told me he'd done a review, but had not replied yet.  I'll ask him
> when he's online and reply to confirm.

I'm sorry for blocking this change.

The only comment I have is about the new control variable added in
STP.getPubSource(). Why would you ever need to create an source
publication that was *never* uploaded and doesn't contain any file ?
It will look like a source imported by 'gina' but with no files.

I suppose you are trying to avoid hitting the librarian and it will
certainly speed you test up as long as the logic doesn't need that
logic, but I don't think those implication are clear in the method
docstring as they should.

Since there isn't any callsite using it, I guess that change doesn't
need be landed right now.

Source creation is really simple and doesn't really depend on any
distroseries (chroot) setup, it won't be that difficult to move it to
the Factory and once chroot-procedures get integrated in the DAS api
it won't be any harder to move binary creation methods either.

Let me know what you think.

--
Celso Provid...

Read more...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 01 September 2009 14:15:16 Celso Providelo wrote:
> The only comment I have is about the new control variable added in
> STP.getPubSource(). Why would you ever need to create an source
> publication that was *never* uploaded and doesn't contain any file ?
> It will look like a source imported by 'gina' but with no files.
>
> I suppose you are trying to avoid hitting the librarian and it will
> certainly speed you test up as long as the logic doesn't need that
> logic, but I don't think those implication are clear in the method
> docstring as they should.

Right, it should be clearer about the implications.

But I feel it's the right thing to have in the majority of tests. We want
them to be as skinny and focused as possible. Introducing packageupload
creation in some cases causes the need for dbuser switching, which is really
horrible to have in the middle of a doctest.

Revision history for this message
Celso Providelo (cprov) wrote :

On Tue, Sep 1, 2009 at 11:27 AM, Julian
Edwards<email address hidden> wrote:
> On Tuesday 01 September 2009 14:15:16 Celso Providelo wrote:
>> The only comment I have is about the new control variable added in
>> STP.getPubSource(). Why would you ever need to create an source
>> publication that was *never* uploaded and doesn't contain any file ?
>> It will look like a source imported by 'gina' but with no files.
>>
>> I suppose you are trying to avoid hitting the librarian and it will
>> certainly speed you test up as long as the logic doesn't need that
>> logic, but I don't think those implication are clear in the method
>> docstring as they should.
>
> Right, it should be clearer about the implications.
>
> But I feel it's the right thing to have in the majority of tests.  We want
> them to be as skinny and focused as possible.  Introducing packageupload
> creation in some cases causes the need for dbuser switching, which is really
> horrible to have in the middle of a doctest.

Right, I agree, but it only implies that the DB isolation aspect have
to be better encapsulated in the test helper, not necessarily that the
created objects should be incomplete.

After the patch, passing 'create_packageupload=False' means that the
returned object is not suitable for UI or disk publication test.

It is not only confusing for people trying to use the method (remember
we are FOSS) but also is controversial with the reasons why it was
introduced. If we want developers to use more 'naive' publications in
their tests because they are faster, why don't we make it easier to
use ?

One way to make it super-easy and clear to use is to port it directly
to the Factory:

* makeSourcePackageRelease(...)
* makeSourcePackagePublication(...)

I'm sure people will find it quicker than in STP and the confusion
will be gone. Use the factory normally and if you really need it
switch to STP (and think about improving the factory method)

(Sorry, it is getting way OT, we should start a separate thread for
this discussion)

--
Celso Providelo <email address hidden>
IRC: cprov, Jabber: <email address hidden>, Skype: cprovidelo
1024D/681B6469 C858 2652 1A6E F6A6 037B B3F7 9FF2 583E 681B 6469

Revision history for this message
Celso Providelo (cprov) wrote :

Jono,

You changes are great, thanks for working on it. Two small nitpicks, though.

1) If remove the change on STP I'd be happier, if you find time to move the features I pointed before (makeSPR, makeSPPH) to the factory itself you will my hero for the whole month ;)

2) Factory.makeGPGKey() cannot be used more than once, since GPGKey.fingerprint has to be globally unique (which makes a lot of sense in the real world, right ?) Can you fix this with the existing unique-string support we already have ?

Other than that, great! r=me

review: Approve (approve)
Revision history for this message
Jonathan Lange (jml) wrote :

On Tue, Sep 1, 2009 at 9:24 PM, Julian
Edwards<email address hidden> wrote:
> On Monday 31 August 2009 03:51:07 Jonathan Lange wrote:
>> > Whenever I see exceptions used, I ask myself "is this caused by an
>> > exceptional circumstance?"  In this case, I'm not sure it is, but I can
>> > see why you've used one - so that you can return an error string as well.
>> >  Is that a good enough reason to use an exception here?  I guess the
>> > code's neater but I still have a nagging feeling that it's bad form.
>> >
>> > How does this look in comparison:
>> >
>> >    error = verify_upload(
>> >        signer, source_name, archive, self.changes.dsc.component,
>> >        not self.is_new)
>> >    if error is not None:
>> >        self.reject(error)
>>
>> I've never really understood the reasoning behind the "exceptional
>> circumstance" argument.
>
> Exceptions should be thrown only when there is an abnormal condition that the
> code cannot handle.  Exceptions should convey: "Woa!  Something went wrong
> here and I can't deal with it.  BOOM."  In this case, you're using them as a
> routine return value, which doesn't fit that requirement.

That's just restating your point in different words. I really want to
understand the _reason_ for saying so.

> Also bearing in
> mind that they're pretty expensive, if a piece of code can return a value to
> indicate its results, then it should do so.
>

Are they really that expensive in Python?
http://www.python.org/doc/faq/general/#how-fast-are-exceptions is
unclear

>> I also love deleting unneeded tests.
>
> And correcting my grammar ;)
>

Oh, I didn't notice. Sorry :)

jml

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 02 September 2009 03:21:08 Jonathan Lange wrote:
> That's just restating your point in different words. I really want to
> understand the _reason_ for saying so.

Maybe my experience is different to yours, but this is a well-established
programming convention in every place I've done programming work for the last
16 years. I can't find any exceptions (haha) to this convention when using
Google, either. Everything I've found talks about using them only for error
handling. Break strong conventions at your peril.

> > Also bearing in
> > mind that they're pretty expensive, if a piece of code can return a value
> > to indicate its results, then it should do so.
>
> Are they really that expensive in Python?
> http://www.python.org/doc/faq/general/#how-fast-are-exceptions is
> unclear

"Actually executing an exception is expensive" seems clear to me. :)

When you think about what happens, it makes sense.

Revision history for this message
Jonathan Lange (jml) wrote :

On Wed, Sep 2, 2009 at 6:06 PM, Julian
Edwards<email address hidden> wrote:
> On Wednesday 02 September 2009 03:21:08 Jonathan Lange wrote:
>> That's just restating your point in different words. I really want to
>> understand the _reason_ for saying so.
>
> Maybe my experience is different to yours, but this is a well-established
> programming convention in every place I've done programming work for the last
> 16 years.  I can't find any exceptions (haha) to this convention when using
> Google, either.  Everything I've found talks about using them only for error
> handling.  Break strong conventions at your peril.
>

I wasn't trying to disagree with the convention at all, just to
understand it a bit better. In general, it sounds very sensible to me.

I've heard it cited as a reason to always return None rather than
raise "not found" exceptions, for example, and that's never really sat
well with me.

>> > Also bearing in
>> > mind that they're pretty expensive, if a piece of code can return a value
>> > to indicate its results, then it should do so.
>>
>> Are they really that expensive in Python?
>> http://www.python.org/doc/faq/general/#how-fast-are-exceptions is
>> unclear
>
> "Actually executing an exception is expensive" seems clear to me. :)
>
> When you think about what happens, it makes sense.

Well, it means that it's only expensive if it gets raised. What I mean
is that it's not clear to me that that matters for this function. I'm
guessing it does.

jml

Revision history for this message
Robert Collins (lifeless) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

As a data point, and not talking about the wisdom or not of using
exceptions for flow control.

In bzr we found the overhead of a
try:
except:

Made a substantial performance difference, inside innermost loops.

- -Rob
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkqfj28ACgkQ42zgmrPGrq4UZgCgyw36QL+CTnmu7vK0LCyIHFsU
6LoAnRmSFaxN6ZzHtTsAPLEr17cqDamz
=82Fk
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2009-08-05 23:08:37 +0000
+++ .bzrignore 2009-08-27 07:16:25 +0000
@@ -49,4 +49,5 @@
49./download-cache49./download-cache
50./_pythonpath.py50./_pythonpath.py
51./production-configs51./production-configs
52lib/lp/archiveuploader/tests/data/suite/bar_1.0-2/bar_1.0.orig.tar.gz
52bzr.dev53bzr.dev
5354
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2009-08-20 12:36:07 +0000
+++ lib/canonical/launchpad/security.py 2009-08-27 07:16:25 +0000
@@ -63,7 +63,7 @@
63 IMilestone, IProjectMilestone)63 IMilestone, IProjectMilestone)
64from canonical.launchpad.interfaces.oauth import (64from canonical.launchpad.interfaces.oauth import (
65 IOAuthAccessToken, IOAuthRequestToken)65 IOAuthAccessToken, IOAuthRequestToken)
66from lp.soyuz.interfaces.packageset import IPackagesetSet66from lp.soyuz.interfaces.packageset import IPackageset, IPackagesetSet
67from lp.translations.interfaces.pofile import IPOFile67from lp.translations.interfaces.pofile import IPOFile
68from lp.translations.interfaces.potemplate import (68from lp.translations.interfaces.potemplate import (
69 IPOTemplate, IPOTemplateSubset)69 IPOTemplate, IPOTemplateSubset)
@@ -195,6 +195,7 @@
195 return (user.inTeam(celebrities.registry_experts)195 return (user.inTeam(celebrities.registry_experts)
196 or user.inTeam(celebrities.admin))196 or user.inTeam(celebrities.admin))
197197
198
198class ReviewProduct(ReviewByRegistryExpertsOrAdmins):199class ReviewProduct(ReviewByRegistryExpertsOrAdmins):
199 usedfor = IProduct200 usedfor = IProduct
200201
@@ -211,8 +212,6 @@
211 usedfor = IProjectSet212 usedfor = IProjectSet
212213
213214
214
215
216class ViewPillar(AuthorizationBase):215class ViewPillar(AuthorizationBase):
217 usedfor = IPillar216 usedfor = IPillar
218 permission = 'launchpad.View'217 permission = 'launchpad.View'
@@ -385,8 +384,7 @@
385 if user.inTeam(driver):384 if user.inTeam(driver):
386 return True385 return True
387 admins = getUtility(ILaunchpadCelebrities).admin386 admins = getUtility(ILaunchpadCelebrities).admin
388 return (user.inTeam(self.obj.target.owner) or387 return (user.inTeam(targetowner) or user.inTeam(admins))
389 user.inTeam(admins))
390388
391389
392class DriverSpecification(AuthorizationBase):390class DriverSpecification(AuthorizationBase):
@@ -514,6 +512,7 @@
514 """IProjectMilestone is a fake content object."""512 """IProjectMilestone is a fake content object."""
515 return False513 return False
516514
515
517class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):516class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
518 permission = 'launchpad.Edit'517 permission = 'launchpad.Edit'
519 usedfor = IMilestone518 usedfor = IMilestone
@@ -2268,6 +2267,15 @@
2268 or user.inTeam(celebrities.admin))2267 or user.inTeam(celebrities.admin))
22692268
22702269
2270class EditPackageset(AuthorizationBase):
2271 permission = 'launchpad.Edit'
2272 usedfor = IPackageset
2273
2274 def checkAuthenticated(self, user):
2275 """The owner of a package set can edit the object."""
2276 return user.inTeam(self.obj.owner)
2277
2278
2271class EditPackagesetSet(AuthorizationBase):2279class EditPackagesetSet(AuthorizationBase):
2272 permission = 'launchpad.Edit'2280 permission = 'launchpad.Edit'
2273 usedfor = IPackagesetSet2281 usedfor = IPackagesetSet
22742282
=== modified file 'lib/lp/archiveuploader/nascentupload.py'
--- lib/lp/archiveuploader/nascentupload.py 2009-07-17 00:26:05 +0000
+++ lib/lp/archiveuploader/nascentupload.py 2009-08-28 05:25:25 +0000
@@ -28,8 +28,8 @@
28from lp.archiveuploader.nascentuploadfile import (28from lp.archiveuploader.nascentuploadfile import (
29 UploadError, UploadWarning, CustomUploadFile, SourceUploadFile,29 UploadError, UploadWarning, CustomUploadFile, SourceUploadFile,
30 BaseBinaryUploadFile)30 BaseBinaryUploadFile)
31from lp.archiveuploader.permission import CannotUploadToArchive, verify_upload
31from lp.soyuz.interfaces.archive import ArchivePurpose, MAIN_ARCHIVE_PURPOSES32from lp.soyuz.interfaces.archive import ArchivePurpose, MAIN_ARCHIVE_PURPOSES
32from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
33from lp.soyuz.interfaces.publishing import PackagePublishingPocket33from lp.soyuz.interfaces.publishing import PackagePublishingPocket
34from canonical.launchpad.interfaces import (34from canonical.launchpad.interfaces import (
35 IBinaryPackageNameSet, IDistributionSet, ILibraryFileAliasSet,35 IBinaryPackageNameSet, IDistributionSet, ILibraryFileAliasSet,
@@ -477,16 +477,6 @@
477 # Signature and ACL stuff477 # Signature and ACL stuff
478 #478 #
479479
480 def _components_valid_for(self, person):
481 """Return the set of components this person could upload to."""
482 permission_set = getUtility(IArchivePermissionSet)
483 permissions = permission_set.componentsForUploader(
484 self.policy.archive, person)
485 possible_components = set(
486 permission.component for permission in permissions)
487
488 return possible_components
489
490 def verify_acl(self):480 def verify_acl(self):
491 """Check the signer's upload rights.481 """Check the signer's upload rights.
492482
@@ -494,6 +484,14 @@
494 or the explicit source package, or in the case of a PPA must own484 or the explicit source package, or in the case of a PPA must own
495 it or be in the owning team.485 it or be in the owning team.
496 """486 """
487 # Binary uploads are never checked (they come in via the security
488 # policy or from the buildds) so they don't need any ACL checks --
489 # this goes for PPAs as well as other archives. The only uploaded file
490 # that matters is the DSC file for sources because it is the only
491 # object that is overridden and created in the database.
492 if self.binaryful:
493 return
494
497 # Set up some convenient shortcut variables.495 # Set up some convenient shortcut variables.
498 signer = self.changes.signer496 signer = self.changes.signer
499 archive = self.policy.archive497 archive = self.policy.archive
@@ -503,68 +501,15 @@
503 self.logger.debug("No signer, therefore ACL not processed")501 self.logger.debug("No signer, therefore ACL not processed")
504 return502 return
505503
506 # Verify PPA uploads.
507 if self.is_ppa:
508 self.logger.debug("Don't verify signer ACL for PPA")
509 if not archive.canUpload(signer):
510 self.reject("Signer has no upload rights to this PPA.")
511 return
512
513 # Binary uploads are never checked (they come in via the security
514 # policy or from the buildds) so they don't need any ACL checks.
515 # The only uploaded file that matters is the DSC file for sources
516 # because it is the only object that is overridden and created in
517 # the database.
518 if self.binaryful:
519 return
520
521 # Sometimes an uploader may upload a new package to a component
522 # that he does not have rights to (but has rights to other components)
523 # but we let this through because an archive admin may wish to
524 # override it later. Consequently, if an uploader has no rights
525 # at all to any component, we reject the upload right now even if it's
526 # NEW.
527
528 # Check if the user has package-specific rights.
529 source_name = getUtility(504 source_name = getUtility(
530 ISourcePackageNameSet).queryByName(self.changes.dsc.package)505 ISourcePackageNameSet).queryByName(self.changes.dsc.package)
531 if (source_name is not None and506
532 archive.canUpload(signer, source_name)):507 try:
533 return508 verify_upload(
534509 signer, source_name, archive, self.changes.dsc.component,
535 # Now check whether this upload can be approved due to510 not self.is_new)
536 # package set based permissions.511 except CannotUploadToArchive, e:
537 ap_set = getUtility(IArchivePermissionSet)512 self.reject(str(e))
538 if source_name is not None and signer is not None:
539 if ap_set.isSourceUploadAllowed(archive, source_name, signer):
540 return
541
542 # If source_name is None then the package must be new, but we
543 # kick it out anyway because it's impossible to look up
544 # any permissions for it.
545 possible_components = self._components_valid_for(signer)
546 if not possible_components:
547 # The user doesn't have package-specific rights or
548 # component rights, so kick him out entirely.
549 self.reject(
550 "The signer of this package has no upload rights to this "
551 "distribution's primary archive. Did you mean to upload "
552 "to a PPA?")
553 return
554
555 # New packages go straight to the upload queue; we only check upload
556 # rights for old packages.
557 if self.is_new:
558 return
559
560 component = self.changes.dsc.component
561 if component not in possible_components:
562 # The uploader has no rights to the component.
563 self.reject(
564 "Signer is not permitted to upload to the component "
565 "'%s' of file '%s'." % (component.name,
566 self.changes.dsc.filename))
567
568513
569 #514 #
570 # Handling checking of versions and overrides515 # Handling checking of versions and overrides
571516
=== added file 'lib/lp/archiveuploader/permission.py'
--- lib/lp/archiveuploader/permission.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/permission.py 2009-08-28 05:53:16 +0000
@@ -0,0 +1,104 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Permissions for uploading a package to an archive."""
5
6__metaclass__ = type
7__all__ = [
8 'CannotUploadToArchive',
9 'CannotUploadToPPA',
10 'components_valid_for',
11 'verify_upload',
12 ]
13
14from zope.component import getUtility
15
16from lp.soyuz.interfaces.archive import ArchivePurpose
17from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
18
19
20class CannotUploadToArchive(Exception):
21 """Raised when a person cannot upload to archive."""
22
23 _fmt = '%(person)s has no upload rights to %(archive)s.'
24
25 def __init__(self, **args):
26 """Construct a `CannotUploadToArchive`."""
27 Exception.__init__(self, self._fmt % args)
28
29
30class CannotUploadToPPA(CannotUploadToArchive):
31 """Raised when a person cannot upload to a PPA."""
32
33 _fmt = 'Signer has no upload rights to this PPA.'
34
35
36class NoRightsForArchive(CannotUploadToArchive):
37 """Raised when a person has absolutely no upload rights to an archive."""
38
39 _fmt = (
40 "The signer of this package has no upload rights to this "
41 "distribution's primary archive. Did you mean to upload to "
42 "a PPA?")
43
44
45class NoRightsForComponent(CannotUploadToArchive):
46 """Raised when a person tries to upload to a component without permission.
47 """
48
49 _fmt = (
50 "Signer is not permitted to upload to the component '%(component)s'.")
51
52 def __init__(self, component):
53 CannotUploadToArchive.__init__(self, component=component.name)
54
55
56def components_valid_for(archive, person):
57 """Return the components that 'person' can upload to 'archive'.
58
59 :param archive: The `IArchive` than 'person' wishes to upload to.
60 :param person: An `IPerson` wishing to upload to an archive.
61 :return: A `set` of `IComponent`s that 'person' can upload to.
62 """
63 permission_set = getUtility(IArchivePermissionSet)
64 permissions = permission_set.componentsForUploader(archive, person)
65 return set(permission.component for permission in permissions)
66
67
68def verify_upload(person, sourcepackagename, archive, component,
69 strict_component=True):
70 """Can 'person' upload 'sourcepackagename' to 'archive'?
71
72 :param person: The `IPerson` trying to upload to the package. Referred to
73 as 'the signer' in upload code.
74 :param sourcepackagename: The source package being uploaded. None if the
75 package is new.
76 :param archive: The `IArchive` being uploaded to.
77 :param component: The `IComponent` that the source package belongs to.
78 :param strict_component: True if access to the specific component for the
79 package is needed to upload to it. If False, then access to any
80 package will do.
81 :raise CannotUploadToArchive: If 'person' cannot upload to the archive.
82 :return: Nothing of interest.
83 """
84 # For PPAs...
85 if archive.purpose == ArchivePurpose.PPA:
86 if not archive.canUpload(person):
87 raise CannotUploadToPPA()
88 else:
89 return True
90
91 # For any other archive...
92 ap_set = getUtility(IArchivePermissionSet)
93 if sourcepackagename is not None and (
94 archive.canUpload(person, sourcepackagename)
95 or ap_set.isSourceUploadAllowed(archive, sourcepackagename, person)):
96 return
97
98 if not components_valid_for(archive, person):
99 raise NoRightsForArchive()
100
101 if (component is not None
102 and strict_component
103 and not archive.canUpload(person, component)):
104 raise NoRightsForComponent(component)
0105
=== added file 'lib/lp/archiveuploader/tests/test_permission.py'
--- lib/lp/archiveuploader/tests/test_permission.py 1970-01-01 00:00:00 +0000
+++ lib/lp/archiveuploader/tests/test_permission.py 2009-08-28 05:48:38 +0000
@@ -0,0 +1,191 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for the permissions for uploading to an archive."""
5
6__metaclass__ = type
7
8import unittest
9
10from zope.component import getUtility
11from zope.security.proxy import removeSecurityProxy
12
13from canonical.testing import DatabaseFunctionalLayer
14
15from lp.archiveuploader.permission import (
16 CannotUploadToArchive, components_valid_for, verify_upload)
17from lp.registry.interfaces.gpg import GPGKeyAlgorithm, IGPGKeySet
18from lp.soyuz.interfaces.archive import ArchivePurpose
19from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
20from lp.testing import TestCaseWithFactory
21
22
23class TestComponents(TestCaseWithFactory):
24
25 layer = DatabaseFunctionalLayer
26
27 def test_no_components_for_arbitrary_person(self):
28 # By default, a person cannot upload to any component of an archive.
29 archive = self.factory.makeArchive()
30 person = self.factory.makePerson()
31 self.assertEqual(set(), components_valid_for(archive, person))
32
33 def test_components_for_person_with_permissions(self):
34 # If a person has been explicitly granted upload permissions to a
35 # particular component, then those components are included in
36 # components_valid_for.
37 archive = self.factory.makeArchive()
38 component = self.factory.makeComponent()
39 person = self.factory.makePerson()
40 ap_set = removeSecurityProxy(getUtility(IArchivePermissionSet))
41 ap_set.newComponentUploader(archive, person, component)
42 self.assertEqual(
43 set([component]), components_valid_for(archive, person))
44
45
46class TestPermission(TestCaseWithFactory):
47
48 layer = DatabaseFunctionalLayer
49
50 def setUp(self):
51 TestCaseWithFactory.setUp(self)
52 permission_set = getUtility(IArchivePermissionSet)
53 self.permission_set = removeSecurityProxy(permission_set)
54
55 def assertCanUpload(self, person, spn, archive, component,
56 strict_component=True):
57 """Assert that 'person' can upload 'ssp' to 'archive'."""
58 # For now, just check that doesn't raise an exception.
59 verify_upload(person, spn, archive, component, strict_component)
60
61 def makeGPGKey(self, owner):
62 """Give 'owner' a crappy GPG key for the purposes of testing."""
63 return getUtility(IGPGKeySet).new(
64 owner.id,
65 keyid='DEADBEEF',
66 fingerprint='A' * 40,
67 keysize=self.factory.getUniqueInteger(),
68 algorithm=GPGKeyAlgorithm.R,
69 active=True,
70 can_encrypt=False)
71
72 def test_random_person_cannot_upload_to_ppa(self):
73 # Arbitrary people cannot upload to a PPA.
74 person = self.factory.makePerson()
75 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
76 spn = self.factory.makeSourcePackageName()
77 exception = self.assertRaises(
78 CannotUploadToArchive, verify_upload, person, spn, ppa, None)
79 self.assertEqual(
80 'Signer has no upload rights to this PPA.', str(exception))
81
82 def test_owner_can_upload_to_ppa(self):
83 # If the archive is a PPA, and you own it, then you can upload pretty
84 # much anything to it.
85 team = self.factory.makeTeam()
86 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, owner=team)
87 person = self.factory.makePerson()
88 removeSecurityProxy(team).addMember(person, team.teamowner)
89 spn = self.factory.makeSourcePackageName()
90 self.assertCanUpload(person, spn, ppa, None)
91
92 def test_owner_can_upload_to_ppa_no_sourcepackage(self):
93 # The owner can upload to PPAs even if the source package doesn't
94 # exist yet.
95 team = self.factory.makeTeam()
96 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA, owner=team)
97 person = self.factory.makePerson()
98 removeSecurityProxy(team).addMember(person, team.teamowner)
99 self.assertCanUpload(person, None, ppa, None)
100
101 def test_arbitrary_person_cannot_upload_to_primary_archive(self):
102 # By default, you can't upload to the primary archive.
103 person = self.factory.makePerson()
104 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
105 spn = self.factory.makeSourcePackageName()
106 exception = self.assertRaises(
107 CannotUploadToArchive, verify_upload, person, spn, archive, None)
108 self.assertEqual(
109 ("The signer of this package has no upload rights to this "
110 "distribution's primary archive. Did you mean to upload to "
111 "a PPA?"),
112 str(exception))
113
114 def test_package_specific_rights(self):
115 # A person can be granted specific rights for uploading a package,
116 # based only on the source package name. If they have these rights,
117 # they can upload to the package.
118 person = self.factory.makePerson()
119 spn = self.factory.makeSourcePackageName()
120 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
121 # We can't use a PPA, because they have a different logic for
122 # permissions. We can't create an arbitrary archive, because there's
123 # only one primary archive per distro.
124 self.permission_set.newPackageUploader(archive, person, spn)
125 self.assertCanUpload(person, spn, archive, None)
126
127 def test_packageset_specific_rights(self):
128 # A person with rights to upload to the package set can upload the
129 # package set to the archive.
130 person = self.factory.makePerson()
131 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
132 spn = self.factory.makeSourcePackageName()
133 package_set = self.factory.makePackageset(packages=[spn])
134 self.permission_set.newPackagesetUploader(
135 archive, person, package_set)
136 self.assertCanUpload(person, spn, archive, None)
137
138 def test_component_rights(self):
139 # A person allowed to upload to a particular component of an archive
140 # can upload basically whatever they want to that component.
141 person = self.factory.makePerson()
142 spn = self.factory.makeSourcePackageName()
143 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
144 component = self.factory.makeComponent()
145 self.permission_set.newComponentUploader(archive, person, component)
146 self.assertCanUpload(person, spn, archive, component)
147
148 def test_incorrect_component_rights(self):
149 # Even if a person has upload rights for a particular component in an
150 # archive, it doesn't mean they have upload rights for everything in
151 # that archive.
152 person = self.factory.makePerson()
153 spn = self.factory.makeSourcePackageName()
154 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
155 permitted_component = self.factory.makeComponent()
156 forbidden_component = self.factory.makeComponent()
157 self.permission_set.newComponentUploader(
158 archive, person, permitted_component)
159 exception = self.assertRaises(
160 CannotUploadToArchive, verify_upload, person, spn, archive,
161 forbidden_component)
162 self.assertEqual(
163 u"Signer is not permitted to upload to the component '%s'." % (
164 forbidden_component.name),
165 str(exception))
166
167 def test_component_rights_no_package(self):
168 # A person allowed to upload to a particular component of an archive
169 # can upload basically whatever they want to that component, even if
170 # the package doesn't exist yet.
171 person = self.factory.makePerson()
172 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
173 component = self.factory.makeComponent()
174 self.permission_set.newComponentUploader(archive, person, component)
175 self.assertCanUpload(person, None, archive, component)
176
177 def test_non_strict_component_rights(self):
178 # If we aren't testing strict component access, then we only need to
179 # have access to an arbitrary component.
180 person = self.factory.makePerson()
181 spn = self.factory.makeSourcePackageName()
182 archive = self.factory.makeArchive(purpose=ArchivePurpose.PRIMARY)
183 component_a = self.factory.makeComponent()
184 component_b = self.factory.makeComponent()
185 self.permission_set.newComponentUploader(archive, person, component_b)
186 self.assertCanUpload(
187 person, spn, archive, component_a, strict_component=False)
188
189
190def test_suite():
191 return unittest.TestLoader().loadTestsFromName(__name__)
0192
=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2009-07-17 00:26:05 +0000
+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2009-08-28 05:25:25 +0000
@@ -1268,8 +1268,7 @@
1268 # Make sure it failed.1268 # Make sure it failed.
1269 self.assertEqual(1269 self.assertEqual(
1270 uploadprocessor.last_processed_upload.rejection_message,1270 uploadprocessor.last_processed_upload.rejection_message,
1271 u"Signer is not permitted to upload to the component 'universe'"1271 u"Signer is not permitted to upload to the component 'universe'.")
1272 " of file 'bar_1.0-2.dsc'.")
12731272
1274 # Now add permission to upload "bar" for name16.1273 # Now add permission to upload "bar" for name16.
1275 bar_package = getUtility(ISourcePackageNameSet).queryByName("bar")1274 bar_package = getUtility(ISourcePackageNameSet).queryByName("bar")
@@ -1322,8 +1321,7 @@
1322 # Make sure it failed.1321 # Make sure it failed.
1323 self.assertEqual(1322 self.assertEqual(
1324 uploadprocessor.last_processed_upload.rejection_message,1323 uploadprocessor.last_processed_upload.rejection_message,
1325 u"Signer is not permitted to upload to the component 'universe'"1324 "Signer is not permitted to upload to the component 'universe'.")
1326 " of file 'bar_1.0-2.dsc'.")
13271325
1328 # Now put in place a package set, add 'bar' to it and define a1326 # Now put in place a package set, add 'bar' to it and define a
1329 # permission for the former.1327 # permission for the former.
13301328
=== modified file 'lib/lp/soyuz/interfaces/archivepermission.py'
--- lib/lp/soyuz/interfaces/archivepermission.py 2009-07-17 00:26:05 +0000
+++ lib/lp/soyuz/interfaces/archivepermission.py 2009-08-03 17:10:12 +0000
@@ -335,8 +335,8 @@
335 :param archive: The archive the permission applies to.335 :param archive: The archive the permission applies to.
336 :param person: An `IPerson` for whom you want to add permission.336 :param person: An `IPerson` for whom you want to add permission.
337 :param packageset: An `IPackageset` or a string package set name.337 :param packageset: An `IPackageset` or a string package set name.
338 :param explicit: True if the package set in question requires338 :param explicit: True if the permissions granted by this package set
339 specialist skills for proper handling.339 exclude permissions granted by non-explicit package sets.
340 :raises ValueError: if an `ArchivePermission` record for this340 :raises ValueError: if an `ArchivePermission` record for this
341 person and packageset already exists *but* with a different341 person and packageset already exists *but* with a different
342 'explicit' flag value.342 'explicit' flag value.
343343
=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
--- lib/lp/soyuz/tests/test_publishing.py 2009-07-19 04:41:14 +0000
+++ lib/lp/soyuz/tests/test_publishing.py 2009-08-06 16:57:36 +0000
@@ -152,7 +152,8 @@
152 dsc_binaries='foo-bin', build_conflicts=None,152 dsc_binaries='foo-bin', build_conflicts=None,
153 build_conflicts_indep=None,153 build_conflicts_indep=None,
154 dsc_maintainer_rfc822='Foo Bar <foo@bar.com>',154 dsc_maintainer_rfc822='Foo Bar <foo@bar.com>',
155 maintainer=None, creator=None, date_uploaded=UTC_NOW):155 maintainer=None, creator=None, date_uploaded=UTC_NOW,
156 do_upload=True):
156 """Return a mock source publishing record."""157 """Return a mock source publishing record."""
157 if sourcename is None:158 if sourcename is None:
158 sourcename = self.default_package_name159 sourcename = self.default_package_name
@@ -194,17 +195,18 @@
194 archive=archive, dateuploaded=date_uploaded)195 archive=archive, dateuploaded=date_uploaded)
195196
196 changes_file_name = "%s_%s_source.changes" % (sourcename, version)197 changes_file_name = "%s_%s_source.changes" % (sourcename, version)
197 package_upload = self.addPackageUpload(198 if do_upload:
198 archive, distroseries, pocket,199 package_upload = self.addPackageUpload(
199 changes_file_name=changes_file_name,200 archive, distroseries, pocket,
200 changes_file_content=changes_file_content)201 changes_file_name=changes_file_name,
201 package_upload.addSource(spr)202 changes_file_content=changes_file_content)
203 package_upload.addSource(spr)
202204
203 if filename is None:205 if filename is None:
204 filename = "%s_%s.dsc" % (sourcename, version)206 filename = "%s_%s.dsc" % (sourcename, version)
205 alias = self.addMockFile(207 alias = self.addMockFile(
206 filename, filecontent, restricted=archive.private)208 filename, filecontent, restricted=archive.private)
207 spr.addFile(alias)209 spr.addFile(alias)
208210
209 sspph = SecureSourcePackagePublishingHistory(211 sspph = SecureSourcePackagePublishingHistory(
210 distroseries=distroseries,212 distroseries=distroseries,
211213
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2009-08-21 18:06:18 +0000
+++ lib/lp/testing/factory.py 2009-08-28 05:48:38 +0000
@@ -26,6 +26,9 @@
26import pytz26import pytz
27from storm.store import Store27from storm.store import Store
28import transaction28import transaction
29
30from twisted.python.util import mergeFunctionMetadata
31
29from zope.component import ComponentLookupError, getUtility32from zope.component import ComponentLookupError, getUtility
30from zope.security.proxy import removeSecurityProxy33from zope.security.proxy import removeSecurityProxy
3134
@@ -100,7 +103,9 @@
100from lp.registry.interfaces.sourcepackagename import (103from lp.registry.interfaces.sourcepackagename import (
101 ISourcePackageNameSet)104 ISourcePackageNameSet)
102from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType105from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyType
103from lp.testing import time_counter106from lp.soyuz.interfaces.component import IComponentSet
107from lp.soyuz.interfaces.packageset import IPackagesetSet
108from lp.testing import run_with_login, time_counter
104109
105SPACE = ' '110SPACE = ' '
106111
@@ -125,7 +130,7 @@
125 return func(*args, **kw)130 return func(*args, **kw)
126 finally:131 finally:
127 store_selector.pop()132 store_selector.pop()
128 return with_default_master_store133 return mergeFunctionMetadata(func, with_default_master_store)
129134
130135
131# We use this for default paramters where None has a specific meaning. For136# We use this for default paramters where None has a specific meaning. For
@@ -1354,6 +1359,12 @@
1354 architecturetag, processorfamily, official, owner,1359 architecturetag, processorfamily, official, owner,
1355 supports_virtualized)1360 supports_virtualized)
13561361
1362 def makeComponent(self, name=None):
1363 """Make a new `IComponent`."""
1364 if name is None:
1365 name = self.getUniqueString()
1366 return getUtility(IComponentSet).ensure(name)
1367
1357 def makeArchive(self, distribution=None, owner=None, name=None,1368 def makeArchive(self, distribution=None, owner=None, name=None,
1358 purpose = None):1369 purpose = None):
1359 """Create and return a new arbitrary archive.1370 """Create and return a new arbitrary archive.
@@ -1374,6 +1385,11 @@
1374 if purpose is None:1385 if purpose is None:
1375 purpose = ArchivePurpose.PPA1386 purpose = ArchivePurpose.PPA
13761387
1388 # Making a distribution makes an archive, and there can be only one
1389 # per distribution.
1390 if purpose == ArchivePurpose.PRIMARY:
1391 return distribution.main_archive
1392
1377 return getUtility(IArchiveSet).new(1393 return getUtility(IArchiveSet).new(
1378 owner=owner, purpose=purpose,1394 owner=owner, purpose=purpose,
1379 distribution=distribution, name=name)1395 distribution=distribution, name=name)
@@ -1581,6 +1597,24 @@
1581 distroseries = self.makeDistroRelease()1597 distroseries = self.makeDistroRelease()
1582 return distroseries.getSourcePackage(sourcepackagename)1598 return distroseries.getSourcePackage(sourcepackagename)
15831599
1600 def makePackageset(self, name=None, description=None, owner=None,
1601 packages=()):
1602 """Make an `IPackageset`."""
1603 if name is None:
1604 name = self.getUniqueString(u'package-set-name')
1605 if description is None:
1606 description = self.getUniqueString(u'package-set-description')
1607 if owner is None:
1608 person = self.getUniqueString(u'package-set-owner')
1609 owner = self.makePerson(name=person)
1610 techboard = getUtility(ILaunchpadCelebrities).ubuntu_techboard
1611 ps_set = getUtility(IPackagesetSet)
1612 package_set = run_with_login(
1613 techboard.teamowner,
1614 lambda: ps_set.new(name, description, owner))
1615 run_with_login(owner, lambda: package_set.add(packages))
1616 return package_set
1617
1584 def getAnyPocket(self):1618 def getAnyPocket(self):
1585 return PackagePublishingPocket.RELEASE1619 return PackagePublishingPocket.RELEASE
15861620