diff --git a/atst/domain/permission_sets.py b/atst/domain/permission_sets.py index 4d02682a..52131a20 100644 --- a/atst/domain/permission_sets.py +++ b/atst/domain/permission_sets.py @@ -86,6 +86,7 @@ _PORTFOLIO_APP_MGMT_PERMISSION_SETS = [ "permissions": [ Permissions.EDIT_APPLICATION, Permissions.CREATE_APPLICATION, + Permissions.DELETE_APPLICATION, Permissions.EDIT_APPLICATION_MEMBER, Permissions.CREATE_APPLICATION_MEMBER, Permissions.EDIT_ENVIRONMENT, diff --git a/atst/models/permissions.py b/atst/models/permissions.py index cb03020d..98f25b36 100644 --- a/atst/models/permissions.py +++ b/atst/models/permissions.py @@ -8,6 +8,7 @@ class Permissions(object): VIEW_APPLICATION = "view_application" EDIT_APPLICATION = "edit_application" CREATE_APPLICATION = "create_application" + DELETE_APPLICATION = "delete_application" VIEW_APPLICATION_MEMBER = "view_application_member" EDIT_APPLICATION_MEMBER = "edit_application_member" CREATE_APPLICATION_MEMBER = "create_application_member" diff --git a/atst/routes/portfolios/applications.py b/atst/routes/portfolios/applications.py index 20603b3c..955683dd 100644 --- a/atst/routes/portfolios/applications.py +++ b/atst/routes/portfolios/applications.py @@ -15,6 +15,7 @@ from atst.domain.portfolios import Portfolios from atst.forms.application import NewApplicationForm, ApplicationForm from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.permissions import Permissions +from atst.utils.flash import formatted_flash as flash @portfolios_bp.route("/portfolios//applications") @@ -118,3 +119,18 @@ def access_environment(portfolio_id, environment_id): token = app.csp.cloud.get_access_token(env_role) return redirect(url_for("atst.csp_environment_access", token=token)) + + +@portfolios_bp.route( + "/portfolios//applications//delete", methods=["POST"] +) +@user_can(Permissions.DELETE_APPLICATION, message="delete application") +def delete_application(portfolio_id, application_id): + application = Applications.get(application_id) + Applications.delete(application) + + flash("application_deleted", application_name=application.name) + + return redirect( + url_for("portfolios.portfolio_applications", portfolio_id=portfolio_id) + ) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index d2b42979..dd32f5cc 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -148,6 +148,15 @@ MESSAGES = { """, "category": "success", }, + "application_deleted": { + "title_template": "Success!", + "message_template": """ + You have successfully deleted the {{ application_name }} application. + To view the retained activity log, visit the portfolio administration + page. Undo this action. + """, + "category": "success", + }, } diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index ab958f8a..8df582e3 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -1,4 +1,4 @@ -from flask import url_for +from flask import url_for, get_flashed_messages from tests.factories import ( UserFactory, @@ -290,3 +290,42 @@ def test_environment_access_with_no_role(client, user_session): ) ) assert response.status_code == 404 + + +def test_delete_application(client, user_session): + user = UserFactory.create() + port = PortfolioFactory.create( + owner=user, + applications=[ + { + "name": "mos eisley", + "environments": [ + {"name": "bar"}, + {"name": "booth"}, + {"name": "band stage"}, + ], + } + ], + ) + application = port.applications[0] + user_session(user) + + response = client.post( + url_for( + "portfolios.delete_application", + portfolio_id=port.id, + application_id=application.id, + ) + ) + # appropriate response and redirect + assert response.status_code == 302 + assert response.location == url_for( + "portfolios.portfolio_applications", portfolio_id=port.id, _external=True + ) + # appropriate flash message + message = get_flashed_messages()[0] + assert "deleted" in message["message"] + assert application.name in message["message"] + # app and envs are soft deleted + assert len(port.applications) == 0 + assert len(application.environments) == 0 diff --git a/tests/test_access.py b/tests/test_access.py index c79fd64d..0208887b 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -203,6 +203,44 @@ def test_portfolios_create_member_access(post_url_assert_status): post_url_assert_status(rando, url, 404) +# portfolios.delete_application +def test_portfolios_delete_application_access(post_url_assert_status, monkeypatch): + ccpo = UserFactory.create_ccpo() + owner = user_with() + app_admin = user_with() + rando = user_with() + + portfolio = PortfolioFactory.create( + owner=owner, applications=[{"name": "mos eisley"}] + ) + application = portfolio.applications[0] + + ApplicationRoleFactory.create( + user=app_admin, + application=application, + permission_sets=PermissionSets.get_many( + [ + PermissionSets.VIEW_APPLICATION, + PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + PermissionSets.EDIT_APPLICATION_TEAM, + PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + ] + ), + ) + + monkeypatch.setattr("atst.domain.applications.Applications.delete", lambda *a: True) + + url = url_for( + "portfolios.delete_application", + portfolio_id=portfolio.id, + application_id=application.id, + ) + post_url_assert_status(app_admin, url, 404) + post_url_assert_status(rando, url, 404) + post_url_assert_status(owner, url, 302) + post_url_assert_status(ccpo, url, 302) + + # portfolios.edit_application def test_portfolios_edit_application_access(get_url_assert_status): ccpo = user_with(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)