Add RESTful endpoints for Story permissions
Having to update the whole Story in order to add/remove a single permission entry is quite limiting. This commit addresses this flaw by adding subcontrollers to retrieve and modify the lists of Users and Teams who have access to a Story, without having to modify the whole Story. This allows much simpler use for clients in situations where only the ACL needs modification. Change-Id: I2ecee2c38456c5a23ae1dc7bdecb94efb2daac04
This commit is contained in:
parent
b7e1b2e2ae
commit
1e0338c5dc
@ -1,5 +1,5 @@
|
|||||||
# Copyright (c) 2013 Mirantis Inc.
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
# Copyright (c) 2016 Codethink Ltd.
|
# Copyright (c) 2016, 2019 Codethink Ltd.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -37,8 +37,10 @@ from storyboard.api.v1 import validations
|
|||||||
from storyboard.api.v1 import wmodels
|
from storyboard.api.v1 import wmodels
|
||||||
from storyboard.common import decorators
|
from storyboard.common import decorators
|
||||||
from storyboard.common import exception as exc
|
from storyboard.common import exception as exc
|
||||||
|
from storyboard.db.api import base as api_base
|
||||||
from storyboard.db.api import stories as stories_api
|
from storyboard.db.api import stories as stories_api
|
||||||
from storyboard.db.api import subscriptions as subscription_api
|
from storyboard.db.api import subscriptions as subscription_api
|
||||||
|
from storyboard.db.api import teams as teams_api
|
||||||
from storyboard.db.api import timeline_events as events_api
|
from storyboard.db.api import timeline_events as events_api
|
||||||
from storyboard.db.api import users as users_api
|
from storyboard.db.api import users as users_api
|
||||||
|
|
||||||
@ -60,6 +62,133 @@ def create_story_wmodel(story):
|
|||||||
return story_model
|
return story_model
|
||||||
|
|
||||||
|
|
||||||
|
class UsersSubcontroller(rest.RestController):
|
||||||
|
"""Manage Users who can access the Story."""
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.guest)
|
||||||
|
@wsme_pecan.wsexpose([wmodels.User], int)
|
||||||
|
def get(self, story_id):
|
||||||
|
"""Get users with access to a story.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
curl https://my.example.org/api/v1/stories/1/users
|
||||||
|
|
||||||
|
:param story_id: ID of the story to get users for.
|
||||||
|
"""
|
||||||
|
story = stories_api.story_get_simple(
|
||||||
|
story_id, current_user=request.current_user_id)
|
||||||
|
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
return []
|
||||||
|
|
||||||
|
permission = story.permissions[0]
|
||||||
|
users = [api_base._filter_non_public_fields(user, user._public_fields)
|
||||||
|
for user in permission.users]
|
||||||
|
return [wmodels.User.from_db_model(user) for user in users]
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.authenticated)
|
||||||
|
@wsme_pecan.wsexpose(wmodels.User, int, int)
|
||||||
|
def put(self, story_id, user_id):
|
||||||
|
"""Add a user to a story.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param story_id: ID of the story to add a user to.
|
||||||
|
:param user_id: ID of the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stories_api.add_user(story_id, user_id, request.current_user_id)
|
||||||
|
user = users_api.user_get(user_id)
|
||||||
|
user = api_base._filter_non_public_fields(user, user._public_fields)
|
||||||
|
|
||||||
|
return wmodels.User.from_db_model(user)
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.authenticated)
|
||||||
|
@wsme_pecan.wsexpose(None, int, int, status_code=204)
|
||||||
|
def delete(self, story_id, user_id):
|
||||||
|
"""Delete a user from a team.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param team_id: An ID of the team.
|
||||||
|
:param user_id: An ID of the user.
|
||||||
|
"""
|
||||||
|
stories_api.delete_user(story_id, user_id, request.current_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamsSubcontroller(rest.RestController):
|
||||||
|
"""Manage Teams who can access the story."""
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.guest)
|
||||||
|
@wsme_pecan.wsexpose([wmodels.Team], int)
|
||||||
|
def get(self, story_id):
|
||||||
|
"""Get users inside a team.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
curl https://my.example.org/api/v1/teams/1/users
|
||||||
|
|
||||||
|
:param team_id: An ID of the team.
|
||||||
|
"""
|
||||||
|
story = stories_api.story_get_simple(
|
||||||
|
story_id, current_user=request.current_user_id)
|
||||||
|
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
return []
|
||||||
|
|
||||||
|
permission = story.permissions[0]
|
||||||
|
return [wmodels.Team.from_db_model(team) for team in permission.teams]
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.authenticated)
|
||||||
|
@wsme_pecan.wsexpose(wmodels.Team, int, int)
|
||||||
|
def put(self, story_id, team_id):
|
||||||
|
"""Add a team to a story.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param story_id: ID of the story to add a team to.
|
||||||
|
:param team_id: ID of the team.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stories_api.add_team(story_id, team_id, request.current_user_id)
|
||||||
|
team = teams_api.team_get(team_id)
|
||||||
|
|
||||||
|
return wmodels.Team.from_db_model(team)
|
||||||
|
|
||||||
|
@decorators.db_exceptions
|
||||||
|
@secure(checks.authenticated)
|
||||||
|
@wsme_pecan.wsexpose(None, int, int, status_code=204)
|
||||||
|
def delete(self, story_id, team_id):
|
||||||
|
"""Delete a team from a story.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
:param story_id: ID of the story to remove a team from.
|
||||||
|
:param team_id: ID of the team.
|
||||||
|
"""
|
||||||
|
stories_api.delete_team(story_id, team_id, request.current_user_id)
|
||||||
|
|
||||||
|
|
||||||
class StoriesController(rest.RestController):
|
class StoriesController(rest.RestController):
|
||||||
"""Manages operations on stories."""
|
"""Manages operations on stories."""
|
||||||
|
|
||||||
@ -364,6 +493,8 @@ class StoriesController(rest.RestController):
|
|||||||
events = NestedTimeLineEventsController()
|
events = NestedTimeLineEventsController()
|
||||||
tasks = TasksNestedController()
|
tasks = TasksNestedController()
|
||||||
tags = TagsController()
|
tags = TagsController()
|
||||||
|
teams = TeamsSubcontroller()
|
||||||
|
users = UsersSubcontroller()
|
||||||
|
|
||||||
@decorators.db_exceptions
|
@decorators.db_exceptions
|
||||||
@secure(checks.guest)
|
@secure(checks.guest)
|
||||||
|
@ -17,6 +17,7 @@ import datetime
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from sqlalchemy.orm import subqueryload
|
from sqlalchemy.orm import subqueryload
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
|
||||||
from storyboard._i18n import _
|
from storyboard._i18n import _
|
||||||
from storyboard.common import exception as exc
|
from storyboard.common import exception as exc
|
||||||
@ -454,3 +455,125 @@ def update_permission(story, users, teams, session=None):
|
|||||||
return api_base.entity_update(models.Permission,
|
return api_base.entity_update(models.Permission,
|
||||||
permission.id,
|
permission.id,
|
||||||
permission_dict)
|
permission_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def add_user(story_id, user_id, current_user=None):
|
||||||
|
session = api_base.get_session()
|
||||||
|
|
||||||
|
with session.begin(subtransactions=True):
|
||||||
|
story = story_get_simple(
|
||||||
|
story_id, session=session, current_user=current_user)
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
user = users_api.user_get(user_id, session=session)
|
||||||
|
if not user:
|
||||||
|
raise exc.NotFound(_("User %s not found") % user_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
create_permission(story, [user], [], session)
|
||||||
|
return
|
||||||
|
permission = story.permissions[0]
|
||||||
|
if user_id in [u.id for u in permission.users]:
|
||||||
|
raise ClientSideError(_("The User %{user_id}d is already in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"user_id": user_id, "story_id": story_id})
|
||||||
|
permission.users.append(user)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
return story
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user(story_id, user_id, current_user=None):
|
||||||
|
session = api_base.get_session()
|
||||||
|
|
||||||
|
with session.begin(subtransactions=True):
|
||||||
|
story = story_get_simple(
|
||||||
|
story_id, session=session, current_user=current_user)
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
user = users_api.user_get(user_id, session=session)
|
||||||
|
if not user:
|
||||||
|
raise exc.NotFound(_("User %s not found") % user_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
raise ClientSideError(_("The User %{user_id}d isn't in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"user_id": user_id, "story_id": story_id})
|
||||||
|
|
||||||
|
permission = story.permissions[0]
|
||||||
|
if user_id not in [u.id for u in permission.users]:
|
||||||
|
raise ClientSideError(_("The User %{user_id}d isn't in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"user_id": user_id, "story_id": story_id})
|
||||||
|
|
||||||
|
entry = [u for u in permission.users if u.id == user_id][0]
|
||||||
|
permission.users.remove(entry)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
return story
|
||||||
|
|
||||||
|
|
||||||
|
def add_team(story_id, team_id, current_user=None):
|
||||||
|
session = api_base.get_session()
|
||||||
|
|
||||||
|
with session.begin(subtransactions=True):
|
||||||
|
story = story_get_simple(
|
||||||
|
story_id, session=session, current_user=current_user)
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
team = teams_api.team_get(team_id, session=session)
|
||||||
|
if not team:
|
||||||
|
raise exc.NotFound(_("Team %s not found") % team_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
create_permission(story, [], [team], session)
|
||||||
|
return
|
||||||
|
permission = story.permissions[0]
|
||||||
|
if team_id in [t.id for t in permission.teams]:
|
||||||
|
raise ClientSideError(_("The Team %{team_id}d is already in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"team_id": team_id, "story_id": story_id})
|
||||||
|
permission.teams.append(team)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
return story
|
||||||
|
|
||||||
|
|
||||||
|
def delete_team(story_id, team_id, current_user=None):
|
||||||
|
session = api_base.get_session()
|
||||||
|
|
||||||
|
with session.begin(subtransactions=True):
|
||||||
|
story = story_get_simple(
|
||||||
|
story_id, session=session, current_user=current_user)
|
||||||
|
if not story:
|
||||||
|
raise exc.NotFound(_("Story %s not found") % story_id)
|
||||||
|
|
||||||
|
team = teams_api.team_get(team_id, session=session)
|
||||||
|
if not team:
|
||||||
|
raise exc.NotFound(_("User %s not found") % team_id)
|
||||||
|
|
||||||
|
if not story.permissions:
|
||||||
|
raise ClientSideError(_("The Team %{team_id}d isn't in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"team_id": team_id, "story_id": story_id})
|
||||||
|
|
||||||
|
permission = story.permissions[0]
|
||||||
|
if team_id not in [t.id for t in permission.teams]:
|
||||||
|
raise ClientSideError(_("The Team %{team_id}d isn't in the "
|
||||||
|
"permission list for Story "
|
||||||
|
"%{story_id}d") %
|
||||||
|
{"team_id": team_id, "story_id": story_id})
|
||||||
|
|
||||||
|
entry = [t for t in permission.teams if t.id == team_id][0]
|
||||||
|
permission.teams.remove(entry)
|
||||||
|
session.add(permission)
|
||||||
|
|
||||||
|
return story
|
||||||
|
@ -34,8 +34,8 @@ def _entity_get(id, session=None):
|
|||||||
return query.first()
|
return query.first()
|
||||||
|
|
||||||
|
|
||||||
def team_get(team_id):
|
def team_get(team_id, session=None):
|
||||||
return _entity_get(team_id)
|
return _entity_get(team_id, session=session)
|
||||||
|
|
||||||
|
|
||||||
def _team_build_query(project_id=None, **kwargs):
|
def _team_build_query(project_id=None, **kwargs):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user