diff --git a/atst/domain/authz/decorator.py b/atst/domain/authz/decorator.py index 17755749..29d12feb 100644 --- a/atst/domain/authz/decorator.py +++ b/atst/domain/authz/decorator.py @@ -5,21 +5,31 @@ from flask import g, current_app as app, request from . import user_can_access from atst.domain.portfolios import Portfolios from atst.domain.task_orders import TaskOrders +from atst.domain.applications import Applications +from atst.domain.invitations import Invitations from atst.domain.exceptions import UnauthorizedError def check_access(permission, message, exception, *args, **kwargs): access_args = {"message": message} - if "portfolio_id" in kwargs: + if "application_id" in kwargs: + application = Applications.get(kwargs["application_id"]) + access_args["portfolio"] = application.portfolio + + elif "task_order_id" in kwargs: + task_order = TaskOrders.get(kwargs["task_order_id"]) + access_args["portfolio"] = task_order.portfolio + + elif "token" in kwargs: + invite = Invitations._get(kwargs["token"]) + access_args["portfolio"] = invite.portfolio_role.portfolio + + elif "portfolio_id" in kwargs: access_args["portfolio"] = Portfolios.get( g.current_user, kwargs["portfolio_id"] ) - if "task_order_id" in kwargs: - task_order = TaskOrders.get(kwargs["task_order_id"]) - access_args["portfolio"] = task_order.portfolio - if exception is not None and exception(g.current_user, **access_args, **kwargs): return True diff --git a/tests/routes/portfolios/test_applications.py b/tests/routes/portfolios/test_applications.py index b6245aaa..4f3bac58 100644 --- a/tests/routes/portfolios/test_applications.py +++ b/tests/routes/portfolios/test_applications.py @@ -157,6 +157,54 @@ def test_user_without_permission_cannot_update_application(client, user_session) assert application.description == "Cool stuff happening here!" +def test_user_can_only_access_apps_in_their_portfolio(client, user_session): + portfolio = PortfolioFactory.create() + other_portfolio = PortfolioFactory.create( + applications=[ + { + "name": "Awesome Application", + "description": "More cool stuff happening here!", + "environments": [{"name": "dev"}], + } + ] + ) + other_application = other_portfolio.applications[0] + user_session(portfolio.owner) + + # user can't view application edit form + response = client.get( + url_for( + "portfolios.edit_application", + portfolio_id=portfolio.id, + application_id=other_application.id, + ) + ) + assert response.status_code == 404 + + # user can't post update application form + time_updated = other_application.time_updated + response = client.post( + url_for( + "portfolios.update_application", + portfolio_id=portfolio.id, + application_id=other_application.id, + ), + data={"name": "New Name", "description": "A new description."}, + ) + assert response.status_code == 404 + assert time_updated == other_application.time_updated + + # user can't view application members + response = client.get( + url_for( + "portfolios.application_members", + portfolio_id=portfolio.id, + application_id=other_application.id, + ) + ) + assert response.status_code == 404 + + def create_environment(user): portfolio = PortfolioFactory.create() portfolio_role = PortfolioRoleFactory.create(portfolio=portfolio, user=user) diff --git a/tests/routes/portfolios/test_invitations.py b/tests/routes/portfolios/test_invitations.py index a4352808..4baa895c 100644 --- a/tests/routes/portfolios/test_invitations.py +++ b/tests/routes/portfolios/test_invitations.py @@ -169,6 +169,58 @@ def test_revoke_invitation(client, user_session): assert invite.is_revoked +def test_user_can_only_revoke_invites_in_their_portfolio(client, user_session): + portfolio = PortfolioFactory.create() + other_portfolio = PortfolioFactory.create() + user = UserFactory.create() + portfolio_role = PortfolioRoleFactory.create( + user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING + ) + invite = InvitationFactory.create( + user_id=user.id, + portfolio_role=portfolio_role, + status=InvitationStatus.REJECTED_EXPIRED, + expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + ) + user_session(portfolio.owner) + response = client.post( + url_for( + "portfolios.revoke_invitation", + portfolio_id=portfolio.id, + token=invite.token, + ) + ) + + assert response.status_code == 404 + assert not invite.is_revoked + + +def test_user_can_only_resend_invites_in_their_portfolio(client, user_session, queue): + portfolio = PortfolioFactory.create() + other_portfolio = PortfolioFactory.create() + user = UserFactory.create() + portfolio_role = PortfolioRoleFactory.create( + user=user, portfolio=other_portfolio, status=PortfolioRoleStatus.PENDING + ) + invite = InvitationFactory.create( + user_id=user.id, + portfolio_role=portfolio_role, + status=InvitationStatus.REJECTED_EXPIRED, + expiration_time=datetime.datetime.now() - datetime.timedelta(seconds=1), + ) + user_session(portfolio.owner) + response = client.post( + url_for( + "portfolios.resend_invitation", + portfolio_id=portfolio.id, + token=invite.token, + ) + ) + + assert response.status_code == 404 + assert len(queue.get_queue()) == 0 + + def test_resend_invitation_sends_email(client, user_session, queue): user = UserFactory.create() portfolio = PortfolioFactory.create() diff --git a/tests/routes/portfolios/test_task_orders.py b/tests/routes/portfolios/test_task_orders.py index 3948eaec..48aaefd0 100644 --- a/tests/routes/portfolios/test_task_orders.py +++ b/tests/routes/portfolios/test_task_orders.py @@ -128,6 +128,20 @@ class TestPortfolioFunding: assert context["funding_end_date"] is expiring_to.end_date assert context["funded"] == False + def test_user_can_only_access_to_in_their_portfolio( + self, app, user_session, portfolio + ): + other_task_order = TaskOrderFactory.create() + user_session(portfolio.owner) + response = app.test_client().get( + url_for( + "portfolios.view_task_order", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ) + ) + assert response.status_code == 404 + class TestTaskOrderInvitations: def setup(self): @@ -227,6 +241,54 @@ class TestTaskOrderInvitations: assert len(queue.get_queue()) == queue_length assert response.status_code == 400 + def test_user_can_only_invite_to_task_order_in_their_portfolio( + self, user_session, client, portfolio + ): + other_task_order = TaskOrderFactory.create() + user_session(portfolio.owner) + + # user can't see invites + response = client.get( + url_for( + "portfolios.task_order_invitations", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ) + ) + assert response.status_code == 404 + + # user can't send invites + time_updated = other_task_order.time_updated + response = client.post( + url_for( + "portfolios.edit_task_order_invitations", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ), + data={ + "contracting_officer-first_name": "Luke", + "contracting_officer-last_name": "Skywalker", + "contracting_officer-dod_id": "0123456789", + "contracting_officer-email": "luke@skywalker.mil", + "contracting_officer-phone_number": "0123456789", + "contracting_officer-invite": "y", + }, + ) + assert response.status_code == 404 + assert time_updated == other_task_order.time_updated + + # user can't resend invites + response = client.post( + url_for( + "portfolios.resend_invite", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + invite_type="ko_invite", + ) + ) + assert response.status_code == 404 + assert time_updated == other_task_order.time_updated + def test_ko_can_view_task_order(client, user_session, portfolio, user): PortfolioRoleFactory.create( @@ -464,6 +526,57 @@ def test_submit_completed_ko_review_page_as_ko( assert task_order.loas == loa_list +def test_ko_can_only_access_their_to(app, user_session, client, portfolio, pdf_upload): + ko = UserFactory.create() + + PortfolioRoleFactory.create( + portfolio=portfolio, + user=ko, + status=PortfolioStatus.ACTIVE, + permission_sets=[ + PermissionSets.get(PermissionSets.VIEW_PORTFOLIO), + PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING), + ], + ) + + task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko) + dd_254 = DD254Factory.create() + TaskOrders.add_dd_254(task_order, dd_254.to_dictionary()) + other_task_order = TaskOrderFactory.create() + user_session(ko) + + # KO can't see TO + response = client.get( + url_for( + "portfolios.ko_review", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ) + ) + assert response.status_code == 404 + + # KO can't submit review for TO + form_data = { + "start_date": "02/10/2019", + "end_date": "03/10/2019", + "number": "1938745981", + "loas-0": "1231231231", + "custom_clauses": "hi im a custom clause", + "pdf": pdf_upload, + } + + response = client.post( + url_for( + "portfolios.submit_ko_review", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ), + data=form_data, + ) + assert response.status_code == 404 + assert not TaskOrders.is_signed_by_ko(other_task_order) + + def test_so_review_page(app, client, user_session, portfolio): so = UserFactory.create() PortfolioRoleFactory.create( @@ -541,6 +654,45 @@ def test_submit_so_review(app, client, user_session, portfolio): assert task_order.dd_254.certifying_official == dd_254_data["certifying_official"] +def test_so_can_only_access_their_to(app, client, user_session, portfolio): + so = UserFactory.create() + PortfolioRoleFactory.create( + portfolio=portfolio, + user=so, + status=PortfolioStatus.ACTIVE, + permission_sets=[ + PermissionSets.get(PermissionSets.VIEW_PORTFOLIO), + PermissionSets.get(PermissionSets.VIEW_PORTFOLIO_FUNDING), + ], + ) + task_order = TaskOrderFactory.create(portfolio=portfolio, security_officer=so) + dd_254_data = DD254Factory.dictionary() + other_task_order = TaskOrderFactory.create() + user_session(so) + + # SO can't view dd254 + response = client.get( + url_for( + "portfolios.so_review", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ) + ) + assert response.status_code == 404 + + # SO can't submit dd254 + response = client.post( + url_for( + "portfolios.submit_so_review", + portfolio_id=portfolio.id, + task_order_id=other_task_order.id, + ), + data=dd_254_data, + ) + assert response.status_code == 404 + assert not other_task_order.dd_254 + + def test_resend_invite_when_invalid_invite_officer( app, client, user_session, portfolio, user ):