From 03d2a20fbe8c43a4fbcc4c64cabd1d5af9295dfb Mon Sep 17 00:00:00 2001 From: Adam Coldrick Date: Wed, 2 Dec 2015 16:09:14 +0000 Subject: [PATCH] Implement complex permissions for worklists and boards We default to two levels of permissions, each with potential for containing multiple users. This commit adds a database migration which adds two tables (worklist_permissions and board_permissions) to represent the relationships between worklists and permissions and boards and permissions. Board permissions override worklist permissions if the worklist is a lane in that board. Existing boards and worklists will have permissions created such that the creator has full permissions on the board/worklist. Change-Id: I4168572cc98a93a59d8fe6f35d48432959ac7adf --- storyboard/api/v1/boards.py | 124 +++++++++++------- storyboard/api/v1/wmodels.py | 12 ++ storyboard/api/v1/worklists.py | 122 ++++++++++++++--- storyboard/db/api/boards.py | 71 +++++++++- storyboard/db/api/users.py | 5 +- storyboard/db/api/worklists.py | 68 +++++++++- ...add_detailed_permissions_to_boards_and_.py | 118 +++++++++++++++++ storyboard/db/models.py | 19 ++- 8 files changed, 470 insertions(+), 69 deletions(-) create mode 100644 storyboard/db/migration/alembic_migrations/versions/050_add_detailed_permissions_to_boards_and_.py diff --git a/storyboard/api/v1/boards.py b/storyboard/api/v1/boards.py index 92459a41..78d0ac64 100644 --- a/storyboard/api/v1/boards.py +++ b/storyboard/api/v1/boards.py @@ -15,7 +15,6 @@ from oslo_config import cfg from pecan import abort -from pecan import expose from pecan import request from pecan import response from pecan import rest @@ -39,8 +38,9 @@ def visible(board, user=None): if not board: return False if user and board.private: - # TODO(SotK): Permissions - return user == board.creator_id + permissions = boards_api.get_permissions(board.id, user) + return any(name in permissions + for name in ['edit_board', 'move_cards']) return not board.private @@ -49,8 +49,7 @@ def editable(board, user=None): return False if not user: return False - # TODO(SotK): Permissions - return user == board.creator_id + return 'edit_board' in boards_api.get_permissions(board.id, user) def get_lane(list_id, board): @@ -79,6 +78,47 @@ def update_lanes(board_dict, board_id): del board_dict['lanes'] +class PermissionsController(rest.RestController): + """Manages operations on board permissions.""" + + @decorators.db_exceptions + @secure(checks.guest) + @wsme_pecan.wsexpose([wtypes.text], int) + def get(self, board_id): + """Get board permissions for the current user. + + :param board_id: The ID of the board. + + """ + return boards_api.get_permissions(board_id, request.current_user_id) + + @decorators.db_exceptions + @secure(checks.authenticated) + @wsme_pecan.wsexpose(wtypes.text, int, + body=wtypes.DictType(wtypes.text, wtypes.text)) + def post(self, board_id, permission): + """Add a new permission to the board. + + :param board_id: The ID of the board. + :param permission: The dict to use to create the permission. + + """ + return boards_api.create_permission(board_id) + + @decorators.db_exceptions + @secure(checks.authenticated) + @wsme_pecan.wsexpose(wtypes.text, int, + body=wtypes.DictType(wtypes.text, wtypes.text)) + def put(self, board_id, permission): + """Update a permission of the board. + + :param board_id: The ID of the board. + :param permission: The new contents of the permission. + + """ + return boards_api.update_permission(board_id, permission).codename + + class BoardsController(rest.RestController): """Manages operations on boards.""" @@ -98,6 +138,8 @@ class BoardsController(rest.RestController): board_model = wmodels.Board.from_db_model(board) board_model.lanes = [wmodels.Lane.from_db_model(lane) for lane in board.lanes] + board_model.owners = boards_api.get_owners(id) + board_model.users = boards_api.get_users(id) return board_model else: raise exc.NotFound(_("Board %s not found") % id) @@ -105,9 +147,10 @@ class BoardsController(rest.RestController): @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Board], wtypes.text, int, int, - bool, wtypes.text, wtypes.text) + bool, int, wtypes.text, wtypes.text) def get_all(self, title=None, creator_id=None, project_id=None, - archived=False, sort_field='id', sort_dir='asc'): + archived=False, user_id=None, sort_field='id', + sort_dir='asc'): """Retrieve definitions of all of the boards. :param title: A string to filter the title by. @@ -120,6 +163,7 @@ class BoardsController(rest.RestController): """ boards = boards_api.get_all(title=title, creator_id=creator_id, + user_id=user_id, project_id=project_id, sort_field=sort_field, sort_dir=sort_dir) @@ -131,6 +175,8 @@ class BoardsController(rest.RestController): board_model = wmodels.Board.from_db_model(board) board_model.lanes = [wmodels.Lane.from_db_model(lane) for lane in board.lanes] + board_model.owners = boards_api.get_owners(board.id) + board_model.users = boards_api.get_users(board.id) visible_boards.append(board_model) # Apply the query response headers @@ -154,11 +200,30 @@ class BoardsController(rest.RestController): abort(400, _("You can't select the creator of a board.")) board_dict.update({"creator_id": user_id}) lanes = board_dict.pop('lanes') + owners = board_dict.pop('owners') + users = board_dict.pop('users') + if not owners: + owners = [user_id] + if not users: + users = [] created_board = boards_api.create(board_dict) for lane in lanes: boards_api.add_lane(created_board.id, lane.as_dict()) + edit_permission = { + 'name': 'edit_board_%d' % created_board.id, + 'codename': 'edit_board', + 'users': owners + } + move_permission = { + 'name': 'move_cards_%d' % created_board.id, + 'codename': 'move_cards', + 'users': users + } + boards_api.create_permission(created_board.id, edit_permission) + boards_api.create_permission(created_board.id, move_permission) + return wmodels.Board.from_db_model(created_board) @decorators.db_exceptions @@ -177,13 +242,15 @@ class BoardsController(rest.RestController): board_dict = board.as_dict(omit_unset=True) update_lanes(board_dict, id) - boards_api.update(id, board_dict) + updated_board = boards_api.update(id, board_dict) if visible(board, user_id): - board_model = wmodels.Board.from_db_model(board) + board_model = wmodels.Board.from_db_model(updated_board) if board.lanes: board_model.lanes = [wmodels.Lane.from_db_model(lane) for lane in board.lanes] + board_model.owners = boards_api.get_owners(id) + board_model.users = boards_api.get_users(id) return board_model else: raise exc.NotFound(_("Board %s not found") % id) @@ -202,40 +269,9 @@ class BoardsController(rest.RestController): if not editable(board, user_id): raise exc.NotFound(_("Board %s not found") % id) - board_dict = wmodels.Board.from_db_model(board).as_dict( - omit_unset=True) - board_dict['lanes'] = board.lanes - board_dict.update({"archived": True}) - boards_api.update(id, board_dict) + boards_api.update(id, {"archived": True}) - for lane in board_dict['lanes']: - worklist = lane.worklist - worklist_dict = wmodels.Worklist.from_db_model(worklist).as_dict( - omit_unset=True) - worklist_dict.update({'archived': True}) - worklists_api.update(worklist.id, worklist_dict) + for lane in board.lanes: + worklists_api.update(lane.worklist.id, {"archived": True}) - @decorators.db_exceptions - @secure(checks.guest) - @wsme_pecan.wsexpose(wtypes.DictType(str, bool), int) - def permissions(self, id): - """Get the permissions the current user has for the board. - - :param id: The ID of the board to check permissions for. - - """ - board = boards_api.get(id) - user_id = request.current_user_id - return { - 'edit_board': editable(board, user_id), - 'move_cards': editable(board, user_id) # TODO(SotK): check lanes - } - - @expose() - def _route(self, args, request): - if request.method == 'GET' and len(args) > 1: - if args[1] == "permissions": - # Request to a permissions endpoint - return self.permissions, [args[0]] - - return super(BoardsController, self)._route(args, request) + permissions = PermissionsController() diff --git a/storyboard/api/v1/wmodels.py b/storyboard/api/v1/wmodels.py index 730e09f0..057056bc 100644 --- a/storyboard/api/v1/wmodels.py +++ b/storyboard/api/v1/wmodels.py @@ -472,6 +472,12 @@ class Worklist(base.APIBase): """A flag to identify whether the contents are obtained by a filter or are stored in the database.""" + owners = wtypes.ArrayType(int) + """A list of the IDs of the users who have full permissions.""" + + users = wtypes.ArrayType(int) + """A list of the IDs of the users who can move items in the worklist.""" + # NOTE(SotK): Criteria/Criterion is used as the existing code in the webclient # refers to such filters as Criteria. @@ -549,3 +555,9 @@ class Board(base.APIBase): lanes = wtypes.ArrayType(Lane) """A list containing the representions of the lanes in this board.""" + + owners = wtypes.ArrayType(int) + """A list of the IDs of the users who have full permissions.""" + + users = wtypes.ArrayType(int) + """A list of the IDs of the users who can move cards in the board.""" diff --git a/storyboard/api/v1/worklists.py b/storyboard/api/v1/worklists.py index 8780a925..159f01da 100644 --- a/storyboard/api/v1/worklists.py +++ b/storyboard/api/v1/worklists.py @@ -26,6 +26,7 @@ from storyboard.api.auth import authorization_checks as checks from storyboard.api.v1 import wmodels from storyboard.common import decorators from storyboard.common import exception as exc +from storyboard.db.api import boards as boards_api from storyboard.db.api import worklists as worklists_api from storyboard.openstack.common.gettextutils import _ # noqa @@ -39,9 +40,17 @@ def visible(worklist, user=None, hide_lanes=False): return False if not worklist: return False + if worklists_api.is_lane(worklist): + board = boards_api.get_from_lane(worklist) + permissions = boards_api.get_permissions(board.id, user) + if board.private: + return any(name in permissions + for name in ['edit_board', 'move_cards']) + return not board.private if user and worklist.private: - # TODO(SotK): Permissions - return user == worklist.creator_id + permissions = worklists_api.get_permissions(worklist.id, user) + return any(name in permissions + for name in ['edit_worklist', 'move_items']) return not worklist.private @@ -50,8 +59,55 @@ def editable(worklist, user=None): return False if not user: return False - # TODO(SotK): Permissions - return user == worklist.creator_id + if worklists_api.is_lane(worklist): + board = boards_api.get_from_lane(worklist) + permissions = boards_api.get_permissions(board.id, user) + return any(name in permissions + for name in ['edit_board', 'move_cards']) + return 'edit_worklist' in worklists_api.get_permissions(worklist.id, user) + + +class PermissionsController(rest.RestController): + """Manages operations on worklist permissions.""" + + @decorators.db_exceptions + @secure(checks.guest) + @wsme_pecan.wsexpose([wtypes.text], int) + def get(self, worklist_id): + """Get worklist permissions for the current user. + + :param worklist_id: The ID of the worklist. + + """ + return worklists_api.get_permissions( + worklist_id, request.current_user_id) + + @decorators.db_exceptions + @secure(checks.authenticated) + @wsme_pecan.wsexpose(wtypes.text, int, + body=wtypes.DictType(wtypes.text, wtypes.text)) + def post(self, worklist_id, permission): + """Add a new permission to the worklist. + + :param worklist_id: The ID of the worklist. + :param permission: The dict to use to create the permission. + + """ + return worklists_api.create_permission(worklist_id) + + @decorators.db_exceptions + @secure(checks.authenticated) + @wsme_pecan.wsexpose(wtypes.text, int, + body=wtypes.DictType(wtypes.text, wtypes.text)) + def put(self, worklist_id, permission): + """Update a permission of the worklist. + + :param worklist_id: The ID of the worklist. + :param permission: The new contents of the permission. + + """ + return worklists_api.update_permission( + worklist_id, permission).codename class ItemsSubcontroller(rest.RestController): @@ -149,23 +205,27 @@ class WorklistsController(rest.RestController): user_id = request.current_user_id if worklist and visible(worklist, user_id): - return wmodels.Worklist.from_db_model(worklist) + worklist_model = wmodels.Worklist.from_db_model(worklist) + worklist_model.owners = worklists_api.get_owners(worklist.id) + worklist_model.users = worklists_api.get_users(worklist.id) + return worklist_model else: raise exc.NotFound(_("Worklist %s not found") % worklist_id) @decorators.db_exceptions @secure(checks.guest) @wsme_pecan.wsexpose([wmodels.Worklist], wtypes.text, int, int, - bool, bool, wtypes.text, wtypes.text, int) + bool, int, bool, wtypes.text, wtypes.text, int) def get_all(self, title=None, creator_id=None, project_id=None, - archived=False, hide_lanes=True, sort_field='id', - sort_dir='asc', board_id=None): + archived=False, user_id=None, hide_lanes=True, + sort_field='id', sort_dir='asc', board_id=None): """Retrieve definitions of all of the worklists. :param title: A string to filter the title by. :param creator_id: Filter worklists by their creator. :param project_id: Filter worklists by project ID. :param archived: Filter worklists by whether they are archived or not. + :param user_id: Filter worklists by the users with permissions. :param hide_lanes: If true, don't return worklists which are lanes in a board. :param sort_field: The name of the field to sort on. @@ -179,14 +239,19 @@ class WorklistsController(rest.RestController): project_id=project_id, archived=archived, board_id=board_id, + user_id=user_id, sort_field=sort_field, sort_dir=sort_dir) user_id = request.current_user_id - visible_worklists = [wmodels.Worklist.from_db_model(w) - for w in worklists - if w.archived == archived - and visible(w, user_id, hide_lanes)] + visible_worklists = [] + for worklist in worklists: + if (visible(worklist, user_id, hide_lanes) and + worklist.archived == archived): + worklist_model = wmodels.Worklist.from_db_model(worklist) + worklist_model.owners = worklists_api.get_owners(worklist.id) + worklist_model.users = worklists_api.get_users(worklist.id) + visible_worklists.append(worklist_model) # Apply the query response headers response.headers['X-Total'] = str(len(visible_worklists)) @@ -208,9 +273,28 @@ class WorklistsController(rest.RestController): if worklist.creator_id and worklist.creator_id != user_id: abort(400, _("You can't select the creator of a worklist.")) worklist_dict.update({"creator_id": user_id}) + owners = worklist_dict.pop('owners') + users = worklist_dict.pop('users') + if not owners: + owners = [user_id] + if not users: + users = [] created_worklist = worklists_api.create(worklist_dict) + edit_permission = { + 'name': 'edit_worklist_%d' % created_worklist.id, + 'codename': 'edit_worklist', + 'users': owners + } + move_permission = { + 'name': 'move_items_%d' % created_worklist.id, + 'codename': 'move_items', + 'users': users + } + worklists_api.create_permission(created_worklist.id, edit_permission) + worklists_api.create_permission(created_worklist.id, move_permission) + return wmodels.Worklist.from_db_model(created_worklist) @decorators.db_exceptions @@ -230,7 +314,14 @@ class WorklistsController(rest.RestController): updated_worklist = worklists_api.update( id, worklist.as_dict(omit_unset=True)) - return wmodels.Worklist.from_db_model(updated_worklist) + if visible(updated_worklist, user_id): + worklist_model = wmodels.Worklist.from_db_model(updated_worklist) + worklist_model.owners = worklists_api.get_owners( + updated_worklist.id) + worklist_model.users = worklists_api.get_users(updated_worklist.id) + return worklist_model + else: + raise exc.NotFound(_("Worklist %s not found")) @decorators.db_exceptions @secure(checks.authenticated) @@ -246,8 +337,7 @@ class WorklistsController(rest.RestController): if not editable(worklist, user_id): raise exc.NotFound(_("Worklist %s not found") % worklist_id) - worklist_dict = wmodels.Worklist.from_db_model(worklist).as_dict() - worklist_dict.update({"archived": True}) - worklists_api.update(worklist_id, worklist_dict) + worklists_api.update(worklist_id, {"archived": True}) items = ItemsSubcontroller() + permissions = PermissionsController() diff --git a/storyboard/db/api/boards.py b/storyboard/db/api/boards.py index d519ba54..c983b063 100644 --- a/storyboard/db/api/boards.py +++ b/storyboard/db/api/boards.py @@ -18,6 +18,7 @@ from wsme.exc import ClientSideError from storyboard.common import exception as exc from storyboard.db.api import base as api_base +from storyboard.db.api import users as users_api from storyboard.db import models from storyboard.openstack.common.gettextutils import _ # noqa @@ -35,8 +36,17 @@ def get(id): return _board_get(id) -def get_all(title=None, creator_id=None, project_id=None, +def get_all(title=None, creator_id=None, user_id=None, project_id=None, sort_field=None, sort_dir=None, **kwargs): + if user_id is not None: + user = users_api.user_get(user_id) + boards = [] + for board in get_all(): + if any(permission in board.permissions + for permission in user.permissions): + boards.append(board) + return boards + return api_base.entity_get_all(models.Board, title=title, creator_id=creator_id, @@ -82,3 +92,62 @@ def update_lane(board_id, lane, new_lane): raise ClientSideError(_("A lane must have a worklist_id.")) api_base.entity_update(models.BoardWorklist, lane.id, new_lane) + + +def get_from_lane(worklist): + for board in get_all(): + if worklist.id in [lane.list_id for lane in board.lanes]: + return board + + +def get_owners(board_id): + board = _board_get(board_id) + for permission in board.permissions: + if permission.codename == 'edit_board': + return [user.id for user in permission.users] + + +def get_users(board_id): + board = _board_get(board_id) + for permission in board.permissions: + if permission.codename == 'move_cards': + return [user.id for user in permission.users] + + +def get_permissions(board_id, user_id): + board = _board_get(board_id) + user = users_api.user_get(user_id) + if user is not None: + return [permission.codename for permission in board.permissions + if permission in user.permissions] + return [] + + +def create_permission(board_id, permission_dict, session=None): + board = _board_get(board_id, session=session) + users = permission_dict.pop('users') + permission = api_base.entity_create( + models.Permission, permission_dict, session=session) + board.permissions.append(permission) + for user_id in users: + user = users_api.user_get(user_id, session=session) + user.permissions.append(permission) + return permission + + +def update_permission(board_id, permission_dict): + board = _board_get(board_id) + id = None + for permission in board.permissions: + if permission.codename == permission_dict['codename']: + id = permission.id + users = permission_dict.pop('users') + permission_dict['users'] = [] + for user_id in users: + user = users_api.user_get(user_id) + permission_dict['users'].append(user) + + if id is None: + raise ClientSideError(_("Permission %s does not exist") + % permission_dict['codename']) + return api_base.entity_update(models.Permission, id, permission_dict) diff --git a/storyboard/db/api/users.py b/storyboard/db/api/users.py index d9cac9b1..3059d822 100644 --- a/storyboard/db/api/users.py +++ b/storyboard/db/api/users.py @@ -18,9 +18,10 @@ from storyboard.db import models from storyboard.plugin.user_preferences import PREFERENCE_DEFAULTS -def user_get(user_id, filter_non_public=False): +def user_get(user_id, filter_non_public=False, session=None): entity = api_base.entity_get(models.User, user_id, - filter_non_public=filter_non_public) + filter_non_public=filter_non_public, + session=session) return entity diff --git a/storyboard/db/api/worklists.py b/storyboard/db/api/worklists.py index de219b04..1a226561 100644 --- a/storyboard/db/api/worklists.py +++ b/storyboard/db/api/worklists.py @@ -21,6 +21,7 @@ from storyboard.db.api import base as api_base from storyboard.db.api import boards from storyboard.db.api import stories from storyboard.db.api import tasks +from storyboard.db.api import users as users_api from storyboard.db import models from storyboard.openstack.common.gettextutils import _ # noqa @@ -38,9 +39,17 @@ def get(worklist_id): return _worklist_get(worklist_id) -def get_all(title=None, creator_id=None, project_id=None, - board_id=None, sort_field=None, sort_dir=None, - **kwargs): +def get_all(title=None, creator_id=None, project_id=None, board_id=None, + user_id=None, sort_field=None, sort_dir=None, **kwargs): + if user_id is not None: + user = users_api.user_get(user_id) + worklists = [] + for worklist in get_all(): + if any(permission in worklist.permissions + for permission in user.permissions): + worklists.append(worklist) + return worklists + if board_id is not None: board = boards.get(board_id) return [lane.worklist for lane in board.lanes] @@ -168,3 +177,56 @@ def is_lane(worklist): if lanes: return True return False + + +def get_owners(worklist_id): + worklist = _worklist_get(worklist_id) + for permission in worklist.permissions: + if permission.codename == 'edit_worklist': + return [user.id for user in permission.users] + + +def get_users(worklist_id): + worklist = _worklist_get(worklist_id) + for permission in worklist.permissions: + if permission.codename == 'move_items': + return [user.id for user in permission.users] + + +def get_permissions(worklist_id, user_id): + worklist = _worklist_get(worklist_id) + user = users_api.user_get(user_id) + if user is not None: + return [permission.codename for permission in worklist.permissions + if permission in user.permissions] + return [] + + +def create_permission(worklist_id, permission_dict, session=None): + worklist = _worklist_get(worklist_id, session=session) + users = permission_dict.pop('users') + permission = api_base.entity_create( + models.Permission, permission_dict, session=session) + worklist.permissions.append(permission) + for user_id in users: + user = users_api.user_get(user_id, session=session) + user.permissions.append(permission) + return permission + + +def update_permission(worklist_id, permission_dict): + worklist = _worklist_get(worklist_id) + id = None + for permission in worklist.permissions: + if permission.codename == permission_dict['codename']: + id = permission.id + users = permission_dict.pop('users') + permission_dict['users'] = [] + for user_id in users: + user = users_api.user_get(user_id) + permission_dict['users'].append(user) + + if id is None: + raise ClientSideError(_("Permission %s does not exist") + % permission_dict['codename']) + return api_base.entity_update(models.Permission, id, permission_dict) diff --git a/storyboard/db/migration/alembic_migrations/versions/050_add_detailed_permissions_to_boards_and_.py b/storyboard/db/migration/alembic_migrations/versions/050_add_detailed_permissions_to_boards_and_.py new file mode 100644 index 00000000..5263a740 --- /dev/null +++ b/storyboard/db/migration/alembic_migrations/versions/050_add_detailed_permissions_to_boards_and_.py @@ -0,0 +1,118 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Add detailed permissions to boards and worklists + +Revision ID: 050 +Revises: 049 +Create Date: 2015-10-09 10:25:47.338906 + +""" + +# revision identifiers, used by Alembic. +revision = '050' +down_revision = '049' + + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +from storyboard.db.api import base as api_base +from storyboard.db.api import boards +from storyboard.db.api import worklists +from storyboard.db import models + + +def upgrade(active_plugins=None, options=None): + op.create_table('board_permissions', + sa.Column('board_id', sa.Integer(), nullable=True), + sa.Column('permission_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['board_id'], ['boards.id'], ), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ) + ) + op.create_table('worklist_permissions', + sa.Column('worklist_id', sa.Integer(), nullable=True), + sa.Column('permission_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['worklist_id'], ['worklists.id'], ) + ) + + session = api_base.get_session(in_request=False) + for board in boards.get_all(session=session): + edit_permission = { + 'name': 'edit_board_%d' % board.id, + 'codename': 'edit_board', + 'users': [board.creator_id] + } + move_permission = { + 'name': 'move_cards_%d' % board.id, + 'codename': 'move_cards', + 'users': [] + } + print('Creating permissions for Board with id: %d' % board.id) + boards.create_permission(board.id, edit_permission, session=session) + boards.create_permission(board.id, move_permission, session=session) + + for worklist in worklists.get_all(session=session): + edit_permission = { + 'name': 'edit_worklist_%d' % worklist.id, + 'codename': 'edit_worklist', + 'users': [worklist.creator_id] + } + move_permission = { + 'name': 'move_items_%d' % worklist.id, + 'codename': 'move_items', + 'users': [] + } + print('Creating permissions for Worklist with id: %d' % worklist.id) + worklists.create_permission( + worklist.id, edit_permission, session=session) + worklists.create_permission( + worklist.id, move_permission, session=session) + session.flush() + + op.drop_constraint(u'boards_ibfk_2', 'boards', type_='foreignkey') + op.drop_column(u'boards', 'permission_id') + op.drop_constraint(u'worklists_ibfk_2', 'worklists', type_='foreignkey') + op.drop_column(u'worklists', 'permission_id') + + +def downgrade(active_plugins=None, options=None): + op.add_column(u'worklists', sa.Column('permission_id', + mysql.INTEGER(display_width=11), autoincrement=False, + nullable=True)) + op.create_foreign_key(u'worklists_ibfk_2', 'worklists', 'permissions', + ['permission_id'], ['id']) + op.add_column(u'boards', sa.Column('permission_id', + mysql.INTEGER(display_width=11), autoincrement=False, + nullable=True)) + op.create_foreign_key(u'boards_ibfk_2', 'boards', 'permissions', + ['permission_id'], ['id']) + + session = api_base.get_session(in_request=False) + to_delete = [] + for board in boards.get_all(session=session): + for permission in board.permissions: + to_delete.append(permission) + + for worklist in worklists.get_all(session=session): + for permission in worklist.permissions: + to_delete.append(permission) + + op.drop_table('worklist_permissions') + op.drop_table('board_permissions') + + for permission in to_delete: + api_base.entity_hard_delete( + models.Permission, permission.id, session=session) diff --git a/storyboard/db/models.py b/storyboard/db/models.py index f6ed3429..cfe840b7 100644 --- a/storyboard/db/models.py +++ b/storyboard/db/models.py @@ -150,7 +150,8 @@ class User(FullText, ModelBuilder, Base): is_superuser = Column(Boolean, default=False) last_login = Column(UTCDateTime) teams = relationship("Team", secondary="team_membership") - permissions = relationship("Permission", secondary="user_permissions") + permissions = relationship( + "Permission", secondary="user_permissions", backref="users") enable_login = Column(Boolean, default=True) preferences = relationship("UserPreference") @@ -544,12 +545,12 @@ class Worklist(FullText, ModelBuilder, Base): title = Column(Unicode(CommonLength.top_middle_length), nullable=True) creator_id = Column(Integer, ForeignKey('users.id')) project_id = Column(Integer, ForeignKey('projects.id')) - permission_id = Column(Integer, ForeignKey('permissions.id')) private = Column(Boolean, default=False) archived = Column(Boolean, default=False) automatic = Column(Boolean, default=False) items = relationship(WorklistItem) criteria = relationship(WorklistCriteria) + permissions = relationship("Permission", secondary="worklist_permissions") _public_fields = ["id", "title", "creator_id", "project_id", "permission_id", "private", "archived", "automatic"] @@ -574,10 +575,22 @@ class Board(FullText, ModelBuilder, Base): description = Column(UnicodeText(), nullable=True) creator_id = Column(Integer, ForeignKey('users.id')) project_id = Column(Integer, ForeignKey('projects.id')) - permission_id = Column(Integer, ForeignKey('permissions.id')) private = Column(Boolean, default=False) archived = Column(Boolean, default=False) lanes = relationship(BoardWorklist) + permissions = relationship("Permission", secondary="board_permissions") _public_fields = ["id", "title", "description", "creator_id", "project_id", "permission_id", "private", "archived"] + +board_permissions = Table( + 'board_permissions', Base.metadata, + Column('board_id', Integer, ForeignKey('boards.id')), + Column('permission_id', Integer, ForeignKey('permissions.id')), +) + +worklist_permissions = Table( + 'worklist_permissions', Base.metadata, + Column('worklist_id', Integer, ForeignKey('worklists.id')), + Column('permission_id', Integer, ForeignKey('permissions.id')), +)