feat(FastAPI): add /packages (post) action: 'delete'

Improvements:

- Package deletion now creates a PackageRequest on behalf of
  the deleter if one does not yet exist.
- All package deletions are now logged to keep track of who did what.

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-10-10 20:10:45 -07:00
parent 81417ea8b2
commit 60bffa4fb6
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
3 changed files with 121 additions and 1 deletions

View file

@ -9,7 +9,7 @@ from sqlalchemy import and_, case
import aurweb.filters import aurweb.filters
import aurweb.packages.util import aurweb.packages.util
from aurweb import db, defaults, l10n, models, util from aurweb import db, defaults, l10n, logging, models, util
from aurweb.auth import auth_required from aurweb.auth import auth_required
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
from aurweb.models.relation_type import CONFLICTS_ID from aurweb.models.relation_type import CONFLICTS_ID
@ -20,6 +20,7 @@ from aurweb.scripts import notify, popupdate
from aurweb.scripts.rendercomment import update_comment_render from aurweb.scripts.rendercomment import update_comment_render
from aurweb.templates import make_context, make_variable_context, render_raw_template, render_template from aurweb.templates import make_context, make_variable_context, render_raw_template, render_template
logger = logging.get_logger(__name__)
router = APIRouter() router = APIRouter()
@ -1227,6 +1228,47 @@ async def packages_disown(request: Request, package_ids: List[int] = [],
return (True, ["The selected packages have been disowned."]) return (True, ["The selected packages have been disowned."])
async def packages_delete(request: Request, package_ids: List[int] = [],
confirm: bool = False, merge_into: str = str(),
**kwargs):
if not package_ids:
return (False, ["You did not select any packages to delete."])
if not confirm:
return (False, ["The selected packages have not been deleted, "
"check the confirmation checkbox."])
if not request.user.has_credential("CRED_PKGBASE_DELETE"):
return (False, ["You do not have permission to delete packages."])
# A "memo" used to store names of packages that we delete.
# We'll use this to log out a message about the deletions that occurred.
deleted_pkgs = []
# set-ify package_ids and query the database for related records.
package_ids = set(package_ids)
packages = db.query(models.Package).filter(
models.Package.ID.in_(package_ids)).all()
if len(packages) != len(package_ids):
# Let the user know there was an issue with their input: they have
# provided at least one package_id which does not exist in the DB.
# TODO: This error has not yet been translated.
return (False, ["One of the packages you selected does not exist."])
# Now let's actually walk through and delete all of the packages,
# using the same method we use in our /pkgbase/{name}/delete route.
for pkg in packages:
deleted_pkgs.append(pkg.Name)
delete_package(request.user, pkg)
# Log out the fact that this happened for accountability.
logger.info(f"Privileged user '{request.user.Username}' deleted the "
f"following packages: {str(deleted_pkgs)}.")
return (True, ["The selected packages have been deleted."])
# A mapping of action string -> callback functions used within the # A mapping of action string -> callback functions used within the
# `packages_post` route below. We expect any action callback to # `packages_post` route below. We expect any action callback to
# return a tuple in the format: (succeeded: bool, message: List[str]). # return a tuple in the format: (succeeded: bool, message: List[str]).
@ -1236,6 +1278,7 @@ PACKAGE_ACTIONS = {
"unnotify": packages_unnotify, "unnotify": packages_unnotify,
"adopt": packages_adopt, "adopt": packages_adopt,
"disown": packages_disown, "disown": packages_disown,
"delete": packages_delete,
} }

View file

@ -1020,6 +1020,10 @@ msgstr ""
msgid "You did not select any packages to delete." msgid "You did not select any packages to delete."
msgstr "" msgstr ""
#: aurweb/routers/packages.py
msgid "One of the packages you selected does not exist."
msgstr ""
#: lib/pkgbasefuncs.inc.php #: lib/pkgbasefuncs.inc.php
msgid "The selected packages have been deleted." msgid "The selected packages have been deleted."
msgstr "" msgstr ""

View file

@ -2318,3 +2318,76 @@ def test_packages_post_disown(client: TestClient, user: User,
successes = get_successes(resp.text) successes = get_successes(resp.text)
expected = "The selected packages have been disowned." expected = "The selected packages have been disowned."
assert successes[0].text.strip() == expected assert successes[0].text.strip() == expected
def test_packages_post_delete(caplog: pytest.fixture, client: TestClient,
user: User, tu_user: User, package: Package):
# First, let's try to use the delete action with no packages IDs.
user_cookies = {"AURSID": user.login(Request(), "testPassword")}
with client as request:
resp = request.post("/packages", data={
"action": "delete"
}, cookies=user_cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = "You did not select any packages to delete."
assert errors[0].text.strip() == expected
# Now, let's try to delete real packages without supplying "confirm".
with client as request:
resp = request.post("/packages", data={
"action": "delete",
"IDs": [package.ID]
}, cookies=user_cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = ("The selected packages have not been deleted, "
"check the confirmation checkbox.")
assert errors[0].text.strip() == expected
# And again, with everything, but `user` doesn't have permissions.
with client as request:
resp = request.post("/packages", data={
"action": "delete",
"IDs": [package.ID],
"confirm": True
}, cookies=user_cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = "You do not have permission to delete packages."
assert errors[0].text.strip() == expected
# Now, let's switch over to making the requests as a TU.
# However, this next request will be rejected due to supplying
# an invalid package ID.
tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")}
with client as request:
resp = request.post("/packages", data={
"action": "delete",
"IDs": [0],
"confirm": True
}, cookies=tu_cookies)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
errors = get_errors(resp.text)
expected = "One of the packages you selected does not exist."
assert errors[0].text.strip() == expected
# Whoo. Now, let's finally make a valid request as `tu_user`
# to delete `package`.
with client as request:
resp = request.post("/packages", data={
"action": "delete",
"IDs": [package.ID],
"confirm": True
}, cookies=tu_cookies)
assert resp.status_code == int(HTTPStatus.OK)
successes = get_successes(resp.text)
expected = "The selected packages have been deleted."
assert successes[0].text.strip() == expected
# Expect that the package deletion was logged.
packages = [package.Name]
expected = (f"Privileged user '{tu_user.Username}' deleted the "
f"following packages: {str(packages)}.")
assert expected in caplog.text