diff --git a/openslides/agenda/access_permissions.py b/openslides/agenda/access_permissions.py index 173dfbf58..47b85d1a4 100644 --- a/openslides/agenda/access_permissions.py +++ b/openslides/agenda/access_permissions.py @@ -1,9 +1,20 @@ -class AccessPermissions: - def get_serializer_class(self, user): - return None +from ..utils.access_permissions import BaseAccessPermissions + +class ItemAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Item and ItemViewSet. + """ def can_retrieve(self, user): """ - TODO + Returns True if the user has read access model instances. """ - return user.has_perm('assignments.can_see') + return user.has_perm('agenda.can_see') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import ItemSerializer + + return ItemSerializer diff --git a/openslides/agenda/apps.py b/openslides/agenda/apps.py index 6e0f2c913..a0c573577 100644 --- a/openslides/agenda/apps.py +++ b/openslides/agenda/apps.py @@ -33,4 +33,4 @@ class AgendaAppConfig(AppConfig): dispatch_uid='listen_to_related_object_post_delete') # Register viewsets. - router.register('agenda/item', ItemViewSet) + router.register(self.get_model('Item').get_collection_string(), ItemViewSet) diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index efd6c0d63..23483d59e 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -15,6 +15,8 @@ from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import RESTModelMixin from openslides.utils.utils import to_roman +from .access_permissions import ItemAccessPermissions + class ItemManager(models.Manager): """ @@ -184,6 +186,7 @@ class Item(RESTModelMixin, models.Model): """ An Agenda Item """ + access_permissions = ItemAccessPermissions() objects = ItemManager() AGENDA_ITEM = 1 diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index d680d5f92..0fd57a092 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy from reportlab.platypus import Paragraph from openslides.core.config import config -from openslides.agenda.access_permissions import AccessPermissions from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet from openslides.utils.rest_api import ( @@ -22,8 +21,8 @@ from openslides.utils.rest_api import ( ) from openslides.utils.views import PDFView +from .access_permissions import ItemAccessPermissions from .models import Item, Speaker -from .serializers import ItemSerializer # Viewsets for the REST API @@ -35,16 +34,15 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV There are the following views: metadata, list, retrieve, create, partial_update, update, destroy, manage_speaker, speak and tree. """ + access_permissions = ItemAccessPermissions() queryset = Item.objects.all() - serializer_class = ItemSerializer - access_permissions = AccessPermissions() def check_view_permissions(self): """ Returns True if the user has required permissions. """ if self.action == 'retrieve': - result = self.access_permissions.can_retrieve(self.request.user) + result = self.get_access_permissions().can_retrieve(self.request.user) elif self.action in ('metadata', 'list', 'manage_speaker', 'tree'): result = self.request.user.has_perm('agenda.can_see') # For manage_speaker and tree requests the rest of the check is @@ -65,6 +63,7 @@ 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 f31b0c528..9391068b4 100644 --- a/openslides/assignments/access_permissions.py +++ b/openslides/assignments/access_permissions.py @@ -1,18 +1,24 @@ +from ..utils.access_permissions import BaseAccessPermissions + + +class AssignmentAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Assignment and AssignmentViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('assignments.can_see') -class AccessPermissions: def get_serializer_class(self, user): """ Returns different serializer classes according to users permissions. """ - from openslides.assignments.serializers import AssignmentFullSerializer, AssignmentShortSerializer + from .serializers import AssignmentFullSerializer, AssignmentShortSerializer + if user.has_perm('assignments.can_manage'): serializer_class = AssignmentFullSerializer else: serializer_class = AssignmentShortSerializer return serializer_class - - def can_retrieve(self, user): - """ - TODO - """ - return user.has_perm('agenda.can_see') diff --git a/openslides/assignments/apps.py b/openslides/assignments/apps.py index 7fa797c72..adc090c04 100644 --- a/openslides/assignments/apps.py +++ b/openslides/assignments/apps.py @@ -22,5 +22,5 @@ class AssignmentsAppConfig(AppConfig): config_signal.connect(setup_assignment_config, dispatch_uid='setup_assignment_config') # Register viewsets. - router.register(self.get_model('Assignment').get_collection_name(), AssignmentViewSet) + router.register(self.get_model('Assignment').get_collection_string(), AssignmentViewSet) router.register('assignments/poll', AssignmentPollViewSet) diff --git a/openslides/assignments/models.py b/openslides/assignments/models.py index f32ef95f3..f777369c5 100644 --- a/openslides/assignments/models.py +++ b/openslides/assignments/models.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.agenda.models import Item, Speaker -from openslides.assignments.access_permissions import AccessPermissions from openslides.core.config import config from openslides.core.models import Tag from openslides.poll.models import ( @@ -21,6 +20,8 @@ from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import RESTModelMixin from openslides.utils.search import user_name_helper +from .access_permissions import AssignmentAccessPermissions + class AssignmentRelatedUser(RESTModelMixin, models.Model): """ @@ -50,7 +51,10 @@ class AssignmentRelatedUser(RESTModelMixin, models.Model): class Assignment(RESTModelMixin, models.Model): - access_permissions = AccessPermissions() + """ + Model for assignments. + """ + access_permissions = AssignmentAccessPermissions() PHASE_SEARCH = 0 PHASE_VOTING = 1 diff --git a/openslides/assignments/views.py b/openslides/assignments/views.py index 7071b1db6..9a88e5465 100644 --- a/openslides/assignments/views.py +++ b/openslides/assignments/views.py @@ -17,7 +17,6 @@ from reportlab.platypus import ( TableStyle, ) -from openslides.assignments.access_permissions import AccessPermissions from openslides.core.config import config from openslides.utils.pdf import stylesheet from openslides.utils.rest_api import ( @@ -31,12 +30,9 @@ from openslides.utils.rest_api import ( ) from openslides.utils.views import PDFView +from .access_permissions import AssignmentAccessPermissions from .models import Assignment, AssignmentPoll -from .serializers import ( - AssignmentAllPollSerializer, - AssignmentFullSerializer, - AssignmentShortSerializer, -) +from .serializers import AssignmentAllPollSerializer # Viewsets for the REST API @@ -49,15 +45,15 @@ class AssignmentViewSet(ModelViewSet): partial_update, update, destroy, candidature_self, candidature_other, mark_elected and create_poll. """ + access_permissions = AssignmentAccessPermissions() queryset = Assignment.objects.all() - access_permissions = AccessPermissions() def check_view_permissions(self): """ Returns True if the user has required permissions. """ if self.action == 'retrieve': - result = self.access_permissions.can_retrieve(self.request.user) + result = self.get_access_permissions().can_retrieve(self.request.user) elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('assignments.can_see') elif self.action in ('create', 'partial_update', 'update', 'destroy', diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index 38985369b..458c3cc43 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -1,9 +1,109 @@ -class AccessPermissions: - def get_serializer_class(self, user): - return None +from ..utils.access_permissions import BaseAccessPermissions + +class ProjectorAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Projector and ProjectorViewSet. + """ def can_retrieve(self, user): """ - TODO + Returns True if the user has read access model instances. """ return user.has_perm('core.can_see_projector') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import ProjectorSerializer + + return ProjectorSerializer + + +class CustomSlideAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for CustomSlide and CustomSlideViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('core.can_manage_projector') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import CustomSlideSerializer + + return CustomSlideSerializer + + +class TagAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Tag and TagViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + from .config import config + + # Every authenticated user can retrieve tags. Anonymous users can do + # so if they are enabled. + return user.is_authenticated() or config['general_system_enable_anonymous'] + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import TagSerializer + + return TagSerializer + + +class ChatMessageAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for ChatMessage and ChatMessageViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + # Anonymous users can see the chat if the anonymous group has the + # 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): + """ + Returns serializer class. + """ + from .serializers import ChatMessageSerializer + + return ChatMessageSerializer + + +class ConfigAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for the config (ConfigStore and + ConfigViewSet). + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + from .config import config + + # Every authenticated user can see the metadata and list or retrieve + # 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): + """ + Returns the serlialized config data or None if the user is not + allowed to see it. + """ + from .config import config + + if self.can_retrieve(user) is not None: + return {'key': instance.key, 'value': config[instance.key]} diff --git a/openslides/core/apps.py b/openslides/core/apps.py index ef99c6d88..456d08552 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -12,11 +12,11 @@ class CoreAppConfig(AppConfig): # Load projector elements. # Do this by just importing all from these files. from . import projector # noqa + # Import all required stuff. from django.db.models import signals from openslides.core.signals import config_signal, post_permission_creation - from openslides.utils.autoupdate import inform_changed_data_receiver - from openslides.utils.autoupdate import inform_deleted_data_receiver + from openslides.utils.autoupdate import inform_changed_data_receiver, inform_deleted_data_receiver from openslides.utils.rest_api import router from openslides.utils.search import index_add_instance, index_del_instance from .signals import delete_django_app_permissions, setup_general_config @@ -37,11 +37,11 @@ class CoreAppConfig(AppConfig): dispatch_uid='delete_django_app_permissions') # Register viewsets. - router.register('core/projector', ProjectorViewSet) - router.register('core/chatmessage', ChatMessageViewSet) - router.register('core/customslide', CustomSlideViewSet) - router.register('core/tag', TagViewSet) - router.register('core/config', ConfigViewSet, 'config') + router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) + router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet) + router.register(self.get_model('CustomSlide').get_collection_string(), CustomSlideViewSet) + router.register(self.get_model('Tag').get_collection_string(), TagViewSet) + router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config') # Update data when any model of any installed app is saved or deleted. # TODO: Test if the m2m_changed signal is also needed. diff --git a/openslides/core/models.py b/openslides/core/models.py index 843d9dd0e..fe2f50e0f 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -1,6 +1,5 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.core.urlresolvers import reverse from django.db import models from jsonfield import JSONField @@ -8,6 +7,13 @@ from openslides.mediafiles.models import Mediafile from openslides.utils.models import RESTModelMixin from openslides.utils.projector import ProjectorElement +from .access_permissions import ( + ChatMessageAccessPermissions, + ConfigAccessPermissions, + CustomSlideAccessPermissions, + ProjectorAccessPermissions, + TagAccessPermissions, +) from .exceptions import ProjectorException @@ -51,6 +57,8 @@ class Projector(RESTModelMixin, models.Model): The projector can be controlled using the REST API with POST requests on e. g. the URL /rest/core/projector/1/activate_elements/. """ + access_permissions = ProjectorAccessPermissions() + config = JSONField() scale = models.IntegerField(default=0) @@ -121,6 +129,8 @@ class CustomSlide(RESTModelMixin, models.Model): """ Model for slides with custom content. """ + access_permissions = CustomSlideAccessPermissions() + title = models.CharField( max_length=256) text = models.TextField( @@ -175,6 +185,8 @@ class Tag(RESTModelMixin, models.Model): Model for tags. This tags can be used for other models like agenda items, motions or assignments. """ + access_permissions = TagAccessPermissions() + name = models.CharField( max_length=255, unique=True) @@ -189,10 +201,11 @@ class Tag(RESTModelMixin, models.Model): return self.name -class ConfigStore(models.Model): +class ConfigStore(RESTModelMixin, models.Model): """ A model class to store all config variables in the database. """ + access_permissions = ConfigAccessPermissions() key = models.CharField(max_length=255, unique=True, db_index=True) """A string, the key of the config variable.""" @@ -205,11 +218,15 @@ class ConfigStore(models.Model): permissions = ( ('can_manage_config', 'Can manage configuration'),) - def get_root_rest_url(self): + @classmethod + def get_collection_string(cls): + return 'core/config' + + def get_rest_pk(self): """ - Returns the detail url of config value. + Returns the primary key used in the REST API. """ - return reverse('config-detail', args=[str(self.key)]) + return self.key class ChatMessage(RESTModelMixin, models.Model): @@ -218,6 +235,8 @@ class ChatMessage(RESTModelMixin, models.Model): At the moment we only have one global chat room for managers. """ + access_permissions = ChatMessageAccessPermissions() + message = models.TextField() timestamp = models.DateTimeField(auto_now_add=True) diff --git a/openslides/core/views.py b/openslides/core/views.py index 52f3e0575..5cf070389 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -13,7 +13,6 @@ from django.http import Http404, HttpResponse from django.utils.timezone import now from openslides import __version__ as version -from openslides.core.access_permissions import AccessPermissions from openslides.utils import views as utils_views from openslides.utils.plugins import ( get_plugin_description, @@ -31,15 +30,16 @@ from openslides.utils.rest_api import ( ) from openslides.utils.search import search +from .access_permissions import ( + ChatMessageAccessPermissions, + ConfigAccessPermissions, + CustomSlideAccessPermissions, + ProjectorAccessPermissions, + TagAccessPermissions, +) from .config import config from .exceptions import ConfigError, ConfigNotFound from .models import ChatMessage, CustomSlide, Projector, Tag -from .serializers import ( - ChatMessageSerializer, - CustomSlideSerializer, - ProjectorSerializer, - TagSerializer, -) # Special Django views @@ -153,16 +153,15 @@ class ProjectorViewSet(ReadOnlyModelViewSet): activate_elements, prune_elements, update_elements, deactivate_elements, clear_elements and control_view. """ + access_permissions = ProjectorAccessPermissions() queryset = Projector.objects.all() - serializer_class = ProjectorSerializer - access_permissions = AccessPermissions() def check_view_permissions(self): """ Returns True if the user has required permissions. """ if self.action == 'retrieve': - result = self.access_permissions.can_retrieve(self.request.user) + result = self.get_access_permissions().can_retrieve(self.request.user) elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('core.can_see_projector') elif self.action in ('activate_elements', 'prune_elements', 'update_elements', @@ -370,14 +369,18 @@ class CustomSlideViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = CustomSlideAccessPermissions() queryset = CustomSlide.objects.all() - serializer_class = CustomSlideSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - return self.request.user.has_perm('core.can_manage_projector') + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + else: + result = self.request.user.has_perm('core.can_manage_projector') + return result class TagViewSet(ModelViewSet): @@ -387,16 +390,18 @@ class TagViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = TagAccessPermissions() queryset = Tag.objects.all() - serializer_class = TagSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve'): - # Every authenticated user can see the metadata and list or - # retrieve tags. Anonymous users can do so if they are enabled. + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): + # Every authenticated user can see the metadata and list tags. + # Anonymous users can do so if they are enabled. result = self.request.user.is_authenticated() or config['general_system_enable_anonymous'] elif self.action in ('create', 'update', 'destroy'): result = self.request.user.has_perm('core.can_manage_tags') @@ -444,13 +449,16 @@ class ConfigViewSet(ViewSet): There are the following views: metadata, list, retrieve and update. """ + access_permissions = ConfigAccessPermissions() metadata_class = ConfigMetadata def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): # Every authenticated user can see the metadata and list or # retrieve the config. Anonymous users can do so if they are # enabled. @@ -476,6 +484,8 @@ class ConfigViewSet(ViewSet): value = config[key] except ConfigNotFound: raise Http404 + # Attention: The format of this response has to be the same as in + # the get_serialized_data method of ConfigAccessPermissions. return Response({'key': key, 'value': value}) def update(self, request, *args, **kwargs): @@ -508,18 +518,23 @@ class ChatMessageViewSet(ModelViewSet): There are the following views: metadata, list, retrieve and create. The views partial_update, update and destroy are disabled. """ + access_permissions = ChatMessageAccessPermissions() queryset = ChatMessage.objects.all() - serializer_class = ChatMessageSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - # We do not want anonymous users to use the chat even the anonymous - # group has the permission core.can_use_chat. - return (self.action in ('metadata', 'list', 'retrieve', 'create') and + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + else: + # We do not want anonymous users to use the chat even the anonymous + # group has the permission core.can_use_chat. + result = ( + self.action in ('metadata', 'list', 'create') and self.request.user.is_authenticated() and self.request.user.has_perm('core.can_use_chat')) + return result def perform_create(self, serializer): """ diff --git a/openslides/mediafiles/access_permissions.py b/openslides/mediafiles/access_permissions.py new file mode 100644 index 000000000..832b9bffa --- /dev/null +++ b/openslides/mediafiles/access_permissions.py @@ -0,0 +1,20 @@ +from ..utils.access_permissions import BaseAccessPermissions + + +class MediafileAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Mediafile and MediafileViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('mediafiles.can_see') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import MediafileSerializer + + return MediafileSerializer diff --git a/openslides/mediafiles/apps.py b/openslides/mediafiles/apps.py index 78c4d673a..8f2b69dcf 100644 --- a/openslides/mediafiles/apps.py +++ b/openslides/mediafiles/apps.py @@ -18,4 +18,4 @@ class MediafilesAppConfig(AppConfig): from .views import MediafileViewSet # Register viewsets. - router.register('mediafiles/mediafile', MediafileViewSet) + router.register(self.get_model('Mediafile').get_collection_string(), MediafileViewSet) diff --git a/openslides/mediafiles/models.py b/openslides/mediafiles/models.py index d387bdb19..037c717b3 100644 --- a/openslides/mediafiles/models.py +++ b/openslides/mediafiles/models.py @@ -5,12 +5,15 @@ from django.utils.translation import ugettext as _ from openslides.utils.search import user_name_helper from ..utils.models import RESTModelMixin +from .access_permissions import MediafileAccessPermissions class Mediafile(RESTModelMixin, models.Model): """ Class for uploaded files which can be delivered under a certain url. """ + access_permissions = MediafileAccessPermissions() + mediafile = models.FileField(upload_to='file') """ See https://docs.djangoproject.com/en/dev/ref/models/fields/#filefield diff --git a/openslides/mediafiles/views.py b/openslides/mediafiles/views.py index e39ed9339..f6a598cef 100644 --- a/openslides/mediafiles/views.py +++ b/openslides/mediafiles/views.py @@ -1,6 +1,6 @@ from ..utils.rest_api import ModelViewSet, ValidationError +from .access_permissions import MediafileAccessPermissions from .models import Mediafile -from .serializers import MediafileSerializer # Viewsets for the REST API @@ -12,14 +12,16 @@ class MediafileViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = MediafileAccessPermissions() queryset = Mediafile.objects.all() - serializer_class = MediafileSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('mediafiles.can_see') elif self.action == 'create': result = (self.request.user.has_perm('mediafiles.can_see') and diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py new file mode 100644 index 000000000..10c1cf01c --- /dev/null +++ b/openslides/motions/access_permissions.py @@ -0,0 +1,58 @@ +from ..utils.access_permissions import BaseAccessPermissions + + +class MotionAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Motion and MotionViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('motions.can_see') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import MotionSerializer + + return MotionSerializer + + +class CategoryAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Category and CategoryViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('motions.can_see') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import CategorySerializer + + return CategorySerializer + + +class WorkflowAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for Workflow and WorkflowViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('motions.can_see') + + def get_serializer_class(self, user): + """ + Returns serializer class. + """ + from .serializers import WorkflowSerializer + + return WorkflowSerializer diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 20f5626e6..582090fb5 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -25,7 +25,7 @@ class MotionsAppConfig(AppConfig): post_migrate.connect(create_builtin_workflows, dispatch_uid='motion_create_builtin_workflows') # Register viewsets. - router.register('motions/category', CategoryViewSet) - router.register('motions/motion', MotionViewSet) + router.register(self.get_model('Category').get_collection_string(), CategoryViewSet) + router.register(self.get_model('Motion').get_collection_string(), MotionViewSet) + router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet) router.register('motions/motionpoll', MotionPollViewSet) - router.register('motions/workflow', WorkflowViewSet) diff --git a/openslides/motions/models.py b/openslides/motions/models.py index 2c24416c9..4f51ab033 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -20,6 +20,11 @@ from openslides.poll.models import ( from openslides.utils.models import RESTModelMixin from openslides.utils.search import user_name_helper +from .access_permissions import ( + CategoryAccessPermissions, + MotionAccessPermissions, + WorkflowAccessPermissions, +) from .exceptions import WorkflowError @@ -29,6 +34,7 @@ class Motion(RESTModelMixin, models.Model): This class is the main entry point to all other classes related to a motion. """ + access_permissions = MotionAccessPermissions() active_version = models.ForeignKey( 'MotionVersion', @@ -624,6 +630,11 @@ class MotionVersion(RESTModelMixin, models.Model): class Category(RESTModelMixin, models.Model): + """ + Model for categories of motions. + """ + access_permissions = CategoryAccessPermissions() + name = models.CharField(max_length=255) """Name of the category.""" @@ -879,7 +890,10 @@ class State(RESTModelMixin, models.Model): class Workflow(RESTModelMixin, models.Model): - """Defines a workflow for a motion.""" + """ + Defines a workflow for a motion. + """ + access_permissions = WorkflowAccessPermissions() name = models.CharField(max_length=255) """A string representing the workflow.""" diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 8af5de929..cb10330bd 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -18,15 +18,15 @@ from openslides.utils.rest_api import ( ) from openslides.utils.views import PDFView, SingleObjectMixin +from .access_permissions import ( + CategoryAccessPermissions, + MotionAccessPermissions, + WorkflowAccessPermissions, +) from .exceptions import WorkflowError from .models import Category, Motion, MotionPoll, MotionVersion, Workflow from .pdf import motion_poll_to_pdf, motion_to_pdf, motions_to_pdf -from .serializers import ( - CategorySerializer, - MotionPollSerializer, - MotionSerializer, - WorkflowSerializer, -) +from .serializers import MotionPollSerializer # Viewsets for the REST API @@ -39,14 +39,16 @@ class MotionViewSet(ModelViewSet): partial_update, update, destroy, manage_version, support, set_state and create_poll. """ + access_permissions = MotionAccessPermissions() queryset = Motion.objects.all() - serializer_class = MotionSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve', 'partial_update', 'update'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list', 'partial_update', 'update'): result = self.request.user.has_perm('motions.can_see') # For partial_update and update requests the rest of the check is # done in the update method. See below. @@ -281,14 +283,16 @@ class CategoryViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = CategoryAccessPermissions() queryset = Category.objects.all() - serializer_class = CategorySerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('motions.can_see') elif self.action in ('create', 'partial_update', 'update', 'destroy'): result = (self.request.user.has_perm('motions.can_see') and @@ -305,14 +309,16 @@ class WorkflowViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update and destroy. """ + access_permissions = WorkflowAccessPermissions() queryset = Workflow.objects.all() - serializer_class = WorkflowSerializer def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list'): result = self.request.user.has_perm('motions.can_see') elif self.action in ('create', 'partial_update', 'update', 'destroy'): result = (self.request.user.has_perm('motions.can_see') and diff --git a/openslides/users/access_permissions.py b/openslides/users/access_permissions.py new file mode 100644 index 000000000..675fdd603 --- /dev/null +++ b/openslides/users/access_permissions.py @@ -0,0 +1,26 @@ +from ..utils.access_permissions import BaseAccessPermissions + + +class UserAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for User and UserViewSet. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return user.has_perm('users.can_see_name') + + def get_serializer_class(self, user): + """ + Returns different serializer classes with respect user's permissions. + """ + from .serializers import UserFullSerializer, UserShortSerializer + + if 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 diff --git a/openslides/users/apps.py b/openslides/users/apps.py index 3b36eda7e..623adc8f0 100644 --- a/openslides/users/apps.py +++ b/openslides/users/apps.py @@ -28,5 +28,5 @@ class UsersAppConfig(AppConfig): dispatch_uid='create_builtin_groups_and_admin') # Register viewsets. - router.register('users/user', UserViewSet) + router.register(self.get_model('User').get_collection_string(), UserViewSet) router.register('users/group', GroupViewSet) diff --git a/openslides/users/models.py b/openslides/users/models.py index 3c56ad55d..3fdb3295c 100644 --- a/openslides/users/models.py +++ b/openslides/users/models.py @@ -13,6 +13,7 @@ from openslides.utils.search import user_name_helper from ..core.config import config from ..utils.models import RESTModelMixin +from .access_permissions import UserAccessPermissions from .exceptions import UsersError @@ -94,6 +95,8 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser): in other OpenSlides apps like motion submitter or (assignment) election candidates. """ + access_permissions = UserAccessPermissions() + USERNAME_FIELD = 'username' username = models.CharField( diff --git a/openslides/users/views.py b/openslides/users/views.py index 111f63390..1591691ff 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -14,13 +14,10 @@ from ..utils.rest_api import ( status, ) from ..utils.views import APIView, PDFView +from .access_permissions import UserAccessPermissions from .models import Group, User from .pdf import users_passwords_to_pdf, users_to_pdf -from .serializers import ( - GroupSerializer, - UserFullSerializer, - UserShortSerializer, -) +from .serializers import GroupSerializer, UserFullSerializer # Viewsets for the REST API @@ -32,13 +29,16 @@ class UserViewSet(ModelViewSet): There are the following views: metadata, list, retrieve, create, partial_update, update, destroy and reset_password. """ + access_permissions = UserAccessPermissions() queryset = User.objects.all() def check_view_permissions(self): """ Returns True if the user has required permissions. """ - if self.action in ('metadata', 'list', 'retrieve', 'update', 'partial_update'): + if self.action == 'retrieve': + result = self.get_access_permissions().can_retrieve(self.request.user) + elif self.action in ('metadata', 'list', 'update', 'partial_update'): result = self.request.user.has_perm('users.can_see_name') elif self.action in ('create', 'destroy', 'reset_password'): result = (self.request.user.has_perm('users.can_see_name') and @@ -50,16 +50,13 @@ class UserViewSet(ModelViewSet): def get_serializer_class(self): """ - Returns different serializer classes with respect to action and user's - permissions. + Returns different serializer classes with respect to action. """ - if (self.action in ('create', 'partial_update', 'update') or - self.request.user.has_perm('users.can_see_extra_data')): - # Return the UserFullSerializer for edit requests or for - # list/retrieve requests of users with more permissions. + if self.action in ('create', 'partial_update', 'update'): + # Return the UserFullSerializer for edit requests. serializer_class = UserFullSerializer else: - serializer_class = UserShortSerializer + serializer_class = super().get_serializer_class() return serializer_class def list(self, request, *args, **kwargs): @@ -78,6 +75,7 @@ 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 new file mode 100644 index 000000000..844be9970 --- /dev/null +++ b/openslides/utils/access_permissions.py @@ -0,0 +1,30 @@ +class BaseAccessPermissions: + """ + Base access permissions container. + """ + def can_retrieve(self, user): + """ + Returns True if the user has read access model instances. + """ + return False + + def get_serializer_class(self, user): + """ + Returns different serializer classes according to users permissions. + """ + raise NotImplementedError( + "You have to add the classmethod 'get_serializer_class' to your " + "access permissions class.".format(self)) + + def get_serialized_data(self, instance, user): + """ + Returns the serialized data for the instance prepared for the user. + + Returns None if the user has no read access. + """ + if self.can_retrieve(user): + serializer_class = self.get_serializer_class(user) + data = serializer_class(instance).data + else: + data = None + return data diff --git a/openslides/utils/autoupdate.py b/openslides/utils/autoupdate.py index d530bf590..9278bf424 100644 --- a/openslides/utils/autoupdate.py +++ b/openslides/utils/autoupdate.py @@ -1,15 +1,12 @@ -import json import os import posixpath +from importlib import import_module from urllib.parse import unquote + from django.conf import settings -from openslides.users.auth import get_user from django.core.wsgi import get_wsgi_application -from django.utils.importlib import import_module from sockjs.tornado import SockJSConnection, SockJSRouter -from tornado.httpclient import AsyncHTTPClient, HTTPRequest from tornado.httpserver import HTTPServer -from tornado.httputil import HTTPHeaders from tornado.ioloop import IOLoop from tornado.options import parse_command_line from tornado.web import ( @@ -19,7 +16,8 @@ from tornado.web import ( StaticFileHandler, ) from tornado.wsgi import WSGIContainer -from .rest_api import get_collection_and_id_from_url + +from openslides.users.auth import AnonymousUser, get_user RUNNING_HOST = None RUNNING_PORT = None @@ -59,9 +57,6 @@ class DjangoStaticFileHandler(StaticFileHandler): return absolute_path -class FakeRequest: - pass - class OpenSlidesSockJSConnection(SockJSConnection): """ SockJS connection for OpenSlides. @@ -80,36 +75,29 @@ class OpenSlidesSockJSConnection(SockJSConnection): """ Sends an OpenSlides object to all connected clients (waiters). """ - # Send out internal HTTP request to get data from the REST api. for waiter in cls.waiters: - # Read waiter's former cookies and parse session cookie to new header object. - headers = HTTPHeaders() + # Read waiter's former cookies and parse session cookie to get user instance. try: session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME] + except KeyError: + # There is no session cookie so use anonymous user here. + user = AnonymousUser() + else: + # Get session from session store and use it to retrieve the user. engine = import_module(settings.SESSION_ENGINE) - session = engine.SessionStore(session_cookie) - - request = FakeRequest() - request.session = session - - user = get_user(request) - serializer_class = instance.access_permissions.get_serializer_class(user) - serialized_instance_data = serializer_class(instance).data - + session = engine.SessionStore(session_cookie.value) + 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 = { - 'url': "foobar", - 'status_code': 404 if is_delete else 200, - 'collection': instance.get_collection_name(), - 'id': instance.id, + '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) - except KeyError: - # There is no session cookie - pass - else: - headers.add('Cookie', '%s=%s' % (settings.SESSION_COOKIE_NAME, session_cookie.value)) - - def run_tornado(addr, port, *args, **kwargs): @@ -152,22 +140,28 @@ def inform_changed_data(is_delete, *args): """ Informs all users about changed data. - The arguments are Django/OpenSlides models. + The first argument is whether the object or the objects are deleted. + The other arguments are the changed or deleted Django/OpenSlides model + instances. """ - root_instances = set() - for instance in args: - try: - root_instances.add(instance.get_root_rest_element()) - except AttributeError: - # Instance has no method get_root_rest_url. Just skip it. - pass - if settings.USE_TORNADO_AS_WSGI_SERVER: - for root_instance in root_instances: - OpenSlidesSockJSConnection.send_object(root_instance, is_delete) + for instance in args: + try: + root_instance = instance.get_root_rest_element() + except AttributeError: + # Instance has no method get_root_rest_element. Just skip it. + pass + else: + if is_delete and instance == root_instance: + # A root instance is deleted. + OpenSlidesSockJSConnection.send_object(root_instance, is_delete) + else: + # A non root instance is deleted or any instance is just changed. + root_instance.refresh_from_db() + OpenSlidesSockJSConnection.send_object(root_instance, False) else: pass - # TODO: Implement big varainte with Apache or Nginx as wsgi webserver. + # TODO: Implement big variant with Apache or Nginx as WSGI webserver. def inform_changed_data_receiver(sender, instance, **kwargs): diff --git a/openslides/utils/models.py b/openslides/utils/models.py index bc7d0f0ea..127da3eb2 100644 --- a/openslides/utils/models.py +++ b/openslides/utils/models.py @@ -1,4 +1,3 @@ -from django.core.urlresolvers import reverse from django.db import models @@ -19,15 +18,11 @@ class MinMaxIntegerField(models.IntegerField): class RESTModelMixin: """ - Mixin for django models which are used in our rest api. + Mixin for Django models which are used in our REST API. """ access_permissions = None - @classmethod - def get_collection_name(cls): - return "{0}/{1}".format(cls._meta.app_label.lower(), cls._meta.object_name.lower()) - def get_root_rest_element(self): """ Returns the root rest instance. @@ -36,20 +31,25 @@ class RESTModelMixin: """ return self - def get_root_rest_url(self): + def get_access_permissions(self): """ - Returns the detail url of the root model of this object. + Returns a container to handle access permissions for this model and + its corresponding viewset. """ - # Gets the default url-name in the same way as django rest framework - # does in relations.HyperlinkedModelSerializer - root_instance = self.get_root_rest_element() - rest_url = '%s-detail' % type(root_instance)._meta.object_name.lower() - return reverse(rest_url, args=[str(root_instance.pk)]) + return self.access_permissions - def get_collection_string(self): + @classmethod + def get_collection_string(cls): """ - Returns the string representing the name of the collection. + Returns the string representing the name of the collection. Returns + None if this is not a so called root rest instance. """ - # TODO: find a way not to use the url. See #1791 - from .rest_api import get_collection_and_id_from_url - return get_collection_and_id_from_url(self.get_root_rest_url())[0] + # TODO Check if this is a root rest element class and return None if not. + return '/'.join((cls._meta.app_label.lower(), cls._meta.object_name.lower())) + + def get_rest_pk(self): + """ + Returns the primary key used in the REST API. By default this is + the database pk. + """ + return self.pk diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 58d587bfd..596bc0e8f 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -101,20 +101,17 @@ class PermissionMixin: The methods check_view_permissions or check_projector_requirements are evaluated. If both return False self.permission_denied() is called. - Django REST framework's permission system is disabled. - """ + Django REST Framework's permission system is disabled. - def get_serializer_class(self): - """ - TODO - """ - serializer_class = self.access_permissions.get_serializer_class(self.request.user) if self.access_permissions is not None else None - return super().get_serializer_class() if serializer_class is None else serializer_class + Also connects container to handle access permissions for model and + viewset. + """ + access_permissions = None def get_permissions(self): """ - Overriden method to check view and projector permissions. Returns an - empty interable so Django REST framework won't do any other + Overridden method to check view and projector permissions. Returns an + empty iterable so Django REST framework won't do any other permission checks by evaluating Django REST framework style permission classes and the request passes. """ @@ -126,6 +123,8 @@ class PermissionMixin: """ Override this and return True if the requesting user should be able to get access to your view. + + Use access permissions container for retrieve requests. """ return False @@ -144,6 +143,24 @@ class PermissionMixin: break return result + def get_access_permissions(self): + """ + Returns a container to handle access permissions for this viewset and + its corresponding model. + """ + return self.access_permissions + + def get_serializer_class(self): + """ + Overridden method to return the serializer class given by the + access permissions container. + """ + if self.get_access_permissions() is not None: + serializer_class = self.get_access_permissions().get_serializer_class(self.request.user) + else: + serializer_class = super().get_serializer_class() + return serializer_class + class ModelSerializer(_ModelSerializer): """ @@ -172,7 +189,7 @@ class GenericViewSet(PermissionMixin, _GenericViewSet): class ModelViewSet(PermissionMixin, _ModelViewSet): - access_permissions = None + pass class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet): @@ -183,12 +200,13 @@ 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/api/users/user/3/') + 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. @@ -196,5 +214,5 @@ def get_collection_and_id_from_url(url): 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) + raise OpenSlidesError('Invalid REST API URL: %s' % url) return match.group('collection'), match.group('id') diff --git a/openslides/utils/utils.py b/openslides/utils/utils.py index 620083636..b2d01f01e 100644 --- a/openslides/utils/utils.py +++ b/openslides/utils/utils.py @@ -10,7 +10,3 @@ def to_roman(number): return roman.toRoman(number) except (roman.NotIntegerError, roman.OutOfRangeError): return None - - -def collection_name(model_class): - return "{1}/{2}".format(model_class.Meta.app_label.lower(), model_class.Meta.object_name)