Merge pull request #801 from dod-ccpo/app-team-permissions
Application Team Table Permissions
This commit is contained in:
commit
fb7efc6057
@ -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
|
||||
|
@ -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()
|
||||
)
|
||||
|
54
atst/forms/team.py
Normal file
54
atst/forms/team.py
Normal 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))
|
@ -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
|
||||
|
@ -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/<application_id>/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
|
||||
@applications_bp.route("/application/<application_id>/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)
|
||||
|
||||
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
|
||||
]
|
||||
member_form = NewMemberForm(data={"environment_roles": env_roles})
|
||||
ApplicationRoles.update_permission_sets(app_role, new_perms)
|
||||
flash("updated_application_members_permissions")
|
||||
|
||||
return render_template(
|
||||
"portfolios/applications/team.html",
|
||||
application=application,
|
||||
environment_users=environment_users,
|
||||
member_form=member_form,
|
||||
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"])
|
||||
|
@ -173,6 +173,13 @@ MESSAGES = {
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"updated_application_members_permissions": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
<p>{{ "flash.updated_application_members_permissions" | translate }}</p>
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -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],
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -126,6 +126,7 @@ table {
|
||||
@include h4;
|
||||
|
||||
font-size: $lead-font-size;
|
||||
justify-content: space-between;
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
@ -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) -%}
|
||||
<optionsinput
|
||||
name='{{ field.name }}'
|
||||
inline-template
|
||||
{% 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 %}
|
||||
key='{{ field.name }}'
|
||||
v-bind:watch='{{ watch | string | lower }}'
|
||||
>
|
||||
<div
|
||||
v-bind:class="['usa-input', { 'usa-input--error': showError, 'usa-input--success': showValid }]">
|
||||
|
@ -9,23 +9,23 @@
|
||||
</div>
|
||||
<div class='form-row'>
|
||||
<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 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 class='form-row'>
|
||||
<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 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 class='form-row'>
|
||||
<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 class='form-col form-col--half'>
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% for environment_data in member_form.environment_roles %}
|
||||
{% for environment_data in new_member_form.environment_roles %}
|
||||
<optionsinput inline-template
|
||||
v-bind:initial-value="'{{ environment_data.role.data | string }}'"
|
||||
>
|
||||
@ -86,9 +86,9 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
<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") }}
|
||||
{% 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 %}
|
||||
<nestedcheckboxinput
|
||||
name='{{ field.name }}'
|
||||
inline-template
|
||||
@ -128,7 +128,7 @@
|
||||
{% endset %}
|
||||
{{ MultiStepModalForm(
|
||||
'add-app-mem',
|
||||
member_form,
|
||||
new_member_form,
|
||||
url_for("applications.create_member", application_id=application.id),
|
||||
[step_one, step_two],
|
||||
button_text=("portfolios.admin.add_new_member" | translate),
|
||||
|
48
templates/fragments/applications/edit_team.html
Normal file
48
templates/fragments/applications/edit_team.html
Normal 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>
|
45
templates/fragments/applications/read_only_team.html
Normal file
45
templates/fragments/applications/read_only_team.html
Normal 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 %}
|
@ -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,18 +25,18 @@
|
||||
</div>
|
||||
|
||||
<section class="member-list application-list" id="application-members">
|
||||
<base-form inline-template>
|
||||
<div class='responsive-table-wrapper panel'>
|
||||
{% if g.matchesPath("application-members") %}
|
||||
{% include "fragments/flash.html" %}
|
||||
{% endif %}
|
||||
<header>
|
||||
<div class="responsive-table-wrapper__header">
|
||||
<div class="responsive-table-wrapper__title">
|
||||
<div class="responsive-table-wrapper__title row">
|
||||
<div class="h3">
|
||||
{{ "portfolios.applications.team_settings.section.title" | translate({ "application_name": application.name }) }}
|
||||
</div>
|
||||
</div>
|
||||
<a class='icon-link'>
|
||||
<a class="icon-link">
|
||||
{{ Icon('info') }}
|
||||
{{ "portfolios.admin.settings_info" | translate }}
|
||||
</a>
|
||||
@ -48,77 +49,41 @@
|
||||
{{ "common.name" | translate }}
|
||||
</div>
|
||||
<div class="col col--grow">
|
||||
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
||||
{{ "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.team_management" | translate }}
|
||||
{{ "portfolios.applications.team_settings.section.table.delete_access" | translate }}
|
||||
</div>
|
||||
<div class="col col--grow">
|
||||
|
||||
</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 %}
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="members-table-footer">
|
||||
<div class="panel__footer">
|
||||
<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) %}
|
||||
{% include "fragments/applications/add_new_application_member.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</base-form>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -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)
|
||||
|
16
tests/domain/test_environment_roles.py
Normal file
16
tests/domain/test_environment_roles.py
Normal 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
0
tests/forms/__init__.py
Normal file
31
tests/forms/test_team.py
Normal file
31
tests/forms/test_team.py
Normal 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()
|
0
tests/routes/applications/__init__.py
Normal file
0
tests/routes/applications/__init__.py
Normal file
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user