Merge pull request #1084 from dod-ccpo/revoke-app-invite
Revoke app invite
This commit is contained in:
commit
7d0db1c185
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-09-25T09:43:11Z",
|
||||
"generated_at": "2019-09-26T13:53:31Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -194,7 +194,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 525,
|
||||
"line_number": 543,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -1,6 +1,7 @@
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from atst.database import db
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.models import ApplicationRole, ApplicationRoleStatus
|
||||
from .permission_sets import PermissionSets
|
||||
from .exceptions import NotFoundError
|
||||
@ -70,3 +71,24 @@ class ApplicationRoles(object):
|
||||
db.session.commit()
|
||||
|
||||
return application_role
|
||||
|
||||
@classmethod
|
||||
def _update_status(cls, application_role, new_status):
|
||||
application_role.status = new_status
|
||||
db.session.add(application_role)
|
||||
db.session.commit()
|
||||
|
||||
return application_role
|
||||
|
||||
@classmethod
|
||||
def disable(cls, application_role):
|
||||
cls._update_status(application_role, ApplicationRoleStatus.DISABLED)
|
||||
application_role.deleted = True
|
||||
|
||||
for env in application_role.application.environments:
|
||||
EnvironmentRoles.delete(
|
||||
application_role_id=application_role.id, environment_id=env.id
|
||||
)
|
||||
|
||||
db.session.add(application_role)
|
||||
db.session.commit()
|
||||
|
@ -2,7 +2,6 @@ from . import BaseDomainClass
|
||||
from flask import g
|
||||
from atst.database import db
|
||||
from atst.domain.application_roles import ApplicationRoles
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.invitations import ApplicationInvitations
|
||||
@ -119,16 +118,3 @@ class Applications(BaseDomainClass):
|
||||
db.session.commit()
|
||||
|
||||
return invitation
|
||||
|
||||
@classmethod
|
||||
def remove_member(cls, application_role):
|
||||
application_role.status = ApplicationRoleStatus.DISABLED
|
||||
application_role.deleted = True
|
||||
|
||||
for env in application_role.application.environments:
|
||||
EnvironmentRoles.delete(
|
||||
application_role_id=application_role.id, environment_id=env.id
|
||||
)
|
||||
|
||||
db.session.add(application_role)
|
||||
db.session.commit()
|
||||
|
@ -143,3 +143,10 @@ class PortfolioInvitations(BaseInvitations):
|
||||
class ApplicationInvitations(BaseInvitations):
|
||||
model = ApplicationInvitation
|
||||
role_domain_class = ApplicationRoles
|
||||
|
||||
@classmethod
|
||||
def _update_status(cls, invite, new_status):
|
||||
invite = super()._update_status(invite, new_status)
|
||||
ApplicationRoles.disable(invite.role)
|
||||
|
||||
return invite
|
||||
|
@ -8,6 +8,7 @@ from atst.domain.application_roles import ApplicationRoles
|
||||
from atst.domain.audit_log import AuditLog
|
||||
from atst.domain.common import Paginator
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.invitations import ApplicationInvitations
|
||||
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
|
||||
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
||||
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||
@ -332,7 +333,7 @@ def create_member(application_id):
|
||||
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
|
||||
def remove_member(application_id, application_role_id):
|
||||
application_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
Applications.remove_member(application_role)
|
||||
ApplicationRoles.disable(application_role)
|
||||
|
||||
flash(
|
||||
"application_member_removed",
|
||||
@ -379,3 +380,38 @@ def update_member(application_id, application_role_id):
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
"/applications/<application_id>/members/<application_role_id>/revoke_invite",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_can(
|
||||
Permissions.DELETE_APPLICATION_MEMBER, message="revoke application invitation"
|
||||
)
|
||||
def revoke_invite(application_id, application_role_id):
|
||||
app_role = ApplicationRoles.get_by_id(application_role_id)
|
||||
invite = app_role.latest_invitation
|
||||
|
||||
if invite.is_pending:
|
||||
ApplicationInvitations.revoke(invite.token)
|
||||
flash(
|
||||
"application_invite_revoked",
|
||||
user_name=app_role.user_name,
|
||||
application_name=g.application.name,
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
"application_invite_error",
|
||||
user_name=app_role.user_name,
|
||||
application_name=g.application.name,
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application_id,
|
||||
fragment="application-members",
|
||||
_anchor="application-members",
|
||||
)
|
||||
)
|
||||
|
@ -27,6 +27,16 @@ MESSAGES = {
|
||||
"message_template": "Application environments have been updated",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_error": {
|
||||
"title_template": "Application invitation error",
|
||||
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
|
||||
"category": "error",
|
||||
},
|
||||
"application_invite_revoked": {
|
||||
"title_template": "Application invitation revoked",
|
||||
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_removed": {
|
||||
"title_template": "Team member removed from application",
|
||||
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
|
||||
|
@ -22,6 +22,7 @@ from atst.domain.csp.reports import MockReportingProvider
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.exceptions import AlreadyExistsError, NotFoundError
|
||||
from atst.domain.invitations import ApplicationInvitations
|
||||
from atst.domain.permission_sets import PermissionSets, APPLICATION_PERMISSION_SETS
|
||||
from atst.domain.portfolio_roles import PortfolioRoles
|
||||
from atst.domain.portfolios import Portfolios
|
||||
@ -245,6 +246,10 @@ def add_applications_to_portfolio(portfolio):
|
||||
permission_set_names=[PermissionSets.EDIT_APPLICATION_TEAM],
|
||||
)
|
||||
|
||||
ApplicationInvitations.create(
|
||||
portfolio.owner, app_role, user_data, commit=True
|
||||
)
|
||||
|
||||
user_environments = random.sample(
|
||||
application.environments,
|
||||
k=random.randint(1, len(application.environments)),
|
||||
|
@ -274,6 +274,10 @@
|
||||
.task-order__modal-cancel_buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.clin-card {
|
||||
|
@ -138,6 +138,22 @@
|
||||
</form>
|
||||
</base-form>
|
||||
{% endcall %}
|
||||
|
||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %}
|
||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
||||
{% call Modal(name=revoke_invite_modal, dismissable=True) %}
|
||||
<div class="task-order__modal-cancel">
|
||||
<form method="post" action="{{ url_for('applications.revoke_invite', application_id=application.id, application_role_id=member.role_id) }}">
|
||||
{{ member.form.csrf_token }}
|
||||
<h1>{{ "invites.revoke.modal_heading" | translate({'user_name': member.user_name}) }}</h1>
|
||||
<div class="task-order__modal-cancel_buttons">
|
||||
<button class="usa-button usa-button-primary" type="submit">{{ "invites.revoke.submit" | translate }}</button>
|
||||
<button type='button' v-on:click='closeModal("{{revoke_invite_modal}}")' class="usa-button usa-button-primary">{{ "invites.revoke.cancel" | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<table>
|
||||
<thead>
|
||||
@ -175,9 +191,10 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
{% if member.role_status == 'pending' %}
|
||||
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) and member.role_status == 'pending' %}
|
||||
{% set revoke_invite_modal = "revoke_invite_{}".format(member.role_id) %}
|
||||
<a href="#">Resend Invite</a><br>
|
||||
<a href="#">Revoke Invite</a>
|
||||
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke.button' | translate }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from atst.domain.application_roles import ApplicationRoles
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.models import ApplicationRoleStatus
|
||||
@ -66,3 +67,21 @@ def test_get_by_id():
|
||||
|
||||
with pytest.raises(NotFoundError):
|
||||
ApplicationRoles.get_by_id(app_role.id)
|
||||
|
||||
|
||||
def test_disable(session):
|
||||
application = ApplicationFactory.create()
|
||||
user = UserFactory.create()
|
||||
member_role = ApplicationRoleFactory.create(
|
||||
application=application, user=user, status=ApplicationRoleStatus.ACTIVE
|
||||
)
|
||||
environment = EnvironmentFactory.create(application=application)
|
||||
environment_role = EnvironmentRoleFactory.create(
|
||||
application_role=member_role, environment=environment
|
||||
)
|
||||
assert member_role.status == ApplicationRoleStatus.ACTIVE
|
||||
|
||||
ApplicationRoles.disable(member_role)
|
||||
session.refresh(member_role)
|
||||
assert member_role.status == ApplicationRoleStatus.DISABLED
|
||||
assert not EnvironmentRoles.get_by_user_and_environment(user.id, environment.id)
|
||||
|
@ -134,37 +134,6 @@ def test_for_user():
|
||||
assert len(user_applications) == 2
|
||||
|
||||
|
||||
def test_remove_member():
|
||||
application = ApplicationFactory.create()
|
||||
user = UserFactory.create()
|
||||
member_role = ApplicationRoleFactory.create(application=application, user=user)
|
||||
environment = EnvironmentFactory.create(application=application)
|
||||
environment_role = EnvironmentRoleFactory.create(
|
||||
application_role=member_role, environment=environment
|
||||
)
|
||||
|
||||
assert member_role == ApplicationRoles.get(
|
||||
user_id=user.id, application_id=application.id
|
||||
)
|
||||
|
||||
Applications.remove_member(member_role)
|
||||
|
||||
assert (
|
||||
ApplicationRoles.get(user_id=user.id, application_id=application.id).status
|
||||
== ApplicationRoleStatus.DISABLED
|
||||
)
|
||||
|
||||
#
|
||||
# TODO: Why does above raise NotFoundError and this returns None
|
||||
#
|
||||
assert (
|
||||
EnvironmentRoles.get(
|
||||
application_role_id=member_role.id, environment_id=environment.id
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_invite():
|
||||
application = ApplicationFactory.create()
|
||||
env1 = EnvironmentFactory.create(application=application)
|
||||
|
@ -259,6 +259,7 @@ class ApplicationInvitationFactory(Base):
|
||||
email = factory.Faker("email")
|
||||
status = InvitationStatus.PENDING
|
||||
expiration_time = PortfolioInvitations.current_expiration_time()
|
||||
role = factory.SubFactory(ApplicationRoleFactory)
|
||||
|
||||
|
||||
class AttachmentFactory(Base):
|
||||
|
@ -14,6 +14,7 @@ from atst.domain.common import Paginator
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.exceptions import NotFoundError
|
||||
from atst.models.application_role import Status as ApplicationRoleStatus
|
||||
from atst.models.environment_role import CSPRole
|
||||
from atst.models.permissions import Permissions
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
@ -540,3 +541,21 @@ def test_update_member(client, user_session):
|
||||
# check that the user has roles in the correct envs
|
||||
assert environment_roles[0].environment in [env, env_2]
|
||||
assert environment_roles[1].environment in [env, env_2]
|
||||
|
||||
|
||||
def test_revoke_invite(client, user_session):
|
||||
invite = ApplicationInvitationFactory.create()
|
||||
app_role = invite.role
|
||||
application = app_role.application
|
||||
|
||||
user_session(application.portfolio.owner)
|
||||
response = client.post(
|
||||
url_for(
|
||||
"applications.revoke_invite",
|
||||
application_id=application.id,
|
||||
application_role_id=app_role.id,
|
||||
)
|
||||
)
|
||||
|
||||
assert invite.is_revoked
|
||||
assert app_role.status == ApplicationRoleStatus.DISABLED
|
||||
|
@ -572,6 +572,24 @@ def test_applications_update_member(post_url_assert_status):
|
||||
post_url_assert_status(rando, url, 404)
|
||||
|
||||
|
||||
# applications.revoke_invite
|
||||
def test_applications_revoke_invite(post_url_assert_status):
|
||||
ccpo = UserFactory.create_ccpo()
|
||||
rando = UserFactory.create()
|
||||
application = ApplicationFactory.create()
|
||||
|
||||
for user, status in [(ccpo, 302), (application.portfolio.owner, 302), (rando, 404)]:
|
||||
app_role = ApplicationRoleFactory.create()
|
||||
invite = ApplicationInvitationFactory.create(role=app_role)
|
||||
|
||||
url = url_for(
|
||||
"applications.revoke_invite",
|
||||
application_id=application.id,
|
||||
application_role_id=app_role.id,
|
||||
)
|
||||
post_url_assert_status(user, url, status)
|
||||
|
||||
|
||||
# task_orders.download_task_order_pdf
|
||||
def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
|
@ -257,6 +257,12 @@ fragments:
|
||||
update_btn: Update
|
||||
update_ppoc_confirmation_title: Confirmation
|
||||
update_ppoc_title: Update primary point of contact
|
||||
invites:
|
||||
revoke:
|
||||
button: Revoke Invite
|
||||
modal_heading: 'Do you want to revoke the invite for {user_name}?'
|
||||
submit: Yes, revoke it
|
||||
cancel: No, do not revoke it
|
||||
login:
|
||||
ccpo_logo_alt_text: Cloud Computing Program Office Logo
|
||||
certificate_selection:
|
||||
|
Loading…
x
Reference in New Issue
Block a user