Merge pull request #394 from dod-ccpo/revoke-member-access

Revoke members' environment access
This commit is contained in:
richard-dds 2018-10-25 10:26:36 -04:00 committed by GitHub
commit e9588221e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 234 additions and 103 deletions

View File

@ -14,3 +14,10 @@ class EnvironmentRoles(object):
.one_or_none()
)
return existing_env_role
@classmethod
def delete(cls, user_id, environment_id):
existing_env_role = EnvironmentRoles.get(user_id, environment_id)
if existing_env_role:
db.session.delete(existing_env_role)
db.session.commit()

View File

@ -58,10 +58,10 @@ class Environments(object):
return env
@classmethod
def update_environment_role(cls, user, ids_and_roles, workspace_user):
def update_environment_roles(cls, user, workspace, workspace_user, ids_and_roles):
Authorization.check_workspace_permission(
user,
workspace_user.workspace,
workspace,
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
"assign environment roles",
)
@ -69,13 +69,19 @@ class Environments(object):
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
if new_role is None:
EnvironmentRoles.delete(workspace_user.user.id, environment.id)
else:
env_role = EnvironmentRole(
user=workspace_user.user, environment=environment, role=new_role
env_role = EnvironmentRoles.get(
workspace_user.user.id, id_and_role["id"]
)
db.session.add(env_role)
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

@ -153,6 +153,10 @@ ENVIRONMENT_ROLES = [
"description": "Views cloud resource usage and budget reports.",
},
),
(
"",
{"name": "No Access", "description": "User has no access to this environment."},
),
]
ENV_ROLE_MODAL_DESCRIPTION = {

View File

@ -56,6 +56,18 @@ class WorkspaceUser(object):
.count()
)
@property
def environment_roles(self):
return (
db.session.query(EnvironmentRole)
.join(EnvironmentRole.environment)
.join(Environment.project)
.join(Project.workspace)
.filter(Project.workspace_id == self.workspace_id)
.filter(EnvironmentRole.user_id == self.user_id)
.all()
)
@property
def has_environment_roles(self):
return self.num_environment_roles > 0

View File

@ -285,12 +285,10 @@ def update_member(workspace_id, member_id):
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})
env_role = form_dict[entry] or None
ids_and_roles.append({"id": env_id, "role": env_role})
form = EditMemberForm(http_request.form)
if form.validate():
new_role_name = None
if form.data["workspace_role"] != member.role:
@ -299,7 +297,9 @@ def update_member(workspace_id, member_id):
)
new_role_name = member.role_displayname
Environments.update_environment_role(g.current_user, ids_and_roles, member)
Environments.update_environment_roles(
g.current_user, workspace, member, ids_and_roles
)
return redirect(
url_for(

View File

@ -4,8 +4,9 @@ import Selector from '../selector'
import Modal from '../../mixins/modal'
import toggler from '../toggler'
export default {
name: 'edit-workspace-member',
name: 'edit-environment-role',
mixins: [FormMixin, Modal],
@ -19,6 +20,7 @@ export default {
props: {
choices: Array,
initialData: String,
projectId: String
},
data: function () {
@ -27,28 +29,37 @@ export default {
}
},
mounted: function() {
this.$root.$on('revoke-' + this.projectId, this.revoke)
},
methods: {
change: function (e) {
e.preventDefault()
this.new_role = e.target.value
},
cancel: function () {
this.new_role = this.initialData
},
revoke: function () {
this.new_role = ""
}
},
computed: {
displayName: function () {
const newRole = this.newRole
for (var arr in this.choices) {
if (this.choices[arr][0] == this.new_role) {
if (this.choices[arr][0] == newRole) {
return this.choices[arr][1].name
}
}
return this.new_role ? this.new_role : "no access"
},
label_class: function () {
return this.displayName === "no access" ?
return this.newRole === "" ?
"label" : "label label--success"
},
}
newRole: function () {
return this.new_role
}
},
}

View File

@ -0,0 +1,26 @@
import FormMixin from '../../mixins/form'
import Modal from '../../mixins/modal'
import toggler from '../toggler'
import EditEnvironmentRole from './edit_environment_role'
export default {
name: 'edit-project-roles',
mixins: [FormMixin, Modal],
components: {
toggler,
EditEnvironmentRole,
},
props: {
name: String,
id: String
},
methods: {
doRevoke: function () {
this.$root.$emit('revoke-' + this.id)
}
}
}

View File

@ -13,7 +13,8 @@ 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 EditEnvironmentRole from './components/forms/edit_environment_role'
import EditProjectRoles from './components/forms/edit_project_roles'
import Modal from './mixins/modal'
import selector from './components/selector'
import BudgetChart from './components/charts/budget_chart'
@ -41,7 +42,8 @@ const app = new Vue({
SpendTable,
CcpoApproval,
LocalDatetime,
EditWorkspaceMember,
EditEnvironmentRole,
EditProjectRoles,
},
mounted: function() {

View File

@ -51,89 +51,98 @@
</div>
{% 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 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">{{ project.name }}</h3>
</button>
<span><a href="#" class="icon-link icon-link--danger">revoke all access</a></span>
</header>
<ul v-show='props.isVisible'>
{% for env in project.environments %}
{% 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'>
<div class='block-list__header'>
<div>
{% if env_role_modal_description %}
<h1>{{ env_role_modal_description.header }}</h1>
<p>{{ env_role_modal_description.body | safe }}</p>
{% endif %}
</div>
</div>
<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'
autofocus
{% 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'>
<button type='button' v-on:click="closeModal('{{ env.name }}RolesModal')" class='action-group__action usa-button'>Select Access Role</button>
<button type='button' 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</button>
</div>
</div>
</div>
{% endcall %}
{% set revoke_modal_name = project.name + 'RevokeModal' %}
<edit-project-roles inline-template v-bind:name="'{{ project.name }}'" v-bind:id="'{{ project.id }}'">
<div is='toggler' default-visible class='block-list project-list-item'>
<template slot-scope='props'>
<header class='block-list__header'>
<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">{{ project.name }}</h3>
</button>
<span><a v-on:click="openModal('{{ revoke_modal_name }}')" class="icon-link icon-link--danger">revoke all access</a></span>
</header>
{% call Modal(name=revoke_modal_name, dismissable=False) %}
<div>
<h1>Revoke Access</h1>
<p>
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ project.name }}.
</p>
<div class='action-group'>
<a v-on:click="doRevoke(); closeModal('{{ revoke_modal_name }}')" class='action-group__action usa-button'>Confirm</a>
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{ revoke_modal_name }}'); cancel();">Cancel</a>
</div>
</div>
</div>
</edit-workspace-member>
</li>
{% endcall %}
<ul v-show='props.isVisible'>
{% for env in project.environments %}
{% endfor %}
</ul>
</template>
</div>
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
<li class='block-list__item'>
<edit-environment-role inline-template initial-data='{{ role or "" }}' v-bind:choices='{{ choices | tojson }}' v-bind:project-id="'{{ project.id }}'">
<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'>
<div class='block-list__header'>
<div>
{% if env_role_modal_description %}
<h1>{{ env_role_modal_description.header }}</h1>
<p>{{ env_role_modal_description.body | safe }}</p>
{% endif %}
</div>
</div>
<ul>
{% for choice in choices %}
<li class='block-list__item block-list__item--selectable'>
<input
name='radio_input_{{ env.id }}'
v-on:change.prevent='change'
type='radio'
id="env_{{ env.id }}_{{ choice[0] }}"
value='{{ choice[0] }}'
:checked="new_role === '{{ choice[0]}}'"
/>
<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>
</li>
{% endfor %}
</ul>
<input type='hidden' name='env_{{ env.id }}' v-bind:value='newRole'/>
<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-environment-role>
</li>
{% endfor %}
</ul>
</template>
</div>
</edit-project-roles>
{% endfor %}
<div class='action-group'>

View File

@ -1,5 +1,6 @@
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.workspace_users import WorkspaceUsers
from tests.factories import UserFactory, WorkspaceFactory
@ -37,7 +38,9 @@ def test_update_environment_roles():
]
workspace_user = workspace.members[0]
Environments.update_environment_role(owner, new_ids_and_roles, workspace_user)
Environments.update_environment_roles(
owner, workspace, workspace_user, new_ids_and_roles
)
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)
@ -45,6 +48,57 @@ def test_update_environment_roles():
assert staging_env_role.role == "developer"
def test_remove_environment_role():
owner = UserFactory.create()
developer = UserFactory.from_atat_role("developer")
workspace = WorkspaceFactory.create(
owner=owner,
members=[{"user": developer, "role_name": "developer"}],
projects=[
{
"name": "project1",
"environments": [
{
"name": "project1 dev",
"members": [{"user": developer, "role_name": "devops"}],
},
{
"name": "project1 staging",
"members": [{"user": developer, "role_name": "developer"}],
},
{
"name": "project1 uat",
"members": [
{"user": developer, "role_name": "financial_auditor"}
],
},
{"name": "project1 prod"},
],
}
],
)
project = workspace.projects[0]
now_ba = project.environments[0].id
now_none = project.environments[1].id
still_fa = project.environments[2].id
new_environment_roles = [
{"id": now_ba, "role": "billing_auditor"},
{"id": now_none, "role": None},
]
workspace_user = WorkspaceUsers.get(workspace.id, developer.id)
Environments.update_environment_roles(
owner, workspace, workspace_user, new_environment_roles
)
assert workspace_user.num_environment_roles == 2
assert EnvironmentRoles.get(developer.id, now_ba).role == "billing_auditor"
assert EnvironmentRoles.get(developer.id, now_none) is None
assert EnvironmentRoles.get(developer.id, still_fa).role == "financial_auditor"
def test_get_scoped_environments(db):
developer = UserFactory.create()
workspace = WorkspaceFactory.create(