Skip to content

Commit 30f2389

Browse files
calvinewdurbin
authored andcommitted
Add disallow deletion AdminFlag (#6518)
* Add AdminFlagValue * Add disallow deletion AdminFlag See #3218 * Add disallow new upload AdminFlag * Convert `AdminFlagValue` to enum
1 parent 7212190 commit 30f2389

File tree

12 files changed

+319
-14
lines changed

12 files changed

+319
-14
lines changed

tests/unit/accounts/test_views.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
TokenMissing,
3232
TooManyFailedLogins,
3333
)
34-
from warehouse.admin.flags import AdminFlag
34+
from warehouse.admin.flags import AdminFlag, AdminFlagValue
3535

3636
from ...common.db.accounts import EmailFactory, UserFactory
3737

@@ -903,7 +903,9 @@ def test_register_redirect(self, db_request, monkeypatch):
903903

904904
def test_register_fails_with_admin_flag_set(self, db_request):
905905
# This flag was already set via migration, just need to enable it
906-
flag = db_request.db.query(AdminFlag).get("disallow-new-user-registration")
906+
flag = db_request.db.query(AdminFlag).get(
907+
AdminFlagValue.DISALLOW_NEW_USER_REGISTRATION.value
908+
)
907909
flag.enabled = True
908910

909911
db_request.method = "POST"

tests/unit/admin/test_flags.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
import enum
14+
1315
from ...common.db.admin import AdminFlagFactory
1416

1517

18+
class TestAdminFlagValues(enum.Enum):
19+
NOT_A_REAL_FLAG = "not-a-real-flag"
20+
THIS_FLAG_IS_ENABLED = "this-flag-is-enabled"
21+
22+
1623
class TestAdminFlag:
1724
def test_default(self, db_request):
18-
assert not db_request.flags.enabled("not-a-real-flag")
25+
assert not db_request.flags.enabled(TestAdminFlagValues.NOT_A_REAL_FLAG)
1926

2027
def test_enabled(self, db_request):
2128
AdminFlagFactory(id="this-flag-is-enabled")
2229

23-
assert db_request.flags.enabled("this-flag-is-enabled")
30+
assert db_request.flags.enabled(TestAdminFlagValues.THIS_FLAG_IS_ENABLED)

tests/unit/forklift/test_legacy.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from wtforms.form import Form
3232
from wtforms.validators import ValidationError
3333

34-
from warehouse.admin.flags import AdminFlag
34+
from warehouse.admin.flags import AdminFlag, AdminFlagValue
3535
from warehouse.admin.squats import Squat
3636
from warehouse.classifiers.models import Classifier
3737
from warehouse.forklift import legacy
@@ -753,6 +753,25 @@ def test_is_duplicate_false(self, pyramid_config, db_request):
753753

754754

755755
class TestFileUpload:
756+
def test_fails_disallow_new_upload(self, pyramid_config, pyramid_request):
757+
pyramid_config.testing_securitypolicy(userid=1)
758+
pyramid_request.flags = pretend.stub(
759+
enabled=lambda value: value == AdminFlagValue.DISALLOW_NEW_UPLOAD
760+
)
761+
pyramid_request.help_url = pretend.call_recorder(lambda **kw: "/the/help/url/")
762+
pyramid_request.user = pretend.stub(primary_email=pretend.stub(verified=True))
763+
764+
with pytest.raises(HTTPForbidden) as excinfo:
765+
legacy.file_upload(pyramid_request)
766+
767+
resp = excinfo.value
768+
769+
assert resp.status_code == 403
770+
assert resp.status == (
771+
"403 New uploads are temporarily disabled. "
772+
"See /the/help/url/ for details"
773+
)
774+
756775
@pytest.mark.parametrize("version", ["2", "3", "-1", "0", "dog", "cat"])
757776
def test_fails_invalid_version(self, pyramid_config, pyramid_request, version):
758777
pyramid_config.testing_securitypolicy(userid=1)
@@ -1118,7 +1137,9 @@ def test_fails_with_stdlib_names(self, pyramid_config, db_request, name):
11181137
def test_fails_with_admin_flag_set(self, pyramid_config, db_request):
11191138
admin_flag = (
11201139
db_request.db.query(AdminFlag)
1121-
.filter(AdminFlag.id == "disallow-new-project-registration")
1140+
.filter(
1141+
AdminFlag.id == AdminFlagValue.DISALLOW_NEW_PROJECT_REGISTRATION.value
1142+
)
11221143
.first()
11231144
)
11241145
admin_flag.enabled = True

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
]

tests/unit/test_db.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from sqlalchemy.exc import OperationalError
2525

2626
from warehouse import db
27+
from warehouse.admin.flags import AdminFlagValue
2728
from warehouse.db import (
2829
DEFAULT_ISOLATION,
2930
DatabaseNotAvailable,
@@ -273,7 +274,7 @@ def test_create_session_read_only_mode(
273274
)
274275

275276
assert _create_session(request) is session_obj
276-
assert get.calls == [pretend.call("read-only")]
277+
assert get.calls == [pretend.call(AdminFlagValue.READ_ONLY.value)]
277278
assert request.tm.doom.calls == doom_calls
278279

279280

warehouse/accounts/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
TooManyFailedLogins,
4545
)
4646
from warehouse.accounts.models import Email, User
47+
from warehouse.admin.flags import AdminFlagValue
4748
from warehouse.cache.origin import origin_cache
4849
from warehouse.email import send_email_verification_email, send_password_reset_email
4950
from warehouse.packaging.models import Project, Release
@@ -377,7 +378,7 @@ def register(request, _form_class=RegistrationForm):
377378
if request.method == "POST" and request.POST.get("confirm_form"):
378379
return HTTPSeeOther(request.route_path("index"))
379380

380-
if request.flags.enabled("disallow-new-user-registration"):
381+
if request.flags.enabled(AdminFlagValue.DISALLOW_NEW_USER_REGISTRATION):
381382
request.session.flash(
382383
(
383384
"New user registration temporarily disabled. "

0 commit comments

Comments
 (0)