User without permission to see users can now see some required users.

These are
- agenda item speakers,
- motion submitters and supporters,
- assignment candidates,
- mediafile uploader and
- chat message users
but only if the user has respective permissions. Fixed #3002.
This commit is contained in:
Norman Jäckel 2017-04-10 16:28:38 +02:00 committed by Emanuel Schütze
parent 152585a73f
commit c4ec26c4c0
14 changed files with 194 additions and 12 deletions

View File

@ -19,6 +19,12 @@ Core:
- No reload on logoff. OpenSlides is now a full single page - No reload on logoff. OpenSlides is now a full single page
application [#3172]. application [#3172].
Users:
- User without permission to see users can now see agenda item speakers,
motion submitters and supporters, assignment candidates, mediafile
uploader and chat message users if they have the respective
permissions [#3191].
Version 2.1.1 (2017-04-05) Version 2.1.1 (2017-04-05)
========================== ==========================

View File

@ -17,11 +17,12 @@ class AgendaAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save
from openslides.core.config import config from openslides.core.config import config
from openslides.core.signals import permission_change from openslides.core.signals import permission_change, user_data_required
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .config_variables import get_config_variables from .config_variables import get_config_variables
from .signals import ( from .signals import (
get_permission_change_data, get_permission_change_data,
is_user_data_required,
listen_to_related_object_post_delete, listen_to_related_object_post_delete,
listen_to_related_object_post_save) listen_to_related_object_post_save)
from .views import ItemViewSet from .views import ItemViewSet
@ -39,6 +40,9 @@ class AgendaAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='agenda_get_permission_change_data') dispatch_uid='agenda_get_permission_change_data')
user_data_required.connect(
is_user_data_required,
dispatch_uid='agenda_is_user_data_required')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Item').get_collection_string(), ItemViewSet) router.register(self.get_model('Item').get_collection_string(), ItemViewSet)

View File

@ -1,8 +1,9 @@
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from openslides.utils.autoupdate import inform_changed_data from ..utils.auth import has_perm
from ..utils.autoupdate import inform_changed_data
from ..utils.collection import Collection
from .models import Item from .models import Item
@ -51,3 +52,22 @@ def get_permission_change_data(sender, permissions, **kwargs):
and permission.codename in ('can_see', 'can_see_hidden_items')): and permission.codename in ('can_see', 'can_see_hidden_items')):
yield from agenda_app.get_startup_elements() yield from agenda_app.get_startup_elements()
break break
def is_user_data_required(sender, request_user, user_data, **kwargs):
"""
Returns True if request user can see the agenda and user_data is required
to be displayed as speaker.
"""
result = False
if has_perm(request_user, 'agenda.can_see'):
for item_collection_element in Collection(Item.get_collection_string()).element_generator():
full_data = item_collection_element.get_full_data()
for speaker in full_data['speakers']:
if user_data['id'] == speaker['user_id']:
result = True
break
else:
continue
break
return result

View File

@ -16,10 +16,10 @@ class AssignmentsAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from openslides.core.config import config from openslides.core.config import config
from openslides.core.signals import permission_change from openslides.core.signals import permission_change, user_data_required
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .config_variables import get_config_variables from .config_variables import get_config_variables
from .signals import get_permission_change_data from .signals import get_permission_change_data, is_user_data_required
from .views import AssignmentViewSet, AssignmentPollViewSet from .views import AssignmentViewSet, AssignmentPollViewSet
# Define config variables # Define config variables
@ -29,6 +29,9 @@ class AssignmentsAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='assignments_get_permission_change_data') dispatch_uid='assignments_get_permission_change_data')
user_data_required.connect(
is_user_data_required,
dispatch_uid='assignments_is_user_data_required')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet)

View File

@ -1,5 +1,9 @@
from django.apps import apps from django.apps import apps
from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import Assignment
def get_permission_change_data(sender, permissions=None, **kwargs): def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
@ -10,3 +14,32 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
# There could be only one 'assignment.can_see' and then we want to return data. # There could be only one 'assignment.can_see' and then we want to return data.
if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see': if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see':
yield from assignments_app.get_startup_elements() yield from assignments_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs):
"""
Returns True if request user can see assignments and user_data is required
to be displayed as candidates (including poll options).
"""
result = False
if has_perm(request_user, 'assignments.can_see'):
for assignment_collection_element in Collection(Assignment.get_collection_string()).element_generator():
full_data = assignment_collection_element.get_full_data()
for related_user in full_data['assignment_related_users']:
if user_data['id'] == related_user['user_id']:
result = True
break
else:
for poll in full_data['polls']:
for option in poll['options']:
if user_data['id'] == option['candidate_id']:
result = True
break
else:
continue
break
else:
continue
break
break
return result

View File

@ -23,7 +23,9 @@ class CoreAppConfig(AppConfig):
from .signals import ( from .signals import (
delete_django_app_permissions, delete_django_app_permissions,
get_permission_change_data, get_permission_change_data,
permission_change) is_user_data_required,
permission_change,
user_data_required)
from .views import ( from .views import (
ChatMessageViewSet, ChatMessageViewSet,
ConfigViewSet, ConfigViewSet,
@ -43,6 +45,9 @@ class CoreAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='core_get_permission_change_data') dispatch_uid='core_get_permission_change_data')
user_data_required.connect(
is_user_data_required,
dispatch_uid='core_is_user_data_required')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)

View File

@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.dispatch import Signal from django.dispatch import Signal
from ..utils.auth import has_perm
from ..utils.collection import Collection from ..utils.collection import Collection
from .models import ChatMessage
# This signal is send when the migrate command is done. That means it is sent # This signal is send when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it # after post_migrate sending and creating all Permission objects. Don't use it
@ -15,6 +17,12 @@ post_permission_creation = Signal()
# permission). Connected receivers may yield Collections. # permission). Connected receivers may yield Collections.
permission_change = Signal() permission_change = Signal()
# This signal is sent if someone wants to see basic user data. Connected
# receivers may answer True if the user data is required for the request user
# (this can be anything that is allowd as argument for utils.auth.has_perm())
# e. g. as motion submitter or assignment candidate.
user_data_required = Signal(providing_args=['request_user', 'user_data'])
def delete_django_app_permissions(sender, **kwargs): def delete_django_app_permissions(sender, **kwargs):
""" """
@ -42,3 +50,18 @@ def get_permission_change_data(sender, permissions, **kwargs):
yield Collection(core_app.get_model('Countdown').get_collection_string()) yield Collection(core_app.get_model('Countdown').get_collection_string())
elif permission.codename == 'can_use_chat': elif permission.codename == 'can_use_chat':
yield Collection(core_app.get_model('ChatMessage').get_collection_string()) yield Collection(core_app.get_model('ChatMessage').get_collection_string())
def is_user_data_required(sender, request_user, user_data, **kwargs):
"""
Returns True if request user can use chat and user_data is required
to be displayed as chatter.
"""
result = False
if has_perm(request_user, 'core.can_use_chat'):
for chat_message_collection_element in Collection(ChatMessage.get_collection_string()).element_generator():
full_data = chat_message_collection_element.get_full_data()
if user_data['id'] == full_data['user_id']:
result = True
break
return result

View File

@ -15,15 +15,18 @@ class MediafilesAppConfig(AppConfig):
from . import projector # noqa from . import projector # noqa
# Import all required stuff. # Import all required stuff.
from openslides.core.signals import permission_change from openslides.core.signals import permission_change, user_data_required
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .signals import get_permission_change_data from .signals import get_permission_change_data, is_user_data_required
from .views import MediafileViewSet from .views import MediafileViewSet
# Connect signals. # Connect signals.
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='mediafiles_get_permission_change_data') dispatch_uid='mediafiles_get_permission_change_data')
user_data_required.connect(
is_user_data_required,
dispatch_uid='mediafiles_is_user_data_required')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet) router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet)

View File

@ -1,5 +1,9 @@
from django.apps import apps from django.apps import apps
from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import Mediafile
def get_permission_change_data(sender, permissions=None, **kwargs): def get_permission_change_data(sender, permissions=None, **kwargs):
""" """
@ -10,3 +14,18 @@ def get_permission_change_data(sender, permissions=None, **kwargs):
# There could be only one 'mediafiles.can_see' and then we want to return data. # There could be only one 'mediafiles.can_see' and then we want to return data.
if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see': if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see':
yield from mediafiles_app.get_startup_elements() yield from mediafiles_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs):
"""
Returns True if request user can see mediafiles and user_data is required
to be displayed as uploader.
"""
result = False
if has_perm(request_user, 'mediafiles.can_see'):
for mediafile_collection_element in Collection(Mediafile.get_collection_string()).element_generator():
full_data = mediafile_collection_element.get_full_data()
if user_data['id'] == full_data['uploader_id']:
result = True
break
return result

View File

@ -17,10 +17,10 @@ class MotionsAppConfig(AppConfig):
# Import all required stuff. # Import all required stuff.
from openslides.core.config import config from openslides.core.config import config
from openslides.core.signals import permission_change from openslides.core.signals import permission_change, user_data_required
from openslides.utils.rest_api import router from openslides.utils.rest_api import router
from .config_variables import get_config_variables from .config_variables import get_config_variables
from .signals import create_builtin_workflows, get_permission_change_data from .signals import create_builtin_workflows, get_permission_change_data, is_user_data_required
from .views import CategoryViewSet, MotionViewSet, MotionBlockViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet from .views import CategoryViewSet, MotionViewSet, MotionBlockViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet
# Define config variables # Define config variables
@ -31,6 +31,9 @@ class MotionsAppConfig(AppConfig):
permission_change.connect( permission_change.connect(
get_permission_change_data, get_permission_change_data,
dispatch_uid='motions_get_permission_change_data') dispatch_uid='motions_get_permission_change_data')
user_data_required.connect(
is_user_data_required,
dispatch_uid='motions_is_user_data_required')
# Register viewsets. # Register viewsets.
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)

View File

@ -1,7 +1,9 @@
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from .models import State, Workflow from ..utils.auth import has_perm
from ..utils.collection import Collection
from .models import Motion, State, Workflow
def create_builtin_workflows(sender, **kwargs): def create_builtin_workflows(sender, **kwargs):
@ -114,3 +116,18 @@ def get_permission_change_data(sender, permissions, **kwargs):
# There could be only one 'motions.can_see' and then we want to return data. # There could be only one 'motions.can_see' and then we want to return data.
if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see': if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see':
yield from motions_app.get_startup_elements() yield from motions_app.get_startup_elements()
def is_user_data_required(sender, request_user, user_data, **kwargs):
"""
Returns True if request user can see motions and user_data is required
to be displayed as motion submitter or supporter.
"""
result = False
if has_perm(request_user, 'motions.can_see'):
for motion_collection_element in Collection(Motion.get_collection_string()).element_generator():
full_data = motion_collection_element.get_full_data()
if user_data['id'] in full_data['submitters_id'] or user_data['id'] in full_data['supporters_id']:
result = True
break
return result

View File

@ -1,5 +1,6 @@
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from ..core.signals import user_data_required
from ..utils.access_permissions import BaseAccessPermissions from ..utils.access_permissions import BaseAccessPermissions
from ..utils.auth import anonymous_is_enabled, has_perm from ..utils.auth import anonymous_is_enabled, has_perm
@ -49,7 +50,18 @@ class UserAccessPermissions(BaseAccessPermissions):
# to see himself. # to see himself.
case = LITTLE_DATA case = LITTLE_DATA
else: else:
case = NO_DATA # Now check if the user to be sent out is required by any app e. g.
# as motion submitter or assignment candidate.
receiver_responses = user_data_required.send(
sender=self.__class__,
request_user=user,
user_data=full_data)
for receiver, response in receiver_responses:
if response:
case = LITTLE_DATA
break
else:
case = NO_DATA
# Setup data. # Setup data.
if case == FULL_DATA: if case == FULL_DATA:

View File

@ -413,6 +413,27 @@ class RetrieveMotion(TestCase):
response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk])) response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_user_without_can_see_user_permission_to_see_motion_and_submitter_data(self):
self.motion.submitters.add(get_user_model().objects.get(username='admin'))
group = get_user_model().groups.field.related_model.objects.get(pk=1) # Group with pk 1 is for anonymous and default users.
permission_string = 'users.can_see_name'
app_label, codename = permission_string.split('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission)
config['general_system_enable_anonymous'] = True
guest_client = APIClient()
response_1 = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
self.assertEqual(response_1.status_code, status.HTTP_200_OK)
response_2 = guest_client.get(reverse('user-detail', args=[response_1.data['submitters_id'][0]]))
self.assertEqual(response_2.status_code, status.HTTP_200_OK)
extra_user = get_user_model().objects.create_user(
username='username_wequePhieFoom0hai3wa',
password='password_ooth7taechai5Oocieya')
response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk]))
self.assertEqual(response_3.status_code, status.HTTP_403_FORBIDDEN)
class UpdateMotion(TestCase): class UpdateMotion(TestCase):
""" """

View File

@ -103,6 +103,19 @@ class UserGetTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_get_with_user_without_permissions(self):
group = Group.objects.get(pk=1)
permission_string = 'users.can_see_name'
app_label, codename = permission_string.split('.')
permission = group.permissions.get(content_type__app_label=app_label, codename=codename)
group.permissions.remove(permission)
config['general_system_enable_anonymous'] = True
guest_client = APIClient()
response = guest_client.get('/rest/users/user/1/')
self.assertEqual(response.status_code, 403)
class UserCreate(TestCase): class UserCreate(TestCase):
""" """