Merge pull request #801 from dod-ccpo/app-team-permissions

Application Team Table Permissions
This commit is contained in:
montana-mil 2019-05-07 16:27:23 -04:00 committed by GitHub
commit fb7efc6057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 550 additions and 149 deletions

View File

@ -1,6 +1,9 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db from atst.database import db
from atst.models import ApplicationRole, ApplicationRoleStatus 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): class ApplicationRoles(object):
@ -28,3 +31,27 @@ class ApplicationRoles(object):
db.session.add(role) db.session.add(role)
db.session.commit() 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

View File

@ -1,7 +1,7 @@
from flask import current_app as app from flask import current_app as app
from atst.database import db from atst.database import db
from atst.models import EnvironmentRole from atst.models import EnvironmentRole, Application, Environment
class EnvironmentRoles(object): class EnvironmentRoles(object):
@ -35,3 +35,15 @@ class EnvironmentRoles(object):
return True return True
else: else:
return False 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()
)

54
atst/forms/team.py Normal file
View File

@ -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))

View File

@ -28,6 +28,7 @@ class Application(
back_populates="application", back_populates="application",
primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)", primaryjoin="and_(Environment.application_id==Application.id, Environment.deleted==False)",
) )
# TODO: filter condition on this relationship?
roles = relationship("ApplicationRole") roles = relationship("ApplicationRole")
@property @property

View File

@ -2,62 +2,120 @@ from flask import render_template, request as http_request, g, url_for, redirect
from . import applications_bp from . import applications_bp
from atst.domain.environments import Environments
from atst.domain.applications import Applications 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.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.exceptions import AlreadyExistsError
from atst.domain.permission_sets import PermissionSets
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.models import Permissions from atst.models import Permissions
from atst.services.invitation import Invitation as InvitationService from atst.services.invitation import Invitation as InvitationService
from atst.utils.flash import formatted_flash as flash 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): if member.has_permission_set(edit_perm_set):
return translate("portfolios.members.permissions.edit_access") return edit_perm_set
else: 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/<application_id>/team") @applications_bp.route("/applications/<application_id>/team")
@user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications") @user_can(Permissions.VIEW_APPLICATION, message="view portfolio applications")
def team(application_id): def team(application_id):
application = Applications.get(resource_id=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 = [ @applications_bp.route("/application/<application_id>/team", methods=["POST"])
{"environment_id": e.id, "environment_name": e.name} @user_can(Permissions.EDIT_APPLICATION_MEMBER, message="update application member")
for e in application.environments def update_team(application_id):
] application = Applications.get(application_id)
member_form = NewMemberForm(data={"environment_roles": env_roles}) form = TeamForm(http_request.form)
return render_template( if form.validate():
"portfolios/applications/team.html", for member in form.members:
application=application, app_role = ApplicationRoles.get(member.data["user_id"], application.id)
environment_users=environment_users, new_perms = [
member_form=member_form, 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/<application_id>/members/new", methods=["POST"]) @applications_bp.route("/application/<application_id>/members/new", methods=["POST"])

View File

@ -173,6 +173,13 @@ MESSAGES = {
""", """,
"category": "success", "category": "success",
}, },
"updated_application_members_permissions": {
"title_template": translate("flash.success"),
"message_template": """
<p>{{ "flash.updated_application_members_permissions" | translate }}</p>
""",
"category": "success",
},
} }

View File

@ -1,26 +1,28 @@
import ally from 'ally.js' 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 checkboxinput from '../checkbox_input'
import DateSelector from '../date_selector'
import FormMixin from '../../mixins/form'
import levelofwarrant from '../levelofwarrant' 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 optionsinput from '../options_input'
import textinput from '../text_input'
import toggler from '../toggler'
export default { export default {
name: 'base-form', name: 'base-form',
components: { components: {
textinput,
optionsinput,
DateSelector,
MultiStepModalForm,
multicheckboxinput,
checkboxinput, checkboxinput,
DateSelector,
levelofwarrant, levelofwarrant,
Modal, Modal,
multicheckboxinput,
MultiStepModalForm,
optionsinput,
textinput,
toggler,
}, },
mixins: [FormMixin], mixins: [FormMixin],
} }

View File

@ -1,8 +1,11 @@
import { emitEvent } from '../lib/emitters' import { emitEvent } from '../lib/emitters'
import FormMixin from '../mixins/form'
export default { export default {
name: 'optionsinput', name: 'optionsinput',
mixins: [FormMixin],
props: { props: {
name: String, name: String,
initialErrors: { initialErrors: {
@ -10,6 +13,10 @@ export default {
default: () => [], default: () => [],
}, },
initialValue: String, initialValue: String,
watch: {
type: Boolean,
default: false,
},
}, },
data: function() { data: function() {
@ -27,6 +34,7 @@ export default {
emitEvent('field-change', this, { emitEvent('field-change', this, {
value: e.target.value, value: e.target.value,
name: this.name, name: this.name,
watch: this.watch,
}) })
this.showError = false this.showError = false
this.showValid = true this.showValid = true

View File

@ -1,4 +1,5 @@
import FormMixin from '../mixins/form' import FormMixin from '../mixins/form'
import optionsinput from './options_input'
import textinput from './text_input' import textinput from './text_input'
export default { export default {
@ -8,6 +9,7 @@ export default {
components: { components: {
textinput, textinput,
optionsinput,
}, },
data: function() { data: function() {

View File

@ -16,8 +16,7 @@ export default {
const { name, valid, parent_uid } = event const { name, valid, parent_uid } = event
if (typeof this[name] !== undefined) { if (typeof this[name] !== undefined) {
this.fields[name] = valid this.fields[name] = valid
if (event['parent_uid'] === this._uid || event['watch']) {
if (parent_uid === this._uid) {
this.changed = true this.changed = true
} }
} }

View File

@ -39,6 +39,15 @@
.accordion-table__item-content { .accordion-table__item-content {
padding: ($gap * 2); padding: ($gap * 2);
.usa-input {
margin: 0;
}
select {
border: none;
font-weight: $font-normal;
}
} }
.accordion-table__items { .accordion-table__items {

View File

@ -126,6 +126,7 @@ table {
@include h4; @include h4;
font-size: $lead-font-size; font-size: $lead-font-size;
justify-content: space-between;
flex: 2; flex: 2;
} }
} }

View File

@ -1,13 +1,14 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from "components/tooltip.html" import Tooltip %} {% 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) -%}
<optionsinput <optionsinput
name='{{ field.name }}' name='{{ field.name }}'
inline-template inline-template
{% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %} {% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
{% if field.data and field.data != "None" %}v-bind:initial-value="'{{ field.data }}'"{% endif %} {% if field.data and field.data != "None" %}v-bind:initial-value="'{{ field.data }}'"{% endif %}
key='{{ field.name }}' key='{{ field.name }}'
v-bind:watch='{{ watch | string | lower }}'
> >
<div <div
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]"> v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">

View File

@ -9,23 +9,23 @@
</div> </div>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.first_name, validation='requiredField') }} {{ TextInput(new_member_form.user_data.first_name, validation='requiredField') }}
</div> </div>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.last_name, validation='requiredField') }} {{ TextInput(new_member_form.user_data.last_name, validation='requiredField') }}
</div> </div>
</div> </div>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.email, validation='email') }} {{ TextInput(new_member_form.user_data.email, validation='email') }}
</div> </div>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.phone_number, validation='usPhone', optional=True) }} {{ TextInput(new_member_form.user_data.phone_number, validation='usPhone', optional=True) }}
</div> </div>
</div> </div>
<div class='form-row'> <div class='form-row'>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
{{ TextInput(member_form.user_data.dod_id, validation='dodId') }} {{ TextInput(new_member_form.user_data.dod_id, validation='dodId') }}
</div> </div>
<div class='form-col form-col--half'> <div class='form-col form-col--half'>
</div> </div>
@ -61,7 +61,7 @@
</span> </span>
</div> </div>
</div> </div>
{% for environment_data in member_form.environment_roles %} {% for environment_data in new_member_form.environment_roles %}
<optionsinput inline-template <optionsinput inline-template
v-bind:initial-value="'{{ environment_data.role.data | string }}'" v-bind:initial-value="'{{ environment_data.role.data | string }}'"
> >
@ -86,9 +86,9 @@
{% endfor %} {% endfor %}
</div> </div>
<h1>{{ "portfolios.applications.members.new.manage_perms" | translate({"application_name": application.name}) }}</h1> <h1>{{ "portfolios.applications.members.new.manage_perms" | translate({"application_name": application.name}) }}</h1>
{{ CheckboxInput(member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }} {{ CheckboxInput(new_member_form.permission_sets.perms_team_mgmt, classes="input__inline-fields") }}
{% call CheckboxInput(member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %} {% call CheckboxInput(new_member_form.permission_sets.perms_env_mgmt, classes="input__inline-fields") %}
{% set field=member_form.permission_sets.perms_del_env %} {% set field=new_member_form.permission_sets.perms_del_env %}
<nestedcheckboxinput <nestedcheckboxinput
name='{{ field.name }}' name='{{ field.name }}'
inline-template inline-template
@ -128,7 +128,7 @@
{% endset %} {% endset %}
{{ MultiStepModalForm( {{ MultiStepModalForm(
'add-app-mem', 'add-app-mem',
member_form, new_member_form,
url_for("applications.create_member", application_id=application.id), url_for("applications.create_member", application_id=application.id),
[step_one, step_two], [step_one, step_two],
button_text=("portfolios.admin.add_new_member" | translate), button_text=("portfolios.admin.add_new_member" | translate),

View File

@ -0,0 +1,48 @@
{% from "components/options_input.html" import OptionsInput %}
<form method='POST' id="team" action='{{ url_for("applications.update_team", application_id=application.id) }}' autocomplete="off" enctype="multipart/form-data">
{{ 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 %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">{{ member_form.user_name.data }}</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_team_mgmt, label=False, watch=True) }}</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_env_mgmt, label=False, watch=True) }}</div>
<div class="col col--grow">{{ OptionsInput(permissions_form.perms_del_env, label=False, watch=True) }}</div>
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ environment_roles_form | length }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_html,
close_html=close_html,
section_name="environments"
)
}}
</div>
</div>
{% call ToggleSection(section_name="environments") %}
<ul>
{% for environment_form in environment_roles_form %}
<li class="accordion-table__item__expanded">
{{ environment_form.environment_name.data }}
</li>
{% endfor %}
</ul>
{% endcall %}
{{ member_form.user_id() }}
</li>
</toggler>
{% endfor %}
</form>

View File

@ -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) %}
<div class="col col--grow user-permission{% if "Edit" in value or "Yes" in value %} green{% endif %}">{{ value }}</div>
{% endmacro %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">{{ member.user_name.data }}</div>
{% for permission in user_permissions %}
{% set perm = dict(permission.choices).get(permission.data) %}
{{ PermissionField(perm) }}
{% endfor %}
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ member.environment_roles | length }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_html,
close_html=close_html,
section_name="environments"
)
}}
</div>
</div>
{% call ToggleSection(section_name="environments") %}
<ul>
{% for environment in member.environment_roles %}
<li class="accordion-table__item__expanded">
{{ environment.environment_name.data }}
</li>
{% endfor %}
</ul>
{% endcall %}
</li>
</toggler>
{% endfor %}

View File

@ -2,6 +2,7 @@
{% from "components/empty_state.html" import EmptyState %} {% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% from 'components/save_button.html' import SaveButton %}
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %} {% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
{% 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 }) %}
@ -24,101 +25,65 @@
</div> </div>
<section class="member-list application-list" id="application-members"> <section class="member-list application-list" id="application-members">
<div class='responsive-table-wrapper panel'> <base-form inline-template>
{% if g.matchesPath("application-members") %} <div class='responsive-table-wrapper panel'>
{% include "fragments/flash.html" %} {% if g.matchesPath("application-members") %}
{% endif %} {% include "fragments/flash.html" %}
<header> {% endif %}
<div class="responsive-table-wrapper__header"> <header>
<div class="responsive-table-wrapper__title"> <div class="responsive-table-wrapper__header">
<div class="h3"> <div class="responsive-table-wrapper__title row">
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }} <div class="h3">
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
</div>
<a class="icon-link">
{{ Icon('info') }}
{{ "portfolios.admin.settings_info" | translate }}
</a>
</div>
</header>
<div class="accordion-table accordion-table-list">
<div class="accordion-table__head row">
<div class="col col--grow">
{{ "common.name" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
</div>
<div class="col col--grow">
&nbsp;
</div> </div>
</div> </div>
<a class='icon-link'> <ul class="accordion-table__items">
{{ Icon('info') }} {% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
{{ "portfolios.admin.settings_info" | translate }} {% include "fragments/applications/edit_team.html" %}
</a> {% elif user_can(permissions.VIEW_APPLICATION_MEMBER) %}
{% include "fragments/applications/read_only_team.html" %}
{% endif %}
</ul>
</div> </div>
</header>
<div class="accordion-table accordion-table-list"> <div class="panel__footer">
<div class="accordion-table__head row">
<div class="col col--grow">
{{ "common.name" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.environment_management" | translate }}
</div>
<div class="col col--grow">
{{ "portfolios.applications.team_settings.section.table.team_management" | translate }}
</div>
<div class="col col--grow">
&nbsp;
</div>
</div>
<ul class="accordion-table__items">
{% 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) %}
<div class="col col--grow user-permission{% if "Edit" in value %} green{% endif %}">{{ value }}</div>
{% endmacro %}
<toggler inline-template>
<li class="accordion-table__item">
<div class="accordion-table__item-content row">
<div class="col col--grow">{{ user.full_name }}</div>
{{ PermissionField(user_permissions["delete_access"]) }}
{{ PermissionField(user_permissions["environment_management"]) }}
{{ PermissionField(user_permissions["team_management"]) }}
<div class="col col--grow icon-link icon-link--large accordion-table__item__toggler">
{% set open_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }}) {{ Icon('caret_down') }}
{% endset %}
{% set close_html %}
{{ "portfolios.applications.team_settings.environments" | translate }} ({{ user_info['environments'] | length }}) {{ Icon('caret_up') }}
{% endset %}
{{
ToggleButton(
open_html=open_html,
close_html=close_html,
section_name="environments"
)
}}
</div>
</div>
{% call ToggleSection(section_name="environments") %}
<ul>
{% for environment in user_info["environments"] %}
<li class="accordion-table__item__expanded">
{{ environment.name }}
</li>
{% endfor %}
</ul>
{% endcall %}
</li>
</toggler>
{% endfor %}
</ul>
</div>
<div class="members-table-footer">
<div class="action-group save"> <div class="action-group save">
{% if user_can(permissions.EDIT_APPLICATION_MEMBER) %}
{{ SaveButton(text=('common.save' | translate), element="input", form="team") }}
{% endif %}
{% if user_can(permissions.CREATE_APPLICATION_MEMBER) %} {% if user_can(permissions.CREATE_APPLICATION_MEMBER) %}
{% include "fragments/applications/add_new_application_member.html" %} {% include "fragments/applications/add_new_application_member.html" %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
</form>
</div> </div>
</base-form>
</section> </section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,4 +1,7 @@
import pytest
from atst.domain.application_roles import ApplicationRoles from atst.domain.application_roles import ApplicationRoles
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
@ -33,3 +36,38 @@ def test_enabled_application_role():
ApplicationRoles.enable(app_role) ApplicationRoles.enable(app_role)
assert app_role.status == ApplicationRoleStatus.ACTIVE 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)

View File

@ -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

0
tests/forms/__init__.py Normal file
View File

31
tests/forms/test_team.py Normal file
View File

@ -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()

View File

View File

@ -1,6 +1,9 @@
import pytest
from flask import url_for 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): def test_application_team(client, user_session):
@ -10,10 +13,83 @@ def test_application_team(client, user_session):
user_session(portfolio.owner) user_session(portfolio.owner)
response = client.get(url_for("applications.team", application_id=application.id)) response = client.get(url_for("applications.team", application_id=application.id))
assert response.status_code == 200 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): def test_create_member(client, user_session):
user = UserFactory.create() user = UserFactory.create()
application = ApplicationFactory.create( application = ApplicationFactory.create(

View File

@ -73,6 +73,7 @@ flash:
portfolio_home: Go to my portfolio home page portfolio_home: Go to my portfolio home page
success: Success! success: Success!
new_application_member: 'You have successfully invited {user_name} to the team.' new_application_member: 'You have successfully invited {user_name} to the team.'
updated_application_members_permissions: 'You have successfully updated member permissions.'
footer: footer:
about_link_text: Joint Enterprise Defense Infrastructure about_link_text: Joint Enterprise Defense Infrastructure
browser_support: JEDI Cloud supported on these web browsers browser_support: JEDI Cloud supported on these web browsers