Merge pull request #1084 from dod-ccpo/revoke-app-invite

Revoke app invite
This commit is contained in:
leigh-mil 2019-09-26 13:16:47 -04:00 committed by GitHub
commit 7d0db1c185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 169 additions and 50 deletions

View File

@ -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"
}
]

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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",
)
)

View File

@ -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 }}",

View File

@ -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)),

View File

@ -274,6 +274,10 @@
.task-order__modal-cancel_buttons {
display: flex;
justify-content: center;
button {
margin-top: 0;
}
}
.clin-card {

View File

@ -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>

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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(

View File

@ -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: