diff --git a/atst/domain/application_roles.py b/atst/domain/application_roles.py index af000706..e1956aba 100644 --- a/atst/domain/application_roles.py +++ b/atst/domain/application_roles.py @@ -1,6 +1,9 @@ +from sqlalchemy.orm.exc import NoResultFound + from atst.database import db from atst.models import ApplicationRole, ApplicationRoleStatus -from atst.domain.permission_sets import PermissionSets +from .permission_sets import PermissionSets +from .exceptions import NotFoundError class ApplicationRoles(object): @@ -28,3 +31,27 @@ class ApplicationRoles(object): db.session.add(role) db.session.commit() + + @classmethod + def get(cls, user_id, application_id): + try: + app_role = ( + db.session.query(ApplicationRole) + .filter_by(user_id=user_id, application_id=application_id) + .one() + ) + except NoResultFound: + raise NotFoundError("application_role") + + return app_role + + @classmethod + def update_permission_sets(cls, application_role, new_perm_sets_names): + application_role.permission_sets = ApplicationRoles._permission_sets_for_names( + new_perm_sets_names + ) + + db.session.add(application_role) + db.session.commit() + + return application_role diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py index 99728467..bb8fdb85 100644 --- a/atst/domain/environment_roles.py +++ b/atst/domain/environment_roles.py @@ -1,7 +1,7 @@ from flask import current_app as app from atst.database import db -from atst.models import EnvironmentRole +from atst.models import EnvironmentRole, Application, Environment class EnvironmentRoles(object): @@ -35,3 +35,15 @@ class EnvironmentRoles(object): return True else: return False + + @classmethod + def get_for_application_and_user(cls, user_id, application_id): + return ( + db.session.query(EnvironmentRole) + .join(Environment) + .join(Application, Environment.application_id == Application.id) + .filter(EnvironmentRole.user_id == user_id) + .filter(Application.id == application_id) + .filter(EnvironmentRole.environment_id == Environment.id) + .all() + ) diff --git a/atst/forms/team.py b/atst/forms/team.py new file mode 100644 index 00000000..51e9c883 --- /dev/null +++ b/atst/forms/team.py @@ -0,0 +1,54 @@ +from flask_wtf import FlaskForm +from wtforms.fields import FormField, FieldList, HiddenField, StringField +from wtforms.validators import Required + +from .application_member import EnvironmentForm +from .forms import BaseForm +from atst.forms.fields import SelectField +from atst.domain.permission_sets import PermissionSets +from atst.utils.localization import translate + + +class PermissionsForm(FlaskForm): + perms_team_mgmt = SelectField( + translate("portfolios.applications.members.new.manage_team"), + choices=[ + (PermissionSets.VIEW_APPLICATION, "View only"), + (PermissionSets.EDIT_APPLICATION_TEAM, "Edit access"), + ], + ) + perms_env_mgmt = SelectField( + translate("portfolios.applications.members.new.manage_envs"), + choices=[ + (PermissionSets.VIEW_APPLICATION, "View only"), + (PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, "Edit access"), + ], + ) + perms_del_env = SelectField( + choices=[ + (PermissionSets.VIEW_APPLICATION, "No"), + (PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, "Yes"), + ] + ) + + @property + def data(self): + _data = super().data + _data.pop("csrf_token", None) + permission_sets = [] + for field in _data: + if _data[field] is not None: + permission_sets.append(_data[field]) + + return permission_sets + + +class MemberForm(FlaskForm): + user_id = HiddenField(validators=[Required()]) + user_name = StringField() + environment_roles = FieldList(FormField(EnvironmentForm)) + permission_sets = FormField(PermissionsForm) + + +class TeamForm(BaseForm): + members = FieldList(FormField(MemberForm)) diff --git a/atst/models/application.py b/atst/models/application.py index 9edb5166..ef783ff6 100644 --- a/atst/models/application.py +++ b/atst/models/application.py @@ -28,6 +28,7 @@ class Application( back_populates="application", primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)", ) + # TODO: filter condition on this relationship? roles = relationship("ApplicationRole") @property diff --git a/atst/routes/applications/team.py b/atst/routes/applications/team.py index 57994a3c..3b823f7a 100644 --- a/atst/routes/applications/team.py +++ b/atst/routes/applications/team.py @@ -2,62 +2,120 @@ from flask import render_template, request as http_request, g, url_for, redirect from . import applications_bp -from atst.domain.environments import Environments from atst.domain.applications import Applications +from atst.domain.application_roles import ApplicationRoles from atst.domain.authz.decorator import user_can_access_decorator as user_can -from atst.domain.permission_sets import PermissionSets +from atst.domain.environment_roles import EnvironmentRoles from atst.domain.exceptions import AlreadyExistsError +from atst.domain.permission_sets import PermissionSets from atst.forms.application_member import NewForm as NewMemberForm +from atst.forms.team import TeamForm from atst.models import Permissions from atst.services.invitation import Invitation as InvitationService from atst.utils.flash import formatted_flash as flash -from atst.utils.localization import translate -def permission_str(member, edit_perm_set): +def get_form_permission_value(member, edit_perm_set): if member.has_permission_set(edit_perm_set): - return translate("portfolios.members.permissions.edit_access") + return edit_perm_set else: - return translate("portfolios.members.permissions.view_only") + return PermissionSets.VIEW_APPLICATION + + +def get_team_form(application): + team_data = [] + for member in application.members: + user_id = member.user.id + user_name = member.user.full_name + permission_sets = { + "perms_team_mgmt": get_form_permission_value( + member, PermissionSets.EDIT_APPLICATION_TEAM + ), + "perms_env_mgmt": get_form_permission_value( + member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS + ), + "perms_del_env": get_form_permission_value( + member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS + ), + } + roles = EnvironmentRoles.get_for_application_and_user( + member.user.id, application.id + ) + environment_roles = [ + { + "environment_id": str(role.environment.id), + "environment_name": role.environment.name, + "role": role.role, + } + for role in roles + ] + team_data.append( + { + "user_id": str(user_id), + "user_name": user_name, + "permission_sets": permission_sets, + "environment_roles": environment_roles, + } + ) + + return TeamForm(data={"members": team_data}) + + +def get_new_member_form(application): + env_roles = [ + {"environment_id": e.id, "environment_name": e.name} + for e in application.environments + ] + + return NewMemberForm(data={"environment_roles": env_roles}) + + +def render_team_page(application): + team_form = get_team_form(application) + new_member_form = get_new_member_form(application) + + return render_template( + "portfolios/applications/team.html", + application=application, + team_form=team_form, + new_member_form=new_member_form, + ) @applications_bp.route("/applications//team") @user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications") def team(application_id): application = Applications.get(resource_id=application_id) + return render_team_page(application) - environment_users = {} - for member in application.members: - user_id = member.user.id - environment_users[user_id] = { - "permissions": { - "delete_access": permission_str( - member, PermissionSets.DELETE_APPLICATION_ENVIRONMENTS - ), - "environment_management": permission_str( - member, PermissionSets.EDIT_APPLICATION_ENVIRONMENTS - ), - "team_management": permission_str( - member, PermissionSets.EDIT_APPLICATION_TEAM - ), - }, - "environments": Environments.for_user( - user=member.user, application=application - ), - } - env_roles = [ - {"environment_id": e.id, "environment_name": e.name} - for e in application.environments - ] - member_form = NewMemberForm(data={"environment_roles": env_roles}) +@applications_bp.route("/application//team", methods=["POST"]) +@user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member") +def update_team(application_id): + application = Applications.get(application_id) + form = TeamForm(http_request.form) - return render_template( - "portfolios/applications/team.html", - application=application, - environment_users=environment_users, - member_form=member_form, - ) + if form.validate(): + for member in form.members: + app_role = ApplicationRoles.get(member.data["user_id"], application.id) + new_perms = [ + perm + for perm in member.data["permission_sets"] + if perm != PermissionSets.VIEW_APPLICATION + ] + ApplicationRoles.update_permission_sets(app_role, new_perms) + flash("updated_application_members_permissions") + + return redirect( + url_for( + "applications.team", + application_id=application_id, + fragment="application-members", + _anchor="application-members", + ) + ) + else: + return (render_team_page(application), 400) @applications_bp.route("/application//members/new", methods=["POST"]) diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 9855628e..38383fdf 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -173,6 +173,13 @@ MESSAGES = { """, "category": "success", }, + "updated_application_members_permissions": { + "title_template": translate("flash.success"), + "message_template": """ +

{{ "flash.updated_application_members_permissions" | translate }}

+ """, + "category": "success", + }, } diff --git a/js/components/forms/base_form.js b/js/components/forms/base_form.js index 535a098d..a7259eae 100644 --- a/js/components/forms/base_form.js +++ b/js/components/forms/base_form.js @@ -1,26 +1,28 @@ import ally from 'ally.js' -import FormMixin from '../../mixins/form' -import textinput from '../text_input' -import optionsinput from '../options_input' -import DateSelector from '../date_selector' -import MultiStepModalForm from './multi_step_modal_form' -import multicheckboxinput from '../multi_checkbox_input' 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 optionsinput from '../options_input' +import textinput from '../text_input' +import toggler from '../toggler' export default { name: 'base-form', components: { - textinput, - optionsinput, - DateSelector, - MultiStepModalForm, - multicheckboxinput, checkboxinput, + DateSelector, levelofwarrant, Modal, + multicheckboxinput, + MultiStepModalForm, + optionsinput, + textinput, + toggler, }, mixins: [FormMixin], } diff --git a/js/components/options_input.js b/js/components/options_input.js index fa4d8a57..2970b135 100644 --- a/js/components/options_input.js +++ b/js/components/options_input.js @@ -1,8 +1,11 @@ import { emitEvent } from '../lib/emitters' +import FormMixin from '../mixins/form' export default { name: 'optionsinput', + mixins: [FormMixin], + props: { name: String, initialErrors: { @@ -10,6 +13,10 @@ export default { default: () => [], }, initialValue: String, + watch: { + type: Boolean, + default: false, + }, }, data: function() { @@ -27,6 +34,7 @@ export default { emitEvent('field-change', this, { value: e.target.value, name: this.name, + watch: this.watch, }) this.showError = false this.showValid = true diff --git a/js/components/toggler.js b/js/components/toggler.js index 39eca87f..f1c34c96 100644 --- a/js/components/toggler.js +++ b/js/components/toggler.js @@ -1,4 +1,5 @@ import FormMixin from '../mixins/form' +import optionsinput from './options_input' import textinput from './text_input' export default { @@ -8,6 +9,7 @@ export default { components: { textinput, + optionsinput, }, data: function() { diff --git a/js/mixins/form.js b/js/mixins/form.js index 497497b1..f8e97f4d 100644 --- a/js/mixins/form.js +++ b/js/mixins/form.js @@ -16,8 +16,7 @@ export default { const { name, valid, parent_uid } = event if (typeof this[name] !== undefined) { this.fields[name] = valid - - if (parent_uid === this._uid) { + if (event['parent_uid'] === this._uid || event['watch']) { this.changed = true } } diff --git a/styles/components/_accordion_table.scss b/styles/components/_accordion_table.scss index e9060bc5..28cd08de 100644 --- a/styles/components/_accordion_table.scss +++ b/styles/components/_accordion_table.scss @@ -39,6 +39,15 @@ .accordion-table__item-content { padding: ($gap * 2); + + .usa-input { + margin: 0; + } + + select { + border: none; + font-weight: $font-normal; + } } .accordion-table__items { diff --git a/styles/elements/_tables.scss b/styles/elements/_tables.scss index 5ad64929..b18b1884 100644 --- a/styles/elements/_tables.scss +++ b/styles/elements/_tables.scss @@ -126,6 +126,7 @@ table { @include h4; font-size: $lead-font-size; + justify-content: space-between; flex: 2; } } diff --git a/templates/components/options_input.html b/templates/components/options_input.html index ff8af362..81a1e50c 100644 --- a/templates/components/options_input.html +++ b/templates/components/options_input.html @@ -1,13 +1,14 @@ {% from "components/icon.html" import Icon %} {% from "components/tooltip.html" import Tooltip %} -{% macro OptionsInput(field, tooltip, inline=False, label=True, disabled=False) -%} +{% macro OptionsInput(field, tooltip, inline=False, label=True, disabled=False, watch=False) -%}
diff --git a/templates/fragments/applications/add_new_application_member.html b/templates/fragments/applications/add_new_application_member.html index 99f8f99a..3294b21c 100644 --- a/templates/fragments/applications/add_new_application_member.html +++ b/templates/fragments/applications/add_new_application_member.html @@ -9,23 +9,23 @@
- {{ TextInput(member_form.user_data.first_name, validation='requiredField') }} + {{ TextInput(new_member_form.user_data.first_name, validation='requiredField') }}
- {{ TextInput(member_form.user_data.last_name, validation='requiredField') }} + {{ TextInput(new_member_form.user_data.last_name, validation='requiredField') }}
- {{ TextInput(member_form.user_data.email, validation='email') }} + {{ TextInput(new_member_form.user_data.email, validation='email') }}
- {{ TextInput(member_form.user_data.phone_number, validation='usPhone', optional=True) }} + {{ TextInput(new_member_form.user_data.phone_number, validation='usPhone', optional=True) }}
- {{ TextInput(member_form.user_data.dod_id, validation='dodId') }} + {{ TextInput(new_member_form.user_data.dod_id, validation='dodId') }}
@@ -61,7 +61,7 @@
- {% for environment_data in member_form.environment_roles %} + {% for environment_data in new_member_form.environment_roles %} @@ -86,9 +86,9 @@ {% endfor %}

{{ "portfolios.applications.members.new.manage_perms" | translate({"application_name": application.name}) }}

- {{ CheckboxInput(member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }} - {% call CheckboxInput(member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %} - {% set field=member_form.permission_sets.perms_del_env %} + {{ CheckboxInput(new_member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }} + {% call CheckboxInput(new_member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %} + {% set field=new_member_form.permission_sets.perms_del_env %} + {{ team_form.csrf_token }} + + {% for member_form in team_form.members %} + {% set environment_roles_form = member_form.environment_roles %} + {% set permissions_form = member_form.permission_sets %} + + +
  • +
    +
    {{ member_form.user_name.data }}
    +
    {{ OptionsInput(permissions_form.perms_team_mgmt, label=False, watch=True) }}
    +
    {{ OptionsInput(permissions_form.perms_env_mgmt, label=False, watch=True) }}
    +
    {{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}
    + +
    + {% call ToggleSection(section_name="environments") %} +
      + {% for environment_form in environment_roles_form %} +
    • + {{ environment_form.environment_name.data }} +
    • + {% endfor %} +
    + {% endcall %} + {{ member_form.user_id() }} +
  • +
    + {% endfor %} + diff --git a/templates/fragments/applications/read_only_team.html b/templates/fragments/applications/read_only_team.html new file mode 100644 index 00000000..cb0e3be1 --- /dev/null +++ b/templates/fragments/applications/read_only_team.html @@ -0,0 +1,45 @@ +{% for member in team_form.members %} + {% set user_permissions = [member.permission_sets.perms_team_mgmt, member.permission_sets.perms_env_mgmt, member.permission_sets.perms_del_env] %} + + {% macro PermissionField(value) %} +
    {{ value }}
    + {% endmacro %} + + +
  • +
    +
    {{ member.user_name.data }}
    + {% for permission in user_permissions %} + {% set perm = dict(permission.choices).get(permission.data) %} + {{ PermissionField(perm) }} + {% endfor %} + +
    + {% call ToggleSection(section_name="environments") %} +
      + {% for environment in member.environment_roles %} +
    • + {{ environment.environment_name.data }} +
    • + {% endfor %} +
    + {% endcall %} +
  • +
    +{% endfor %} diff --git a/templates/portfolios/applications/team.html b/templates/portfolios/applications/team.html index e02a5459..ed79d259 100644 --- a/templates/portfolios/applications/team.html +++ b/templates/portfolios/applications/team.html @@ -2,6 +2,7 @@ {% from "components/empty_state.html" import EmptyState %} {% from "components/icon.html" import Icon %} +{% from 'components/save_button.html' import SaveButton %} {% from "components/toggle_list.html" import ToggleButton, ToggleSection %} {% set secondary_breadcrumb = 'portfolios.applications.team_settings.title' | translate({ "application_name": application.name }) %} @@ -24,101 +25,65 @@
    -
    - {% if g.matchesPath("application-members") %} - {% include "fragments/flash.html" %} - {% endif %} -
    -
    -
    -
    - {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} + +
    + {% if g.matchesPath("application-members") %} + {% include "fragments/flash.html" %} + {% endif %} +
    +
    +
    +
    + {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} +
    + + {{ Icon('info') }} + {{ "portfolios.admin.settings_info" | translate }} + +
    +
    + +
    +
    +
    + {{ "common.name" | translate }} +
    +
    + {{ "portfolios.applications.team_settings.section.table.team_management" | translate }} +
    +
    + {{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} +
    +
    + {{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} +
    +
    +  
    - - {{ Icon('info') }} - {{ "portfolios.admin.settings_info" | translate }} - +
      + {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %} + {% include "fragments/applications/edit_team.html" %} + {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %} + {% include "fragments/applications/read_only_team.html" %} + {% endif %} +
    -
    -
    -
    -
    - {{ "common.name" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.delete_access" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.environment_management" | translate }} -
    -
    - {{ "portfolios.applications.team_settings.section.table.team_management" | translate }} -
    -
    -   -
    -
    -
      - {% for member in application.members %} - {% set user = member.user %} - {% set user_info = environment_users[user.id] %} - {% set user_permissions = user_info["permissions"] %} - - {% macro PermissionField(value) %} -
      {{ value }}
      - {% endmacro %} - - -
    • -
      -
      {{ user.full_name }}
      - {{ PermissionField(user_permissions["delete_access"]) }} - {{ PermissionField(user_permissions["environment_management"]) }} - {{ PermissionField(user_permissions["team_management"]) }} - -
      - {% call ToggleSection(section_name="environments") %} -
        - {% for environment in user_info["environments"] %} -
      • - {{ environment.name }} -
      • - {% endfor %} -
      - {% endcall %} -
    • -
      - {% endfor %} -
    -
    - - + +
    +
    {% endif %} {% endblock %} diff --git a/tests/domain/test_application_roles.py b/tests/domain/test_application_roles.py index f430c8cb..b26dd13e 100644 --- a/tests/domain/test_application_roles.py +++ b/tests/domain/test_application_roles.py @@ -1,4 +1,7 @@ +import pytest + from atst.domain.application_roles import ApplicationRoles +from atst.domain.exceptions import NotFoundError from atst.domain.permission_sets import PermissionSets from atst.models import ApplicationRoleStatus @@ -33,3 +36,38 @@ def test_enabled_application_role(): ApplicationRoles.enable(app_role) assert app_role.status == ApplicationRoleStatus.ACTIVE + + +def test_get(): + user = UserFactory.create() + application = ApplicationFactory.create() + app_role = ApplicationRoleFactory.create(user=user, application=application) + + assert ApplicationRoles.get(user.id, application.id) + assert app_role.application == application + assert app_role.user == user + + +def test_get_handles_invalid_id(): + user = UserFactory.create() + application = ApplicationFactory.create() + + with pytest.raises(NotFoundError): + ApplicationRoles.get(user.id, application.id) + + +def test_update_permission_sets(): + user = UserFactory.create() + application = ApplicationFactory.create() + app_role = ApplicationRoleFactory.create(user=user, application=application) + + view_app = [PermissionSets.get(PermissionSets.VIEW_APPLICATION)] + new_perms_names = [ + PermissionSets.EDIT_APPLICATION_TEAM, + PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + ] + new_perms = PermissionSets.get_many(new_perms_names) + # view application permission is included by default + assert app_role.permission_sets == view_app + assert ApplicationRoles.update_permission_sets(app_role, new_perms_names) + assert set(app_role.permission_sets) == set(new_perms + view_app) diff --git a/tests/domain/test_environment_roles.py b/tests/domain/test_environment_roles.py new file mode 100644 index 00000000..de42d70e --- /dev/null +++ b/tests/domain/test_environment_roles.py @@ -0,0 +1,16 @@ +from atst.domain.environment_roles import EnvironmentRoles + +from tests.factories import * + + +def test_get_for_application_and_user(): + user = UserFactory.create() + application = ApplicationFactory.create() + env1 = EnvironmentFactory.create(application=application) + EnvironmentFactory.create(application=application) + EnvironmentRoleFactory.create(user=user, environment=env1) + + roles = EnvironmentRoles.get_for_application_and_user(user.id, application.id) + assert len(roles) == 1 + assert roles[0].environment == env1 + assert roles[0].user == user diff --git a/tests/forms/__init__.py b/tests/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/forms/test_team.py b/tests/forms/test_team.py new file mode 100644 index 00000000..c5990698 --- /dev/null +++ b/tests/forms/test_team.py @@ -0,0 +1,31 @@ +from wtforms.validators import ValidationError +import pytest + +from atst.domain.permission_sets import PermissionSets +from atst.forms.team import * + + +def test_permissions_form_permission_sets(): + form_data = { + "perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "perms_env_mgmt": PermissionSets.VIEW_APPLICATION, + "perms_del_env": PermissionSets.VIEW_APPLICATION, + } + form = PermissionsForm(data=form_data) + + assert form.validate() + assert form.data == [ + PermissionSets.EDIT_APPLICATION_TEAM, + PermissionSets.VIEW_APPLICATION, + PermissionSets.VIEW_APPLICATION, + ] + + +def test_permissions_form_invalid(): + form_data = { + "perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "perms_env_mgmt": "not a real choice", + "perms_del_env": PermissionSets.VIEW_APPLICATION, + } + form = PermissionsForm(data=form_data) + assert not form.validate() diff --git a/tests/routes/applications/__init__.py b/tests/routes/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/routes/applications/test_team.py b/tests/routes/applications/test_team.py index 1c3e0a17..108c4f25 100644 --- a/tests/routes/applications/test_team.py +++ b/tests/routes/applications/test_team.py @@ -1,6 +1,9 @@ +import pytest from flask import url_for -from tests.factories import PortfolioFactory, ApplicationFactory, UserFactory +from atst.domain.permission_sets import PermissionSets + +from tests.factories import * def test_application_team(client, user_session): @@ -10,10 +13,83 @@ def test_application_team(client, user_session): user_session(portfolio.owner) response = client.get(url_for("applications.team", application_id=application.id)) - assert response.status_code == 200 +def test_update_team(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + app_user = app_role.user + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-user_id": app_user.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + }, + ) + + assert response.status_code == 302 + actual_perms_names = [perm.name for perm in app_role.permission_sets] + expected_perms_names = [ + PermissionSets.VIEW_APPLICATION, + PermissionSets.EDIT_APPLICATION_TEAM, + PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + ] + assert expected_perms_names == actual_perms_names + + +def test_update_team_with_bad_permission_sets(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + app_user = app_role.user + permission_sets = app_user.permission_sets + + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-user_id": app_user.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": "some random string", + }, + ) + assert response.status_code == 400 + assert app_user.permission_sets == permission_sets + + +def test_update_team_with_non_app_user(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + non_app_user = UserFactory.create() + app_user = app_role.user + + user_session(owner) + response = client.post( + url_for("applications.update_team", application_id=application.id), + data={ + "members-0-user_id": non_app_user.id, + "members-0-permission_sets-perms_team_mgmt": PermissionSets.EDIT_APPLICATION_TEAM, + "members-0-permission_sets-perms_env_mgmt": PermissionSets.EDIT_APPLICATION_ENVIRONMENTS, + "members-0-permission_sets-perms_del_env": PermissionSets.DELETE_APPLICATION_ENVIRONMENTS, + }, + ) + + assert response.status_code == 404 + + def test_create_member(client, user_session): user = UserFactory.create() application = ApplicationFactory.create( diff --git a/translations.yaml b/translations.yaml index 3e1fcc9d..16824faf 100644 --- a/translations.yaml +++ b/translations.yaml @@ -73,6 +73,7 @@ flash: portfolio_home: Go to my portfolio home page success: Success! new_application_member: 'You have successfully invited {user_name} to the team.' + updated_application_members_permissions: 'You have successfully updated member permissions.' footer: about_link_text: Joint Enterprise Defense Infrastructure browser_support: JEDI Cloud supported on these web browsers