Merge pull request #394 from dod-ccpo/revoke-member-access
Revoke members' environment access
This commit is contained in:
commit
e9588221e7
@ -14,3 +14,10 @@ class EnvironmentRoles(object):
|
|||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
return existing_env_role
|
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()
|
||||||
|
@ -58,10 +58,10 @@ class Environments(object):
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
@classmethod
|
@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(
|
Authorization.check_workspace_permission(
|
||||||
user,
|
user,
|
||||||
workspace_user.workspace,
|
workspace,
|
||||||
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
|
Permissions.ADD_AND_ASSIGN_CSP_ROLES,
|
||||||
"assign environment roles",
|
"assign environment roles",
|
||||||
)
|
)
|
||||||
@ -69,13 +69,19 @@ class Environments(object):
|
|||||||
for id_and_role in ids_and_roles:
|
for id_and_role in ids_and_roles:
|
||||||
new_role = id_and_role["role"]
|
new_role = id_and_role["role"]
|
||||||
environment = Environments.get(id_and_role["id"])
|
environment = Environments.get(id_and_role["id"])
|
||||||
env_role = EnvironmentRoles.get(workspace_user.user_id, id_and_role["id"])
|
|
||||||
if env_role:
|
if new_role is None:
|
||||||
env_role.role = new_role
|
EnvironmentRoles.delete(workspace_user.user.id, environment.id)
|
||||||
else:
|
else:
|
||||||
env_role = EnvironmentRole(
|
env_role = EnvironmentRoles.get(
|
||||||
user=workspace_user.user, environment=environment, role=new_role
|
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()
|
db.session.commit()
|
||||||
|
@ -153,6 +153,10 @@ ENVIRONMENT_ROLES = [
|
|||||||
"description": "Views cloud resource usage and budget reports.",
|
"description": "Views cloud resource usage and budget reports.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"",
|
||||||
|
{"name": "No Access", "description": "User has no access to this environment."},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
ENV_ROLE_MODAL_DESCRIPTION = {
|
ENV_ROLE_MODAL_DESCRIPTION = {
|
||||||
|
@ -56,6 +56,18 @@ class WorkspaceUser(object):
|
|||||||
.count()
|
.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
|
@property
|
||||||
def has_environment_roles(self):
|
def has_environment_roles(self):
|
||||||
return self.num_environment_roles > 0
|
return self.num_environment_roles > 0
|
||||||
|
@ -285,12 +285,10 @@ def update_member(workspace_id, member_id):
|
|||||||
for entry in form_dict:
|
for entry in form_dict:
|
||||||
if re.match("env_", entry):
|
if re.match("env_", entry):
|
||||||
env_id = entry[4:]
|
env_id = entry[4:]
|
||||||
env_role = form_dict[entry]
|
env_role = form_dict[entry] or None
|
||||||
if env_role:
|
ids_and_roles.append({"id": env_id, "role": env_role})
|
||||||
ids_and_roles.append({"id": env_id, "role": env_role})
|
|
||||||
|
|
||||||
form = EditMemberForm(http_request.form)
|
form = EditMemberForm(http_request.form)
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
new_role_name = None
|
new_role_name = None
|
||||||
if form.data["workspace_role"] != member.role:
|
if form.data["workspace_role"] != member.role:
|
||||||
@ -299,7 +297,9 @@ def update_member(workspace_id, member_id):
|
|||||||
)
|
)
|
||||||
new_role_name = member.role_displayname
|
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(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
|
@ -4,8 +4,9 @@ import Selector from '../selector'
|
|||||||
import Modal from '../../mixins/modal'
|
import Modal from '../../mixins/modal'
|
||||||
import toggler from '../toggler'
|
import toggler from '../toggler'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'edit-workspace-member',
|
name: 'edit-environment-role',
|
||||||
|
|
||||||
mixins: [FormMixin, Modal],
|
mixins: [FormMixin, Modal],
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
choices: Array,
|
choices: Array,
|
||||||
initialData: String,
|
initialData: String,
|
||||||
|
projectId: String
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function () {
|
data: function () {
|
||||||
@ -27,28 +29,37 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.$root.$on('revoke-' + this.projectId, this.revoke)
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
change: function (e) {
|
change: function (e) {
|
||||||
e.preventDefault()
|
|
||||||
this.new_role = e.target.value
|
this.new_role = e.target.value
|
||||||
},
|
},
|
||||||
cancel: function () {
|
cancel: function () {
|
||||||
this.new_role = this.initialData
|
this.new_role = this.initialData
|
||||||
},
|
},
|
||||||
|
revoke: function () {
|
||||||
|
this.new_role = ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
displayName: function () {
|
displayName: function () {
|
||||||
|
const newRole = this.newRole
|
||||||
for (var arr in this.choices) {
|
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.choices[arr][1].name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.new_role ? this.new_role : "no access"
|
|
||||||
},
|
},
|
||||||
label_class: function () {
|
label_class: function () {
|
||||||
return this.displayName === "no access" ?
|
return this.newRole === "" ?
|
||||||
"label" : "label label--success"
|
"label" : "label label--success"
|
||||||
},
|
},
|
||||||
}
|
newRole: function () {
|
||||||
|
return this.new_role
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
26
js/components/forms/edit_project_roles.js
Normal file
26
js/components/forms/edit_project_roles.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,8 @@ import poc from './components/forms/poc'
|
|||||||
import financial from './components/forms/financial'
|
import financial from './components/forms/financial'
|
||||||
import toggler from './components/toggler'
|
import toggler from './components/toggler'
|
||||||
import NewProject from './components/forms/new_project'
|
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 Modal from './mixins/modal'
|
||||||
import selector from './components/selector'
|
import selector from './components/selector'
|
||||||
import BudgetChart from './components/charts/budget_chart'
|
import BudgetChart from './components/charts/budget_chart'
|
||||||
@ -41,7 +42,8 @@ const app = new Vue({
|
|||||||
SpendTable,
|
SpendTable,
|
||||||
CcpoApproval,
|
CcpoApproval,
|
||||||
LocalDatetime,
|
LocalDatetime,
|
||||||
EditWorkspaceMember,
|
EditEnvironmentRole,
|
||||||
|
EditProjectRoles,
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
|
@ -51,89 +51,98 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for project in projects %}
|
{% for project in projects %}
|
||||||
<div is='toggler' default-visible class='block-list project-list-item'>
|
{% set revoke_modal_name = project.name + 'RevokeModal' %}
|
||||||
<template slot-scope='props'>
|
<edit-project-roles inline-template v-bind:name="'{{ project.name }}'" v-bind:id="'{{ project.id }}'">
|
||||||
<header class='block-list__header'>
|
<div is='toggler' default-visible class='block-list project-list-item'>
|
||||||
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
|
<template slot-scope='props'>
|
||||||
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
|
<header class='block-list__header'>
|
||||||
<template v-else>{{ Icon('caret_right') }}</template>
|
<button v-on:click='props.toggle' class='icon-link icon-link--large icon-link--default spend-table__project__toggler'>
|
||||||
<h3 class="block-list__title">{{ project.name }}</h3>
|
<template v-if='props.isVisible'>{{ Icon('caret_down') }}</template>
|
||||||
</button>
|
<template v-else>{{ Icon('caret_right') }}</template>
|
||||||
<span><a href="#" class="icon-link icon-link--danger">revoke all access</a></span>
|
<h3 class="block-list__title">{{ project.name }}</h3>
|
||||||
</header>
|
</button>
|
||||||
<ul v-show='props.isVisible'>
|
<span><a v-on:click="openModal('{{ revoke_modal_name }}')" class="icon-link icon-link--danger">revoke all access</a></span>
|
||||||
{% for env in project.environments %}
|
</header>
|
||||||
|
{% call Modal(name=revoke_modal_name, dismissable=False) %}
|
||||||
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
|
<div>
|
||||||
|
<h1>Revoke Access</h1>
|
||||||
<li class='block-list__item'>
|
<p>
|
||||||
<edit-workspace-member inline-template initial-data='{{ role }}' v-bind:choices='{{ choices | tojson }}'>
|
Confirming will revoke access for {{ member.user.full_name }} to any environments associated with {{ project.name }}.
|
||||||
<div class='project-list-item__environment'>
|
</p>
|
||||||
<span class='project-list-item__environment__link'>
|
<div class='action-group'>
|
||||||
{{ env.name }}
|
<a v-on:click="doRevoke(); closeModal('{{ revoke_modal_name }}')" class='action-group__action usa-button'>Confirm</a>
|
||||||
</span>
|
<a class='action-group__action icon-link icon-link--danger' v-on:click="closeModal('{{ revoke_modal_name }}'); cancel();">Cancel</a>
|
||||||
|
</div>
|
||||||
<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 %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endcall %}
|
||||||
</edit-workspace-member>
|
<ul v-show='props.isVisible'>
|
||||||
</li>
|
{% for env in project.environments %}
|
||||||
|
|
||||||
{% endfor %}
|
{% set role = EnvironmentRoles.get(member.user_id, env.id).role %}
|
||||||
</ul>
|
|
||||||
</template>
|
<li class='block-list__item'>
|
||||||
</div>
|
<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 %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class='action-group'>
|
<div class='action-group'>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from atst.domain.environments import Environments
|
from atst.domain.environments import Environments
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
|
from atst.domain.workspace_users import WorkspaceUsers
|
||||||
|
|
||||||
from tests.factories import UserFactory, WorkspaceFactory
|
from tests.factories import UserFactory, WorkspaceFactory
|
||||||
|
|
||||||
@ -37,7 +38,9 @@ def test_update_environment_roles():
|
|||||||
]
|
]
|
||||||
|
|
||||||
workspace_user = workspace.members[0]
|
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)
|
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)
|
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"
|
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):
|
def test_get_scoped_environments(db):
|
||||||
developer = UserFactory.create()
|
developer = UserFactory.create()
|
||||||
workspace = WorkspaceFactory.create(
|
workspace = WorkspaceFactory.create(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user