Merge pull request #817 from dod-ccpo/delete-user-from-application
Delete an application member
This commit is contained in:
commit
b8ea1349b2
@ -3,6 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound
|
|||||||
from atst.database import db
|
from atst.database import db
|
||||||
from . import BaseDomainClass
|
from . import BaseDomainClass
|
||||||
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.users import Users
|
from atst.domain.users import Users
|
||||||
@ -100,3 +101,16 @@ class Applications(BaseDomainClass):
|
|||||||
Environments.add_member(environment, user, env_role_data.get("role"))
|
Environments.add_member(environment, user, env_role_data.get("role"))
|
||||||
|
|
||||||
return application_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)
|
||||||
|
@ -88,6 +88,7 @@ _PORTFOLIO_APP_MGMT_PERMISSION_SETS = [
|
|||||||
Permissions.CREATE_APPLICATION,
|
Permissions.CREATE_APPLICATION,
|
||||||
Permissions.DELETE_APPLICATION,
|
Permissions.DELETE_APPLICATION,
|
||||||
Permissions.EDIT_APPLICATION_MEMBER,
|
Permissions.EDIT_APPLICATION_MEMBER,
|
||||||
|
Permissions.DELETE_APPLICATION_MEMBER,
|
||||||
Permissions.CREATE_APPLICATION_MEMBER,
|
Permissions.CREATE_APPLICATION_MEMBER,
|
||||||
Permissions.EDIT_ENVIRONMENT,
|
Permissions.EDIT_ENVIRONMENT,
|
||||||
Permissions.CREATE_ENVIRONMENT,
|
Permissions.CREATE_ENVIRONMENT,
|
||||||
@ -205,6 +206,7 @@ _APPLICATION_TEAM_PERMISSION_SET = {
|
|||||||
"display_name": "Manage team",
|
"display_name": "Manage team",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
Permissions.EDIT_APPLICATION_MEMBER,
|
Permissions.EDIT_APPLICATION_MEMBER,
|
||||||
|
Permissions.DELETE_APPLICATION_MEMBER,
|
||||||
Permissions.CREATE_APPLICATION_MEMBER,
|
Permissions.CREATE_APPLICATION_MEMBER,
|
||||||
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
|
Permissions.ASSIGN_ENVIRONMENT_MEMBER,
|
||||||
],
|
],
|
||||||
|
@ -11,6 +11,7 @@ class Permissions(object):
|
|||||||
DELETE_APPLICATION = "delete_application"
|
DELETE_APPLICATION = "delete_application"
|
||||||
VIEW_APPLICATION_MEMBER = "view_application_member"
|
VIEW_APPLICATION_MEMBER = "view_application_member"
|
||||||
EDIT_APPLICATION_MEMBER = "edit_application_member"
|
EDIT_APPLICATION_MEMBER = "edit_application_member"
|
||||||
|
DELETE_APPLICATION_MEMBER = "delete_application_member"
|
||||||
CREATE_APPLICATION_MEMBER = "create_application_member"
|
CREATE_APPLICATION_MEMBER = "create_application_member"
|
||||||
VIEW_ENVIRONMENT = "view_environment"
|
VIEW_ENVIRONMENT = "view_environment"
|
||||||
EDIT_ENVIRONMENT = "edit_environment"
|
EDIT_ENVIRONMENT = "edit_environment"
|
||||||
|
@ -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.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
from atst.domain.permission_sets import PermissionSets
|
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.application_member import NewForm as NewMemberForm
|
||||||
from atst.forms.team import TeamForm
|
from atst.forms.team import TeamForm
|
||||||
from atst.models import Permissions
|
from atst.models import Permissions
|
||||||
@ -158,3 +159,27 @@ def create_member(application_id):
|
|||||||
_anchor="application-members",
|
_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",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -2,6 +2,11 @@ from flask import flash, render_template_string
|
|||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
MESSAGES = {
|
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": {
|
"environment_deleted": {
|
||||||
"title_template": "{{ environment_name }} deleted",
|
"title_template": "{{ environment_name }} deleted",
|
||||||
"message_template": 'The environment "{{ environment_name }}" has been deleted',
|
"message_template": 'The environment "{{ environment_name }}" has been deleted',
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import ally from 'ally.js'
|
import ally from 'ally.js'
|
||||||
|
|
||||||
import checkboxinput from '../checkbox_input'
|
|
||||||
import DateSelector from '../date_selector'
|
import DateSelector from '../date_selector'
|
||||||
import FormMixin from '../../mixins/form'
|
import FormMixin from '../../mixins/form'
|
||||||
import levelofwarrant from '../levelofwarrant'
|
|
||||||
import Modal from '../../mixins/modal'
|
import Modal from '../../mixins/modal'
|
||||||
import multicheckboxinput from '../multi_checkbox_input'
|
|
||||||
import MultiStepModalForm from './multi_step_modal_form'
|
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 optionsinput from '../options_input'
|
||||||
import textinput from '../text_input'
|
import textinput from '../text_input'
|
||||||
import toggler from '../toggler'
|
import toggler from '../toggler'
|
||||||
@ -14,12 +14,12 @@ import toggler from '../toggler'
|
|||||||
export default {
|
export default {
|
||||||
name: 'base-form',
|
name: 'base-form',
|
||||||
components: {
|
components: {
|
||||||
checkboxinput,
|
|
||||||
DateSelector,
|
DateSelector,
|
||||||
levelofwarrant,
|
|
||||||
Modal,
|
Modal,
|
||||||
multicheckboxinput,
|
|
||||||
MultiStepModalForm,
|
MultiStepModalForm,
|
||||||
|
checkboxinput,
|
||||||
|
levelofwarrant,
|
||||||
|
multicheckboxinput,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
textinput,
|
textinput,
|
||||||
toggler,
|
toggler,
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
{{ team_form.csrf_token }}
|
{{ team_form.csrf_token }}
|
||||||
|
|
||||||
{% for member_form in team_form.members %}
|
{% 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 environment_roles_form = member_form.environment_roles %}
|
||||||
{% set permissions_form = member_form.permission_sets %}
|
{% set permissions_form = member_form.permission_sets %}
|
||||||
|
|
||||||
@ -38,6 +39,13 @@
|
|||||||
{{ environment_form.environment_name.data }}
|
{{ environment_form.environment_name.data }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% 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>
|
</ul>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{{ member_form.user_id() }}
|
{{ member_form.user_id() }}
|
||||||
|
@ -81,8 +81,8 @@
|
|||||||
|
|
||||||
{{
|
{{
|
||||||
Alert(
|
Alert(
|
||||||
title="portfolios.applications.delete.alert.title" | translate,
|
title=("components.modal.destructive_title" | translate),
|
||||||
message="portfolios.applications.delete.alert.message" | translate,
|
message=("portfolios.applications.delete.alert.message" | translate),
|
||||||
level="warning"
|
level="warning"
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
||||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||||
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
|
{% 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 }) %}
|
{% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %}
|
||||||
|
|
||||||
@ -109,6 +112,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</base-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) %}
|
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
|
||||||
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
|
{% import "fragments/applications/new_member_modal_content.html" as member_steps %}
|
||||||
{{ MultiStepModalForm(
|
{{ MultiStepModalForm(
|
||||||
|
@ -2,16 +2,19 @@ import pytest
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from atst.models import CSPRole, ApplicationRoleStatus
|
from atst.models import CSPRole, ApplicationRoleStatus
|
||||||
|
from atst.domain.application_roles import ApplicationRoles
|
||||||
from atst.domain.applications import Applications
|
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.exceptions import NotFoundError
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
ApplicationFactory,
|
ApplicationFactory,
|
||||||
ApplicationRoleFactory,
|
ApplicationRoleFactory,
|
||||||
UserFactory,
|
|
||||||
PortfolioFactory,
|
|
||||||
EnvironmentFactory,
|
EnvironmentFactory,
|
||||||
|
EnvironmentRoleFactory,
|
||||||
|
PortfolioFactory,
|
||||||
|
UserFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -155,3 +158,27 @@ def test_for_user():
|
|||||||
assert len(portfolio.applications) == 4
|
assert len(portfolio.applications) == 4
|
||||||
user_applications = Applications.for_user(user, portfolio)
|
user_applications = Applications.for_user(user, portfolio)
|
||||||
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(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
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
import uuid
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
|
|
||||||
from atst.domain.permission_sets import PermissionSets
|
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 user.application_roles[0].application == application
|
||||||
assert len(user.environment_roles) == 1
|
assert len(user.environment_roles) == 1
|
||||||
assert user.environment_roles[0].environment == env
|
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
|
||||||
|
@ -421,10 +421,14 @@ portfolios:
|
|||||||
app_settings_text: App settings
|
app_settings_text: App settings
|
||||||
create_button_text: Create
|
create_button_text: Create
|
||||||
csp_console_text: CSP console
|
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:
|
delete:
|
||||||
alert:
|
alert:
|
||||||
message: You will lose access to this application and all of its reporting and metrics tools. The activity log will be retained.
|
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
|
button: Delete application
|
||||||
header: Are you sure you want to delete this application?
|
header: Are you sure you want to delete this application?
|
||||||
environments:
|
environments:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user