diff --git a/CHANGELOG b/CHANGELOG index 2321a83d6..c6cb8ac59 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,12 @@ Core: - No reload on logoff. OpenSlides is now a full single page 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) ========================== diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py index 2a7087bbd..429c808d2 100644 --- a/openslides/agenda/apps.py +++ b/openslides/agenda/apps.py @@ -17,11 +17,12 @@ class AgendaAppConfig(AppConfig): # Import all required stuff. from django.db.models.signals import pre_delete, post_save 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 .config_variables import get_config_variables from .signals import ( get_permission_change_data, + is_user_data_required, listen_to_related_object_post_delete, listen_to_related_object_post_save) from .views import ItemViewSet @@ -39,6 +40,9 @@ class AgendaAppConfig(AppConfig): permission_change.connect( 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. router.register(self.get_model('Item').get_collection_string(), ItemViewSet) diff --git a/openslides/agenda/signals.py b/openslides/agenda/signals.py index 2e2064942..4d76dfced 100644 --- a/openslides/agenda/signals.py +++ b/openslides/agenda/signals.py @@ -1,8 +1,9 @@ from django.apps import apps 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 @@ -51,3 +52,22 @@ def get_permission_change_data(sender, permissions, **kwargs): and permission.codename in ('can_see', 'can_see_hidden_items')): yield from agenda_app.get_startup_elements() 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 diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index 16634c46e..ebaba1d22 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -16,10 +16,10 @@ class AssignmentsAppConfig(AppConfig): # Import all required stuff. 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 .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 # Define config variables @@ -29,6 +29,9 @@ class AssignmentsAppConfig(AppConfig): permission_change.connect( 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. router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) diff --git a/openslides/assignments/signals.py b/openslides/assignments/signals.py index 22182ba51..0bd2bf8b6 100644 --- a/openslides/assignments/signals.py +++ b/openslides/assignments/signals.py @@ -1,5 +1,9 @@ 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): """ @@ -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. if permission.content_type.app_label == assignments_app.label and permission.codename == 'can_see': 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 diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 389316682..f58570467 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -23,7 +23,9 @@ class CoreAppConfig(AppConfig): from .signals import ( delete_django_app_permissions, get_permission_change_data, - permission_change) + is_user_data_required, + permission_change, + user_data_required) from .views import ( ChatMessageViewSet, ConfigViewSet, @@ -43,6 +45,9 @@ class CoreAppConfig(AppConfig): permission_change.connect( 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. router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) diff --git a/openslides/core/signals.py b/openslides/core/signals.py index 2d3f2f8e7..c14c42037 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.dispatch import Signal +from ..utils.auth import has_perm from ..utils.collection import Collection +from .models import ChatMessage # 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 @@ -15,6 +17,12 @@ post_permission_creation = Signal() # permission). Connected receivers may yield Collections. 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): """ @@ -42,3 +50,18 @@ def get_permission_change_data(sender, permissions, **kwargs): yield Collection(core_app.get_model('Countdown').get_collection_string()) elif permission.codename == 'can_use_chat': 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 diff --git a/openslides/mediafiles/apps.py b/openslides/mediafiles/apps.py index 76ef8b5b6..c59303576 100644 --- a/openslides/mediafiles/apps.py +++ b/openslides/mediafiles/apps.py @@ -15,15 +15,18 @@ class MediafilesAppConfig(AppConfig): from . import projector # noqa # 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 .signals import get_permission_change_data + from .signals import get_permission_change_data, is_user_data_required from .views import MediafileViewSet # Connect signals. permission_change.connect( 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. router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet) diff --git a/openslides/mediafiles/signals.py b/openslides/mediafiles/signals.py index ad297ac19..ca333da33 100644 --- a/openslides/mediafiles/signals.py +++ b/openslides/mediafiles/signals.py @@ -1,5 +1,9 @@ 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): """ @@ -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. if permission.content_type.app_label == mediafiles_app.label and permission.codename == 'can_see': 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 diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 45f904875..792a09d71 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -17,10 +17,10 @@ class MotionsAppConfig(AppConfig): # Import all required stuff. 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 .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 # Define config variables @@ -31,6 +31,9 @@ class MotionsAppConfig(AppConfig): permission_change.connect( 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. router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) diff --git a/openslides/motions/signals.py b/openslides/motions/signals.py index 64a0cdb9e..087a2b13b 100644 --- a/openslides/motions/signals.py +++ b/openslides/motions/signals.py @@ -1,7 +1,9 @@ from django.apps import apps 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): @@ -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. if permission.content_type.app_label == motions_app.label and permission.codename == 'can_see': 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 diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py index a33efa10a..86ed0d1a5 100644 --- a/openslides/users/access_permissions.py +++ b/openslides/users/access_permissions.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import AnonymousUser +from ..core.signals import user_data_required from ..utils.access_permissions import BaseAccessPermissions from ..utils.auth import anonymous_is_enabled, has_perm @@ -49,7 +50,18 @@ class UserAccessPermissions(BaseAccessPermissions): # to see himself. case = LITTLE_DATA 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. if case == FULL_DATA: diff --git a/tests/integration/motions/test_viewset.py b/tests/integration/motions/test_viewset.py index b770152fc..0807cfbd5 100644 --- a/tests/integration/motions/test_viewset.py +++ b/tests/integration/motions/test_viewset.py @@ -413,6 +413,27 @@ class RetrieveMotion(TestCase): response = submitter_client.get(reverse('motion-detail', args=[self.motion.pk])) 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): """ diff --git a/tests/integration/users/test_viewset.py b/tests/integration/users/test_viewset.py index 859ab0b1a..43856a750 100644 --- a/tests/integration/users/test_viewset.py +++ b/tests/integration/users/test_viewset.py @@ -103,6 +103,19 @@ class UserGetTest(TestCase): 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): """