diff --git a/alembic/versions/ad30159ef19b_add_view_workspace_members_permission.py b/alembic/versions/ad30159ef19b_add_view_workspace_members_permission.py new file mode 100644 index 00000000..8a3ff320 --- /dev/null +++ b/alembic/versions/ad30159ef19b_add_view_workspace_members_permission.py @@ -0,0 +1,53 @@ +"""add view_workspace_members_permission + +Revision ID: ad30159ef19b +Revises: 2c2a2af465d3 +Create Date: 2018-09-05 11:17:17.204089 + +""" +from alembic import op +from sqlalchemy.orm.session import Session + +from atst.models.role import Role +from atst.models.permissions import Permissions + + +# revision identifiers, used by Alembic. +revision = 'ad30159ef19b' +down_revision = 'c1d074288e99' +branch_labels = None +depends_on = None + +def upgrade(): + + session = Session(bind=op.get_bind()) + + all_roles_but_default = session.query(Role).filter(Role.name != "default").all() + for role in all_roles_but_default: + role.add_permission(Permissions.VIEW_WORKSPACE) + session.add(role) + + owner_and_ccpo = session.query(Role).filter(Role.name.in_(["owner", "ccpo", "admin"])).all() + for role in owner_and_ccpo: + role.add_permission(Permissions.VIEW_WORKSPACE_MEMBERS) + session.add(role) + + session.flush() + session.commit() + + +def downgrade(): + session = Session(bind=op.get_bind()) + + all_roles_but_default = session.query(Role).filter(Role.name != "default").all() + for role in all_roles_but_default: + role.remove_permission(Permissions.VIEW_WORKSPACE) + session.add(role) + + owner_and_ccpo = session.query(Role).filter(Role.name.in_(["owner", "ccpo"])).all() + for role in owner_and_ccpo: + role.remove_permission(Permissions.VIEW_WORKSPACE_MEMBERS) + session.add(role) + + session.flush() + session.commit() diff --git a/atst/domain/workspaces.py b/atst/domain/workspaces.py index c1af06ea..b21c8ca0 100644 --- a/atst/domain/workspaces.py +++ b/atst/domain/workspaces.py @@ -3,7 +3,7 @@ from sqlalchemy.orm.exc import NoResultFound from atst.database import db from atst.models.workspace import Workspace from atst.models.workspace_role import WorkspaceRole -from atst.domain.exceptions import NotFoundError, UnauthorizedError +from atst.domain.exceptions import NotFoundError from atst.domain.roles import Roles from atst.domain.authz import Authorization from atst.models.permissions import Permissions @@ -25,19 +25,16 @@ class Workspaces(object): @classmethod def get(cls, user, workspace_id): - try: - workspace = db.session.query(Workspace).filter_by(id=workspace_id).one() - except NoResultFound: - raise NotFoundError("workspace") - - if not Authorization.is_in_workspace(user, workspace): - raise UnauthorizedError(user, "get workspace") + workspace = Workspaces._get(workspace_id) + Authorization.check_workspace_permission( + user, workspace, Permissions.VIEW_WORKSPACE, "get workspace" + ) return workspace @classmethod def get_for_update(cls, user, workspace_id): - workspace = Workspaces.get(user, workspace_id) + workspace = Workspaces._get(workspace_id) Authorization.check_workspace_permission( user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE, "add project" ) @@ -53,6 +50,18 @@ class Workspaces(object): return workspace + @classmethod + def get_with_members(cls, user, workspace_id): + workspace = Workspaces._get(workspace_id) + Authorization.check_workspace_permission( + user, + workspace, + Permissions.VIEW_WORKSPACE_MEMBERS, + "view workspace members", + ) + + return workspace + @classmethod def get_many(cls, user): workspaces = ( @@ -100,3 +109,12 @@ class Workspaces(object): workspace_role = WorkspaceRole(user=user, role=role, workspace=workspace) db.session.add(workspace_role) return workspace_role + + @classmethod + def _get(cls, workspace_id): + try: + workspace = db.session.query(Workspace).filter_by(id=workspace_id).one() + except NoResultFound: + raise NotFoundError("workspace") + + return workspace diff --git a/atst/models/permissions.py b/atst/models/permissions.py index 9536348f..c39d5b71 100644 --- a/atst/models/permissions.py +++ b/atst/models/permissions.py @@ -23,6 +23,8 @@ class Permissions(object): DEACTIVATE_WORKSPACE = "deactivate_workspace" VIEW_ATAT_PERMISSIONS = "view_atat_permissions" TRANSFER_OWNERSHIP_OF_WORKSPACE = "transfer_ownership_of_workspace" + VIEW_WORKSPACE_MEMBERS = "view_workspace_members" + VIEW_WORKSPACE = "view_workspace" ADD_APPLICATION_IN_WORKSPACE = "add_application_in_workspace" DELETE_APPLICATION_IN_WORKSPACE = "delete_application_in_workspace" diff --git a/atst/models/role.py b/atst/models/role.py index 1205dedd..8833d3e3 100644 --- a/atst/models/role.py +++ b/atst/models/role.py @@ -1,5 +1,6 @@ from sqlalchemy import String, Column from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.orm.attributes import flag_modified from atst.models import Base from .types import Id @@ -12,3 +13,15 @@ class Role(Base): name = Column(String, index=True, unique=True) description = Column(String) permissions = Column(ARRAY(String), index=True, server_default="{}") + + def add_permission(self, permission): + perms_set = set(self.permissions) + perms_set.add(permission) + self.permissions = list(perms_set) + flag_modified(self, "permissions") + + def remove_permission(self, permission): + perms_set = set(self.permissions) + perms_set.discard(permission) + self.permissions = list(perms_set) + flag_modified(self, "permissions") diff --git a/atst/models/workspace.py b/atst/models/workspace.py index edeba529..ad5f2948 100644 --- a/atst/models/workspace.py +++ b/atst/models/workspace.py @@ -4,8 +4,8 @@ from sqlalchemy.orm import relationship 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 +from atst.utils import first_or_none class Workspace(Base, TimestampsMixin): diff --git a/atst/models/workspace_user.py b/atst/models/workspace_user.py index 1677a4bd..934c6f89 100644 --- a/atst/models/workspace_user.py +++ b/atst/models/workspace_user.py @@ -38,7 +38,7 @@ class WorkspaceUser(object): @property def status(self): - return "radical" + return "active" @property def has_environment_roles(self): diff --git a/atst/routes/workspaces.py b/atst/routes/workspaces.py index 0e18ed63..ecd97bac 100644 --- a/atst/routes/workspaces.py +++ b/atst/routes/workspaces.py @@ -63,7 +63,7 @@ def show_workspace(workspace_id): @bp.route("/workspaces//members") def workspace_members(workspace_id): - workspace = Workspaces.get(g.current_user, workspace_id) + workspace = Workspaces.get_with_members(g.current_user, workspace_id) return render_template("workspaces/members/index.html", workspace=workspace) diff --git a/script/seed.py b/script/seed.py index e69694a0..01050427 100644 --- a/script/seed.py +++ b/script/seed.py @@ -15,6 +15,29 @@ from atst.domain.exceptions import AlreadyExistsError from tests.factories import RequestFactory from atst.routes.dev import _DEV_USERS as DEV_USERS +WORKSPACE_USERS = [ + { + "first_name": "Danny", + "last_name": "Knight", + "email": "knight@mil.gov", + "workspace_role": "developer", + "dod_id": "0000000001" + }, + { + "first_name": "Mario", + "last_name": "Hudson", + "email": "hudson@mil.gov", + "workspace_role": "ccpo", + "dod_id": "0000000002" + }, + { + "first_name": "Louise", + "last_name": "Greer", + "email": "greer@mil.gov", + "workspace_role": "admin", + "dod_id": "0000000003" + }, +] def seed_db(): users = [] @@ -41,6 +64,9 @@ def seed_db(): requests.append(request) workspace = Workspaces.create(requests[0], name="{}'s workspace".format(user.first_name)) + for workspace_user in WORKSPACE_USERS: + Workspaces.create_member(user, workspace, workspace_user) + Projects.create( workspace=workspace, name="First Project", diff --git a/tests/domain/test_workspaces.py b/tests/domain/test_workspaces.py index 296c16d9..c95989d1 100644 --- a/tests/domain/test_workspaces.py +++ b/tests/domain/test_workspaces.py @@ -155,3 +155,27 @@ def test_need_permission_to_update_workspace_user_role(): with pytest.raises(UnauthorizedError): Workspaces.update_member(random_user, workspace, member, role_name) + + +def test_owner_can_view_workspace_members(): + owner = UserFactory.create() + workspace = Workspaces.create(RequestFactory.create(creator=owner)) + workspace = Workspaces.get_with_members(owner, workspace.id) + + assert workspace + + +def test_ccpo_can_view_workspace_members(): + workspace = Workspaces.create(RequestFactory.create(creator=UserFactory.create())) + ccpo = UserFactory.from_atat_role("ccpo") + workspace = Workspaces.get_with_members(ccpo, workspace.id) + + assert workspace + + +def test_random_user_cannot_view_workspace_members(): + workspace = Workspaces.create(RequestFactory.create(creator=UserFactory.create())) + developer = UserFactory.from_atat_role("developer") + + with pytest.raises(UnauthorizedError): + workspace = Workspaces.get_with_members(developer, workspace.id)