Merge pull request #2755 from normanjaeckel/StatePermission

Fixed state flag required permission to see.
This commit is contained in:
Emanuel Schütze 2016-12-12 13:49:07 +01:00 committed by GitHub
commit 2bf1b3d827
7 changed files with 82 additions and 85 deletions

View File

@ -25,9 +25,15 @@ class MotionAccessPermissions(BaseAccessPermissions):
def get_restricted_data(self, full_data, user): def get_restricted_data(self, full_data, user):
""" """
Returns the restricted serialized data for the instance prepared for Returns the restricted serialized data for the instance prepared for
the user. Removes non public comment fields for some unauthorized the user. Removes motion if the user has not the permission to see
users. the motion in this state. Removes non public comment fields for
some unauthorized users.
""" """
required_permission_to_see = full_data.get('state_required_permission_to_see')
if (not required_permission_to_see or
user.has_perm(required_permission_to_see) or
user.has_perm('motions.can_manage') or
user.pk in full_data.get('submitters_id', [])):
if user.has_perm('motions.can_see_and_manage_comments') or not full_data.get('comments'): if user.has_perm('motions.can_see_and_manage_comments') or not full_data.get('comments'):
data = full_data data = full_data
else: else:
@ -39,6 +45,8 @@ class MotionAccessPermissions(BaseAccessPermissions):
except IndexError: except IndexError:
# No data in range. Just do nothing. # No data in range. Just do nothing.
pass pass
else:
data = None
return data return data
def get_projector_data(self, full_data): def get_projector_data(self, full_data):

View File

@ -42,7 +42,7 @@ class MotionManager(models.Manager):
join and prefetch all related models. join and prefetch all related models.
""" """
return (self.get_queryset() return (self.get_queryset()
.select_related('active_version') .select_related('active_version', 'state')
.prefetch_related( .prefetch_related(
'versions', 'versions',
'agenda_items', 'agenda_items',
@ -604,61 +604,6 @@ class Motion(RESTModelMixin, models.Model):
""" """
return self.agenda_item.pk return self.agenda_item.pk
def get_allowed_actions(self, person):
"""
Return a dictonary with all allowed actions for a specific person.
The dictonary contains the following actions.
* see
* update / edit
* delete
* create_poll
* support
* unsupport
* change_state
* reset_state
* change_recommendation
NOTE: If you update this function please also update the
'isAllowed' function on client side in motions/site.js.
"""
# TODO: Remove this method and implement these things in the views.
actions = {
'see': (person.has_perm('motions.can_see') and
(not self.state.required_permission_to_see or
person.has_perm(self.state.required_permission_to_see) or
self.is_submitter(person))),
'update': (person.has_perm('motions.can_manage') or
(self.is_submitter(person) and
self.state.allow_submitter_edit)),
'delete': person.has_perm('motions.can_manage'),
'create_poll': (person.has_perm('motions.can_manage') and
self.state.allow_create_poll),
'support': (self.state.allow_support and
config['motions_min_supporters'] > 0 and
not self.is_submitter(person) and
not self.is_supporter(person)),
'unsupport': (self.state.allow_support and
self.is_supporter(person)),
'change_state': person.has_perm('motions.can_manage'),
'reset_state': person.has_perm('motions.can_manage'),
'change_recommendation': person.has_perm('motions.can_manage'),
}
actions['edit'] = actions['update']
return actions
def write_log(self, message_list, person=None, skip_autoupdate=False): def write_log(self, message_list, person=None, skip_autoupdate=False):
""" """
Write a log message. Write a log message.

View File

@ -267,6 +267,7 @@ class MotionSerializer(ModelSerializer):
log_messages = MotionLogSerializer(many=True, read_only=True) log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True)
reason = CharField(allow_blank=True, required=False, write_only=True) reason = CharField(allow_blank=True, required=False, write_only=True)
state_required_permission_to_see = SerializerMethodField()
text = CharField(write_only=True) text = CharField(write_only=True)
title = CharField(max_length=255, write_only=True) title = CharField(max_length=255, write_only=True)
versions = MotionVersionSerializer(many=True, read_only=True) versions = MotionVersionSerializer(many=True, read_only=True)
@ -294,6 +295,7 @@ class MotionSerializer(ModelSerializer):
'supporters', 'supporters',
'comments', 'comments',
'state', 'state',
'state_required_permission_to_see',
'workflow_id', 'workflow_id',
'recommendation', 'recommendation',
'tags', 'tags',
@ -366,3 +368,13 @@ class MotionSerializer(ModelSerializer):
attr.add(*validated_data[key]) attr.add(*validated_data[key])
return motion return motion
def get_state_required_permission_to_see(self, motion):
"""
Returns the permission (as string) that is required for non
managers that are not submitters to see this motion in this state.
Hint: Most states have and empty string here so this restriction is
disabled.
"""
return motion.state.required_permission_to_see

View File

@ -418,8 +418,8 @@ angular.module('OpenSlidesApp.motions', [
* - reset_state * - reset_state
* - change_recommendation * - change_recommendation
* *
* NOTE: If you update this function please also update the * NOTE: If you update this function please think about
* 'get_allowed_actions' function on server side in motions/models.py. * server permissions, see motions/views.py.
*/ */
switch (action) { switch (action) {
case 'see': case 'see':
@ -435,7 +435,7 @@ angular.module('OpenSlidesApp.motions', [
return ( return (
operator.hasPerms('motions.can_manage') || operator.hasPerms('motions.can_manage') ||
( (
($.inArray(operator.user, this.submitters) != -1) && (_.indexOf(this.submitters, operator.user) !== -1) &&
this.state.allow_submitter_edit this.state.allow_submitter_edit
) )
); );
@ -453,14 +453,11 @@ angular.module('OpenSlidesApp.motions', [
operator.hasPerms('motions.can_support') && operator.hasPerms('motions.can_support') &&
this.state.allow_support && this.state.allow_support &&
Config.get('motions_min_supporters').value > 0 && Config.get('motions_min_supporters').value > 0 &&
($.inArray(operator.user, this.submitters) == -1) && (_.indexOf(this.submitters, operator.user) === -1) &&
($.inArray(operator.user, this.supporters) == -1) (_.indexOf(this.supporters, operator.user) === -1)
); );
case 'unsupport': case 'unsupport':
return ( return this.state.allow_support && _.indexOf(this.supporters, operator.user) !== -1;
this.state.allow_support &&
($.inArray(operator.user, this.supporters) != -1)
);
case 'change_state': case 'change_state':
return operator.hasPerms('motions.can_manage'); return operator.hasPerms('motions.can_manage');
case 'reset_state': case 'reset_state':

View File

@ -116,14 +116,16 @@ class MotionViewSet(ModelViewSet):
Checks also whether the requesting user can update the motion. He Checks also whether the requesting user can update the motion. He
needs at least the permissions 'motions.can_see' (see needs at least the permissions 'motions.can_see' (see
self.check_view_permissions()). Also the instance method self.check_view_permissions()). Also check manage permission or
get_allowed_actions() is evaluated. submitter and state.
""" """
# Get motion. # Get motion.
motion = self.get_object() motion = self.get_object()
# Check permissions. # Check permissions.
if not motion.get_allowed_actions(request.user)['update']: if (not request.user.has_perm('motions.can_manage') and
not (motion.is_submitter(request.user) and
motion.state.allow_submitter_edit)):
self.permission_denied(request) self.permission_denied(request)
# Check permission to send only some data. # Check permission to send only some data.
@ -215,12 +217,14 @@ class MotionViewSet(ModelViewSet):
""" """
# Retrieve motion and allowed actions. # Retrieve motion and allowed actions.
motion = self.get_object() motion = self.get_object()
allowed_actions = motion.get_allowed_actions(request.user)
# Support or unsupport motion. # Support or unsupport motion.
if request.method == 'POST': if request.method == 'POST':
# Support motion. # Support motion.
if not allowed_actions['support']: if not (motion.state.allow_support and
config['motions_min_supporters'] > 0 and
not motion.is_submitter(request.user) and
not motion.is_supporter(request.user)):
raise ValidationError({'detail': _('You can not support this motion.')}) raise ValidationError({'detail': _('You can not support this motion.')})
motion.supporters.add(request.user) motion.supporters.add(request.user)
motion.write_log([ugettext_noop('Motion supported')], request.user) motion.write_log([ugettext_noop('Motion supported')], request.user)
@ -228,7 +232,7 @@ class MotionViewSet(ModelViewSet):
else: else:
# Unsupport motion. # Unsupport motion.
# request.method == 'DELETE' # request.method == 'DELETE'
if not allowed_actions['unsupport']: if not motion.state.allow_support or not motion.is_supporter(request.user):
raise ValidationError({'detail': _('You can not unsupport this motion.')}) raise ValidationError({'detail': _('You can not unsupport this motion.')})
motion.supporters.remove(request.user) motion.supporters.remove(request.user)
motion.write_log([ugettext_noop('Motion unsupported')], request.user) motion.write_log([ugettext_noop('Motion unsupported')], request.user)
@ -320,6 +324,8 @@ class MotionViewSet(ModelViewSet):
View to create a poll. It is a POST request without any data. View to create a poll. It is a POST request without any data.
""" """
motion = self.get_object() motion = self.get_object()
if not motion.state.allow_create_poll:
raise ValidationError({'detail': 'You can not create a poll in this motion state.'})
try: try:
with transaction.atomic(): with transaction.atomic():
motion.create_poll() motion.create_poll()

View File

@ -298,6 +298,37 @@ class RetrieveMotion(TestCase):
with self.assertNumQueries(16): with self.assertNumQueries(16):
self.client.get(reverse('motion-detail', args=[self.motion.pk])) self.client.get(reverse('motion-detail', args=[self.motion.pk]))
def test__guest_state_with_required_permission_to_see(self):
config['general_system_enable_anonymous'] = True
guest_client = APIClient()
state = self.motion.state
state.required_permission_to_see = 'permission_that_the_user_does_not_have_leeceiz9hi7iuta4ahY2'
state.save()
response = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_admin_state_with_required_permission_to_see(self):
state = self.motion.state
state.required_permission_to_see = 'permission_that_the_user_does_not_have_coo1Iewu8Eing2xahfoo'
state.save()
response = self.client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_submitter_state_with_required_permission_to_see(self):
state = self.motion.state
state.required_permission_to_see = 'permission_that_the_user_does_not_have_eiW8af9caizoh1thaece'
state.save()
user = get_user_model().objects.create_user(
username='username_ohS2opheikaSa5theijo',
password='password_kau4eequaisheeBateef')
self.motion.submitters.add(user)
submitter_client = APIClient()
submitter_client.login(
username='username_ohS2opheikaSa5theijo',
password='password_kau4eequaisheeBateef')
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
class UpdateMotion(TestCase): class UpdateMotion(TestCase):
""" """

View File

@ -73,8 +73,6 @@ class ModelTest(TestCase):
self.motion.state = State.objects.get(pk=6) self.motion.state = State.objects.get(pk=6)
self.assertEqual(self.motion.state.name, 'permitted') self.assertEqual(self.motion.state.name, 'permitted')
self.assertEqual(self.motion.state.get_action_word(), 'Permit') self.assertEqual(self.motion.state.get_action_word(), 'Permit')
self.assertFalse(self.motion.get_allowed_actions(self.test_user)['support'])
self.assertFalse(self.motion.get_allowed_actions(self.test_user)['unsupport'])
def test_new_states_or_workflows(self): def test_new_states_or_workflows(self):
workflow_1 = Workflow.objects.create(name='W1') workflow_1 = Workflow.objects.create(name='W1')