Skip to content

Commit 26a79b7

Browse files
committed
Add disallow deletion AdminFlag
See pypi#3218
1 parent ab3a323 commit 26a79b7

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed

tests/unit/manage/test_views.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import warehouse.utils.otp as otp
2828

2929
from warehouse.accounts.interfaces import IPasswordBreachedService, IUserService
30+
from warehouse.admin.flags import AdminFlagValue
3031
from warehouse.macaroons.interfaces import IMacaroonService
3132
from warehouse.manage import views
3233
from warehouse.packaging.models import (
@@ -2014,6 +2015,7 @@ def test_delete_project_no_confirm(self):
20142015
project = pretend.stub(normalized_name="foo")
20152016
request = pretend.stub(
20162017
POST={},
2018+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
20172019
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
20182020
route_path=lambda *a, **kw: "/foo/bar/",
20192021
)
@@ -2023,6 +2025,9 @@ def test_delete_project_no_confirm(self):
20232025
assert exc.value.status_code == 303
20242026
assert exc.value.headers["Location"] == "/foo/bar/"
20252027

2028+
assert request.flags.enabled.calls == [
2029+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2030+
]
20262031
assert request.session.flash.calls == [
20272032
pretend.call("Confirm the request", queue="error")
20282033
]
@@ -2031,6 +2036,7 @@ def test_delete_project_wrong_confirm(self):
20312036
project = pretend.stub(normalized_name="foo")
20322037
request = pretend.stub(
20332038
POST={"confirm_project_name": "bar"},
2039+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
20342040
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
20352041
route_path=lambda *a, **kw: "/foo/bar/",
20362042
)
@@ -2040,13 +2046,46 @@ def test_delete_project_wrong_confirm(self):
20402046
assert exc.value.status_code == 303
20412047
assert exc.value.headers["Location"] == "/foo/bar/"
20422048

2049+
assert request.flags.enabled.calls == [
2050+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2051+
]
20432052
assert request.session.flash.calls == [
20442053
pretend.call(
20452054
"Could not delete project - 'bar' is not the same as 'foo'",
20462055
queue="error",
20472056
)
20482057
]
20492058

2059+
def test_delete_project_disallow_deletion(self):
2060+
project = pretend.stub(name="foo", normalized_name="foo")
2061+
request = pretend.stub(
2062+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: True)),
2063+
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
2064+
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
2065+
)
2066+
2067+
result = views.delete_project(project, request)
2068+
assert isinstance(result, HTTPSeeOther)
2069+
assert result.headers["Location"] == "/the-redirect"
2070+
2071+
assert request.flags.enabled.calls == [
2072+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2073+
]
2074+
2075+
assert request.session.flash.calls == [
2076+
pretend.call(
2077+
(
2078+
"Project deletion temporarily disabled. "
2079+
"See https://pypi.org/help#admin-intervention for details."
2080+
),
2081+
queue="error",
2082+
)
2083+
]
2084+
2085+
assert request.route_path.calls == [
2086+
pretend.call("manage.project.settings", project_name="foo")
2087+
]
2088+
20502089
def test_delete_project(self, db_request):
20512090
project = ProjectFactory.create(name="foo")
20522091

@@ -2159,6 +2198,7 @@ def test_manage_project_releases(self, db_request):
21592198
filename=f"foobar-{release.version}.tar.gz",
21602199
packagetype="sdist",
21612200
)
2201+
db_request.flags = pretend.stub(enabled=pretend.call_recorder(lambda *a: False))
21622202

21632203
assert views.manage_project_releases(project, db_request) == {
21642204
"project": project,
@@ -2182,6 +2222,48 @@ def test_manage_project_release(self):
21822222
"files": files,
21832223
}
21842224

2225+
def test_delete_project_release_disallow_deletion(self, monkeypatch):
2226+
release = pretend.stub(
2227+
version="1.2.3",
2228+
canonical_version="1.2.3",
2229+
project=pretend.stub(
2230+
name="foobar", record_event=pretend.call_recorder(lambda *a, **kw: None)
2231+
),
2232+
)
2233+
request = pretend.stub(
2234+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: True)),
2235+
method="POST",
2236+
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
2237+
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
2238+
)
2239+
view = views.ManageProjectRelease(release, request)
2240+
2241+
result = view.delete_project_release()
2242+
assert isinstance(result, HTTPSeeOther)
2243+
assert result.headers["Location"] == "/the-redirect"
2244+
2245+
assert request.flags.enabled.calls == [
2246+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2247+
]
2248+
2249+
assert request.session.flash.calls == [
2250+
pretend.call(
2251+
(
2252+
"Project deletion temporarily disabled. "
2253+
"See https://pypi.org/help#admin-intervention for details."
2254+
),
2255+
queue="error",
2256+
)
2257+
]
2258+
2259+
assert request.route_path.calls == [
2260+
pretend.call(
2261+
"manage.project.release",
2262+
project_name=release.project.name,
2263+
version=release.version,
2264+
)
2265+
]
2266+
21852267
def test_delete_project_release(self, monkeypatch):
21862268
release = pretend.stub(
21872269
version="1.2.3",
@@ -2197,6 +2279,7 @@ def test_delete_project_release(self, monkeypatch):
21972279
delete=pretend.call_recorder(lambda a: None),
21982280
add=pretend.call_recorder(lambda a: None),
21992281
),
2282+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
22002283
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
22012284
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
22022285
user=pretend.stub(username=pretend.stub()),
@@ -2215,6 +2298,9 @@ def test_delete_project_release(self, monkeypatch):
22152298

22162299
assert request.db.delete.calls == [pretend.call(release)]
22172300
assert request.db.add.calls == [pretend.call(journal_obj)]
2301+
assert request.flags.enabled.calls == [
2302+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2303+
]
22182304
assert journal_cls.calls == [
22192305
pretend.call(
22202306
name=release.project.name,
@@ -2247,6 +2333,7 @@ def test_delete_project_release_no_confirm(self):
22472333
POST={"confirm_version": ""},
22482334
method="POST",
22492335
db=pretend.stub(delete=pretend.call_recorder(lambda a: None)),
2336+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
22502337
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
22512338
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
22522339
)
@@ -2261,6 +2348,9 @@ def test_delete_project_release_no_confirm(self):
22612348
assert request.session.flash.calls == [
22622349
pretend.call("Confirm the request", queue="error")
22632350
]
2351+
assert request.flags.enabled.calls == [
2352+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2353+
]
22642354
assert request.route_path.calls == [
22652355
pretend.call(
22662356
"manage.project.release",
@@ -2275,6 +2365,7 @@ def test_delete_project_release_bad_confirm(self):
22752365
POST={"confirm_version": "invalid"},
22762366
method="POST",
22772367
db=pretend.stub(delete=pretend.call_recorder(lambda a: None)),
2368+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
22782369
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
22792370
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
22802371
)
@@ -2301,6 +2392,42 @@ def test_delete_project_release_bad_confirm(self):
23012392
)
23022393
]
23032394

2395+
def test_delete_project_release_file_disallow_deletion(self):
2396+
release = pretend.stub(version="1.2.3", project=pretend.stub(name="foobar"))
2397+
request = pretend.stub(
2398+
method="POST",
2399+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: True)),
2400+
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
2401+
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
2402+
)
2403+
view = views.ManageProjectRelease(release, request)
2404+
2405+
result = view.delete_project_release_file()
2406+
2407+
assert isinstance(result, HTTPSeeOther)
2408+
assert result.headers["Location"] == "/the-redirect"
2409+
2410+
assert request.flags.enabled.calls == [
2411+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2412+
]
2413+
2414+
assert request.session.flash.calls == [
2415+
pretend.call(
2416+
(
2417+
"Project deletion temporarily disabled. "
2418+
"See https://pypi.org/help#admin-intervention for details."
2419+
),
2420+
queue="error",
2421+
)
2422+
]
2423+
assert request.route_path.calls == [
2424+
pretend.call(
2425+
"manage.project.release",
2426+
project_name=release.project.name,
2427+
version=release.version,
2428+
)
2429+
]
2430+
23042431
def test_delete_project_release_file(self, db_request):
23052432
user = UserFactory.create()
23062433

@@ -2359,6 +2486,7 @@ def test_delete_project_release_file_no_confirm(self):
23592486
POST={"confirm_project_name": ""},
23602487
method="POST",
23612488
db=pretend.stub(delete=pretend.call_recorder(lambda a: None)),
2489+
flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
23622490
route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"),
23632491
session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)),
23642492
)
@@ -2370,6 +2498,9 @@ def test_delete_project_release_file_no_confirm(self):
23702498
assert result.headers["Location"] == "/the-redirect"
23712499

23722500
assert request.db.delete.calls == []
2501+
assert request.flags.enabled.calls == [
2502+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2503+
]
23732504
assert request.session.flash.calls == [
23742505
pretend.call("Confirm the request", queue="error")
23752506
]
@@ -2396,6 +2527,7 @@ def no_result_found():
23962527
filter=lambda *a: pretend.stub(one=no_result_found)
23972528
),
23982529
)
2530+
db_request.flags = pretend.stub(enabled=pretend.call_recorder(lambda *a: False))
23992531
db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
24002532
db_request.session = pretend.stub(
24012533
flash=pretend.call_recorder(lambda *a, **kw: None)
@@ -2409,6 +2541,9 @@ def no_result_found():
24092541
assert result.headers["Location"] == "/the-redirect"
24102542

24112543
assert db_request.db.delete.calls == []
2544+
assert db_request.flags.enabled.calls == [
2545+
pretend.call(AdminFlagValue.DISALLOW_DELETION)
2546+
]
24122547
assert db_request.session.flash.calls == [
24132548
pretend.call("Could not find file", queue="error")
24142549
]

warehouse/admin/flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717

1818
class AdminFlagValue:
19+
DISALLOW_DELETION = "disallow-deletion"
1920
DISALLOW_NEW_PROJECT_REGISTRATION = "disallow-new-project-registration"
2021
DISALLOW_NEW_USER_REGISTRATION = "disallow-new-user-registration"
2122
READ_ONLY = "read-only"

warehouse/manage/views.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from warehouse.accounts.interfaces import IPasswordBreachedService, IUserService
3131
from warehouse.accounts.models import Email, User
3232
from warehouse.accounts.views import logout
33+
from warehouse.admin.flags import AdminFlagValue
3334
from warehouse.email import (
3435
send_account_deletion_email,
3536
send_added_as_collaborator_email,
@@ -783,6 +784,18 @@ def manage_project_settings(project, request):
783784
permission="manage:project",
784785
)
785786
def delete_project(project, request):
787+
if request.flags.enabled(AdminFlagValue.DISALLOW_DELETION):
788+
request.session.flash(
789+
(
790+
"Project deletion temporarily disabled. "
791+
"See https://pypi.org/help#admin-intervention for details."
792+
),
793+
queue="error",
794+
)
795+
return HTTPSeeOther(
796+
request.route_path("manage.project.settings", project_name=project.name)
797+
)
798+
786799
confirm_project(project, request, fail_route="manage.project.settings")
787800
remove_project(project, request)
788801

@@ -872,6 +885,22 @@ def manage_project_release(self):
872885

873886
@view_config(request_method="POST", request_param=["confirm_version"])
874887
def delete_project_release(self):
888+
if self.request.flags.enabled(AdminFlagValue.DISALLOW_DELETION):
889+
self.request.session.flash(
890+
(
891+
"Project deletion temporarily disabled. "
892+
"See https://pypi.org/help#admin-intervention for details."
893+
),
894+
queue="error",
895+
)
896+
return HTTPSeeOther(
897+
self.request.route_path(
898+
"manage.project.release",
899+
project_name=self.release.project.name,
900+
version=self.release.version,
901+
)
902+
)
903+
875904
version = self.request.POST.get("confirm_version")
876905
if not version:
877906
self.request.session.flash("Confirm the request", queue="error")
@@ -942,6 +971,13 @@ def _error(message):
942971
)
943972
)
944973

974+
if self.request.flags.enabled(AdminFlagValue.DISALLOW_DELETION):
975+
message = (
976+
"Project deletion temporarily disabled. "
977+
"See https://pypi.org/help#admin-intervention for details."
978+
)
979+
return _error(message)
980+
945981
project_name = self.request.POST.get("confirm_project_name")
946982

947983
if not project_name:
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
"""
13+
Add disallow-deletion AdminFlag
14+
15+
Revision ID: 8650482fb903
16+
Revises: 34b18e18775c
17+
Create Date: 2019-08-23 13:29:17.110252
18+
"""
19+
20+
from alembic import op
21+
22+
revision = "8650482fb903"
23+
down_revision = "34b18e18775c"
24+
25+
26+
def upgrade():
27+
op.execute(
28+
"""
29+
INSERT INTO admin_flags(id, description, enabled, notify)
30+
VALUES (
31+
'disallow-deletion',
32+
'Disallow ALL project and release deletions',
33+
FALSE,
34+
FALSE
35+
)
36+
"""
37+
)
38+
39+
40+
def downgrade():
41+
op.execute("DELETE FROM admin_flags WHERE id = 'disallow-deletion'")

0 commit comments

Comments
 (0)