From 60bffa4fb6b2d4374548c325df4018d83461d74e Mon Sep 17 00:00:00 2001 From: Kevin Morris Date: Sun, 10 Oct 2021 20:10:45 -0700 Subject: [PATCH] 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 --- aurweb/routers/packages.py | 45 +++++++++++++++++++++- po/aurweb.pot | 4 ++ test/test_packages_routes.py | 73 ++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index 2de82aaa..59b691ba 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -9,7 +9,7 @@ from sqlalchemy import and_, case import aurweb.filters 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.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_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.templates import make_context, make_variable_context, render_raw_template, render_template +logger = logging.get_logger(__name__) router = APIRouter() @@ -1227,6 +1228,47 @@ async def packages_disown(request: Request, package_ids: List[int] = [], 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 # `packages_post` route below. We expect any action callback to # return a tuple in the format: (succeeded: bool, message: List[str]). @@ -1236,6 +1278,7 @@ PACKAGE_ACTIONS = { "unnotify": packages_unnotify, "adopt": packages_adopt, "disown": packages_disown, + "delete": packages_delete, } diff --git a/po/aurweb.pot b/po/aurweb.pot index 849cae75..0e30b366 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -1020,6 +1020,10 @@ msgstr "" msgid "You did not select any packages to delete." msgstr "" +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "" + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "" diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index df028430..f7f30330 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -2318,3 +2318,76 @@ def test_packages_post_disown(client: TestClient, user: User, successes = get_successes(resp.text) expected = "The selected packages have been disowned." 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