Allow permissions to be set for teams on stories
This commit allows people to add teams to the ACL for viewing and editing a private story. This will make managing who can see stories much easier, since it allows grouping of users such that only one change needs to be made if someone is added to or removed from the group. Previously the list of users needed to be sent every time the story was updated, this commit fixes that such that if no list of users is sent, no change is made to the users who can see the story. Story: 2000568 Task: 2985 Change-Id: Idb7f881c4d772d373b583ecc2f59231be8f7f01b
This commit is contained in:
parent
4926b471e2
commit
28f204d9be
@ -58,10 +58,6 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
|
||||
session = api_base.get_session()
|
||||
|
||||
subquery = api_base.model_query(models.Story, session)
|
||||
|
||||
# Filter out stories that the current user can't see
|
||||
subquery = api_base.filter_private_stories(subquery, current_user)
|
||||
|
||||
subquery = self._build_fulltext_search(models.Story, subquery, q)
|
||||
subquery = self._apply_pagination(models.Story,
|
||||
subquery, marker, offset, limit)
|
||||
@ -73,6 +69,9 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
|
||||
query = query.join(subquery,
|
||||
models.StorySummary.id == subquery.c.id)
|
||||
|
||||
# Filter out stories that the current user can't see
|
||||
query = api_base.filter_private_stories(query, current_user)
|
||||
|
||||
stories = query.all()
|
||||
return stories
|
||||
|
||||
@ -81,11 +80,12 @@ class SqlAlchemySearchImpl(search_engine.SearchEngine):
|
||||
session = api_base.get_session()
|
||||
query = api_base.model_query(models.Task, session)
|
||||
|
||||
query = self._build_fulltext_search(models.Task, query, q)
|
||||
|
||||
# Filter out tasks or stories that the current user can't see
|
||||
query = query.outerjoin(models.Story)
|
||||
query = api_base.filter_private_stories(query, current_user)
|
||||
|
||||
query = self._build_fulltext_search(models.Task, query, q)
|
||||
query = self._apply_pagination(
|
||||
models.Task, query, marker, offset, limit)
|
||||
|
||||
|
@ -50,8 +50,10 @@ def create_story_wmodel(story):
|
||||
story_model.summarize_task_statuses(story)
|
||||
if story.permissions:
|
||||
story_model.resolve_users(story)
|
||||
story_model.resolve_teams(story)
|
||||
else:
|
||||
story_model.users = []
|
||||
story_model.teams = []
|
||||
return story_model
|
||||
|
||||
|
||||
@ -207,17 +209,22 @@ class StoriesController(rest.RestController):
|
||||
if "due_dates" in story_dict:
|
||||
del story_dict['due_dates']
|
||||
|
||||
users = []
|
||||
users = None
|
||||
teams = None
|
||||
if "users" in story_dict:
|
||||
users = story_dict.pop("users")
|
||||
if users is None:
|
||||
users = [wmodels.User.from_db_model(users_api.user_get(user_id))]
|
||||
if "teams" in story_dict:
|
||||
teams = story_dict.pop("teams")
|
||||
if teams is None:
|
||||
teams = []
|
||||
|
||||
created_story = stories_api.story_create(story_dict)
|
||||
events_api.story_created_event(created_story.id, user_id, story.title)
|
||||
|
||||
if story.private:
|
||||
stories_api.create_permission(created_story, users)
|
||||
stories_api.create_permission(created_story, users, teams)
|
||||
|
||||
return wmodels.Story.from_db_model(created_story)
|
||||
|
||||
@ -237,6 +244,7 @@ class StoriesController(rest.RestController):
|
||||
:param story_id: An ID of the story.
|
||||
:param story: A story within the request body.
|
||||
"""
|
||||
user_id = request.current_user_id
|
||||
|
||||
# Reject private story types while ACL is not created.
|
||||
if (story.story_type_id and
|
||||
@ -245,7 +253,7 @@ class StoriesController(rest.RestController):
|
||||
story.story_type_id)
|
||||
|
||||
original_story = stories_api.story_get_simple(
|
||||
story_id, current_user=request.current_user_id)
|
||||
story_id, current_user=user_id)
|
||||
|
||||
if not original_story:
|
||||
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||
@ -267,28 +275,54 @@ class StoriesController(rest.RestController):
|
||||
if 'tags' in story_dict:
|
||||
story_dict.pop('tags')
|
||||
|
||||
users = story_dict.get("users", [])
|
||||
ids = [user.id for user in users]
|
||||
if story.private:
|
||||
if request.current_user_id not in ids \
|
||||
and not original_story.permissions:
|
||||
users.append(wmodels.User.from_db_model(
|
||||
users_api.user_get(request.current_user_id)))
|
||||
users = story_dict.get("users")
|
||||
teams = story_dict.get("teams")
|
||||
|
||||
private = story_dict.get("private", original_story.private)
|
||||
if private:
|
||||
# If trying to make a story private with no permissions set, add
|
||||
# the user making the change to the permission so that at least
|
||||
# the story isn't lost to everyone.
|
||||
if not users and not teams and not original_story.permissions:
|
||||
users = [wmodels.User.from_db_model(
|
||||
users_api.user_get(user_id))]
|
||||
|
||||
original_teams = None
|
||||
original_users = None
|
||||
if original_story.permissions:
|
||||
original_teams = original_story.permissions[0].teams
|
||||
original_users = original_story.permissions[0].users
|
||||
|
||||
# Don't allow both permission lists to be deliberately emptied
|
||||
# on a private story, to make sure the story remains visible to
|
||||
# at least someone.
|
||||
valid = True
|
||||
if users == [] and teams == []:
|
||||
valid = False
|
||||
elif users == [] and original_teams == []:
|
||||
valid = False
|
||||
elif teams == [] and original_users == []:
|
||||
valid = False
|
||||
if not valid and original_story.private:
|
||||
abort(400,
|
||||
_("Can't make a private story have no users or teams"))
|
||||
|
||||
# If the story doesn't already have permissions, create them.
|
||||
if not original_story.permissions:
|
||||
stories_api.create_permission(original_story, users)
|
||||
stories_api.create_permission(original_story, users, teams)
|
||||
|
||||
updated_story = stories_api.story_update(
|
||||
story_id,
|
||||
story_dict,
|
||||
current_user=request.current_user_id)
|
||||
current_user=user_id)
|
||||
|
||||
if users == [] and updated_story.private:
|
||||
abort(400, _("Can't make a private story with no users"))
|
||||
# If the story is private and already has some permissions, update
|
||||
# them as needed. This is done after updating the story in case the
|
||||
# request is trying to both update some story fields and also remove
|
||||
# the user making the change from the ACL.
|
||||
if private and original_story.permissions:
|
||||
stories_api.update_permission(updated_story, users, teams)
|
||||
|
||||
if story.private:
|
||||
stories_api.update_permission(updated_story, users)
|
||||
|
||||
user_id = request.current_user_id
|
||||
events_api.story_details_changed_event(story_id, user_id,
|
||||
updated_story.title)
|
||||
|
||||
|
@ -182,6 +182,24 @@ class User(base.APIBase):
|
||||
last_login=datetime(2014, 1, 1, 16, 42))
|
||||
|
||||
|
||||
class Team(base.APIBase):
|
||||
"""The Team is a group of Users with a fixed set of permissions."""
|
||||
|
||||
name = NameType()
|
||||
"""The Team unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators."""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the team."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard-core",
|
||||
description="Core reviewers of StoryBoard team.")
|
||||
|
||||
|
||||
class Story(base.APIBase):
|
||||
"""The Story is the main element of StoryBoard. It represents a user story
|
||||
(generally a bugfix or a feature) that needs to be implemented. It will be
|
||||
@ -222,6 +240,9 @@ class Story(base.APIBase):
|
||||
users = wtypes.ArrayType(User)
|
||||
"""The set of users with permission to see this story if it is private."""
|
||||
|
||||
teams = wtypes.ArrayType(Team)
|
||||
"""The set of teams with permission to see this story if it is private."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
@ -252,6 +273,12 @@ class Story(base.APIBase):
|
||||
for user in story.permissions[0].users]
|
||||
self.users = [User.from_db_model(user) for user in users]
|
||||
|
||||
@nodoc
|
||||
def resolve_teams(self, story):
|
||||
"""Resolve the teams who can see the story."""
|
||||
self.teams = [Team.from_db_model(team)
|
||||
for team in story.permissions[0].teams]
|
||||
|
||||
|
||||
class Tag(base.APIBase):
|
||||
|
||||
@ -389,24 +416,6 @@ class Milestone(base.APIBase):
|
||||
)
|
||||
|
||||
|
||||
class Team(base.APIBase):
|
||||
"""The Team is a group of Users with a fixed set of permissions."""
|
||||
|
||||
name = NameType()
|
||||
"""The Team unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators."""
|
||||
|
||||
description = wtypes.text
|
||||
"""Details about the team."""
|
||||
|
||||
@classmethod
|
||||
def sample(cls):
|
||||
return cls(
|
||||
name="StoryBoard-core",
|
||||
description="Core reviewers of StoryBoard team.")
|
||||
|
||||
|
||||
class TimeLineEvent(base.APIBase):
|
||||
"""An event object should be created each time a story or a task state
|
||||
changes.
|
||||
|
@ -384,12 +384,13 @@ def filter_private_stories(query, current_user, story_model=models.Story):
|
||||
:param story_model: The database model used for stories in the query.
|
||||
|
||||
"""
|
||||
# First filter based on users with permissions set directly
|
||||
query = query.outerjoin(models.story_permissions,
|
||||
models.Permission,
|
||||
models.user_permissions,
|
||||
models.User)
|
||||
if current_user:
|
||||
query = query.filter(
|
||||
visible_to_users = query.filter(
|
||||
or_(
|
||||
and_(
|
||||
models.User.id == current_user,
|
||||
@ -400,14 +401,40 @@ def filter_private_stories(query, current_user, story_model=models.Story):
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.filter(
|
||||
visible_to_users = query.filter(
|
||||
or_(
|
||||
story_model.private == false(),
|
||||
story_model.id.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
return query
|
||||
# Now filter based on membership of teams with permissions
|
||||
users = aliased(models.User)
|
||||
query = query.outerjoin(models.team_permissions,
|
||||
models.Team,
|
||||
models.team_membership,
|
||||
(users,
|
||||
users.id == models.team_membership.c.user_id))
|
||||
if current_user:
|
||||
visible_to_teams = query.filter(
|
||||
or_(
|
||||
and_(
|
||||
users.id == current_user,
|
||||
story_model.private == true()
|
||||
),
|
||||
story_model.private == false(),
|
||||
story_model.id.is_(None)
|
||||
)
|
||||
)
|
||||
else:
|
||||
visible_to_teams = query.filter(
|
||||
or_(
|
||||
story_model.private == false(),
|
||||
story_model.id.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
return visible_to_users.union(visible_to_teams)
|
||||
|
||||
|
||||
def filter_private_worklists(query, current_user, hide_lanes=True):
|
||||
|
@ -19,6 +19,7 @@ from storyboard.common import exception as exc
|
||||
from storyboard.db.api import base as api_base
|
||||
from storyboard.db.api import story_tags
|
||||
from storyboard.db.api import story_types
|
||||
from storyboard.db.api import teams as teams_api
|
||||
from storyboard.db.api import users as users_api
|
||||
from storyboard.db import models
|
||||
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||
@ -89,7 +90,7 @@ def story_get_all(title=None, description=None, status=None, assignee_id=None,
|
||||
query = api_base.model_query(models.StorySummary)\
|
||||
.options(subqueryload(models.StorySummary.tags))
|
||||
query = query.join(subquery,
|
||||
models.StorySummary.id == subquery.c.id)
|
||||
models.StorySummary.id == subquery.c.stories_id)
|
||||
|
||||
if status:
|
||||
query = query.filter(models.StorySummary.status.in_(status))
|
||||
@ -137,7 +138,7 @@ def story_get_count(title=None, description=None, status=None,
|
||||
query = query.subquery()
|
||||
summary_query = api_base.model_query(models.StorySummary)
|
||||
summary_query = summary_query \
|
||||
.join(query, models.StorySummary.id == query.c.id)
|
||||
.join(query, models.StorySummary.id == query.c.stories_id)
|
||||
query = summary_query.filter(models.StorySummary.status.in_(status))
|
||||
|
||||
return query.count()
|
||||
@ -189,7 +190,7 @@ def _story_build_query(title=None, description=None, assignee_id=None,
|
||||
if project_id:
|
||||
query = query.filter(models.Task.project_id == project_id)
|
||||
|
||||
return query
|
||||
return query.distinct()
|
||||
|
||||
|
||||
def story_create(values):
|
||||
@ -322,7 +323,7 @@ def story_can_mutate(story, new_story_type_id):
|
||||
return False
|
||||
|
||||
|
||||
def create_permission(story, users, session=None):
|
||||
def create_permission(story, users, teams, session=None):
|
||||
story = api_base.model_query(models.Story, session) \
|
||||
.options(subqueryload(models.Story.tags)) \
|
||||
.filter_by(id=story.id).first()
|
||||
@ -332,13 +333,18 @@ def create_permission(story, users, session=None):
|
||||
}
|
||||
permission = api_base.entity_create(models.Permission, permission_dict)
|
||||
story.permissions.append(permission)
|
||||
for user in users:
|
||||
user = users_api.user_get(user.id)
|
||||
user.permissions.append(permission)
|
||||
if users is not None:
|
||||
for user in users:
|
||||
user = users_api.user_get(user.id)
|
||||
user.permissions.append(permission)
|
||||
if teams is not None:
|
||||
for team in teams:
|
||||
team = teams_api.team_get(team.id)
|
||||
team.permissions.append(permission)
|
||||
return permission
|
||||
|
||||
|
||||
def update_permission(story, users, session=None):
|
||||
def update_permission(story, users, teams, session=None):
|
||||
story = api_base.model_query(models.Story, session) \
|
||||
.options(subqueryload(models.Story.tags)) \
|
||||
.filter_by(id=story.id).first()
|
||||
@ -348,9 +354,14 @@ def update_permission(story, users, session=None):
|
||||
permission = story.permissions[0]
|
||||
permission_dict = {
|
||||
'name': permission.name,
|
||||
'codename': permission.codename,
|
||||
'users': [users_api.user_get(user.id) for user in users]
|
||||
'codename': permission.codename
|
||||
}
|
||||
if users is not None:
|
||||
permission_dict['users'] = [users_api.user_get(user.id)
|
||||
for user in users]
|
||||
if teams is not None:
|
||||
permission_dict['teams'] = [teams_api.team_get(team.id)
|
||||
for team in teams]
|
||||
|
||||
return api_base.entity_update(models.Permission,
|
||||
permission.id,
|
||||
|
@ -212,7 +212,8 @@ class Team(ModelBuilder, Base):
|
||||
)
|
||||
name = Column(Unicode(CommonLength.top_large_length))
|
||||
users = relationship("User", secondary="team_membership")
|
||||
permissions = relationship("Permission", secondary="team_permissions")
|
||||
permissions = relationship("Permission", secondary="team_permissions",
|
||||
backref="teams")
|
||||
|
||||
|
||||
project_group_mapping = Table(
|
||||
|
Loading…
x
Reference in New Issue
Block a user