Skip to content

Commit 9d676cb

Browse files
authored
Bulk delete users (#11425)
1 parent 46dcff9 commit 9d676cb

File tree

5 files changed

+178
-16
lines changed

5 files changed

+178
-16
lines changed

tests/unit/admin/test_routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ def test_includeme():
5959
"/admin/users/{user_id}/reset_password/",
6060
domain=warehouse,
6161
),
62+
pretend.call(
63+
"admin.prohibited_user_names.bulk_add",
64+
"/admin/prohibited_user_names/bulk/",
65+
domain=warehouse,
66+
),
6267
pretend.call("admin.project.list", "/admin/projects/", domain=warehouse),
6368
pretend.call(
6469
"admin.project.detail",

tests/unit/admin/views/test_users.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from webob.multidict import MultiDict, NoVars
2121

2222
from warehouse.accounts.interfaces import IUserService
23-
from warehouse.accounts.models import DisableReason
23+
from warehouse.accounts.models import DisableReason, ProhibitedUserName
2424
from warehouse.admin.views import users as views
2525
from warehouse.packaging.models import JournalEntry, Project
2626

@@ -395,3 +395,60 @@ def test_resets_password_bad_confirm(self, db_request, monkeypatch):
395395
]
396396
assert result.status_code == 303
397397
assert result.location == "/foobar"
398+
399+
400+
class TestBulkAddProhibitedUserName:
401+
def test_get(self):
402+
request = pretend.stub(method="GET")
403+
404+
assert views.bulk_add_prohibited_user_names(request) == {}
405+
406+
def test_bulk_add(self, db_request):
407+
db_request.user = UserFactory.create()
408+
db_request.method = "POST"
409+
410+
already_existing_prohibition = ProhibitedUserName(
411+
name="prohibition-already-exists",
412+
prohibited_by=db_request.user,
413+
comment="comment",
414+
)
415+
db_request.db.add(already_existing_prohibition)
416+
417+
already_existing_user = UserFactory.create(username="user-already-exists")
418+
UserFactory.create(username="deleted-user")
419+
420+
user_names = [
421+
already_existing_prohibition.name,
422+
already_existing_user.username,
423+
"doesnt-already-exist",
424+
]
425+
426+
db_request.POST["users"] = "\n".join(user_names)
427+
428+
db_request.session = pretend.stub(
429+
flash=pretend.call_recorder(lambda *a, **kw: None)
430+
)
431+
db_request.route_path = lambda a: "/admin/prohibited_user_names/bulk"
432+
433+
result = views.bulk_add_prohibited_user_names(db_request)
434+
435+
assert db_request.session.flash.calls == [
436+
pretend.call(
437+
f"Prohibited {len(user_names)!r} users",
438+
queue="success",
439+
)
440+
]
441+
assert result.status_code == 303
442+
assert result.headers["Location"] == "/admin/prohibited_user_names/bulk"
443+
444+
for user_name in user_names:
445+
prohibition = (
446+
db_request.db.query(ProhibitedUserName)
447+
.filter(ProhibitedUserName.name == user_name)
448+
.one()
449+
)
450+
451+
assert prohibition.name == user_name
452+
assert prohibition.prohibited_by == db_request.user
453+
454+
assert db_request.db.query(User).filter(User.name == user_name).count() == 0

warehouse/admin/routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def includeme(config):
5454
"/admin/users/{user_id}/reset_password/",
5555
domain=warehouse,
5656
)
57+
config.add_route(
58+
"admin.prohibited_user_names.bulk_add",
59+
"/admin/prohibited_user_names/bulk/",
60+
domain=warehouse,
61+
)
5762

5863
# Project related Admin pages
5964
config.add_route("admin.project.list", "/admin/projects/", domain=warehouse)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "admin/base.html" %}
15+
16+
{% block title %}Bulk User Name Prohibition{% endblock %}
17+
18+
{% block content %}
19+
<div class="box box-primary">
20+
<div class="box-header with-border">
21+
<h3 class="box-title">Prohibit user name</h3>
22+
</div>
23+
<div class="box-body">
24+
<p>
25+
User names separated by whitespace. <b>Note: There is no confirmation step!</b>
26+
</p>
27+
</div>
28+
29+
<form method="POST" action="{{ request.route_path('admin.prohibited_user_names.bulk_add') }}">
30+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
31+
<div class="box-body">
32+
<div class="form-group">
33+
<label for="prohibitedUserName">User name(s)</label>
34+
<textarea name="users" class="form-control" id="prohibitedUserName" rows="20" placeholder="Enter user name(s) to prohibit " {{ "disabled" if not request.has_permission('admin') }} autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
35+
</div>
36+
</div>
37+
38+
<div class="box-footer">
39+
<div class="pull-right">
40+
<button type="submit" class="btn btn-primary" title="{{ "Submitting requires superuser privileges" if not request.has_permission('admin') }}" {{ "disabled" if not request.has_permission('admin') }}>Submit</button>
41+
</div>
42+
</div>
43+
</form>
44+
</div>
45+
{% endblock %}

warehouse/admin/views/users.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage
2020
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther
2121
from pyramid.view import view_config
22-
from sqlalchemy import or_
22+
from sqlalchemy import literal, or_
2323
from sqlalchemy.orm import joinedload
2424
from sqlalchemy.orm.exc import NoResultFound
2525

@@ -180,20 +180,7 @@ def user_add_email(request):
180180
return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
181181

182182

183-
@view_config(
184-
route_name="admin.user.delete",
185-
require_methods=["POST"],
186-
permission="admin",
187-
uses_session=True,
188-
require_csrf=True,
189-
)
190-
def user_delete(request):
191-
user = request.db.query(User).get(request.matchdict["user_id"])
192-
193-
if user.username != request.params.get("username"):
194-
request.session.flash("Wrong confirmation input", queue="error")
195-
return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
196-
183+
def _nuke_user(user, request):
197184
# Delete all the user's projects
198185
projects = request.db.query(Project).filter(
199186
Project.name.in_(
@@ -244,6 +231,24 @@ def user_delete(request):
244231
submitted_from=request.remote_addr,
245232
)
246233
)
234+
235+
236+
@view_config(
237+
route_name="admin.user.delete",
238+
require_methods=["POST"],
239+
permission="admin",
240+
uses_session=True,
241+
require_csrf=True,
242+
)
243+
def user_delete(request):
244+
user = request.db.query(User).get(request.matchdict["user_id"])
245+
246+
if user.username != request.params.get("username"):
247+
request.session.flash("Wrong confirmation input", queue="error")
248+
return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
249+
250+
_nuke_user(user, request)
251+
247252
request.session.flash(f"Nuked user {user.username!r}", queue="success")
248253
return HTTPSeeOther(request.route_path("admin.user.list"))
249254

@@ -269,3 +274,48 @@ def user_reset_password(request):
269274

270275
request.session.flash(f"Reset password for {user.username!r}", queue="success")
271276
return HTTPSeeOther(request.route_path("admin.user.detail", user_id=user.id))
277+
278+
279+
@view_config(
280+
route_name="admin.prohibited_user_names.bulk_add",
281+
renderer="admin/prohibited_user_names/bulk.html",
282+
permission="admin",
283+
uses_session=True,
284+
require_methods=False,
285+
)
286+
def bulk_add_prohibited_user_names(request):
287+
if request.method == "POST":
288+
user_names = request.POST.get("users", "").split()
289+
290+
for user_name in user_names:
291+
292+
# Check to make sure the prohibition doesn't already exist.
293+
if (
294+
request.db.query(literal(True))
295+
.filter(
296+
request.db.query(ProhibitedUserName)
297+
.filter(ProhibitedUserName.name == user_name.lower())
298+
.exists()
299+
)
300+
.scalar()
301+
):
302+
continue
303+
304+
# Go through and delete the usernames
305+
306+
user = request.db.query(User).filter(User.username == user_name).first()
307+
if user is not None:
308+
_nuke_user(user, request)
309+
else:
310+
request.db.add(
311+
ProhibitedUserName(
312+
name=user_name.lower(),
313+
comment="nuked",
314+
prohibited_by=request.user,
315+
)
316+
)
317+
318+
request.session.flash(f"Prohibited {len(user_names)!r} users", queue="success")
319+
320+
return HTTPSeeOther(request.route_path("admin.prohibited_user_names.bulk_add"))
321+
return {}

0 commit comments

Comments
 (0)