Merge pull request #304 from dod-ccpo/assign-csp-roles

Assign and Update Environment Roles for Workspace Users
This commit is contained in:
montana-mil 2018-09-25 11:48:13 -04:00 committed by GitHub
commit b7a33de29d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 338 additions and 181 deletions

View File

@ -0,0 +1,16 @@
from atst.models.environment_role import EnvironmentRole
from atst.database import db
class EnvironmentRoles(object):
@classmethod
def get(cls, user_id, environment_id):
existing_env_role = (
db.session.query(EnvironmentRole)
.filter(
EnvironmentRole.user_id == user_id,
EnvironmentRole.environment_id == environment_id,
)
.one_or_none()
)
return existing_env_role

View File

@ -1,7 +1,14 @@
from sqlalchemy.orm.exc import NoResultFound
from atst.database import db
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole, CSPRole
from atst.models.environment_role import EnvironmentRole
from atst.models.project import Project
from atst.models.permissions import Permissions
from atst.domain.authz import Authorization
from atst.domain.environment_roles import EnvironmentRoles
from .exceptions import NotFoundError
class Environments(object):
@ -20,9 +27,9 @@ class Environments(object):
db.session.commit()
@classmethod
def add_member(cls, user, environment, member, role=CSPRole.NONSENSE_ROLE):
def add_member(cls, environment, user, role):
environment_user = EnvironmentRole(
user=member, environment=environment, role=role.value
user=user, environment=environment, role=role
)
db.session.add(environment_user)
db.session.commit()
@ -39,3 +46,35 @@ class Environments(object):
.filter(Project.id == Environment.project_id)
.all()
)
@classmethod
def get(cls, environment_id):
try:
env = db.session.query(Environment).filter_by(id=environment_id).one()
except NoResultFound:
raise NotFoundError("environment")
return env
@classmethod
def update_environment_role(cls, user, ids_and_roles, workspace_user):
Authorization.check_workspace_permission(
user,
workspace_user.workspace,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
"assign environment roles",
)
for id_and_role in ids_and_roles:
new_role = id_and_role["role"]
environment = Environments.get(id_and_role["id"])
env_role = EnvironmentRoles.get(workspace_user.user_id, id_and_role["id"])
if env_role:
env_role.role = new_role
else:
env_role = EnvironmentRole(
user=workspace_user.user, environment=environment, role=new_role
)
db.session.add(env_role)
db.session.commit()

View File

@ -14,9 +14,6 @@ class Projects(object):
project = Project(workspace=workspace, name=name, description=description)
Environments.create_many(project, environment_names)
for environment in project.environments:
Environments.add_member(user, environment, user)
db.session.add(project)
db.session.commit()
@ -49,3 +46,21 @@ class Projects(object):
.filter(EnvironmentRole.user_id == user.id)
.all()
)
@classmethod
def get_all(cls, user, workspace_user, workspace):
Authorization.check_workspace_permission(
user,
workspace,
Permissions.VIEW_APPLICATION_IN_WORKSPACE,
"view project in workspace",
)
try:
projects = (
db.session.query(Project).filter_by(workspace_id=workspace.id).all()
)
except NoResultFound:
raise NotFoundError("projects")
return projects

View File

@ -1,13 +1,15 @@
from flask_wtf import Form
from wtforms.validators import Optional
from flask_wtf import FlaskForm
from wtforms.validators import Required
from atst.forms.fields import SelectField
from .data import WORKSPACE_ROLES
class EditMemberForm(Form):
class EditMemberForm(FlaskForm):
# This form also accepts a field for each environment in each project
# that the user is a member of
workspace_role = SelectField(
"Workspace Role", choices=WORKSPACE_ROLES, validators=[Optional()]
"Workspace Role", choices=WORKSPACE_ROLES, validators=[Required()]
)

View File

@ -7,7 +7,7 @@ from atst.models import Base, types, mixins
class CSPRole(Enum):
NONSENSE_ROLE = "nonesense_role"
NONSENSE_ROLE = "nonsense_role"
class EnvironmentRole(Base, mixins.TimestampsMixin):

View File

@ -1,3 +1,4 @@
import re
from datetime import date, timedelta
from flask import (
@ -14,10 +15,13 @@ from atst.domain.projects import Projects
from atst.domain.reports import Reports
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_users import WorkspaceUsers
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.forms.new_project import NewProjectForm
from atst.forms.new_member import NewMemberForm
from atst.forms.edit_member import EditMemberForm
from atst.forms.workspace import WorkspaceForm
from atst.forms.data import ENVIRONMENT_ROLES
from atst.domain.authz import Authorization
from atst.models.permissions import Permissions
@ -213,9 +217,16 @@ def view_member(workspace_id, member_id):
"edit this workspace user",
)
member = WorkspaceUsers.get(workspace_id, member_id)
projects = Projects.get_all(g.current_user, member, workspace)
form = EditMemberForm(workspace_role=member.role)
return render_template(
"workspaces/members/edit.html", form=form, workspace=workspace, member=member
"workspaces/members/edit.html",
workspace=workspace,
member=member,
projects=projects,
form=form,
choices=ENVIRONMENT_ROLES,
EnvironmentRoles=EnvironmentRoles,
)
@ -231,6 +242,16 @@ def update_member(workspace_id, member_id):
"edit this workspace user",
)
member = WorkspaceUsers.get(workspace_id, member_id)
ids_and_roles = []
form_dict = http_request.form.to_dict()
for entry in form_dict:
if re.match("env_", entry):
env_id = entry[4:]
env_role = form_dict[entry]
if env_role:
ids_and_roles.append({"id": env_id, "role": env_role})
form = EditMemberForm(http_request.form)
if form.validate():
@ -241,6 +262,8 @@ def update_member(workspace_id, member_id):
)
new_role_name = member.role_displayname
Environments.update_environment_role(g.current_user, ids_and_roles, member)
return redirect(
url_for(
"workspaces.workspace_members",

View File

@ -0,0 +1,54 @@
import FormMixin from '../../mixins/form'
import textinput from '../text_input'
import Selector from '../selector'
import Modal from '../../mixins/modal'
import toggler from '../toggler'
export default {
name: 'edit-workspace-member',
mixins: [FormMixin, Modal],
components: {
toggler,
Modal,
Selector,
textinput
},
props: {
choices: Array,
initialData: String,
},
data: function () {
return {
new_role: this.initialData,
}
},
methods: {
change: function (e) {
e.preventDefault()
this.new_role = e.target.value
},
cancel: function () {
this.new_role = this.initialData
},
},
computed: {
displayName: function () {
for (var arr in this.choices) {
if (this.choices[arr][0] == this.new_role) {
return this.choices[arr][1].name
}
}
return this.new_role ? this.new_role : "no access"
},
label_class: function () {
return this.displayName === "no access" ?
"label" : "label label--success"
},
}
}

View File

@ -13,6 +13,7 @@ import poc from './components/forms/poc'
import financial from './components/forms/financial'
import toggler from './components/toggler'
import NewProject from './components/forms/new_project'
import EditWorkspaceMember from './components/forms/edit_workspace_member'
import Modal from './mixins/modal'
import selector from './components/selector'
import BudgetChart from './components/charts/budget_chart'
@ -39,7 +40,8 @@ const app = new Vue({
BudgetChart,
SpendTable,
CcpoApproval,
LocalDatetime
LocalDatetime,
EditWorkspaceMember,
},
mounted: function() {

View File

@ -1,23 +1,17 @@
export default {
methods: {
closeModal: function(name) {
this.modals[name] = false
this.activeModal = null
this.$emit('modalOpen', false)
},
openModal: function (name) {
this.modals[name] = true
this.activeModal = name
this.$emit('modalOpen', true)
}
},
data: function() {
return {
modals: {
styleguideModal: false,
rolesModal: false,
newProjectConfirmation: false,
pendingFinancialVerification: false,
pendingCCPOApproval: false,
}
activeModal: null,
}
}
}

View File

@ -27,7 +27,7 @@ WORKSPACE_USERS = [
"first_name": "Mario",
"last_name": "Hudson",
"email": "hudson@mil.gov",
"workspace_role": "ccpo",
"workspace_role": "billing_auditor",
"dod_id": "0000000002",
},
{

View File

@ -1,7 +1,7 @@
{% from "components/icon.html" import Icon %}
{% macro Modal(name, dismissable=False) -%}
<template v-if='modals.{{name}} === true' v-cloak>
<div v-show="activeModal === '{{name}}'" v-cloak>
<div class='modal {% if dismissable %}modal--dismissable{% endif%}'>
<div class='modal__container'>
<div class='modal__dialog' role='dialog' aria-modal='true'>
@ -18,5 +18,5 @@
</div>
</div>
</div>
</template>
</div>
{%- endmacro %}

View File

@ -3,6 +3,7 @@
{% from "components/icon.html" import Icon %}
{% from "components/modal.html" import Modal %}
{% from "components/selector.html" import Selector %}
{% from "components/options_input.html" import OptionsInput %}
{% block content %}
@ -49,173 +50,82 @@
</div>
</div>
{% call Modal(name='rolesModal', dismissable=False) %}
<div class="block-list">
<header class="block-list__header">
<div>
<h2 class="block-list__title">
Environment access for Danny Knight
<div class='subtitle'>Project Name - Environment Name</div>
</h2>
<div class="block-list__description">
<p>An environment role determines the permissions a member of the workspace assumes when using the JEDI Cloud.</p>
<p>A member may have different environment roles across different projects. A member can only have one assigned environment role in a given environment.</p>
</div>
</div>
</header>
<form method="post" action="">
<ul>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>Developer</dt>
<dd>Configures cloud-based IaaS and PaaS computing, networking, and storage services.</dd>
</dl>
</label>
</li>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>Database Administrator</dt>
<dd>Configures cloud-based database services.</dd>
</dl>
</label>
</li>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>DevOps</dt>
<dd>Provisions, deprovisions, and deploys cloud-based IaaS and PaaS computing, networking, and storage services, including pre-configured machine images.</dd>
</dl>
</label>
</li>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>Billing Administrator</dt>
<dd>Views cloud resource usage, budget reports, and invoices; tracks budgets, including spend reports, cost planning and projections, and sets limits based on cloud service usage.</dd>
</dl>
</label>
</li>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>Security Administrator</dt>
<dd>Accesses information security and control tools of cloud resources which include viewing cloud resource usage logging, user roles and permissioning history.</dd>
</dl>
</label>
</li>
<li class='block-list__item block-list__item--selectable'>
<input type='radio' name='radio' id='radio-' />
<label for='radio-'>
<dl>
<dt>Financial Auditor</dt>
<dd>Views cloud resource usage and budget reports.</dd>
</dl>
</label>
</li>
</ul>
<div class='block-list__footer'>
<div class='action-group'>
<a v-on:click="closeModal('rolesModal')" class='action-group__action usa-button'>Select Access Role</a>
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('rolesModal')">No Access</a>
</div>
</div>
</form>
</div>
{% endcall %}
{% for project in projects %}
<div is='toggler' default-visible class='block-list project-list-item'>
<template slot-scope='props'>
<header class='block-list__header'>
<button type='button' v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_right') }}</template>
<h3 class="block-list__title">Code.mil</h3>
<h3 class="block-list__title">{{ project.name }}</h3>
</button>
<span><a href="#" class="icon-link icon-link--danger">revoke all access</a></span>
</header>
<ul v-show='props.isVisible'>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Development
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access </span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Sandbox
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access</span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Production
</span>
<div class='project-list-item__environment__actions'>
<span class="label label--success">Billing</span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
</div>
</li>
</ul>
</template>
</div>
{% for env in project.environments %}
<div is="toggler" class='block-list project-list-item'>
<template slot-scope='props'>
<header class='block-list__header'>
<button type='button' v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
<template v-else>{{ Icon('caret_right') }}</template>
<h3 class="block-list__title">Digital Dojo</h3>
</button>
<span class="label">no access</span>
</header>
<ul v-show='props.isVisible'>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Development
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access </span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Sandbox
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access</span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Production
</span>
<div class='project-list-item__environment__actions'>
<span class="label">no access</span><button v-on:click="openModal('rolesModal')" type="button" class="icon-link">set role</button>
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
<li class='block-list__item'>
<edit-workspace-member inline-template initial-data='{{ role }}' v-bind:choices='{{ choices | tojson }}'>
<div class='project-list-item__environment'>
<span class='project-list-item__environment__link'>
{{ env.name }}
</span>
<div class='project-list-item__environment__actions'>
<span v-bind:class="label_class" v-html:on=displayName></span>
<button v-on:click="openModal('{{ env.name }}RolesModal')" type="button" class="icon-link">set role</button>
{% call Modal(name=env.name + 'RolesModal', dismissable=False) %}
<div class='block-list'>
<ul>
{% for choice in choices %}
<li class='block-list__item block-list__item--selectable'>
{% if choice[0] != "" %}
<input
name='radio_input_{{ env.id }}'
v-on:change='change'
type='radio'
id="env_{{ env.id }}_{{ choice[0] }}"
value='{{ choice[0] }}'
{% if role == choice[0] %}
checked='checked'
{% endif %}
/>
<label for="env_{{ env.id }}_{{ choice[0] }}">
{% if choice[1].description %}
<dl>
<dt>{{ choice[1].name }}</dt>
<dd>{{ choice[1].description }}</dd>
</dl>
{% else %}
{{ choice[1].name }}
{% endif %}
</label>
{% endif %}
</li>
{% endfor %}
</ul>
<input type='hidden' name='env_{{ env.id }}' v-bind:value='new_role'/>
<div class='block-list__footer'>
<div class='action-group'>
<a v-on:click="closeModal('{{ env.name }}RolesModal')" class='action-group__action usa-button'>Select Access Role</a>
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{ env.name }}RolesModal'); cancel();" value="{{ value if value == role else role }}" >Cancel</a>
</div>
</div>
</div>
{% endcall %}
</div>
</div>
</edit-workspace-member>
</li>
{% endfor %}
</ul>
</template>
</div>
{% endfor %}
<div class='action-group'>
<button class='action-group__action usa-button usa-button-big'>
@ -227,7 +137,7 @@
</a>
</div>
</form>
</form>

View File

@ -0,0 +1,45 @@
import pytest
from uuid import uuid4
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.projects import Projects
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_users import WorkspaceUsers
from atst.domain.exceptions import NotFoundError
from tests.factories import RequestFactory, UserFactory
def test_update_environment_roles():
owner = UserFactory.create()
developer_data = {
"dod_id": "1234567890",
"first_name": "Test",
"last_name": "User",
"email": "test.user@mail.com",
"workspace_role": "developer",
}
workspace = Workspaces.create(RequestFactory.create(creator=owner))
workspace_user = Workspaces.create_member(owner, workspace, developer_data)
project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
)
dev_env = project.environments[0]
staging_env = project.environments[1]
Environments.add_member(dev_env, workspace_user.user, "devops")
Environments.add_member(staging_env, workspace_user.user, "developer")
new_ids_and_roles = [
{"id": dev_env.id, "role": "billing_admin"},
{"id": staging_env.id, "role": "developer"},
]
Environments.update_environment_role(owner, new_ids_and_roles, workspace_user)
new_dev_env_role = EnvironmentRoles.get(workspace_user.user.id, dev_env.id)
staging_env_role = EnvironmentRoles.get(workspace_user.user.id, staging_env.id)
assert new_dev_env_role.role == "billing_admin"
assert staging_env_role.role == "developer"

View File

@ -169,7 +169,7 @@ def test_scoped_workspace_only_returns_a_users_projects_and_environments(
)
developer = UserFactory.from_atat_role("developer")
dev_environment = Environments.add_member(
workspace_owner, new_project.environments[0], developer
new_project.environments[0], developer, "developer"
)
scoped_workspace = Workspaces.get(developer, workspace.id)

View File

@ -14,5 +14,5 @@ def test_add_user_to_environment():
)
dev_environment = project.environments[0]
dev_environment = Environments.add_member(owner, dev_environment, developer)
dev_environment = Environments.add_member(dev_environment, developer, "developer")
assert developer in dev_environment.users

View File

@ -36,7 +36,7 @@ def test_has_environment_roles():
project = Projects.create(
owner, workspace, "my test project", "It's mine.", ["dev", "staging", "prod"]
)
Environments.add_member(owner, project.environments[0], workspace_user.user)
Environments.add_member(project.environments[0], workspace_user.user, "developer")
assert workspace_user.has_environment_roles

View File

@ -2,6 +2,10 @@ from flask import url_for
from tests.factories import UserFactory, WorkspaceFactory
from atst.domain.workspaces import Workspaces
from atst.domain.workspace_users import WorkspaceUsers
from atst.domain.projects import Projects
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.models.workspace_user import WorkspaceUser
@ -67,3 +71,56 @@ def test_update_workspace_name(client, user_session):
)
assert response.status_code == 200
assert workspace.name == "a cool new name"
def test_update_member_workspace_role(client, user_session):
owner = UserFactory.create()
workspace = WorkspaceFactory.create()
Workspaces._create_workspace_role(owner, workspace, "admin")
user = UserFactory.create()
member = WorkspaceUsers.add(user, workspace.id, "developer")
user_session(owner)
response = client.post(
url_for(
"workspaces.update_member", workspace_id=workspace.id, member_id=user.id
),
data={"workspace_role": "security_auditor"},
follow_redirects=True,
)
assert response.status_code == 200
assert member.role == "security_auditor"
def test_update_member_environment_role(client, user_session):
owner = UserFactory.create()
workspace = WorkspaceFactory.create()
Workspaces._create_workspace_role(owner, workspace, "admin")
user = UserFactory.create()
member = WorkspaceUsers.add(user, workspace.id, "developer")
project = Projects.create(
owner,
workspace,
"Snazzy Project",
"A new project for me and my friends",
{"env1", "env2"},
)
env1_id = project.environments[0].id
env2_id = project.environments[1].id
for env in project.environments:
Environments.add_member(env, user, "developer")
user_session(owner)
response = client.post(
url_for(
"workspaces.update_member", workspace_id=workspace.id, member_id=user.id
),
data={
"workspace_role": "developer",
"env_" + str(env1_id): "security_auditor",
"env_" + str(env2_id): "devops",
},
follow_redirects=True,
)
assert response.status_code == 200
assert EnvironmentRoles.get(user.id, env1_id).role == "security_auditor"
assert EnvironmentRoles.get(user.id, env2_id).role == "devops"