Merge pull request #817 from dod-ccpo/delete-user-from-application

Delete an application member
This commit is contained in:
George Drummond 2019-05-14 15:45:32 -04:00 committed by GitHub
commit b8ea1349b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 171 additions and 12 deletions

View File

@ -3,6 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from . import BaseDomainClass
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.users import Users
@ -100,3 +101,16 @@ class Applications(BaseDomainClass):
Environments.add_member(environment, user, env_role_data.get("role"))
return application_role
@classmethod
def remove_member(cls, application, user_id):
application_role = ApplicationRoles.get(
user_id=user_id, application_id=application.id
)
application_role.status = ApplicationRoleStatus.DISABLED
db.session.add(application_role)
db.session.commit()
for env in application.environments:
EnvironmentRoles.delete(user_id=user_id, environment_id=env.id)

View File

@ -88,6 +88,7 @@ _PORTFOLIO_APP_MGMT_PERMISSION_SETS = [
Permissions.CREATE_APPLICATION,
Permissions.DELETE_APPLICATION,
Permissions.EDIT_APPLICATION_MEMBER,
Permissions.DELETE_APPLICATION_MEMBER,
Permissions.CREATE_APPLICATION_MEMBER,
Permissions.EDIT_ENVIRONMENT,
Permissions.CREATE_ENVIRONMENT,
@ -205,6 +206,7 @@ _APPLICATION_TEAM_PERMISSION_SET = {
"display_name": "Manage team",
"permissions": [
Permissions.EDIT_APPLICATION_MEMBER,
Permissions.DELETE_APPLICATION_MEMBER,
Permissions.CREATE_APPLICATION_MEMBER,
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
],

View File

@ -11,6 +11,7 @@ class Permissions(object):
DELETE_APPLICATION = "delete_application"
VIEW_APPLICATION_MEMBER = "view_application_member"
EDIT_APPLICATION_MEMBER = "edit_application_member"
DELETE_APPLICATION_MEMBER = "delete_application_member"
CREATE_APPLICATION_MEMBER = "create_application_member"
VIEW_ENVIRONMENT = "view_environment"
EDIT_ENVIRONMENT = "edit_environment"

View File

@ -8,6 +8,7 @@ from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.permission_sets import PermissionSets
from atst.domain.users import Users
from atst.forms.application_member import NewForm as NewMemberForm
from atst.forms.team import TeamForm
from atst.models import Permissions
@ -158,3 +159,27 @@ def create_member(application_id):
_anchor="application-members",
)
)
@applications_bp.route(
"/applications/<application_id>/members/<user_id>/delete", methods=["POST"]
)
@user_can(Permissions.DELETE_APPLICATION_MEMBER, message="remove application member")
def remove_member(application_id, user_id):
Applications.remove_member(application=g.application, user_id=user_id)
user = Users.get(user_id)
flash(
"application_member_removed",
user_name=user.full_name,
application_name=g.application.name,
)
return redirect(
url_for(
"applications.team",
_anchor="application-members",
application_id=g.application.id,
fragment="application-members",
)
)

View File

@ -2,6 +2,11 @@ from flask import flash, render_template_string
from atst.utils.localization import translate
MESSAGES = {
"application_member_removed": {
"title_template": "Team member removed from application",
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
"category": "success",
},
"environment_deleted": {
"title_template": "{{ environment_name }} deleted",
"message_template": 'The environment "{{ environment_name }}" has been deleted',

View File

@ -1,12 +1,12 @@
import ally from 'ally.js'
import checkboxinput from '../checkbox_input'
import DateSelector from '../date_selector'
import FormMixin from '../../mixins/form'
import levelofwarrant from '../levelofwarrant'
import Modal from '../../mixins/modal'
import multicheckboxinput from '../multi_checkbox_input'
import MultiStepModalForm from './multi_step_modal_form'
import checkboxinput from '../checkbox_input'
import levelofwarrant from '../levelofwarrant'
import multicheckboxinput from '../multi_checkbox_input'
import optionsinput from '../options_input'
import textinput from '../text_input'
import toggler from '../toggler'
@ -14,12 +14,12 @@ import toggler from '../toggler'
export default {
name: 'base-form',
components: {
checkboxinput,
DateSelector,
levelofwarrant,
Modal,
multicheckboxinput,
MultiStepModalForm,
checkboxinput,
levelofwarrant,
multicheckboxinput,
optionsinput,
textinput,
toggler,

View File

@ -3,6 +3,7 @@
{{ team_form.csrf_token }}
{% for member_form in team_form.members %}
{% set delete_modal_id = "delete-user-{}".format(member_form.id) %}
{% set environment_roles_form = member_form.environment_roles %}
{% set permissions_form = member_form.permission_sets %}
@ -38,6 +39,13 @@
{{ environment_form.environment_name.data }}
</li>
{% endfor %}
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
<li class="accordion-table__item__expanded action-group">
<span class="usa-button button-danger" v-on:click="openModal('{{ delete_modal_id }}')">
{{ "portfolios.members.archive_button" | translate }}
</span>
</li>
{% endif %}
</ul>
{% endcall %}
{{ member_form.user_id() }}

View File

@ -81,8 +81,8 @@
{{
Alert(
title="portfolios.applications.delete.alert.title" | translate,
message="portfolios.applications.delete.alert.message" | translate,
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.delete.alert.message" | translate),
level="warning"
)
}}

View File

@ -6,6 +6,9 @@
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{% from "components/alert.html" import Alert %}
{% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/modal.html" import Modal %}
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
@ -109,6 +112,35 @@
</div>
</form>
</base-form>
{% if user_can(permissions.DELETE_APPLICATION_MEMBER) %}
{% for member_form in team_form.members %}
{% set delete_modal_id = "delete-user-{}".format(member_form.id) %}
{% call Modal(name=delete_modal_id, dismissable=True) %}
<h1>
{{ "portfolios.applications.remove_member.header" | translate }}
</h1>
{{
Alert(
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.remove_member.alert.message" | translate({"user_name": member_form.user_name.data})),
level="warning"
)
}}
{{
DeleteConfirmation(
modal_id=delete_modal_id,
delete_text=('portfolios.applications.remove_member.button' | translate),
delete_action=url_for('applications.remove_member', application_id=application.id, user_id=member_form.data.user_id),
form=member_form
)
}}
{% endcall %}
{% endfor %}
{% endif %}
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
{{ MultiStepModalForm(

View File

@ -2,16 +2,19 @@ import pytest
from uuid import uuid4
from atst.models import CSPRole, ApplicationRoleStatus
from atst.domain.application_roles import ApplicationRoles
from atst.domain.applications import Applications
from atst.domain.permission_sets import PermissionSets
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import NotFoundError
from atst.domain.permission_sets import PermissionSets
from tests.factories import (
ApplicationFactory,
ApplicationRoleFactory,
UserFactory,
PortfolioFactory,
EnvironmentFactory,
EnvironmentRoleFactory,
PortfolioFactory,
UserFactory,
)
@ -155,3 +158,27 @@ def test_for_user():
assert len(portfolio.applications) == 4
user_applications = Applications.for_user(user, portfolio)
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(user=user, environment=environment)
assert member_role == ApplicationRoles.get(
user_id=user.id, application_id=application.id
)
Applications.remove_member(application=application, user_id=member_role.user.id)
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(user_id=user.id, environment_id=environment.id) == None

View File

@ -1,4 +1,5 @@
import pytest
import uuid
from flask import url_for
from atst.domain.permission_sets import PermissionSets
@ -128,3 +129,43 @@ def test_create_member(client, user_session):
assert user.application_roles[0].application == application
assert len(user.environment_roles) == 1
assert user.environment_roles[0].environment == env
def test_remove_member_success(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
application_role = ApplicationRoleFactory.create(application=application, user=user)
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member", application_id=application.id, user_id=user.id
)
)
assert response.status_code == 302
assert response.location == url_for(
"applications.team",
_anchor="application-members",
_external=True,
application_id=application.id,
fragment="application-members",
)
def test_remove_member_failure(client, user_session):
user = UserFactory.create()
application = ApplicationFactory.create()
user_session(application.portfolio.owner)
response = client.post(
url_for(
"applications.remove_member",
application_id=application.id,
user_id=uuid.uuid4(),
)
)
assert response.status_code == 404

View File

@ -421,10 +421,14 @@ portfolios:
app_settings_text: App settings
create_button_text: Create
csp_console_text: CSP console
remove_member:
alert:
message: '{user_name} will no longer be able to access this application'
button: Remove member
header: Are you sure you want to remove this team member?
delete:
alert:
message: You will lose access to this application and all of its reporting and metrics tools. The activity log will be retained.
title: Warning! This action is permanent.
button: Delete application
header: Are you sure you want to delete this application?
environments: