diff --git a/atst/domain/environment_roles.py b/atst/domain/environment_roles.py new file mode 100644 index 00000000..8b10a2db --- /dev/null +++ b/atst/domain/environment_roles.py @@ -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 diff --git a/atst/domain/environments.py b/atst/domain/environments.py index 3f15fb17..eebc6c15 100644 --- a/atst/domain/environments.py +++ b/atst/domain/environments.py @@ -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() diff --git a/atst/domain/projects.py b/atst/domain/projects.py index 4d03f80f..36c1168d 100644 --- a/atst/domain/projects.py +++ b/atst/domain/projects.py @@ -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 diff --git a/atst/forms/edit_member.py b/atst/forms/edit_member.py index e76df6d0..83333632 100644 --- a/atst/forms/edit_member.py +++ b/atst/forms/edit_member.py @@ -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()] ) diff --git a/atst/models/environment_role.py b/atst/models/environment_role.py index 05357759..594b46f3 100644 --- a/atst/models/environment_role.py +++ b/atst/models/environment_role.py @@ -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): diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index d4e79fe9..2a51f499 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -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", diff --git a/js/components/forms/edit_workspace_member.js b/js/components/forms/edit_workspace_member.js new file mode 100644 index 00000000..2a5c5e2a --- /dev/null +++ b/js/components/forms/edit_workspace_member.js @@ -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" + }, + } +} diff --git a/js/index.js b/js/index.js index 653038f4..204f9871 100644 --- a/js/index.js +++ b/js/index.js @@ -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() { diff --git a/js/mixins/modal.js b/js/mixins/modal.js index bb2bba05..df168c59 100644 --- a/js/mixins/modal.js +++ b/js/mixins/modal.js @@ -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, } } } diff --git a/script/seed_sample.py b/script/seed_sample.py index 237b10a3..1e7b9f46 100644 --- a/script/seed_sample.py +++ b/script/seed_sample.py @@ -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", }, { diff --git a/templates/components/modal.html b/templates/components/modal.html index 970b444d..8897238d 100644 --- a/templates/components/modal.html +++ b/templates/components/modal.html @@ -1,7 +1,7 @@ {% from "components/icon.html" import Icon %} {% macro Modal(name, dismissable=False) -%} - + {%- endmacro %} diff --git a/templates/workspaces/members/edit.html b/templates/workspaces/members/edit.html index b12538fa..bc40122c 100644 --- a/templates/workspaces/members/edit.html +++ b/templates/workspaces/members/edit.html @@ -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 @@ - {% call Modal(name='rolesModal', dismissable=False) %} -
-
-
-

- Environment access for Danny Knight -
Project Name - Environment Name
-

-
-

An environment role determines the permissions a member of the workspace assumes when using the JEDI Cloud.

-

A member may have different environment roles across different projects. A member can only have one assigned environment role in a given environment.

-
-
-
- -
- - - -
- -
- - {% endcall %} - + {% for project in projects %}
-
+ {% for env in project.environments %} -
-
+ {% endfor %}
- + diff --git a/tests/domain/test_environments.py b/tests/domain/test_environments.py new file mode 100644 index 00000000..2b7f4995 --- /dev/null +++ b/tests/domain/test_environments.py @@ -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" diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 06d3f836..90464b93 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -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) diff --git a/tests/models/test_environments.py b/tests/models/test_environments.py index 05be623f..32faeef2 100644 --- a/tests/models/test_environments.py +++ b/tests/models/test_environments.py @@ -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 diff --git a/tests/models/test_workspace_user.py b/tests/models/test_workspace_user.py index fac20d68..da1f08d0 100644 --- a/tests/models/test_workspace_user.py +++ b/tests/models/test_workspace_user.py @@ -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 diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py index fcab09d2..3523d207 100644 --- a/tests/routes/test_workspaces.py +++ b/tests/routes/test_workspaces.py @@ -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"