diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index 47b85d1a4..5fbdb884b 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -11,10 +11,23 @@ class ItemAccessPermissions(BaseAccessPermissions): """ return user.has_perm('agenda.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ from .serializers import ItemSerializer return ItemSerializer + + def get_restricted_data(self, full_data, user): + """ + Returns the restricted serialized data for the instance prepared + for the user. + """ + if (self.can_retrieve(user) and + (not full_data['is_hidden'] or + user.has_perm('agenda.can_see_hidden_items'))): + data = full_data + else: + data = None + return data diff --git a/openslides/agenda/serializers.py b/openslides/agenda/serializers.py index 0e975e33c..fb43a7615 100644 --- a/openslides/agenda/serializers.py +++ b/openslides/agenda/serializers.py @@ -1,10 +1,4 @@ -from django.core.urlresolvers import reverse - -from openslides.utils.rest_api import ( - ModelSerializer, - RelatedField, - get_collection_and_id_from_url, -) +from openslides.utils.rest_api import ModelSerializer, RelatedField from .models import Item, Speaker @@ -34,10 +28,7 @@ class RelatedItemRelatedField(RelatedField): Returns info concerning the related object extracted from the api URL of this object. """ - view_name = '%s-detail' % type(value)._meta.object_name.lower() - url = reverse(view_name, kwargs={'pk': value.pk}) - collection, obj_id = get_collection_and_id_from_url(url) - return {'collection': collection, 'id': obj_id} + return {'collection': value.get_collection_string(), 'id': value.get_rest_pk()} class ItemSerializer(ModelSerializer): diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 0fd57a092..53b367ba6 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -63,7 +63,6 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV Checks if the requesting user has permission to see also an organizational item if it is one. """ - #TODO if obj.is_hidden() and not request.user.has_perm('agenda.can_see_hidden_items'): self.permission_denied(request) diff --git a/openslides/assignments/access_permissions.py b/openslides/assignments/access_permissions.py index 9391068b4..db5fbe3ab 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -11,14 +11,27 @@ class AssignmentAccessPermissions(BaseAccessPermissions): """ return user.has_perm('assignments.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns different serializer classes according to users permissions. """ from .serializers import AssignmentFullSerializer, AssignmentShortSerializer - if user.has_perm('assignments.can_manage'): + if user is None or user.has_perm('assignments.can_manage'): serializer_class = AssignmentFullSerializer else: serializer_class = AssignmentShortSerializer return serializer_class + + def get_restricted_data(self, full_data, user): + """ + Returns the restricted serialized data for the instance prepared + for the user. Removes unpublushed polls for non admins so that they + only get a result like the AssignmentShortSerializer would give them. + """ + if user.has_perm('assignments.can_manage'): + data = full_data + else: + data = full_data.copy() + data['polls'] = [poll for poll in data['polls'] if poll['published']] + return data diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index adc090c04..67a455495 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -12,6 +12,7 @@ class AssignmentsAppConfig(AppConfig): # Load projector elements. # Do this by just importing all from these files. from . import projector # noqa + # Import all required stuff. from openslides.core.signals import config_signal from openslides.utils.rest_api import router diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index 458c3cc43..ab978dce1 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -11,7 +11,7 @@ class ProjectorAccessPermissions(BaseAccessPermissions): """ return user.has_perm('core.can_see_projector') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -30,7 +30,7 @@ class CustomSlideAccessPermissions(BaseAccessPermissions): """ return user.has_perm('core.can_manage_projector') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -53,7 +53,7 @@ class TagAccessPermissions(BaseAccessPermissions): # so if they are enabled. return user.is_authenticated() or config['general_system_enable_anonymous'] - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -74,7 +74,7 @@ class ChatMessageAccessPermissions(BaseAccessPermissions): # permission core.can_use_chat. But they can not use it. See views.py. return user.has_perm('core.can_use_chat') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -98,12 +98,12 @@ class ConfigAccessPermissions(BaseAccessPermissions): # the config. Anonymous users can do so if they are enabled. return user.is_authenticated() or config['general_system_enable_anonymous'] - def get_serialized_data(self, instance, user): + def get_full_data(self, instance): """ - Returns the serlialized config data or None if the user is not - allowed to see it. + Returns the serlialized config data. """ from .config import config - if self.can_retrieve(user) is not None: - return {'key': instance.key, 'value': config[instance.key]} + # Attention: The format of this response has to be the same as in + # the retrieve method of ConfigViewSet. + return {'key': instance.key, 'value': config[instance.key]} diff --git a/openslides/core/migrations/0001_initial.py b/openslides/core/migrations/0001_initial.py index b71c40805..6ffa3f415 100644 --- a/openslides/core/migrations/0001_initial.py +++ b/openslides/core/migrations/0001_initial.py @@ -2,6 +2,7 @@ # Generated by Django 1.9.2 on 2016-03-02 01:22 from __future__ import unicode_literals +import json import uuid import django.db.models.deletion @@ -12,18 +13,15 @@ from django.db import migrations, models import openslides.utils.models -def add_default_projector(apps, schema_editor): +def add_default_projector_via_sql(): """ Adds default projector and activates clock. """ - # We get the model from the versioned app registry; - # if we directly import it, it will be the wrong version. - Projector = apps.get_model('core', 'Projector') projector_config = {} projector_config[uuid.uuid4().hex] = { 'name': 'core/clock', 'stable': True} - Projector.objects.create(config=projector_config) + return ["INSERT INTO core_projector (config, scale, scroll) VALUES ('{}', 0, 0);".format(json.dumps(projector_config))] class Migration(migrations.Migration): @@ -107,9 +105,5 @@ class Migration(migrations.Migration): }, bases=(openslides.utils.models.RESTModelMixin, models.Model), ), - migrations.RunPython( - code=add_default_projector, - reverse_code=None, - atomic=True, - ), + migrations.RunSQL(add_default_projector_via_sql()), ] diff --git a/openslides/core/views.py b/openslides/core/views.py index 5cf070389..b76183d1b 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -485,7 +485,7 @@ class ConfigViewSet(ViewSet): except ConfigNotFound: raise Http404 # Attention: The format of this response has to be the same as in - # the get_serialized_data method of ConfigAccessPermissions. + # the get_full_data method of ConfigAccessPermissions. return Response({'key': key, 'value': value}) def update(self, request, *args, **kwargs): diff --git a/openslides/mediafiles/access_permissions.py b/openslides/mediafiles/access_permissions.py index 832b9bffa..3be70f200 100644 --- a/openslides/mediafiles/access_permissions.py +++ b/openslides/mediafiles/access_permissions.py @@ -11,7 +11,7 @@ class MediafileAccessPermissions(BaseAccessPermissions): """ return user.has_perm('mediafiles.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index 10c1cf01c..f4546d5d3 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -11,7 +11,7 @@ class MotionAccessPermissions(BaseAccessPermissions): """ return user.has_perm('motions.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -30,7 +30,7 @@ class CategoryAccessPermissions(BaseAccessPermissions): """ return user.has_perm('motions.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ @@ -49,7 +49,7 @@ class WorkflowAccessPermissions(BaseAccessPermissions): """ return user.has_perm('motions.can_see') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns serializer class. """ diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py index 675fdd603..a00cf3c17 100644 --- a/openslides/users/access_permissions.py +++ b/openslides/users/access_permissions.py @@ -11,16 +11,39 @@ class UserAccessPermissions(BaseAccessPermissions): """ return user.has_perm('users.can_see_name') - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns different serializer classes with respect user's permissions. """ from .serializers import UserFullSerializer, UserShortSerializer - if user.has_perm('users.can_see_extra_data'): + if user is None or user.has_perm('users.can_see_extra_data'): # Return the UserFullSerializer for requests of users with more # permissions. serializer_class = UserFullSerializer else: serializer_class = UserShortSerializer return serializer_class + + def get_restricted_data(self, full_data, user): + """ + Returns the restricted serialized data for the instance prepared + for the user. Removes several fields for non admins so that they do + not get the default_password or even get only the fields as the + UserShortSerializer would give them. + """ + from .serializers import USERSHORTSERIALIZER_FIELDS + + if user.has_perm('users.can_manage'): + data = full_data + elif user.has_perm('users.can_see_extra_data'): + # Only remove default password from full data. + data = full_data.copy() + del data['default_password'] + else: + # Let only fields as in the UserShortSerializer pass this method. + data = {} + for key in full_data.keys(): + if key in USERSHORTSERIALIZER_FIELDS: + data[key] = full_data[key] + return data diff --git a/openslides/users/serializers.py b/openslides/users/serializers.py index f8f48ae30..162179a33 100644 --- a/openslides/users/serializers.py +++ b/openslides/users/serializers.py @@ -11,16 +11,7 @@ from ..utils.rest_api import ( ) from .models import Group, User - -class UserShortSerializer(ModelSerializer): - """ - Serializer for users.models.User objects. - - Serializes only name fields and about me field. - """ - class Meta: - model = User - fields = ( +USERSHORTSERIALIZER_FIELDS = ( 'id', 'username', 'title', @@ -32,6 +23,17 @@ class UserShortSerializer(ModelSerializer): ) +class UserShortSerializer(ModelSerializer): + """ + Serializer for users.models.User objects. + + Serializes only name fields and about me field. + """ + class Meta: + model = User + fields = USERSHORTSERIALIZER_FIELDS + + class UserFullSerializer(ModelSerializer): """ Serializer for users.models.User objects. diff --git a/openslides/users/views.py b/openslides/users/views.py index 1591691ff..625a323b2 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -75,7 +75,6 @@ class UserViewSet(ModelViewSet): Hides the default_password for non admins. """ - #TODO: Hide default_password also in case of autoupdate. response = super().retrieve(request, *args, **kwargs) self.extract_default_password(response) return response diff --git a/openslides/utils/access_permissions.py b/openslides/utils/access_permissions.py index 844be9970..aeecddbf9 100644 --- a/openslides/utils/access_permissions.py +++ b/openslides/utils/access_permissions.py @@ -1,30 +1,77 @@ -class BaseAccessPermissions: +from django.dispatch import Signal + +from .dispatch import SignalConnectMetaClass + + +class BaseAccessPermissions(object, metaclass=SignalConnectMetaClass): """ Base access permissions container. + + Every app which has autoupdate models has to create classes subclassing + from this base class for every autoupdate root model. Each subclass has + to have a globally unique name. The metaclass (SignalConnectMetaClass) + does the rest of the magic. """ + signal = Signal() + + def __init__(self, **kwargs): + """ + Initializes the access permission instance. This is done when the + signal is sent. + + Because of Django's signal API, we have to take wildcard keyword + arguments. But they are not used here. + """ + pass + + @classmethod + def get_dispatch_uid(cls): + """ + Returns the classname as a unique string for each class. Returns None + for the base class so it will not be connected to the signal. + """ + if not cls.__name__ == 'BaseAccessPermissions': + return cls.__name__ + def can_retrieve(self, user): """ - Returns True if the user has read access model instances. + Returns True if the user has read access to model instances. """ return False - def get_serializer_class(self, user): + def get_serializer_class(self, user=None): """ Returns different serializer classes according to users permissions. + + This should return the serializer for full data access if user is + None. See get_full_data(). """ raise NotImplementedError( - "You have to add the classmethod 'get_serializer_class' to your " + "You have to add the method 'get_serializer_class' to your " "access permissions class.".format(self)) - def get_serialized_data(self, instance, user): + def get_full_data(self, instance): """ - Returns the serialized data for the instance prepared for the user. + Returns all possible serialized data for the given instance. + """ + return self.get_serializer_class(user=None)(instance).data - Returns None if the user has no read access. + def get_restricted_data(self, full_data, user): + """ + Returns the restricted serialized data for the instance prepared + for the user. + + Returns None if the user has no read access. Returns reduced data + if the user has limited access. Default: Returns full data if the + user has read access to model instances. + + Hint: You should override this method if your + get_serializer_class() method may return different serializer for + different users or if you have access restrictions in your view or + viewset in methods like retrieve() or check_object_permissions(). """ if self.can_retrieve(user): - serializer_class = self.get_serializer_class(user) - data = serializer_class(instance).data + data = full_data else: data = None return data diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index 9278bf424..a9ec2d78c 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -1,3 +1,4 @@ +import json import os import posixpath from importlib import import_module @@ -17,7 +18,8 @@ from tornado.web import ( ) from tornado.wsgi import WSGIContainer -from openslides.users.auth import AnonymousUser, get_user +from ..users.auth import AnonymousUser, get_user +from .access_permissions import BaseAccessPermissions RUNNING_HOST = None RUNNING_PORT = None @@ -71,10 +73,21 @@ class OpenSlidesSockJSConnection(SockJSConnection): OpenSlidesSockJSConnection.waiters.remove(self) @classmethod - def send_object(cls, instance, is_delete): + def send_object(cls, json_container): """ Sends an OpenSlides object to all connected clients (waiters). """ + # Load JSON + container = json.loads(json_container) + + # Search our AccessPermission class. + for access_permissions in BaseAccessPermissions.get_all(): + if access_permissions.get_dispatch_uid() == container.get('dispatch_uid'): + break + else: + raise ValueError('Invalid container. A valid dispatch_uid is missing.') + + # Loop over all waiters for waiter in cls.waiters: # Read waiter's former cookies and parse session cookie to get user instance. try: @@ -89,15 +102,28 @@ class OpenSlidesSockJSConnection(SockJSConnection): fake_request = type('FakeRequest', (), {})() fake_request.session = session user = get_user(fake_request) - # Fetch serialized data and send them out to the waiter (client). - serialized_instance_data = instance.get_access_permissions().get_serialized_data(instance, user) - if serialized_instance_data is not None: - data = { - 'status_code': 404 if is_delete else 200, # TODO: Refactor this. Use strings like 'change' or 'delete'. - 'collection': instance.get_collection_string(), - 'id': instance.get_rest_pk(), - 'data': serialized_instance_data} - waiter.send(data) + + # Two cases: models instance was changed or deleted + if container.get('action') == 'changed': + data = access_permissions.get_restricted_data(container.get('full_data'), user) + if data is None: + # There are no data for the user so he can't see the object. Skip him. + break + output = { + 'status_code': 200, # TODO: Refactor this. Use strings like 'change' or 'delete'. + 'collection': container['collection_string'], + 'id': container['rest_pk'], + 'data': data} + elif container.get('action') == 'deleted': + output = { + 'status_code': 404, # TODO: Refactor this. Use strings like 'change' or 'delete'. + 'collection': container['collection_string'], + 'id': container['rest_pk']} + else: + raise ValueError('Invalid container. A valid action is missing.') + + # Send output to the waiter (client). + waiter.send(output) def run_tornado(addr, port, *args, **kwargs): @@ -152,13 +178,20 @@ def inform_changed_data(is_delete, *args): # Instance has no method get_root_rest_element. Just skip it. pass else: + access_permissions = root_instance.get_access_permissions() + container = { + 'dispatch_uid': access_permissions.get_dispatch_uid(), + 'collection_string': root_instance.get_collection_string(), + 'rest_pk': root_instance.get_rest_pk()} if is_delete and instance == root_instance: # A root instance is deleted. - OpenSlidesSockJSConnection.send_object(root_instance, is_delete) + container['action'] = 'deleted' else: # A non root instance is deleted or any instance is just changed. + container['action'] = 'changed' root_instance.refresh_from_db() - OpenSlidesSockJSConnection.send_object(root_instance, False) + container['full_data'] = access_permissions.get_full_data(root_instance) + OpenSlidesSockJSConnection.send_object(json.dumps(container)) else: pass # TODO: Implement big variant with Apache or Nginx as WSGI webserver. diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 596bc0e8f..aced06732 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,6 +1,4 @@ -import re from collections import OrderedDict -from urllib.parse import urlparse from rest_framework import status # noqa from rest_framework.decorators import detail_route, list_route # noqa @@ -35,8 +33,6 @@ from rest_framework.viewsets import \ ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa from rest_framework.viewsets import ViewSet as _ViewSet # noqa -from .exceptions import OpenSlidesError - router = DefaultRouter() @@ -198,21 +194,3 @@ class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet): class ViewSet(PermissionMixin, _ViewSet): pass - - -#TODO: Remove this method -def get_collection_and_id_from_url(url): - """ - Helper function. Returns a tuple containing the collection name and the id - extracted out of the given REST api URL. - - For example get_collection_and_id_from_url('http://localhost/rest/users/user/3/') - returns ('users/user', '3'). - - Raises OpenSlidesError if the URL is invalid. - """ - path = urlparse(url).path - match = re.match(r'^/rest/(?P[-\w]+/[-\w]+)/(?P[-\w]+)/$', path) - if not match: - raise OpenSlidesError('Invalid REST API URL: %s' % url) - return match.group('collection'), match.group('id') diff --git a/tests/old/agenda/test_list_of_speakers.py b/tests/old/agenda/test_list_of_speakers.py index 4e00f2197..4569a80e6 100644 --- a/tests/old/agenda/test_list_of_speakers.py +++ b/tests/old/agenda/test_list_of_speakers.py @@ -1,4 +1,5 @@ from openslides.agenda.models import Item, Speaker +from openslides.core.models import CustomSlide from openslides.users.models import User from openslides.utils.exceptions import OpenSlidesError from openslides.utils.test import TestCase @@ -6,8 +7,8 @@ from openslides.utils.test import TestCase class ListOfSpeakerModelTests(TestCase): def setUp(self): - self.item1 = Item.objects.create(title='item1') - self.item2 = Item.objects.create(title='item2') + self.item1 = CustomSlide.objects.create(title='item1').agenda_item + self.item2 = CustomSlide.objects.create(title='item2').agenda_item self.speaker1 = User.objects.create(username='user1') self.speaker2 = User.objects.create(username='user2') diff --git a/tests/old/mediafiles/tests.py b/tests/old/mediafiles/tests.py index fe4ddd04d..909b47ad3 100644 --- a/tests/old/mediafiles/tests.py +++ b/tests/old/mediafiles/tests.py @@ -36,7 +36,7 @@ class MediafileTest(TestCase): os.close(tmpfile_no) def tearDown(self): - self.object.mediafile.delete() + self.object.mediafile.delete(save=False) def test_str(self): self.assertEqual(str(self.object), 'Title File 1')