From ea1a3926accaa193801f43ae94273f39a67a8a31 Mon Sep 17 00:00:00 2001 From: Montana Date: Fri, 31 Aug 2018 14:12:02 -0400 Subject: [PATCH 01/14] Wire up selector for updating a workspace user's role --- atst/forms/update_member.py | 16 ++ atst/models/workspace.py | 3 +- atst/models/workspace_user.py | 17 ++ atst/routes/workspaces.py | 29 +++ templates/member_edit.html | 302 +++++++++++-------------------- templates/workspace_members.html | 6 +- 6 files changed, 177 insertions(+), 196 deletions(-) create mode 100644 atst/forms/update_member.py diff --git a/atst/forms/update_member.py b/atst/forms/update_member.py new file mode 100644 index 00000000..236cfbb8 --- /dev/null +++ b/atst/forms/update_member.py @@ -0,0 +1,16 @@ +from flask_wtf import Form +from wtforms.fields import StringField +from wtforms.fields.html5 import EmailField +from wtforms.validators import Required, Email, Length + +from atst.forms.validators import IsNumber +from atst.forms.fields import SelectField + +from .data import WORKSPACE_ROLES + + +class UpdateMemberForm(Form): + + workspace_role = SelectField( + "Workspace Role", choices=WORKSPACE_ROLES, validators=[Required()], default="" + ) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index aca024a4..1128079a 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -5,6 +5,7 @@ from atst.models import Base from atst.models.types import Id from atst.models.mixins import TimestampsMixin from atst.utils import first_or_none +from atst.models.workspace_user import WorkspaceUser MOCK_MEMBERS = [ @@ -68,4 +69,4 @@ class Workspace(Base, TimestampsMixin): @property def members(self): - return MOCK_MEMBERS + return [ WorkspaceUser(role.user, role) for role in self.roles] diff --git a/atst/models/workspace_user.py b/atst/models/workspace_user.py index 5e3ee1ed..59576a17 100644 --- a/atst/models/workspace_user.py +++ b/atst/models/workspace_user.py @@ -16,3 +16,20 @@ class WorkspaceUser(object): def workspace_id(self): return self.workspace_role.workspace_id + + @property + def user_id(self): + return self.user.id + + @property + def user_name(self): + return self.user.full_name + + @property + def role(self): + return self.workspace_role.role.name + + @property + def status(self): + return "radical" + diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 665d266c..f6e8a5f1 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -9,9 +9,12 @@ from flask import ( from atst.domain.exceptions import UnauthorizedError from atst.domain.workspaces import Workspaces +from atst.domain.workspace_users import WorkspaceUsers from atst.domain.projects import Projects from atst.forms.new_project import NewProjectForm from atst.forms.new_member import NewMemberForm +from atst.forms.update_member import UpdateMemberForm +from atst.forms.forms import ValidatedForm from atst.domain.authz import Authorization from atst.models.permissions import Permissions @@ -119,3 +122,29 @@ def create_member(workspace_id): ) else: return render_template("member_new.html", workspace=workspace, form=form) + + +@bp.route("/workspaces//members//member_edit") +def view_member(workspace_id, member_id): + workspace = Workspaces.get(g.current_user, workspace_id) + member = WorkspaceUsers.get(workspace_id, member_id) + form = NewMemberForm(http_request.form) + + return render_template("member_edit.html", form=form, workspace=workspace, member=member) + +@bp.route("/workspaces//members//member_edit", methods=['POST']) +def update_member(workspace_id, member_id): + workspace = Workspaces.get(g.current_user, workspace_id) + member = WorkspaceUsers.get(workspace_id, member_id) + form = UpdateMemberForm(http_request.form) + + if form.validate(): + return redirect( + url_for( + "workspaces.workspace_members", + workspace_id=workspace.id, + ) + ) + else: + return render_template("member_edit.html", form=form, workspace=workspace, member=member) + diff --git a/templates/member_edit.html b/templates/member_edit.html index db52347b..12eb0245 100644 --- a/templates/member_edit.html +++ b/templates/member_edit.html @@ -2,48 +2,44 @@ {% from "components/icon.html" import Icon %} {% from "components/modal.html" import Modal %} +{% from "components/selector.html" import Selector %} {% block content %} -
-
-

Danny Knight

+
+ {{ form.csrf_token }} + +
+
+

Danny Knight

+ +
+ + {{ Selector(form.workspace_role) }} +
-
- -
- +
+
+
+
DOD ID:
+
789
+
+
+
Email:
+
knight@mil.gov
+
+
+ edit account details +
-
-
-
-
DOD ID:
-
789
-
-
-
Email:
-
knight@mil.gov
-
-
- edit account details -
-
-
-
-

Manage Access
Grant access to an environment

+
+
+

Manage Access
Grant access to an environment

+
-
-
@@ -51,174 +47,96 @@ Search
- -{% call Modal(name='rolesModal', dismissable=False) %} -
-
-

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

-
- -
-
    -
  • - - +
    +
    -{% endcall %} +
    + +
    -
    - -
    +
    + + + {{ Icon('x') }} + Cancel + +
    -
    - -
    - - +
  • diff --git a/templates/workspace_members.html b/templates/workspace_members.html index bf61ea3d..403a0f97 100644 --- a/templates/workspace_members.html +++ b/templates/workspace_members.html @@ -77,10 +77,10 @@ {% for m in workspace.members %} - {{ m['first_name'] }} {{ m['last_name'] }} + {{ m.user_name }} {% if m['num_projects'] == '0' %} No Project Access {% endif %} - {{ m['status'] }} - {{ m['workspace_role'] }} + {{ m.status }} + {{ m.role }} {% endfor %} From b379972446d02e973891bb71c8c7014c4fdf5008 Mon Sep 17 00:00:00 2001 From: Montana Date: Fri, 31 Aug 2018 15:44:15 -0400 Subject: [PATCH 02/14] Check permissions before viewing member edit page --- atst/domain/authz.py | 6 ++++++ atst/models/workspace.py | 2 +- atst/models/workspace_user.py | 1 - atst/routes/workspaces.py | 24 ++++++++++++++++-------- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/atst/domain/authz.py b/atst/domain/authz.py index 20bd539f..65db9894 100644 --- a/atst/domain/authz.py +++ b/atst/domain/authz.py @@ -1,5 +1,6 @@ from atst.domain.workspace_users import WorkspaceUsers from atst.models.permissions import Permissions +from atst.domain.exceptions import UnauthorizedError class Authorization(object): @@ -23,3 +24,8 @@ class Authorization(object): return True return False + + @classmethod + def check_workspace_permission(cls, user, workspace, permission, message): + if not Authorization.has_workspace_permission(user, workspace, permission): + raise UnauthorizedError(user, message) diff --git a/atst/models/workspace.py b/atst/models/workspace.py index 1128079a..9052e204 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -69,4 +69,4 @@ class Workspace(Base, TimestampsMixin): @property def members(self): - return [ WorkspaceUser(role.user, role) for role in self.roles] + return [WorkspaceUser(role.user, role) for role in self.roles] diff --git a/atst/models/workspace_user.py b/atst/models/workspace_user.py index 59576a17..3c92707e 100644 --- a/atst/models/workspace_user.py +++ b/atst/models/workspace_user.py @@ -32,4 +32,3 @@ class WorkspaceUser(object): @property def status(self): return "radical" - diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index f6e8a5f1..7506aa45 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -127,12 +127,22 @@ def create_member(workspace_id): @bp.route("/workspaces//members//member_edit") def view_member(workspace_id, member_id): workspace = Workspaces.get(g.current_user, workspace_id) + Authorization.check_workspace_permission( + g.current_user, + workspace, + Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE, + "edit this workspace user", + ) member = WorkspaceUsers.get(workspace_id, member_id) form = NewMemberForm(http_request.form) + return render_template( + "member_edit.html", form=form, workspace=workspace, member=member + ) - return render_template("member_edit.html", form=form, workspace=workspace, member=member) -@bp.route("/workspaces//members//member_edit", methods=['POST']) +@bp.route( + "/workspaces//members//member_edit", methods=["POST"] +) def update_member(workspace_id, member_id): workspace = Workspaces.get(g.current_user, workspace_id) member = WorkspaceUsers.get(workspace_id, member_id) @@ -140,11 +150,9 @@ def update_member(workspace_id, member_id): if form.validate(): return redirect( - url_for( - "workspaces.workspace_members", - workspace_id=workspace.id, - ) + url_for("workspaces.workspace_members", workspace_id=workspace.id) ) else: - return render_template("member_edit.html", form=form, workspace=workspace, member=member) - + return render_template( + "member_edit.html", form=form, workspace=workspace, member=member + ) From 8faed87e00a2624e14fb47c32813205b279f46f2 Mon Sep 17 00:00:00 2001 From: Montana Date: Fri, 31 Aug 2018 16:45:19 -0400 Subject: [PATCH 03/14] Update workspace role in the DB, add tests --- atst/domain/workspace_users.py | 25 +++++++++++++++++++++++ atst/domain/workspaces.py | 9 +++++++++ atst/routes/workspaces.py | 3 +++ tests/domain/test_workspaces.py | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/atst/domain/workspace_users.py b/atst/domain/workspace_users.py index 6fb761c3..d37ec736 100644 --- a/atst/domain/workspace_users.py +++ b/atst/domain/workspace_users.py @@ -30,6 +30,21 @@ class WorkspaceUsers(object): return WorkspaceUser(user, workspace_role) + @classmethod + def _get_workspace_role(cls, user, workspace_id): + try: + existing_workspace_role = ( + db.session.query(WorkspaceRole) + .filter( + WorkspaceRole.user == user, + WorkspaceRole.workspace_id == workspace_id, + ) + .one() + ) + return existing_workspace_role + except NoResultFound: + raise NotFoundError("workspace role") + @classmethod def add(cls, user, workspace_id, role_name): role = Roles.get(role_name) @@ -57,6 +72,16 @@ class WorkspaceUsers(object): return WorkspaceUser(user, new_workspace_role) + @classmethod + def update_role(cls, member, workspace_id, role_name): + new_role = Roles.get(role_name) + workspace_role = WorkspaceUsers._get_workspace_role(member.user, workspace_id) + workspace_role.role = new_role + + db.session.add(workspace_role) + db.session.commit() + return WorkspaceUser(member.user, workspace_role) + @classmethod def add_many(cls, workspace_id, workspace_user_dicts): workspace_users = [] diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index 6570a831..75a6225b 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -81,6 +81,15 @@ class Workspaces(object): ) return workspace_user + @classmethod + def update_member(cls, user, workspace, member, role_name): + if not Authorization.has_workspace_permission( + user, workspace, Permissions.ASSIGN_AND_UNASSIGN_ATAT_ROLE + ): + raise UnauthorizedError(user, "update workspace member") + + return WorkspaceUsers.update_role(member, workspace.id, role_name) + @classmethod def _create_workspace_role(cls, user, workspace, role_name): role = Roles.get(role_name) diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 7506aa45..06b32008 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -149,6 +149,9 @@ def update_member(workspace_id, member_id): form = UpdateMemberForm(http_request.form) if form.validate(): + Workspaces.update_member( + g.current_user, workspace, member, form.data["workspace_role"] + ) return redirect( url_for("workspaces.workspace_members", workspace_id=workspace.id) ) diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index f3cca7f9..dc48ee48 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -119,3 +119,38 @@ def test_need_permission_to_create_workspace_user(): with pytest.raises(UnauthorizedError): Workspaces.create_member(random_user, workspace, user_data) + + +def test_can_update_workspace_user_role(): + owner = UserFactory.create() + workspace = Workspaces.create(RequestFactory.create(creator=owner)) + user_data = { + "first_name": "New", + "last_name": "User", + "email": "new.user@mail.com", + "workspace_role": "developer", + "dod_id": "1234567890", + } + member = Workspaces.create_member(owner, workspace, user_data) + role_name = "developer" + + updated_member = Workspaces.update_member(owner, workspace, member, role_name) + assert updated_member.workspace == workspace + + +def test_need_permission_to_update_workspace_user_role(): + owner = UserFactory.create() + workspace = Workspaces.create(RequestFactory.create(creator=owner)) + random_user = UserFactory.create() + user_data = { + "first_name": "New", + "last_name": "User", + "email": "new.user@mail.com", + "workspace_role": "developer", + "dod_id": "1234567890", + } + member = Workspaces.create_member(owner, workspace, user_data) + role_name = "developer" + + with pytest.raises(UnauthorizedError): + Workspaces.update_member(random_user, workspace, member, role_name) From ce44e4537a6514f82941e76b2958328bd1f1832a Mon Sep 17 00:00:00 2001 From: Montana Date: Tue, 4 Sep 2018 10:34:30 -0400 Subject: [PATCH 04/14] Add an alert for successful role update --- atst/routes/workspaces.py | 6 +++++- templates/workspace_members.html | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 06b32008..f3ad9092 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -153,7 +153,11 @@ def update_member(workspace_id, member_id): g.current_user, workspace, member, form.data["workspace_role"] ) return redirect( - url_for("workspaces.workspace_members", workspace_id=workspace.id) + url_for( + "workspaces.workspace_members", + workspace_id=workspace.id, + memberName=member.user.full_name, + ) ) else: return render_template( diff --git a/templates/workspace_members.html b/templates/workspace_members.html index 403a0f97..0c1f3c7e 100644 --- a/templates/workspace_members.html +++ b/templates/workspace_members.html @@ -33,6 +33,18 @@ ) }} {% endif %} +{% set updated_member_name = request.args.get("memberName") %} +{% if updated_member_name %} + {% set message -%} +

    {{ updated_member_name }}'s role was successfully updated.

    + {%- endset %} + + {{ Alert('Workspace role updated successfully', + message=message, + level='success' + ) }} +{% endif %} +