diff --git a/atst/forms/app_settings.py b/atst/forms/app_settings.py index 20e1aa25..19801a10 100644 --- a/atst/forms/app_settings.py +++ b/atst/forms/app_settings.py @@ -2,18 +2,18 @@ from flask_wtf import FlaskForm from wtforms.fields import FieldList, FormField, HiddenField, RadioField, StringField from .forms import BaseForm -from .data import ENV_ROLES +from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS class MemberForm(FlaskForm): user_id = HiddenField() user_name = StringField() - role_name = RadioField(choices=ENV_ROLES, default="no_access") + role_name = RadioField(choices=ENV_ROLES, default=NO_ACCESS) @property def data(self): _data = super().data - if "role_name" in _data and _data["role_name"] == "no_access": + if "role_name" in _data and _data["role_name"] == NO_ACCESS: _data["role_name"] = None return _data diff --git a/atst/forms/data.py b/atst/forms/data.py index 2ddc3124..e0d4cf7f 100644 --- a/atst/forms/data.py +++ b/atst/forms/data.py @@ -217,6 +217,7 @@ REQUIRED_DISTRIBUTIONS = [ ("other", "Other as necessary"), ] +ENV_ROLE_NO_ACCESS = "No Access" ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [ - ("no_access", "No access") + (ENV_ROLE_NO_ACCESS, "No access") ] diff --git a/atst/forms/team.py b/atst/forms/team.py index 6e4ce3ee..29e46874 100644 --- a/atst/forms/team.py +++ b/atst/forms/team.py @@ -1,14 +1,31 @@ from flask_wtf import FlaskForm -from wtforms.fields import FormField, FieldList, HiddenField, StringField +from wtforms.fields import FormField, FieldList, HiddenField, RadioField, StringField from wtforms.validators import Required -from .application_member import EnvironmentForm +from .application_member import EnvironmentForm as BaseEnvironmentForm +from .data import ENV_ROLES, ENV_ROLE_NO_ACCESS as NO_ACCESS from .forms import BaseForm from atst.forms.fields import SelectField from atst.domain.permission_sets import PermissionSets from atst.utils.localization import translate +class EnvironmentForm(BaseEnvironmentForm): + role = RadioField( + "Role", + choices=ENV_ROLES, + default=None, + filters=[lambda x: None if x == "None" else x], + ) + + @property + def data(self): + _data = super().data + if "role" in _data and _data["role"] == NO_ACCESS: + _data["role"] = None + return _data + + class PermissionsForm(FlaskForm): perms_team_mgmt = SelectField( translate("portfolios.applications.members.new.manage_team"), diff --git a/atst/routes/applications/settings.py b/atst/routes/applications/settings.py index b3077b77..935aaaf4 100644 --- a/atst/routes/applications/settings.py +++ b/atst/routes/applications/settings.py @@ -5,6 +5,7 @@ from atst.domain.environments import Environments from atst.domain.applications import Applications from atst.forms.app_settings import AppEnvRolesForm from atst.forms.application import ApplicationForm, EditEnvironmentForm +from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from atst.domain.authz.decorator import user_can_access_decorator as user_can from atst.models.environment_role import CSPRole from atst.domain.exceptions import NotFoundError @@ -46,10 +47,10 @@ def sort_env_users_by_role(env): users_list = [] no_access_users = env.application.users - env.users no_access_list = [ - {"user_id": str(user.id), "user_name": user.full_name, "role_name": "no_access"} + {"user_id": str(user.id), "user_name": user.full_name, "role_name": NO_ACCESS} for user in no_access_users ] - users_list.append({"role": "no_access", "members": no_access_list}) + users_list.append({"role": NO_ACCESS, "members": no_access_list}) for role in CSPRole: users_list.append( diff --git a/atst/routes/applications/team.py b/atst/routes/applications/team.py index 97783f89..3aa988e4 100644 --- a/atst/routes/applications/team.py +++ b/atst/routes/applications/team.py @@ -6,6 +6,7 @@ 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.environment_roles import EnvironmentRoles +from atst.domain.environments import Environments from atst.domain.exceptions import AlreadyExistsError from atst.domain.permission_sets import PermissionSets from atst.domain.users import Users @@ -97,15 +98,25 @@ def update_team(application_id): form = TeamForm(http_request.form) if form.validate(): - for member in form.members: - app_role = ApplicationRoles.get(member.data["user_id"], application.id) + for member_form in form.members: + app_role = ApplicationRoles.get(member_form.user_id.data, application.id) new_perms = [ perm - for perm in member.data["permission_sets"] + for perm in member_form.data["permission_sets"] if perm != PermissionSets.VIEW_APPLICATION ] ApplicationRoles.update_permission_sets(app_role, new_perms) - flash("updated_application_members_permissions") + + for environment_role_form in member_form.environment_roles: + user = Users.get(member_form.user_id.data) + environment = Environments.get( + environment_role_form.environment_id.data + ) + Environments.update_env_role( + environment, user, environment_role_form.data.get("role") + ) + + flash("updated_application_team_settings", application_name=application.name) return redirect( url_for( diff --git a/atst/utils/flash.py b/atst/utils/flash.py index 1557a90e..0a96366a 100644 --- a/atst/utils/flash.py +++ b/atst/utils/flash.py @@ -186,10 +186,10 @@ MESSAGES = { """, "category": "success", }, - "updated_application_members_permissions": { + "updated_application_team_settings": { "title_template": translate("flash.success"), "message_template": """ -

{{ "flash.updated_application_members_permissions" | translate }}

+

{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}

""", "category": "success", }, diff --git a/js/components/__tests__/edit_environment_role.test.js b/js/components/__tests__/edit_environment_role.test.js index 4421d58c..8242acb4 100644 --- a/js/components/__tests__/edit_environment_role.test.js +++ b/js/components/__tests__/edit_environment_role.test.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils' -import EditEnvironmentRole from '../forms/edit_environment_role' +import { NO_ACCESS, EditEnvironmentRole } from '../forms/edit_environment_role' describe('EditEnvironmentRole', () => { var initialRoleCategories, wrapper @@ -7,7 +7,7 @@ describe('EditEnvironmentRole', () => { beforeEach(() => { initialRoleCategories = [ { - role: 'no_access', + role: NO_ACCESS, members: [ { role_name: null, user_id: '123' }, { role_name: null, user_id: '456' }, @@ -41,10 +41,10 @@ describe('EditEnvironmentRole', () => { it('removes null roles to no_access', () => { let roles = wrapper.vm.sanitizeValues([ - { role: 'no_access', members: [{ role_name: null }] }, + { role: NO_ACCESS, members: [{ role_name: null }] }, ]) expect(roles).toEqual([ - { role: 'no_access', members: [{ role_name: 'no_access' }] }, + { role: NO_ACCESS, members: [{ role_name: NO_ACCESS }] }, ]) }) diff --git a/js/components/environment_role.js b/js/components/environment_role.js new file mode 100644 index 00000000..23133c25 --- /dev/null +++ b/js/components/environment_role.js @@ -0,0 +1,36 @@ +import optionsinput from './options_input' +import { emitEvent } from '../lib/emitters' + +export default { + name: 'environment-role', + + components: { + optionsinput, + }, + + props: { + initialRole: String, + }, + + data: function() { + return { + role: this.initialRole, + expanded: false, + } + }, + + methods: { + toggle: function() { + this.expanded = !this.expanded + }, + radioChange: function(e) { + this.role = e.target.value + emitEvent('field-change', this, { + value: e.target.value, + valid: true, + name: this.name, + watch: true, + }) + }, + }, +} diff --git a/js/components/forms/edit_application_roles.js b/js/components/forms/edit_application_roles.js index c4f6d490..ef5de676 100644 --- a/js/components/forms/edit_application_roles.js +++ b/js/components/forms/edit_application_roles.js @@ -1,7 +1,7 @@ import FormMixin from '../../mixins/form' import Modal from '../../mixins/modal' import toggler from '../toggler' -import EditEnvironmentRole from './edit_environment_role' +import { EditEnvironmentRole } from './edit_environment_role' export default { name: 'edit-application-roles', diff --git a/js/components/forms/edit_environment_role.js b/js/components/forms/edit_environment_role.js index cbb09457..e9081c34 100644 --- a/js/components/forms/edit_environment_role.js +++ b/js/components/forms/edit_environment_role.js @@ -5,7 +5,9 @@ import Modal from '../../mixins/modal' // https://github.com/dod-ccpo/atst/pull/799/files#r282240663 // May also want to reconsider the data structure by storing the roles and members separately -export default { +export const NO_ACCESS = 'No Access' + +export const EditEnvironmentRole = { name: 'edit-environment-role', mixins: [FormMixin], @@ -26,7 +28,7 @@ export default { roles.forEach(role => { role.members.forEach(member => { if (member.role_name === null) { - member.role_name = 'no_access' + member.role_name = NO_ACCESS } }) }) @@ -34,7 +36,7 @@ export default { }, checkNoAccess: function(role) { - return role === 'no_access' + return role === NO_ACCESS }, toggleSection: function(sectionName) { diff --git a/js/components/toggler.js b/js/components/toggler.js index e433b294..225e144b 100644 --- a/js/components/toggler.js +++ b/js/components/toggler.js @@ -1,7 +1,8 @@ -import editEnvironmentRole from './forms/edit_environment_role' +import { EditEnvironmentRole } from './forms/edit_environment_role' import FormMixin from '../mixins/form' import optionsinput from './options_input' import textinput from './text_input' +import EnvironmentRole from './environment_role' export default { name: 'toggler', @@ -16,10 +17,12 @@ export default { }, components: { - editEnvironmentRole, + EditEnvironmentRole, optionsinput, textinput, optionsinput, + EnvironmentRole, + toggler: this, }, data: function() { diff --git a/js/index.js b/js/index.js index 02a9b404..520dea03 100644 --- a/js/index.js +++ b/js/index.js @@ -18,7 +18,7 @@ import poc from './components/forms/poc' import oversight from './components/forms/oversight' import toggler from './components/toggler' import NewApplication from './components/forms/new_application' -import EditEnvironmentRole from './components/forms/edit_environment_role' +import { EditEnvironmentRole } from './components/forms/edit_environment_role' import EditApplicationRoles from './components/forms/edit_application_roles' import MultiStepModalForm from './components/forms/multi_step_modal_form' import funding from './components/forms/funding' @@ -39,6 +39,7 @@ import KoReview from './components/forms/ko_review' import BaseForm from './components/forms/base_form' import DeleteConfirmation from './components/delete_confirmation' import NewEnvironment from './components/forms/new_environment' +import EnvironmentRole from './components/environment_role' Vue.config.productionTip = false @@ -80,6 +81,7 @@ const app = new Vue({ DeleteConfirmation, nestedcheckboxinput, NewEnvironment, + EnvironmentRole, }, mounted: function() { diff --git a/styles/components/_accordion_table.scss b/styles/components/_accordion_table.scss index 1bcf3331..c5d93b09 100644 --- a/styles/components/_accordion_table.scss +++ b/styles/components/_accordion_table.scss @@ -91,6 +91,30 @@ border-bottom: 1px solid $color-gray-lighter; } } + + .accordion-table__item__action-group { + padding: 1rem 3.2rem; + background-color: $color-gray-lightest; + + button, + a { + margin: 0; + font-size: $small-font-size; + } + + .icon-link { + padding-top: 0.5rem; + float: none; + } + + > *:first-child { + padding-left: 0; + } + + > *:last-child { + float: right; + } + } } .accordion-table__item__toggler { @@ -100,6 +124,10 @@ color: $color-blue; padding: $gap; + > span { + margin-left: auto; + } + .icon { @include icon-size(12); @@ -147,6 +175,17 @@ .accordion-table__item__expanded_first { float: left; } + + .accordion-table__item__expanded-role { + .icon-link { + padding: 0; + vertical-align: text-top; + + .icon { + margin: 0 0 0 0.25rem; + } + } + } } } } diff --git a/styles/components/_portfolio_layout.scss b/styles/components/_portfolio_layout.scss index 60fd04c6..c555ed13 100644 --- a/styles/components/_portfolio_layout.scss +++ b/styles/components/_portfolio_layout.scss @@ -1,7 +1,6 @@ .portfolio-panel-container { @include media($large-screen) { @include grid-row; - min-height: 500px; } @@ -38,7 +37,6 @@ &.icon-link--disabled { color: $color-gray-dark; opacity: 1; - .icon { @include icon-color($color-gray-dark); } @@ -53,7 +51,6 @@ .icon-link { color: $color-gray-medium; pointer-events: none; - &.icon-link--disabled { opacity: 1; } @@ -63,7 +60,6 @@ .portfolio-header { flex-direction: column; - @include media($small-screen) { flex-direction: row; } @@ -103,10 +99,8 @@ .icon-link { padding: 0.8rem 1.2rem; - &.active { color: $color-gray; - .icon { @include icon-color($color-gray); } @@ -150,7 +144,6 @@ .unfunded { color: $color-red; - .icon { @include icon-color($color-red); } @@ -166,7 +159,7 @@ } .portfolio-content { - margin: (6 * $gap) $gap 0 $gap; + margin: 6 * $gap $gap 0 $gap; .panel { @include shadow-panel; @@ -175,7 +168,6 @@ .member-list { .panel { @include shadow-panel; - padding-bottom: 0; } @@ -191,7 +183,7 @@ } tr:first-child { - padding: 0 (2 * $gap) 0 (5 * $gap); + padding: 0 2 * $gap 0 5 * $gap; } td { @@ -203,14 +195,14 @@ th { background-color: $color-gray-lightest; - padding: $gap (2 * $gap); + padding: $gap 2 * $gap; border-top: none; border-bottom: none; color: $color-gray; } td:first-child { - padding: (2 * $gap) (2 * $gap) (2 * $gap) (5 * $gap); + padding: 2 * $gap 2 * $gap 2 * $gap 5 * $gap; } tbody { @@ -218,7 +210,7 @@ border-bottom: 1px solid $color-gray-lightest; font-size: 1.6rem; border-top: 0; - padding: (3 * $gap) (2 * $gap); + padding: 3 * $gap 2 * $gap; .usa-button-disabled { color: $color-gray-medium; @@ -318,6 +310,11 @@ .alert { margin: 4rem; } + + .member-list__subhead { + font-weight: $font-normal; + font-size: $base-font-size; + } } .application-content { @@ -333,7 +330,6 @@ .block-list__footer { border-bottom: none; } - .application-edit__env-list-item { label { color: $color-black; @@ -373,7 +369,6 @@ .portfolio-applications__header--actions { color: $color-blue; font-size: $small-font-size; - .icon { @include icon-color($color-blue); @include icon-size(14); @@ -384,7 +379,6 @@ .application-list { .toggle-link { background-color: $color-blue-light; - .icon { margin: $gap / 2; } @@ -402,7 +396,6 @@ .application-list-item__environment__csp_link { font-size: $small-font-size; font-weight: normal; - &:hover { background-color: $color-aqua-light; } @@ -420,7 +413,6 @@ .subheading { @include subheading; - margin-top: 6 * $gap; margin-bottom: 2 * $gap; } @@ -431,6 +423,7 @@ .pending-task-order { background-color: $color-gold-lightest; + align-items: center; margin: 0; margin-bottom: 2 * $gap; @@ -462,7 +455,6 @@ .icon--tiny { @include icon-size(10); - margin-left: 1rem; } } @@ -487,7 +479,7 @@ th { background-color: $color-gray-lightest; - padding: $gap (2 * $gap); + padding: $gap 2 * $gap; border-top: none; border-bottom: none; color: $color-gray; @@ -566,7 +558,6 @@ .panel { @include shadow-panel; - margin-bottom: 4 * $gap; } } @@ -586,7 +577,6 @@ input { max-width: 30em; } - .icon-validation { left: 30em; } @@ -615,3 +605,44 @@ } } } + +.member-list__name { + margin-top: 1rem; +} + +.member-list__role-select { + overflow: auto; + margin: 1.6rem -3.2rem -1.6rem -3.2rem; + padding: 2rem 3.2rem 2rem 5rem; + background: $color-gray-cool-light; + border-top: 1px solid $color-gray-lighter; + + > label { + font-weight: $font-bold; + margin: 0; + } + + > label:first-child + ul.member-list____role-select__radio { + display: flex; + background: $color-gray-cool-light; + + li { + border-bottom: none; + + label { + margin-top: 1rem; + margin-left: 2rem; + } + } + + li:first-child > label { + margin-left: 0; + } + } + + button { + font-size: $small-font-size; + float: right; + margin-right: 0; + } +} diff --git a/styles/core/_variables.scss b/styles/core/_variables.scss index 65153584..4463f1bd 100644 --- a/styles/core/_variables.scss +++ b/styles/core/_variables.scss @@ -73,7 +73,7 @@ $color-gray-lightest: #f1f1f1; $color-gray-warm-dark: #494440; $color-gray-warm-light: #e4e2e0; -$color-gray-cool-light: #dce4ef; +$color-gray-cool-light: #eff2f7; $color-gold-dark: #cd841b; $color-gold: #fdb81e; diff --git a/templates/components/modal.html b/templates/components/modal.html index 957aee3a..25772f53 100644 --- a/templates/components/modal.html +++ b/templates/components/modal.html @@ -12,7 +12,7 @@ {% endif %} diff --git a/templates/fragments/applications/edit_team.html b/templates/fragments/applications/edit_team.html index 1914a62a..844e22dc 100644 --- a/templates/fragments/applications/edit_team.html +++ b/templates/fragments/applications/edit_team.html @@ -11,7 +11,11 @@
  • -
    {{ member_form.user_name.data }}
    +
    +
    + {{ 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) }}
    @@ -26,10 +30,10 @@ {{ ToggleButton( - open_html=open_html, - close_html=close_html, - section_name="environments" - ) + open_html=open_html, + close_html=close_html, + section_name="environments" + ) }}
    @@ -37,19 +41,58 @@ +
    + {% if user_can(permissions.ASSIGN_ENVIRONMENT_MEMBER) %} + + {{ "portfolios.applications.team_settings.add_to_environment" | translate }} + {{ Icon("plus") }} + + {% endif %} + {% if user_can(permissions.DELETE_APPLICATION_MEMBER) %} + + {% endif %} +
    {% endcall %} - {{ member_form.user_id() }} + {{ member_form.user_id() }}
  • {% endfor %} diff --git a/templates/fragments/applications/read_only_team.html b/templates/fragments/applications/read_only_team.html index cb0e3be1..66d6947a 100644 --- a/templates/fragments/applications/read_only_team.html +++ b/templates/fragments/applications/read_only_team.html @@ -1,3 +1,5 @@ +{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} + {% 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] %} diff --git a/templates/portfolios/applications/team.html b/templates/portfolios/applications/team.html index d33992cd..a1ef3d57 100644 --- a/templates/portfolios/applications/team.html +++ b/templates/portfolios/applications/team.html @@ -52,62 +52,60 @@ {% if g.matchesPath("application-members") %} {% include "fragments/flash.html" %} {% endif %} -
    -
    -
    -
    - {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} +
    +
    +
    +
    + {{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} +

    Members ({{ team_form.members | length }})

    +
    - - {{ Icon('info') }} - {{ "portfolios.admin.settings_info" | translate }} -
    -
    -
    -
    - {{ "common.name" | 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 }} +
    +
    +   +
    -
    - {{ "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 }} -
    -
    -   +
      + {% 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 %} +
    +
    + + -
      - {% 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 %} -
    - - -
    diff --git a/tests/routes/applications/test_settings.py b/tests/routes/applications/test_settings.py index a658107a..40f758ed 100644 --- a/tests/routes/applications/test_settings.py +++ b/tests/routes/applications/test_settings.py @@ -18,11 +18,11 @@ from atst.domain.environments import Environments from atst.domain.permission_sets import PermissionSets from atst.domain.portfolios import Portfolios from atst.domain.exceptions import NotFoundError - from atst.models.environment_role import CSPRole from atst.models.portfolio_role import Status as PortfolioRoleStatus from atst.forms.application import EditEnvironmentForm from atst.forms.app_settings import AppEnvRolesForm +from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from tests.utils import captured_templates @@ -166,7 +166,7 @@ def test_data_for_app_env_roles_form(app, client, user_session): "env_id": env.id, "team_roles": [ { - "role": "no_access", + "role": NO_ACCESS, "members": [ { "user_id": str(app_role.user_id), @@ -309,7 +309,7 @@ def test_update_team_env_roles(client, user_session): "envs-0-team_roles-1-members-1-user_id": env_role_2.user.id, "envs-0-team_roles-1-members-1-role_name": CSPRole.BASIC_ACCESS.value, "envs-0-team_roles-1-members-2-user_id": env_role_3.user.id, - "envs-0-team_roles-1-members-2-role_name": "no_access", + "envs-0-team_roles-1-members-2-role_name": NO_ACCESS, } user_session(application.portfolio.owner) diff --git a/tests/routes/applications/test_team.py b/tests/routes/applications/test_team.py index 39aaf533..99c7da78 100644 --- a/tests/routes/applications/test_team.py +++ b/tests/routes/applications/test_team.py @@ -3,6 +3,8 @@ import uuid from flask import url_for from atst.domain.permission_sets import PermissionSets +from atst.models import CSPRole +from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS from tests.factories import * @@ -17,7 +19,7 @@ def test_application_team(client, user_session): assert response.status_code == 200 -def test_update_team(client, user_session): +def test_update_team_permissions(client, user_session): application = ApplicationFactory.create() owner = application.portfolio.owner app_role = ApplicationRoleFactory.create( @@ -91,6 +93,63 @@ def test_update_team_with_non_app_user(client, user_session): assert response.status_code == 404 +def test_update_team_environment_roles(client, user_session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + app_user = app_role.user + environment = EnvironmentFactory.create(application=application) + env_role = EnvironmentRoleFactory.create( + user=app_user, environment=environment, role=CSPRole.NETWORK_ADMIN.value + ) + 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, + "members-0-environment_roles-0-environment_id": environment.id, + "members-0-environment_roles-0-role": CSPRole.TECHNICAL_READ.value, + }, + ) + + assert response.status_code == 302 + assert env_role.role == CSPRole.TECHNICAL_READ.value + + +def test_update_team_revoke_environment_access(client, user_session, db, session): + application = ApplicationFactory.create() + owner = application.portfolio.owner + app_role = ApplicationRoleFactory.create( + application=application, permission_sets=[] + ) + app_user = app_role.user + environment = EnvironmentFactory.create(application=application) + env_role = EnvironmentRoleFactory.create( + user=app_user, environment=environment, role=CSPRole.BASIC_ACCESS.value + ) + 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, + "members-0-environment_roles-0-environment_id": environment.id, + "members-0-environment_roles-0-role": NO_ACCESS, + }, + ) + + assert response.status_code == 302 + env_role_exists = db.exists().where(EnvironmentRole.id == env_role.id) + assert not session.query(env_role_exists).scalar() + + def test_create_member(client, user_session): user = UserFactory.create() application = ApplicationFactory.create( diff --git a/translations.yaml b/translations.yaml index 41f5b2bf..d7cb4329 100644 --- a/translations.yaml +++ b/translations.yaml @@ -20,6 +20,7 @@ base_public: common: back: Back cancel: Cancel + close: Close confirm: Confirm continue: Continue delete: Delete @@ -46,7 +47,6 @@ common: name: Name components: modal: - close: Close destructive_message: You will no longer be able to access this {resource} destructive_title: Warning! This action is permanent usa_header: @@ -80,7 +80,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.' + updated_application_team_settings: 'You have updated the {application_name} team settings.' footer: about_link_text: Joint Enterprise Defense Infrastructure browser_support: JEDI Cloud supported on these web browsers @@ -468,6 +468,7 @@ portfolios: subheading: Team Settings title: '{application_name} Team Settings' user: User + add_to_environment: Add to existing environment team_text: Team update_button_text: Save members: