Merge pull request #1084 from dod-ccpo/revoke-app-invite
Revoke app invite
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"files": "^.secrets.baseline$",
|
"files": "^.secrets.baseline$",
|
||||||
"lines": null
|
"lines": null
|
||||||
},
|
},
|
||||||
"generated_at": "2019-09-25T09:43:11Z",
|
"generated_at": "2019-09-26T13:53:31Z",
|
||||||
"plugins_used": [
|
"plugins_used": [
|
||||||
{
|
{
|
||||||
"base64_limit": 4.5,
|
"base64_limit": 4.5,
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 525,
|
"line_number": 543,
|
||||||
"type": "Hex High Entropy String"
|
"type": "Hex High Entropy String"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.models import ApplicationRole, ApplicationRoleStatus
|
from atst.models import ApplicationRole, ApplicationRoleStatus
|
||||||
from .permission_sets import PermissionSets
|
from .permission_sets import PermissionSets
|
||||||
from .exceptions import NotFoundError
|
from .exceptions import NotFoundError
|
||||||
@@ -70,3 +71,24 @@ class ApplicationRoles(object):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return application_role
|
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 flask import g
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
|
||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.invitations import ApplicationInvitations
|
from atst.domain.invitations import ApplicationInvitations
|
||||||
@@ -119,16 +118,3 @@ class Applications(BaseDomainClass):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return invitation
|
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):
|
class ApplicationInvitations(BaseInvitations):
|
||||||
model = ApplicationInvitation
|
model = ApplicationInvitation
|
||||||
role_domain_class = ApplicationRoles
|
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.audit_log import AuditLog
|
||||||
from atst.domain.common import Paginator
|
from atst.domain.common import Paginator
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
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_member import NewForm as NewMemberForm, UpdateMemberForm
|
||||||
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
||||||
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
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")
|
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
|
||||||
def remove_member(application_id, application_role_id):
|
def remove_member(application_id, application_role_id):
|
||||||
application_role = ApplicationRoles.get_by_id(application_role_id)
|
application_role = ApplicationRoles.get_by_id(application_role_id)
|
||||||
Applications.remove_member(application_role)
|
ApplicationRoles.disable(application_role)
|
||||||
|
|
||||||
flash(
|
flash(
|
||||||
"application_member_removed",
|
"application_member_removed",
|
||||||
@@ -379,3 +380,38 @@ def update_member(application_id, application_role_id):
|
|||||||
_anchor="application-members",
|
_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",
|
"message_template": "Application environments have been updated",
|
||||||
"category": "success",
|
"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": {
|
"application_member_removed": {
|
||||||
"title_template": "Team member removed from application",
|
"title_template": "Team member removed from application",
|
||||||
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
|
"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.environments import Environments
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.exceptions import AlreadyExistsError, NotFoundError
|
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.permission_sets import PermissionSets, APPLICATION_PERMISSION_SETS
|
||||||
from atst.domain.portfolio_roles import PortfolioRoles
|
from atst.domain.portfolio_roles import PortfolioRoles
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
@@ -245,6 +246,10 @@ def add_applications_to_portfolio(portfolio):
|
|||||||
permission_set_names=[PermissionSets.EDIT_APPLICATION_TEAM],
|
permission_set_names=[PermissionSets.EDIT_APPLICATION_TEAM],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ApplicationInvitations.create(
|
||||||
|
portfolio.owner, app_role, user_data, commit=True
|
||||||
|
)
|
||||||
|
|
||||||
user_environments = random.sample(
|
user_environments = random.sample(
|
||||||
application.environments,
|
application.environments,
|
||||||
k=random.randint(1, len(application.environments)),
|
k=random.randint(1, len(application.environments)),
|
||||||
|
@@ -274,6 +274,10 @@
|
|||||||
.task-order__modal-cancel_buttons {
|
.task-order__modal-cancel_buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.clin-card {
|
.clin-card {
|
||||||
|
@@ -138,6 +138,22 @@
|
|||||||
</form>
|
</form>
|
||||||
</base-form>
|
</base-form>
|
||||||
{% endcall %}
|
{% 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 %}
|
{% endfor %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -175,9 +191,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<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="#">Resend Invite</a><br>
|
||||||
<a href="#">Revoke Invite</a>
|
<a v-on:click='openModal("{{ revoke_invite_modal }}")'>{{ 'invites.revoke.button' | translate }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from atst.domain.application_roles import ApplicationRoles
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.models import ApplicationRoleStatus
|
from atst.models import ApplicationRoleStatus
|
||||||
@@ -66,3 +67,21 @@ def test_get_by_id():
|
|||||||
|
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
ApplicationRoles.get_by_id(app_role.id)
|
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
|
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():
|
def test_invite():
|
||||||
application = ApplicationFactory.create()
|
application = ApplicationFactory.create()
|
||||||
env1 = EnvironmentFactory.create(application=application)
|
env1 = EnvironmentFactory.create(application=application)
|
||||||
|
@@ -259,6 +259,7 @@ class ApplicationInvitationFactory(Base):
|
|||||||
email = factory.Faker("email")
|
email = factory.Faker("email")
|
||||||
status = InvitationStatus.PENDING
|
status = InvitationStatus.PENDING
|
||||||
expiration_time = PortfolioInvitations.current_expiration_time()
|
expiration_time = PortfolioInvitations.current_expiration_time()
|
||||||
|
role = factory.SubFactory(ApplicationRoleFactory)
|
||||||
|
|
||||||
|
|
||||||
class AttachmentFactory(Base):
|
class AttachmentFactory(Base):
|
||||||
|
@@ -14,6 +14,7 @@ from atst.domain.common import Paginator
|
|||||||
from atst.domain.permission_sets import PermissionSets
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.domain.portfolios import Portfolios
|
from atst.domain.portfolios import Portfolios
|
||||||
from atst.domain.exceptions import NotFoundError
|
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.environment_role import CSPRole
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
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
|
# check that the user has roles in the correct envs
|
||||||
assert environment_roles[0].environment in [env, env_2]
|
assert environment_roles[0].environment in [env, env_2]
|
||||||
assert environment_roles[1].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)
|
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
|
# task_orders.download_task_order_pdf
|
||||||
def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monkeypatch):
|
def test_task_orders_download_task_order_pdf_access(get_url_assert_status, monkeypatch):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
@@ -257,6 +257,12 @@ fragments:
|
|||||||
update_btn: Update
|
update_btn: Update
|
||||||
update_ppoc_confirmation_title: Confirmation
|
update_ppoc_confirmation_title: Confirmation
|
||||||
update_ppoc_title: Update primary point of contact
|
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:
|
login:
|
||||||
ccpo_logo_alt_text: Cloud Computing Program Office Logo
|
ccpo_logo_alt_text: Cloud Computing Program Office Logo
|
||||||
certificate_selection:
|
certificate_selection:
|
||||||
|
Reference in New Issue
Block a user