Merge lp:~cjwatson/launchpad/registry-delete-archive into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18859
Proposed branch: lp:~cjwatson/launchpad/registry-delete-archive
Merge into: lp:launchpad
Diff against target: 308 lines (+116/-109)
5 files modified
lib/lp/security.py (+21/-0)
lib/lp/soyuz/configure.zcml (+4/-1)
lib/lp/soyuz/doc/archive-deletion.txt (+0/-71)
lib/lp/soyuz/interfaces/archive.py (+21/-17)
lib/lp/soyuz/tests/test_archive.py (+70/-20)
To merge this branch: bzr merge lp:~cjwatson/launchpad/registry-delete-archive
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+358964@code.launchpad.net

Commit message

Allow registry experts to delete non-main archives.

Description of the change

This makes it easier to deal with PPA description spam. Registry experts already have extensive powers along these lines; suspending accounts is normally sufficient right now, but there are some awkward edge cases where straightforward deletion would be easier.

I also granted the ability for registry experts to see public but disabled archives, as otherwise they'd lose access to the object immediately after calling the delete method.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2018-10-16 11:58:39 +0000
+++ lib/lp/security.py 2018-11-18 18:44:49 +0000
@@ -2686,6 +2686,12 @@
2686 if user.in_admin or user.in_commercial_admin:2686 if user.in_admin or user.in_commercial_admin:
2687 return True2687 return True
26882688
2689 # Registry experts are allowed to view public but disabled archives
2690 # (since they are allowed to disable archives).
2691 if (not self.obj.private and not self.obj.enabled and
2692 user.in_registry_experts):
2693 return True
2694
2689 # Owners can view the PPA.2695 # Owners can view the PPA.
2690 if user.inTeam(self.obj.owner):2696 if user.inTeam(self.obj.owner):
2691 return True2697 return True
@@ -2748,6 +2754,21 @@
2748 return user.isOwner(self.obj) or user.in_admin2754 return user.isOwner(self.obj) or user.in_admin
27492755
27502756
2757class DeleteArchive(EditArchive):
2758 """Restrict archive deletion operations.
2759
2760 People who can edit an archive can delete it. In addition, registry
2761 experts can delete non-main archives, as a spam control mechanism.
2762 """
2763 permission = 'launchpad.Delete'
2764 usedfor = IArchive
2765
2766 def checkAuthenticated(self, user):
2767 return (
2768 super(DeleteArchive, self).checkAuthenticated(user) or
2769 (not self.obj.is_main and user.in_registry_experts))
2770
2771
2751class AppendArchive(AuthorizationBase):2772class AppendArchive(AuthorizationBase):
2752 """Restrict appending (upload and copy) operations on archives.2773 """Restrict appending (upload and copy) operations on archives.
27532774
27542775
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2017-07-18 16:22:03 +0000
+++ lib/lp/soyuz/configure.zcml 2018-11-18 18:44:49 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -330,6 +330,9 @@
330 set_attributes="build_debug_symbols description displayname330 set_attributes="build_debug_symbols description displayname
331 publish publish_debug_symbols status331 publish publish_debug_symbols status
332 suppress_subscription_notifications"/>332 suppress_subscription_notifications"/>
333 <require
334 permission="launchpad.Delete"
335 interface="lp.soyuz.interfaces.archive.IArchiveDelete"/>
333 <!--336 <!--
334 NOTE: The 'private' permission controls who can turn a public337 NOTE: The 'private' permission controls who can turn a public
335 archive into a private one, and vice versa. The logic that338 archive into a private one, and vice versa. The logic that
336339
=== removed file 'lib/lp/soyuz/doc/archive-deletion.txt'
--- lib/lp/soyuz/doc/archive-deletion.txt 2018-05-27 18:32:33 +0000
+++ lib/lp/soyuz/doc/archive-deletion.txt 1970-01-01 00:00:00 +0000
@@ -1,71 +0,0 @@
1= Deleting an archive =
2
3When deleting an archive, the user calls IArchive.delete(), passing in
4the IPerson who is requesting the deletion. The archive is disabled and
5the status set to DELETING.
6
7This status tells the publisher to then set the publications to DELETED
8and delete the repository area. Once it completes that task the status
9of the archive itself is set to DELETED.
10
11 >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
12 >>> login("admin@canonical.com")
13 >>> stp = SoyuzTestPublisher()
14 >>> stp.prepareBreezyAutotest()
15 >>> owner = factory.makePerson(name='archive-owner')
16 >>> archive = factory.makeArchive(owner=owner)
17
18The archive is currently active:
19
20 >>> print(archive.enabled)
21 True
22
23 >>> print(archive.status.name)
24 ACTIVE
25
26We can create some packages in it using the test publisher:
27
28 >>> from lp.soyuz.enums import PackagePublishingStatus
29 >>> ignore = stp.getPubBinaries(
30 ... archive=archive, binaryname="foo-bin1",
31 ... status=PackagePublishingStatus.PENDING)
32 >>> ignore = stp.getPubBinaries(
33 ... archive=archive, binaryname="foo-bin2",
34 ... status=PackagePublishingStatus.PUBLISHED)
35 >>> from storm.store import Store
36 >>> Store.of(archive).flush()
37
38Calling delete() will now do the deletion. It is only callable by someone
39with launchpad.Edit permission on the archive. Here, "duderino" who is
40some random dude is refused:
41
42 >>> person = factory.makePerson(name="duderino")
43 >>> ignored = login_person(person)
44 >>> archive.delete(person)
45 Traceback (most recent call last):
46 ...
47 Unauthorized:...
48
49However we can delete it using the owner of the archive:
50
51 >>> ignored = login_person(archive.owner)
52 >>> archive.delete(archive.owner)
53
54Now the archive is disabled and the status is DELETING to tell the
55publisher to remove the publications and the repository:
56
57 >>> print(archive.enabled)
58 False
59
60 >>> print(archive.status.name)
61 DELETING
62
63Once deleted the archive can't be reenabled.
64
65 >>> archive.enable()
66 Traceback (most recent call last):
67 ...
68 AssertionError: Deleted archives can't be enabled.
69
70 >>> print(archive.enabled)
71 False
720
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2018-08-03 12:16:16 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2018-11-18 18:44:49 +0000
@@ -2046,22 +2046,6 @@
2046 def disable():2046 def disable():
2047 """Disable the archive."""2047 """Disable the archive."""
20482048
2049 @export_destructor_operation()
2050 @call_with(deleted_by=REQUEST_USER)
2051 @operation_for_version('devel')
2052 def delete(deleted_by):
2053 """Delete this archive.
2054
2055 :param deleted_by: The `IPerson` requesting the deletion.
2056
2057 The ArchiveStatus will be set to DELETING and any published
2058 packages will be marked as DELETED by deleted_by.
2059
2060 The publisher is responsible for deleting the repository area
2061 when it sees the status change and sets it to DELETED once
2062 processed.
2063 """
2064
2065 def addArchiveDependency(dependency, pocket, component=None):2049 def addArchiveDependency(dependency, pocket, component=None):
2066 """Record an archive dependency record for the context archive.2050 """Record an archive dependency record for the context archive.
20672051
@@ -2242,6 +2226,26 @@
2242 """2226 """
22432227
22442228
2229class IArchiveDelete(Interface):
2230 """Archive interface for operations restricted by delete privilege."""
2231
2232 @export_destructor_operation()
2233 @call_with(deleted_by=REQUEST_USER)
2234 @operation_for_version('devel')
2235 def delete(deleted_by):
2236 """Delete this archive.
2237
2238 :param deleted_by: The `IPerson` requesting the deletion.
2239
2240 The ArchiveStatus will be set to DELETING and any published
2241 packages will be marked as DELETED by deleted_by.
2242
2243 The publisher is responsible for deleting the repository area
2244 when it sees the status change and sets it to DELETED once
2245 processed.
2246 """
2247
2248
2245class IArchiveAdmin(Interface):2249class IArchiveAdmin(Interface):
2246 """Archive interface for operations restricted by commercial."""2250 """Archive interface for operations restricted by commercial."""
22472251
@@ -2269,7 +2273,7 @@
2269 "with a higher score will build sooner.")))2273 "with a higher score will build sooner.")))
22702274
22712275
2272class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit,2276class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveDelete,
2273 IArchiveSubscriberView, IArchiveView, IArchiveAdmin,2277 IArchiveSubscriberView, IArchiveView, IArchiveAdmin,
2274 IArchiveRestricted):2278 IArchiveRestricted):
2275 """Main Archive interface."""2279 """Main Archive interface."""
22762280
=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py 2018-08-03 12:16:16 +0000
+++ lib/lp/soyuz/tests/test_archive.py 2018-11-18 18:44:49 +0000
@@ -17,6 +17,7 @@
17from pytz import UTC17from pytz import UTC
18import responses18import responses
19import six19import six
20from storm.store import Store
20from testtools.matchers import (21from testtools.matchers import (
21 AllMatch,22 AllMatch,
22 DocTestMatches,23 DocTestMatches,
@@ -1619,29 +1620,78 @@
16191620
16201621
1621class TestArchiveDelete(TestCaseWithFactory):1622class TestArchiveDelete(TestCaseWithFactory):
1622 """Edge-case tests for PPA deletion.1623 """Test PPA deletion."""
16231624
1624 PPA deletion is also documented in lp/soyuz/doc/archive-deletion.txt.1625 layer = LaunchpadFunctionalLayer
1625 """1626
16261627 def makePopulatedArchive(self):
1627 layer = DatabaseFunctionalLayer1628 archive = self.factory.makeArchive()
16281629 self.assertActive(archive)
1629 def setUp(self):1630 publisher = SoyuzTestPublisher()
1630 """Create a test archive and login as the owner."""1631 with admin_logged_in():
1631 super(TestArchiveDelete, self).setUp()1632 publisher.prepareBreezyAutotest()
1632 self.archive = self.factory.makeArchive()1633 publisher.getPubBinaries(
1633 login_person(self.archive.owner)1634 archive=archive, binaryname="foo-bin1",
16341635 status=PackagePublishingStatus.PENDING)
1635 def test_delete(self):1636 publisher.getPubBinaries(
1636 # Sanity check for the unit-test.1637 archive=archive, binaryname="foo-bin2",
1637 self.archive.delete(deleted_by=self.archive.owner)1638 status=PackagePublishingStatus.PUBLISHED)
1638 self.assertEqual(ArchiveStatus.DELETING, self.archive.status)1639 Store.of(archive).flush()
1640 return archive
1641
1642 def assertActive(self, archive):
1643 self.assertTrue(archive.enabled)
1644 self.assertEqual(ArchiveStatus.ACTIVE, archive.status)
1645
1646 def assertDeleting(self, archive):
1647 # Deleting an archive sets the status to DELETING. This tells the
1648 # publisher to set the publications to DELETED and delete the
1649 # published archive from disk, after which the status of the archive
1650 # itself is set to DELETED.
1651 self.assertFalse(archive.enabled)
1652 self.assertEqual(ArchiveStatus.DELETING, archive.status)
1653
1654 def test_delete_unprivileged(self):
1655 # An unprivileged user cannot delete an archive.
1656 archive = self.factory.makeArchive()
1657 self.assertActive(archive)
1658 person = self.factory.makePerson()
1659 with person_logged_in(person):
1660 self.assertRaises(Unauthorized, getattr, archive, 'delete')
1661 self.assertActive(archive)
1662
1663 def test_delete_archive_owner(self):
1664 # The owner of an archive can delete it.
1665 archive = self.makePopulatedArchive()
1666 with person_logged_in(archive.owner):
1667 archive.delete(deleted_by=archive.owner)
1668 self.assertDeleting(archive)
1669
1670 def test_delete_registry_expert(self):
1671 # A registry expert can delete an archive.
1672 archive = self.makePopulatedArchive()
1673 with celebrity_logged_in("registry_experts"):
1674 archive.delete(deleted_by=archive.owner)
1675 self.assertDeleting(archive)
16391676
1640 def test_delete_when_disabled(self):1677 def test_delete_when_disabled(self):
1641 # A disabled archive can also be deleted (bug 574246).1678 # A disabled archive can also be deleted (bug 574246).
1642 self.archive.disable()1679 archive = self.makePopulatedArchive()
1643 self.archive.delete(deleted_by=self.archive.owner)1680 with person_logged_in(archive.owner):
1644 self.assertEqual(ArchiveStatus.DELETING, self.archive.status)1681 archive.disable()
1682 archive.delete(deleted_by=archive.owner)
1683 self.assertDeleting(archive)
1684
1685 def test_cannot_reenable(self):
1686 # A deleted archive cannot be re-enabled.
1687 archive = self.factory.makeArchive()
1688 with person_logged_in(archive.owner):
1689 archive.delete(deleted_by=archive.owner)
1690 self.assertDeleting(archive)
1691 self.assertRaisesWithContent(
1692 AssertionError, "Deleted archives can't be enabled.",
1693 archive.enable)
1694 self.assertDeleting(archive)
16451695
16461696
1647class TestSuppressSubscription(TestCaseWithFactory):1697class TestSuppressSubscription(TestCaseWithFactory):